-- | Caching recently used blocks

{-# LANGUAGE BangPatterns #-}
module Bitcoin.BlockChain.Cache where

--------------------------------------------------------------------------------

import Control.Concurrent
import System.IO.Unsafe as Unsafe

import Data.IntMap        (IntMap) ; import qualified Data.IntMap        as IntMap
import Bitcoin.Misc.BiMap (BiMap ) ; import qualified Bitcoin.Misc.BiMap as BiMap

import Bitcoin.BlockChain.Base
import Bitcoin.BlockChain.Tx
import Bitcoin.BlockChain.Load

import Bitcoin.Script.Base

--------------------------------------------------------------------------------

data BlockCache script = BlockCache
  { _blkMap :: !(IntMap (Block (Tx script script)))
  , _locMap :: !(BiMap BlockLocation Int)
  }

-- | How many blocks we cache (128 at the moment)
theBlockCacheSize :: Int
theBlockCacheSize = 128

-- | if the largest key (approx equals the number of lookups) reaches this limit, we compactify
theBlockCacheCompactLimit :: Int
theBlockCacheCompactLimit = 1024 -- 2^30-1

-- | The global block cache
theBlockCache :: MVar (BlockCache RawScript)
theBlockCache = Unsafe.unsafePerformIO $ newMVar (BlockCache IntMap.empty BiMap.empty)

--------------------------------------------------------------------------------

compactTheBlockCache :: IO ()
compactTheBlockCache = do
  BlockCache blks locs <- takeMVar theBlockCache
  let oldlocs    = BiMap.toList locs
  let locs'      = BiMap.fromList  $ zipWith (\(loc,j) i -> (loc,i)) oldlocs [1..]
      translate  = IntMap.fromList $ zipWith (\(loc,j) i -> (j  ,i)) oldlocs [1..]
      f (!j,blk) = case IntMap.lookup j translate of 
                     Just !i -> (i,blk)
                     Nothing -> error "compactTheBlockCache: fatal error, shouldn't happen"
      blks'     = IntMap.fromList $ map f $ IntMap.toList blks
  putMVar theBlockCache $! (BlockCache blks' locs')

--------------------------------------------------------------------------------

loadBlockCached :: BlockLocation -> IO (Block (Tx RawScript RawScript))
loadBlockCached location = do

  orig@(BlockCache blks locs) <- takeMVar theBlockCache
  let n    = IntMap.size blks                                -- size
      lmax = if n==0 then 0 else fst (IntMap.findMax blks)   -- largest key
      l'   = lmax+1

  if n >= theBlockCacheCompactLimit 

    -- we have to compactify...
    then do 
      putMVar theBlockCache $! orig
      compactTheBlockCache
      loadBlockCached location

    else case BiMap.lookup location locs of

      -- it was in the cache
      Just i -> case IntMap.lookup i blks of
        Just block -> do

          putStrLn $ "block at " ++ show location ++ " was in the cache at pos " ++ show i ++ " (max = " ++ show lmax ++ " ; size = " ++ show n ++ ")"

          let blks' = IntMap.insert l' block $ IntMap.delete i $ blks
              locs' = BiMap.insert location l' locs
          putMVar theBlockCache $! (BlockCache blks' locs')
          return block
        Nothing    -> do
          let locs' = BiMap.delete location locs
          putMVar theBlockCache $! (BlockCache blks locs')         -- delete the location from the map (it was invalid anyway)
          loadBlockCached location                                 -- and try again! now it will load and put it into the map

      -- it was not in the cache
      Nothing -> do

        putStrLn $ "block at " ++ show location ++ " was not in the cache"

        block <- loadBlockAt location
        if n < theBlockCacheSize
          then do
            let blks' = IntMap.insert l' block blks
                locs' = BiMap.insert location l' locs
            putMVar theBlockCache $! BlockCache blks' locs'
            return block
          else
            case IntMap.minViewWithKey blks of
              Nothing -> error "loadBlockCached: fatal error, shouldn't happen; #1"
              Just ((i,_),rest) -> do
                let blks' = IntMap.insert l' block rest
                    locs' = BiMap.insert location l' $ BiMap.deleteRev i $ locs
                putMVar theBlockCache $! (BlockCache blks' locs')         -- delete the location from the map (it was invalid anyway) 
                return block

--------------------------------------------------------------------------------