{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable, Rank2Types, ScopedTypeVariables, ViewPatterns #-}
{-# LANGUAGE TypeFamilies #-}
module Development.Shake.Forward(
    shakeForward, shakeArgsForward,
    forwardOptions, forwardRule,
    cache, cacheAction
    ) where
import Development.Shake
import Development.Shake.Rule
import Development.Shake.Command
import Development.Shake.Classes
import Development.Shake.FilePath
import Data.IORef
import Data.Either
import Data.Typeable.Extra
import Data.List.Extra
import Control.Exception.Extra
import Numeric
import System.IO.Unsafe
import Data.Binary
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as Map
import Data.Functor
import Prelude
{-# NOINLINE forwards #-}
forwards :: IORef (Map.HashMap Forward (Action Forward))
forwards = unsafePerformIO $ newIORef Map.empty
newtype Forward = Forward (String, String, Wrap BS.ByteString) 
    deriving (Hashable,Typeable,Eq,NFData,Binary)
newtype Wrap a = Wrap a
    deriving (Hashable,Typeable,Eq,Binary)
instance NFData (Wrap a) where
    rnf x = x `seq` ()
mkForward :: (Typeable a, Show a, Binary a) => a -> Forward
mkForward x = Forward (show $ typeOf x, show x, Wrap $ encode' x)
unForward :: forall a . (Typeable a, Show a, Binary a) => Forward -> a
unForward (Forward (got,_,Wrap x))
    | got /= want = error $ "Failed to match forward type, wanted " ++ show want ++ ", got " ++ show got
    | otherwise = decode' x
    where want = show $ typeRep (Proxy :: Proxy a)
encode' :: Binary a => a -> BS.ByteString
encode' = BS.concat . LBS.toChunks . encode
decode' :: Binary a => BS.ByteString -> a
decode' = decode . LBS.fromChunks . return
type instance RuleResult Forward = Forward
instance Show Forward where
    show (Forward (_,x,_)) = x
shakeForward :: ShakeOptions -> Action () -> IO ()
shakeForward opts act = shake (forwardOptions opts) (forwardRule act)
shakeArgsForward :: ShakeOptions -> Action () -> IO ()
shakeArgsForward opts act = shakeArgs (forwardOptions opts) (forwardRule act)
forwardRule :: Action () -> Rules ()
forwardRule act = do
    addBuiltinRule noLint noIdentity $ \k old mode ->
        case old of
            Just old | mode == RunDependenciesSame -> return $ RunResult ChangedNothing old (decode' old)
            _ -> do
                res <- liftIO $ atomicModifyIORef forwards $ \mp -> (Map.delete k mp, Map.lookup k mp)
                case res of
                    Nothing -> liftIO $ errorIO $ "Failed to find action name, " ++ show k
                    Just act -> do
                        new <- act
                        return $ RunResult ChangedRecomputeSame (encode' new) new
    action act
forwardOptions :: ShakeOptions -> ShakeOptions
forwardOptions opts = opts{shakeCommandOptions=[AutoDeps]}
cacheAction :: (Typeable a, Binary a, Show a, Typeable b, Binary b, Show b) => a -> Action b -> Action b
cacheAction (mkForward -> key) (action :: Action b) = do
    liftIO $ atomicModifyIORef forwards $ \mp -> (Map.insert key (mkForward <$> action) mp, ())
    res <- apply1 key
    liftIO $ atomicModifyIORef forwards $ \mp -> (Map.delete key mp, ())
    return $ unForward res
cache :: (forall r . CmdArguments r => r) -> Action ()
cache cmd = do
    let CmdArgument args = cmd
    let isDull ['-',_] = True; isDull _ = False
    let name = head $ filter (not . isDull) (drop 1 $ rights args) ++ ["unknown"]
    cacheAction (Command $ toStandard name ++ " #" ++ upper (showHex (abs $ hash $ show args) "")) cmd
newtype Command = Command String
    deriving (Typeable, Binary)
instance Show Command where
    show (Command x) = "command " ++ x