{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude     #-}
{-# LANGUAGE OverloadedLists       #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE UndecidableInstances  #-}
-- | Implementation of "Imm.Database" based on a JSON file.
module Imm.Database.JsonFile
  ( JsonFileDatabase
  , mkJsonFileDatabase
  , defaultDatabase
  , JsonException(..)
  , module Imm.Database.FeedTable
  ) where

-- {{{ Imports
import           Imm.Database                   hiding (commit, delete, insert,
                                                 purge, update)
import           Imm.Database.FeedTable
import           Imm.Error
import           Imm.Prelude                    hiding (delete, keys)
import           Imm.Pretty

import           Control.Concurrent.MVar.Lifted
import           Control.Monad.Reader.Class
import           Control.Monad.Trans.Reader     (ReaderT)
import           Data.Aeson
import           Data.ByteString.Lazy           (hPut)
import           Data.ByteString.Streaming      (hGetContents, toLazy_)
import           Data.Map                       (Map)
import qualified Data.Map                       as Map
import qualified Data.Set                       as Set
import           Streaming.With
import           System.Directory
import           System.FilePath
-- }}}

data CacheStatus = Empty | Clean | Dirty
  deriving(Eq, Show)

data JsonFileDatabase t = JsonFileDatabase FilePath (Map (Key t) (Entry t)) CacheStatus

instance Pretty (JsonFileDatabase t) where
  pretty (JsonFileDatabase file _ _) = "JSON database: " <+> pretty file

mkJsonFileDatabase :: (Table t) => FilePath -> JsonFileDatabase t
mkJsonFileDatabase file = JsonFileDatabase file mempty Empty

-- | Default database is stored in @$XDG_CONFIG_HOME\/imm\/feeds.json@
defaultDatabase :: Table t => IO (MVar (JsonFileDatabase t))
defaultDatabase = do
  databaseFile <- getXdgDirectory XdgConfig "imm/feeds.json"
  newMVar $ mkJsonFileDatabase databaseFile


data JsonException = UnableDecode
  deriving(Eq, Show)

instance Exception JsonException where
  displayException _ = "Unable to parse JSON"


instance (Table t, FromJSON (Key t), FromJSON (Entry t), ToJSON (Key t), ToJSON (Entry t))
  => MonadDatabase t (ReaderT (MVar (JsonFileDatabase t)) IO) where
  _describeDatabase _ = pretty <$> (readMVar =<< ask)
  _fetchList t keys = Map.filterWithKey (\uri _ -> member uri $ Set.fromList keys) <$> fetchAll t
  _fetchAll _ = do
    mvar <- ask
    lift $ modifyMVar mvar $ \database -> do
      a@(JsonFileDatabase _ cache _) <- loadInCache database
      return (a, cache)
  _update _ key f = exec (\a -> update a key f)
  _insertList _ rows = exec $ insert rows
  _deleteList _ keys = exec $ delete keys
  _purge _ = exec purge
  _commit _ = exec commit

exec :: (a -> IO a) -> ReaderT (MVar a) IO ()
exec f = do
  mvar <- ask
  lift $ modifyMVar_ mvar f


-- * Low-level implementation

loadInCache :: (Table t, FromJSON (Key t), FromJSON (Entry t))
            => JsonFileDatabase t -> IO (JsonFileDatabase t)
loadInCache t@(JsonFileDatabase file _ status) = case status of
  Empty -> do
    createDirectoryIfMissing True $ takeDirectory file
    fileContent <- withBinaryFile file ReadWriteMode (toLazy_ . hGetContents)
    cache <- (`failWith` UnableDecode) $ fmap Map.fromList $ decode $ fromEmpty "[]" fileContent
    return $ JsonFileDatabase file cache Clean
  _ -> return t
  where fromEmpty x "" = x
        fromEmpty _ y  = y


insert :: (Table t, FromJSON (Key t), FromJSON (Entry t))
       => [(Key t, Entry t)] -> JsonFileDatabase t -> IO (JsonFileDatabase t)
insert rows t = insertInCache rows <$> loadInCache t

insertInCache :: Table t => [(Key t, Entry t)] -> JsonFileDatabase t -> JsonFileDatabase t
insertInCache rows (JsonFileDatabase file cache _) = JsonFileDatabase file (Map.union cache $ Map.fromList rows) Dirty


update :: (Table t, FromJSON (Key t), FromJSON (Entry t))
       => JsonFileDatabase t -> Key t -> (Entry t -> Entry t) -> IO (JsonFileDatabase t)
update t key f = updateInCache key f <$> loadInCache t

updateInCache :: Table t => Key t -> (Entry t -> Entry t) -> JsonFileDatabase t -> JsonFileDatabase t
updateInCache key f (JsonFileDatabase file cache _) = JsonFileDatabase file newCache Dirty where
  newCache = Map.update (Just . f) key cache

delete :: (Table t, FromJSON (Key t), FromJSON (Entry t))
       => [Key t] -> JsonFileDatabase t -> IO (JsonFileDatabase t)
delete keys t = deleteInCache keys <$> loadInCache t

deleteInCache :: Table t => [Key t] -> JsonFileDatabase t -> JsonFileDatabase t
deleteInCache keys (JsonFileDatabase file cache _) = JsonFileDatabase file newCache Dirty where
  newCache = foldr Map.delete cache keys

purge :: (Table t, FromJSON (Key t), FromJSON (Entry t))
      => JsonFileDatabase t -> IO (JsonFileDatabase t)
purge t = purgeInCache <$> loadInCache t

purgeInCache :: Table t => JsonFileDatabase t -> JsonFileDatabase t
purgeInCache (JsonFileDatabase file _ _) = JsonFileDatabase file mempty Dirty

commit :: (ToJSON (Key t), ToJSON (Entry t))
       => JsonFileDatabase t -> IO (JsonFileDatabase t)
commit t@(JsonFileDatabase file cache status) = case status of
  Dirty -> do
    withFile file WriteMode $ \h -> (hPut h $ encode $ Map.toList cache)
    return $ JsonFileDatabase file cache Clean
  _ -> return t