------------------------------------------------------------------------
-- |
-- Module      :  ALife.Creatur.Database.CachedFileSystemInternal
-- Copyright   :  (c) Amy de Buitléir 2014-2016
-- License     :  BSD-style
-- Maintainer  :  amy@nualeargais.ie
-- Stability   :  experimental
-- Portability :  portable
--
-- A module containing private CachedFileSystem internals.
-- Most developers should use CachedFileSystem instead.
-- This module is subject to change without notice.
--
------------------------------------------------------------------------
{-# LANGUAGE TypeFamilies, FlexibleContexts, ScopedTypeVariables #-}
module ALife.Creatur.Database.CachedFileSystemInternal where

import Prelude hiding (readFile, writeFile, lookup)

import ALife.Creatur.Database (Database(..), DBRecord, Record,
  SizedRecord, delete, key, keys, store, size)
import qualified ALife.Creatur.Database.FileSystem as FS
import ALife.Creatur.Util (stateMap)
import Control.Monad (when)
import Control.Monad.State (StateT, get, gets, modify)

-- | A simple database where each record is stored in a separate file, 
--   and the name of the file is the record's key.
data CachedFSDatabase r = CachedFSDatabase
  {
    database :: FS.FSDatabase r,
    cache :: [r],
    maxCacheSize :: Int
  } deriving (Show, Eq)

instance (SizedRecord r) => Database (CachedFSDatabase r) where
  type DBRecord (CachedFSDatabase r) = r

  keys = withFSDB keys

  numRecords = withFSDB numRecords
  
  archivedKeys = withFSDB archivedKeys

  lookup k = do
    x <- fromCache k
    case x of
      Just r  -> return $ Right r
      Nothing -> do
        y <- withFSDB (lookup k)
        case y of
          Right r -> do
            addToCache r
            return $ Right r
          Left s  -> return $ Left s

  lookupInArchive k = withFSDB (lookupInArchive k)
  -- only the main dir is cached

  store r = do
    addToCache r
    withFSDB (store r :: StateT (FS.FSDatabase r) IO ())

  delete k = do
    deleteByKeyFromCache k
    withFSDB (delete k)

withFSDB 
  :: Monad m
    => StateT (FS.FSDatabase r) m a -> StateT (CachedFSDatabase r) m a
withFSDB f = do
  d <- get
  stateMap (\x -> d{database=x}) database f

fromCache :: Record r => String -> StateT (CachedFSDatabase r) IO (Maybe r)
fromCache k = do
  c <- gets cache
  let rs = filter (\r -> key r == k) c
  return $ if null rs
             then Nothing
             else Just (head rs)

addToCache :: SizedRecord r => r -> StateT (CachedFSDatabase r) IO ()
addToCache r = do
  deleteFromCache r
  modify (\d -> d {cache=r:cache d})
  trimCache

deleteByKeyFromCache
  :: SizedRecord r
    => String -> StateT (CachedFSDatabase r) IO ()
deleteByKeyFromCache k
  = modify (\d -> d {cache=filter (\x -> key x /= k) (cache d)})

deleteFromCache
  :: SizedRecord r
    => r -> StateT (CachedFSDatabase r) IO ()
deleteFromCache r =
  modify (\d -> d {cache=filter (\x -> key x /= key r) (cache d)})

trimCache :: SizedRecord r => StateT (CachedFSDatabase r) IO ()
trimCache = do
  m <- gets maxCacheSize
  xs <- gets cache
  when (listSize xs > m) $
    modify (\d -> d {cache=trim m xs})

trim :: SizedRecord r => Int -> [r] -> [r]
trim m xs = if listSize xs > m 
              then trim m (init xs)
              else xs

listSize :: SizedRecord r => [r] -> Int
listSize [] = 0
listSize xs = sum $ map size xs

-- | @'mkFSDatabase' d@ (re)creates the FSDatabase in the
--   directory @d@.
mkCachedFSDatabase :: FilePath -> Int -> CachedFSDatabase r
mkCachedFSDatabase d s = CachedFSDatabase (FS.mkFSDatabase d) [] s