{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}

-- | Provides a general caching interface along with a simple in-memory
-- (process only) and file based cache implementations.
module Web.Simple.Cache 
  ( -- * Cache Interface
    Cache(..), fetchOr
    -- * Cache Implementations
  , FileSystemCache, newFileSystemCache
  , InMemCache, newInMemCache
  ) where

import Control.Monad
import Control.Monad.IO.Class
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.HashTable.IO as H
import System.FilePath
import System.Directory

-- | A class that captures a simple key-value caching interface. The keys are
-- simply 'String'\'s and values are simply 'ByteString'\'s.
class Cache c where
  put        :: MonadIO m => c -> String -> L8.ByteString -> m L8.ByteString
  -- ^ Store a value in the cache. The returned value should be the value that
  -- was just stored.
  fetch      :: MonadIO m => c -> String -> m (Maybe L8.ByteString)
  -- ^ Retrieve a value from the cache.
  invalidate :: MonadIO m => c -> String -> m ()
  -- ^ Invalidate a potentially existing value in the cache. Depending on the
  -- implementation this may or may not free the space used by the key-value
  -- pair.

fetchOr :: (Cache c, MonadIO m)
        => c
        -> String
        -> m L8.ByteString
        -> m L8.ByteString
fetchOr c path act = do
  mcached <- fetch c path
  maybe (act >>= put c path) return mcached

-- | A file based cache implementation. Files are stored in subdirectories of
-- @fsCacheBase@.
newtype FileSystemCache = FileSystemCache { fsCacheBase :: FilePath }

-- | Create a new @FileSystemCache@.
newFileSystemCache :: FilePath -> FileSystemCache
newFileSystemCache = FileSystemCache

instance Cache FileSystemCache where
  put c path d = liftIO $ do
    let components =  splitDirectories $ dropDrive path
    let fullPath = (fsCacheBase c):components
    createDirectoryIfMissing True $
      joinPath $ reverse $ tail $ reverse fullPath
    L8.writeFile (joinPath fullPath) d
    return d

  fetch c path = liftIO $ do
    let components =  splitDirectories $ dropDrive path
    let fullPath = joinPath $ (fsCacheBase c):components
    exists <- doesFileExist fullPath
    if exists then
      fmap Just $ L8.readFile fullPath
      else return Nothing

  invalidate c path = liftIO $ do
    let components =  splitDirectories $ dropDrive path
    let fullPath = joinPath $ (fsCacheBase c):components
    exists <- doesFileExist fullPath
    when exists $
      removeFile fullPath

-- | An in-memory cache implementation. The current processes heap space is
-- simply used as the cache.
newtype InMemCache = InMemCache (H.BasicHashTable String L8.ByteString)

-- | Create a new @InMemCache@.
newInMemCache :: MonadIO m => m InMemCache
newInMemCache = liftIO $ fmap InMemCache H.new

instance Cache InMemCache where
  put (InMemCache h) path d = liftIO $ H.insert h path d >> return d
  fetch (InMemCache h) = liftIO . (H.lookup h)
  invalidate (InMemCache h) = liftIO . (H.delete h)