-- |
--
-- Copyright:
--   This file is part of the package vimeta. It is subject to the
--   license terms in the LICENSE file found in the top-level
--   directory of this distribution and at:
--
--     https://github.com/pjones/vimeta
--
--   No part of this package, including this file, may be copied,
--   modified, propagated, or distributed except according to the terms
--   contained in the LICENSE file.
--
-- License: BSD-2-Clause
--
-- Caching functions.
module Vimeta.Core.Cache
  ( cacheTMDBConfig,
  )
where

import Data.Aeson as Aeson
import Data.Time.Calendar
import Data.Time.Clock
import qualified Network.API.TheMovieDB as TheMovieDB
import System.Directory
  ( XdgDirectory (..),
    createDirectoryIfMissing,
    doesFileExist,
    getModificationTime,
    getXdgDirectory,
  )
import System.FilePath (takeDirectory, (</>))

-- | Manage cache file expiration.
newtype Age
  = -- | Cap to N days.
    MaxDays Int

ageAsTime :: Age -> UTCTime -> UTCTime
ageAsTime (MaxDays days) now =
  now {utctDay = addDays (fromIntegral (- days)) (utctDay now)}

-- | The file name for catching @TheMovieDB.Configuration@.
tmdbCacheFile :: IO FilePath
tmdbCacheFile =
  getXdgDirectory XdgCache "vimeta"
    <&> (</> "tmdb-config.json")

-- | Produce a cached version of @TheMovieDB.Configuration@ or use
-- the given action to create a cache a new value.
cacheTMDBConfig ::
  (MonadIO m) =>
  m (Either e TheMovieDB.Configuration) ->
  m (Either e TheMovieDB.Configuration)
cacheTMDBConfig action = do
  file <- liftIO tmdbCacheFile
  cache file (MaxDays 3) action

-- | Generic cache reader.
readCache :: (MonadIO m, FromJSON a) => FilePath -> Age -> m (Maybe a)
readCache filename age = do
  exists <- liftIO (doesFileExist filename)
  if not exists then return Nothing else go
  where
    go = do
      now <- liftIO getCurrentTime
      modtime <- liftIO (getModificationTime filename)

      if fresh now modtime
        then Aeson.decode' <$> readFileLBS filename
        else return Nothing
    fresh :: UTCTime -> UTCTime -> Bool
    fresh now modtime = ageAsTime age now <= modtime

-- | Generic cache writer.
writeCache :: (MonadIO m, ToJSON a) => FilePath -> a -> m ()
writeCache filename value = liftIO $ do
  createDirectoryIfMissing True (takeDirectory filename)
  writeFileLBS filename (Aeson.encode value)

-- | Generic caching function.
cache ::
  (MonadIO m, FromJSON a, ToJSON a) =>
  -- | Cache file.
  FilePath ->
  -- | Age of cache file.
  Age ->
  -- | Action to generate new value.
  m (Either e a) ->
  -- | Cached or new value.
  m (Either e a)
cache file age action = do
  cached <- liftIO (readCache file age)

  case cached of
    Just c -> return (Right c)
    Nothing -> do
      result <- action
      either (const $ return ()) (writeCache file) result
      return result