{-# LANGUAGE DeriveAnyClass #-}
module Pier.Core.Persistent
    ( addPersistent
    , askPersistent
    , askPersistents
    , cleaning
    ) where


import Data.Binary (encode, decodeOrFail)
import Development.Shake
import Development.Shake.Classes
import Development.Shake.Rule
import GHC.Generics

import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS

newtype Persistent question = Persistent question
    deriving (Typeable, Eq, Generic, Hashable, Binary, NFData)

-- Improve error messages by just forwarding the instance of the
-- wrapped type.
instance Show q => Show (Persistent q) where
    show (Persistent q) = show q

newtype PersistentA answer = PersistentA { unPersistentA :: answer }
    deriving (Show, Typeable, Eq, Generic, Hashable, Binary, NFData)

type instance RuleResult (Persistent q) = PersistentA (RuleResult q)

addPersistent
    :: (RuleResult q ~ a, ShakeValue q, ShakeValue a)
    => (q -> Action a)
    -> Rules ()
addPersistent act = addBuiltinRule noLint $ \(Persistent q) old depsChanged
                    -> case old of
    Just old' | not depsChanged
              , Just val <- decode' old'
                    -> return $ RunResult ChangedNothing old' val
    _ -> do
            rerunIfCleaned
            new <- PersistentA <$> act q
            return $ RunResult
                    (if (old >>= decode') == Just new
                        then ChangedRecomputeSame
                        else ChangedRecomputeDiff)
                    (encode' new)
                    new
    where
        encode' :: Binary a => a -> BS.ByteString
        encode' = BS.concat . LBS.toChunks . encode

        decode' :: Binary a => BS.ByteString -> Maybe a
        decode' b = case decodeOrFail $ LBS.fromChunks [b] of
                        Right (bs,_,x)
                            | LBS.null bs -> Just x
                        _ -> Nothing


-- Idea: use gitrev, or something similar.
-- Save the current git hash, plus the output of "git diff HEAD",
-- to decide whether persistents need to be recomputed.

askPersistent
    :: (RuleResult q ~ a, ShakeValue q, ShakeValue a)
    => q
    -> Action a
askPersistent question = do
    PersistentA answer <- apply1 $ Persistent question
    return answer

askPersistents
    :: (RuleResult q ~ a, ShakeValue q, ShakeValue a)
    => [q]
    -> Action [a]
askPersistents = fmap (map unPersistentA) . apply . map Persistent


data Cleaner = Cleaner
    deriving (Show, Typeable, Eq, Generic, Binary, NFData, Hashable)

type instance RuleResult Cleaner = ()

cleaning :: Bool -> Rules ()
cleaning shouldClean = do
    action rerunIfCleaned
    addBuiltinRule noLint $ \Cleaner _ _ ->
        let change = if shouldClean
                        then ChangedRecomputeDiff
                        else ChangedNothing
        in return $ RunResult change BS.empty ()

rerunIfCleaned :: Action ()
rerunIfCleaned = apply1 Cleaner