{-# LANGUAGE MonoLocalBinds #-}

-- | Run the actual operations from the generated dependencies from DependencyGenerator.
--
-- This does not work fully as intended right now (computes more than strictly
-- needed) and needs to be rewritten in a more testable way. The idea is to only
-- evaluate something if a future operation depends on it, which is reflected in
-- the ValueExistence type.
module Byg.DependencyRunner
  ( DepRunM
  , runDeps
  , runDepRunMIO
  , extractSndToken
  , extractSndTokenAsList
  ) where

import Byg.Types (evalFunctionIO, functionIOReads, functionIOWrites)
import Byg.Types.Value
import Byg.Types.Token
import Byg.Types.Dependency

import Type.Reflection (Typeable)
import Data.Map (Map)
import qualified Data.Map as M
import Control.Monad (void, forM, filterM)
import Control.Monad.State (MonadState, MonadIO, StateT, evalStateT, get, modify, liftIO)
import Control.Monad.Writer (MonadWriter, WriterT, runWriterT, tell)
import System.Directory (getModificationTime)
import Data.Time.Clock (UTCTime(..))
import qualified System.Directory as SD

data LastUpdated = Never
                 | NeverDebug String
                 | NeverInput
                 | At UTCTime
  deriving (Show, Eq, Ord)

data ValueExistence = Evaluated Value LastUpdated
                    | NotEvaluated (LastUpdated -> DepRunM (Maybe (Value, LastUpdated)))

newtype DepRunM a = DepRunM { unDepRunM :: WriterT [FilePath] (StateT (Map Int ValueExistence) IO) a }
  deriving (Functor, Applicative, Monad, MonadIO,
            MonadState (Map Int ValueExistence),
            MonadWriter [FilePath])

runDeps :: [Dependency] -> DepRunM ()
runDeps = mapM_ runDep

runDepRunMIO :: DepRunM a -> IO (a, [FilePath])
runDepRunMIO m = evalStateT (runWriterT (unDepRunM m)) M.empty

evaluate :: LastUpdated -> ValueExistence -> DepRunM (Maybe (Value, LastUpdated))
evaluate luFuture = \case
  Evaluated v lu -> pure (Just (v, lu))
  NotEvaluated m -> m luFuture

runDep :: Dependency -> DepRunM ()
runDep (Dependency _ a action _ b) =
  if actionWritesAny action
  then void (m Never)
  else putTokenValue b $ NotEvaluated m
  where m :: LastUpdated -> DepRunM (Maybe (Value, LastUpdated))
        m luFuture = do
          mr <- runAction action a luFuture
          case mr of
            Just (result, luResult) -> do
              putTokenValue b $ Evaluated result luResult
              pure $ Just (result, luResult)
            Nothing ->
              pure Nothing

extractSndToken :: Token (a, b) -> Token b
extractSndToken = \case
  TupleToken _ b ->
    b
  _ ->
    error "unsupported"

extractSndTokenAsList :: (Show b, Typeable b) => Token (a, b) -> Token [b]
extractSndTokenAsList = ListToken . (: []) . extractSndToken

getTokenValueByIndex :: LastUpdated -> Int -> DepRunM (Maybe (Value, LastUpdated))
getTokenValueByIndex luFuture i = do
  m <- get
  case m M.!? i of
    Nothing -> pure Nothing
    Just x -> evaluate luFuture x

-- minimumOrNever :: [LastUpdated] -> LastUpdated
-- minimumOrNever = \case
--   [] -> Never
--   times -> minimum times

maximumOrNever :: [LastUpdated] -> LastUpdated
maximumOrNever = \case
  [] -> Never
  times -> maximum times

maximumOrNever' :: [UTCTime] -> LastUpdated
maximumOrNever' = maximumOrNever . map At

getTokenValueRaw :: LastUpdated -> Token a -> DepRunM (Maybe (a, LastUpdated))
getTokenValueRaw luFuture token = case token of
  Token i -> do
    m <- getTokenValueByIndex luFuture i
    pure $ do (a, lu) <- m
              pure (fromValue a, lu)

  TupleToken a b -> do
    m0 <- getTokenValueRaw luFuture a
    m1 <- getTokenValueRaw luFuture b
    case (m0, m1) of
      (Just (a', luA), Just (b', luB)) ->
        pure $ Just ((a', b'), max luA luB)
      (Just (a', luA), Nothing) -> do -- | luA /= NeverInput -> do
        r <- getTokenValueRaw (NeverDebug (show (a', luA))) b
        pure $ case r of
          Nothing -> error ("unexpected " ++ show b ++ " (" ++ show (a', luA) ++ ")")
          Just (b', luB) -> Just ((a', b'), max luA luB)
      (Nothing, Just (b', luB)) -> do -- | luB /= NeverInput -> do
        r <- getTokenValueRaw (NeverDebug (show (b', luB))) a
        pure $ case r of
          Nothing -> error "unexpected"
          Just (a', luA) -> Just ((a', b'), max luA luB)
      _ ->
        pure $ Nothing

  ZipToken a b -> do
    m0 <- getTokenValueRaw luFuture a
    m1 <- getTokenValueRaw luFuture b
    case (m0, m1) of
      (Just (a', luA), Just (b', luB)) ->
        pure $ Just (zip a' b', max luA luB)
      (Just (a', luA), Nothing) -> do -- | luA /= NeverInput -> do
        r <- getTokenValueRaw Never b
        pure $ case r of
          Nothing -> error "unexpected"
          Just (b', luB) -> Just (zip a' b', max luA luB)
      (Nothing, Just (b', luB)) -> do -- | luB /= NeverInput -> do
        r <- getTokenValueRaw Never a
        pure $ case r of
          Nothing -> error "unexpected"
          Just (a', luA) -> Just (zip a' b', max luA luB)
      _ ->
        pure $ Nothing

  ListToken ts -> do
    ms <- mapM (getTokenValueRaw luFuture) ts
    if False -- null $ filter ((/= NeverInput) . snd) (catMaybes ms)
      then pure Nothing
      else do ms' <- case sequence ms of
                Just x -> pure x
                Nothing -> do r <- mapM (getTokenValueRaw Never) ts
                              case sequence r of
                                Nothing -> error "unexpected"
                                Just x -> pure x
              let (as, lus) = unzip ms'
              pure $ Just (as, maximumOrNever lus)

  NoToken ->
    pure $ Just ((), Never)

getTokenValue :: Token a -> LastUpdated -> DepRunM (Maybe (Value, LastUpdated))
getTokenValue token luFuture = case token of
  Token i ->
    getTokenValueByIndex luFuture i
  TupleToken _ _ ->
    fromRaw token
  ZipToken _ _ ->
    fromRaw token
  ListToken _ ->
    fromRaw token
  NoToken ->
    fromRaw token
  where fromRaw :: (Typeable a, Show a) => Token a -> DepRunM (Maybe (Value, LastUpdated))
        fromRaw t = do
          m <- getTokenValueRaw luFuture t
          pure $ do (x, lu) <- m
                    pure (toValue x, lu)

putTokenValue :: Token a -> ValueExistence -> DepRunM ()
putTokenValue t v = case t of
  Token i ->
    modify $ M.insert i v
  NoToken ->
    pure ()
  _ ->
    error "unexpected"

maximumModTime :: [FilePath] -> DepRunM LastUpdated
maximumModTime paths = do
  paths' <- filterM (liftIO . SD.doesPathExist) paths
  times <- mapM (liftIO . getModificationTime) paths'
  pure $ maximumOrNever' times

runAction :: forall a b. Action a b -> Token a -> LastUpdated
          -> DepRunM (Maybe (Value, LastUpdated))
runAction action tokenInput luFuture = case action of
  Function (F f) ->
    calc f
  FunctionIO f -> do
    m <- getTokenValueRaw luFuture $ functionIOWrites f tokenInput
    case m of
      Nothing ->
        pure Nothing -- error "unexpected" -- wrong?
      Just (writes, _writesLu) -> do
        tell writes
        lastWritten <- max luFuture <$> maximumModTime writes
        result <- getTokenValue tokenInput lastWritten
        case result of
          Just (inputValue, luInput) -> do
            let input = inputFromValue inputValue
            lastUpdated <- max luInput <$> (maximumModTime $ functionIOReads f input)
            if lastUpdated > lastWritten
              then do liftIO $ do
                        putStrLn ("input: " ++ show input)
                        putStrLn ("input last updated: " ++ show luInput)
                        putStrLn ("IO function: " ++ show f)
                        putStrLn ("Source timestamp: " ++ show lastUpdated)
                        putStrLn ("Target timestamp: " ++ show lastWritten)
                      v <- toValue <$> (liftIO $ evalFunctionIO f input)
                      -- tell writes
                      let luResult = max luInput lastUpdated
                      liftIO $ do
                        putStrLn ("output: " ++ show v)
                        putStrLn ("output last updated: " ++ show luResult)
                        putStrLn "----------"
                      pure $ Just (v, luResult)
              else do -- liftIO $ putStrLn ("Source timestamp "
                      --                    ++ show lastUpdated
                      --                    ++ " not newer than target timestamp "
                      --                    ++ show lastWritten
                      --                    ++ "; ignoring IO computation.")
                      pure Nothing -- (toValue (), lastWritten) -- assumes writing FunctionIO always return ()
          Nothing ->
            pure Nothing
  Inject x ->
    pure $ Just (toValue x, NeverInput)
  FilterComp ->
    calc (\(vs, mask) -> map fst $ filter snd $ zip vs mask)
  UntupleFst ->
    calc fst
  UntupleSnd ->
    calc snd
  UnzipFst ->
    calc (map fst)
  UnzipSnd ->
    calc (map snd)
  MapComp subDeps innerInput innerOutput -> do
    m <- getTokenValue tokenInput luFuture
    case m of
      Nothing -> pure Nothing
      Just (inputValue, luInput) -> do
        let input = inputFromValue inputValue
        lastUpdated <- maximumModTime $ actionReads action input
        result <- forM input $ \x -> do
          putTokenValue innerInput $ Evaluated (toValue x) (max luInput lastUpdated)
          runDeps subDeps
          mr <- getTokenValue innerOutput luFuture
          pure $ do (vOut, luOut) <- mr
                    pure (fromValue vOut, luOut)
        pure $ do result' <- sequence result
                  let (values, lus) = unzip result'
                  pure $ (toValueRep (actionTargetType action) values, maximumOrNever lus)
  where inputFromValue :: Typeable a => Value -> a
        inputFromValue = fromValueRep (actionSourceType action)

        calc :: (Typeable a, Typeable b, Show b) => (a -> b) -> DepRunM (Maybe (Value, LastUpdated))
        calc f = do
          m <- getTokenValue tokenInput luFuture
          pure $ do (inputValue, luInput) <- m
                    let input = inputFromValue inputValue
                    pure (toValue $ f input, luInput)
