{-# LANGUAGE UnicodeSyntax, TypeFamilies, FlexibleContexts #-}

-- | Represents r FSDatabase (including agents, clock, logging facility,
--   etc.) that can run within the Créatúr framework.
module ALife.Creatur.Database.FileSystem
  (
    FSDatabase,
    mkFSDatabase
  ) where

import ALife.Creatur.Database (Database(..), DBRecord, Record, 
  delete, key, keys, store)
import Prelude hiding (readFile, writeFile)
import Control.Monad (unless, when)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.State (StateT, get, gets, put)
import Data.ByteString as BS (readFile, writeFile)
import qualified Data.Serialize as DS 
  (Serialize, decode, encode)
import System.Directory (createDirectoryIfMissing, doesFileExist, 
  getDirectoryContents, removeFile)

-- | A simple database where each record is stored in a separate file, and
--   the name of the file is the record's key.
data FSDatabase r = FSDatabase
  {
    initialised  Bool,
    mainDir  FilePath,
    archiveDir  FilePath
  } deriving Show

instance Database (FSDatabase r) where
  type DBRecord (FSDatabase r) = r

  keys = do
    initIfNeeded
    d  gets mainDir
    files  liftIO $ getDirectoryContents d
    return $ filter isRecordFileName files

  lookup k = do
    initIfNeeded
    d  gets mainDir
    let f = d ++ '/':k
    liftIO $ readRecord3 f

  store r = do
    initIfNeeded
    writeRecord2 mainDir r

  delete name = do
    initIfNeeded
    fileExists  liftIO $ doesFileExist name
    when fileExists $ liftIO $ removeFile name

-- | @'mkFSDatabase' d@ (re)creates the FSDatabase in the
--   directory @d@.
mkFSDatabase  FilePath  FSDatabase r
mkFSDatabase d = FSDatabase False d (d ++ "/archive")

initIfNeeded  StateT (FSDatabase r) IO ()
initIfNeeded = do
  isInitialised  gets initialised
  unless isInitialised $ do
    u  get
    u'  liftIO $ initialise u
    put u'

initialise  FSDatabase r  IO (FSDatabase r)
initialise u = do
  createDirectoryIfMissing True (mainDir u)
  createDirectoryIfMissing True (archiveDir u)
  return u { initialised=True }

-- | Read a record from a file.
readRecord3  DS.Serialize r  FilePath  IO (Either String r)
readRecord3 f = do
  x  readFile f
  return $ DS.decode x

-- | Write a record to a file.
writeRecord3  (Record r, DS.Serialize r)  FilePath  r  IO ()
writeRecord3 f a = do
  let x = DS.encode a
  writeFile f x

writeRecord2  (Record r, DS.Serialize r)  
  (FSDatabase r  FilePath)  r  StateT (FSDatabase r) IO ()
writeRecord2 dirGetter r = do
  d  gets dirGetter
  let f = d ++ '/':key r
  liftIO $ writeRecord3 f r
  -- liftIO $ agentId r ++ " archived to " ++ show f     

isRecordFileName  String  Bool
isRecordFileName s =
  s `notElem` [ "archive", ".", ".." ]