{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE LambdaCase            #-}
{-# LANGUAGE MultiParamTypeClasses #-}

-- |
-- This module defines the remote caching mechanism of funflow which is used to
-- keep several funflow stores (possibly on different machines) in sync.
module Data.CAS.RemoteCache
  ( Cacher(..)
  , PullResult(..), PushResult(..), AliasResult(..)
  , NoCache(..), memoryCache
  , pullAsArchive, pushAsArchive
  ) where

import qualified Codec.Archive.Tar               as Tar
import           Control.Concurrent.MVar
import           Control.Monad.IO.Class          (MonadIO, liftIO)
import           Data.ByteString.Lazy            (ByteString)
import           Data.CAS.ContentHashable
import           Data.Map.Strict                 (Map)
import qualified Data.Map.Strict                 as Map
import           Path


-- |
-- The result of a tentative pull from the remote cache
data PullResult a
  = PullOK a
  | NotInCache
  | PullError String
  deriving (Eq, Ord, Show)

-- |
-- The result of a tentative push to the remote cache
data PushResult
  = PushOK
  | PushError String
  deriving (Eq, Ord, Show)

data AliasResult
 = AliasOK
 | TargetNotInCache
 | AliasError String

-- |
-- A simple mechanism for remote-caching.
--
-- Provides a way to push a path to the cache and pull it back.
--
-- No assumption is made on the availability of a store path. In particular,
-- pushing a path to the cache doesn't mean that we can pull it back.
class Monad m => Cacher m a where
  push ::
       a
    -> ContentHash -- ^ "Primary" key: hash of the content
    -> Maybe ContentHash -- ^ "Secondary" key: hash of the dependencies
    -> Path Abs Dir -- ^ Path to the content
    -> m PushResult
  pull :: a -> ContentHash -> Path Abs Dir -> m (PullResult ())

-- |
-- Push the path as an archive to the remote cache
pushAsArchive ::
     MonadIO m
  => (ContentHash -> ContentHash -> m (Either String ())) -- ^ How to create the aliases
  -> (ContentHash -> ByteString -> m PushResult) -- ^ How to push the content
  -> ContentHash -- ^ Primary key
  -> Maybe ContentHash -- ^ Secondary key
  -> Path Abs Dir
  -> m PushResult
pushAsArchive alias pushArchive primaryKey mSecondaryKey path = do
  archive <- liftIO $ Tar.write <$> Tar.pack (toFilePath path) ["."]
  pushArchive primaryKey archive >>= \case
    PushError e -> pure $ PushError e
    res ->
      case mSecondaryKey of
        Just secondaryKey ->
          alias primaryKey secondaryKey >>= \case
          Left err -> pure $ PushError err
          Right () -> pure res
        Nothing -> pure res

pullAsArchive ::
     MonadIO m
  => (ContentHash -> m (PullResult ByteString))
  -> ContentHash
  -> Path Abs Dir
  -> m (PullResult ())
pullAsArchive pullArchive hash path =
  pullArchive hash >>= \case
    PullOK archive -> do
      liftIO $ Tar.unpack (toFilePath path) $ Tar.read archive
      pure $ PullOK ()
    NotInCache -> pure NotInCache
    PullError e -> pure $ PullError e

-- |
-- A dummy remote cache implementation which does nothing
data NoCache = NoCache

instance Monad m => Cacher m NoCache where
  pull _ _ _ = pure NotInCache
  push _ _ _ _ = pure PushOK

-- |
-- An in-memory cache, for testing purposes
data MemoryCache = MemoryCache (MVar (Map ContentHash ByteString))
instance MonadIO m => Cacher m MemoryCache where
  pull (MemoryCache cacheVar) = pullAsArchive $ \hash -> do
    cacheMap <- liftIO $ readMVar cacheVar
    case Map.lookup hash cacheMap of
      Nothing -> pure NotInCache
      Just x  -> pure (PullOK x)
  push (MemoryCache cacheVar) = pushAsArchive alias $ \hash content -> do
    liftIO $ modifyMVar_
      cacheVar
      (\cacheMap -> pure $ Map.insert hash content cacheMap)
    pure PushOK
    where
      alias from to = liftIO $ Right <$> modifyMVar_ cacheVar
        (\cacheMap -> pure $ Map.insert to (cacheMap Map.! from) cacheMap)

memoryCache :: MonadIO m => m MemoryCache
memoryCache = liftIO $ MemoryCache <$> newMVar mempty

-- |
-- If 'a' is a 'Cacher' then 'Maybe a' is a cacher such that 'Just x' behavies
-- like 'x' and 'Nothing' doesn't cache anything
instance Cacher m a => Cacher m (Maybe a) where
  pull (Just x) = pull x
  pull Nothing  = pull NoCache

  push (Just x) = push x
  push Nothing  = push NoCache