{-# LANGUAGE EmptyDataDecls            #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE GADTs                     #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE TypeOperators             #-}
{-# LANGUAGE ViewPatterns              #-}

-- | Core Funflow types and functions.
--
--   In particular, you will probably care about the 'Flow' type, which is the
--   type of all Funflow workflows.
module Control.Funflow.Base where

import           Control.Arrow                   (Kleisli (..))
import           Control.Arrow.Free
import           Control.Exception.Safe          (SomeException)
import           Control.Funflow.ContentHashable
import qualified Control.Funflow.ContentStore    as CS
import           Control.Funflow.Diagram
import           Control.Funflow.External
import           Data.ByteString                 (ByteString)
import           Data.Default
import           Data.Functor.Identity
import           Data.Int                        (Int64)
import           Data.Proxy                      (Proxy (..))
import qualified Data.Store                      as Store
import qualified Data.Text                       as T
import           Path
import           Prelude                         hiding (id, (.))
import           System.Random                   (randomIO)

-- | Metadata writer
type MDWriter i o = Maybe (i -> o -> [(T.Text, ByteString)])

-- | A cacher is responsible for controlling how steps are cached.
data Cacher i o =
    NoCache -- ^ This step cannot be cached (default).
  | Cache
    { -- | Function to encode the input into a content
      --   hash.
      --   This function additionally takes an
      --   'identities' which gets incorporated into
      --   the cacher.
      cacherKey        :: Int -> i -> ContentHash
    , cacherStoreValue :: o -> ByteString
      -- | Attempt to read the cache value back. May throw exceptions.
    , cacherReadValue  :: ByteString -> o
    }

defaultCacherWithIdent :: (Store.Store o, ContentHashable Identity i)
                       => Int -- ^ Seed for the cacher
                       -> Cacher i o
defaultCacherWithIdent ident = Cache
  { cacherKey = \i ident' -> runIdentity $ contentHash (ident', ident, i)
  , cacherStoreValue = Store.encode
  , cacherReadValue = Store.decodeEx
  }

data Properties i o = Properties
  { -- | Name of this step. Used when describing the step in diagrams
    --   or other reporting.
    name     :: Maybe T.Text
    -- | Specify whether this step can be cached or not and, if so,
    --   how to do so.
  , cache    :: Cacher i o
    -- | Write additional metadata to the content store.
  , mdpolicy :: MDWriter i o
  }

instance Default (Properties i o) where
  def = Properties
    { name = Nothing
    , cache = NoCache
    , mdpolicy = Nothing
    }

data EpPurity = EpPure | EpImpure (IO Int64)

instance Default EpPurity where
  def = EpPure

alwaysRecompile :: EpPurity
alwaysRecompile = EpImpure randomIO

-- | Additional properties associated with external tasks.
data ExternalProperties a = ExternalProperties
  { -- | Write additional metadata to the content store.
    ep_mdpolicy :: MDWriter a ()
    -- | Specify that this external step is impure, and as such should not be
    -- cached.
  , ep_impure :: EpPurity
  }

instance Default (ExternalProperties a) where
  def = ExternalProperties
    { ep_mdpolicy = Nothing
    , ep_impure = def
    }

data Flow' eff a b where
  Step :: Properties a b -> (a -> b) -> Flow' eff a b
  StepIO :: Properties a b -> (a -> IO b) -> Flow' eff a b
  External :: ExternalProperties a
           -> (a -> ExternalTask)
           -> Flow' eff a CS.Item
  -- XXX: Constrain allowed user actions.
  PutInStore :: ContentHashable IO a => (Path Abs Dir -> a -> IO ()) -> Flow' eff a CS.Item
  -- XXX: Constrain allowed user actions.
  GetFromStore :: (Path Abs t -> IO a) -> Flow' eff (CS.Content t) a
  -- Internally manipulate the store. This should not be used by
  -- client libraries.
  InternalManipulateStore :: (CS.ContentStore -> a -> IO b)
                          -> Flow' eff a b
  Wrapped :: Properties a b -> eff a b -> Flow' eff a b

type Flow eff ex = ErrorChoice ex (Flow' eff)

data NoEffect a b

-- | Since there are no constructors for 'NoEffect', this code can never be
--   reached and so is fine.
runNoEffect :: forall arr. NoEffect ~> arr
runNoEffect = error "Impossible!"

type SimpleFlow = Flow NoEffect SomeException
type (==>) = SimpleFlow

-- | Convert a flow to a diagram, for inspection/pretty printing
toDiagram :: Flow eff ex a b -> Diagram ex a b
toDiagram = eval toDiagram' where
  toDiagram' (Step (name -> Just n) f)  = node f [n]
  toDiagram' (StepIO (name -> Just n) f)  = node (Kleisli f) [n]
  toDiagram' _
      = Node emptyNodeProperties (Proxy :: Proxy a1) (Proxy :: Proxy b1)