{-# LANGUAGE MultiParamTypeClasses, TypeFamilies #-}
module Hadrian.Expression (
    -- * Expressions
    Expr, Predicate, Args,

    -- ** Construction and modification
    expr, exprIO, arg, remove,

    -- ** Predicates
    (?), input, inputs, output, outputs,
    ToPredicate(..),

    -- ** Evaluation
    interpret, interpretInContext,

    -- * Convenient accessors
    getBuildRoot, getContext, getBuilder, getOutputs, getInputs, getInput, getOutput
    ) where

import Control.Monad.Extra
import Control.Monad.Trans
import Control.Monad.Trans.Reader
import Development.Shake

import qualified Hadrian.Target as Target
import Hadrian.Target (Target, target)
import Hadrian.Utilities

-- | 'Expr' @c b a@ is a computation that produces a value of type 'Action' @a@
-- and can read parameters of the current build 'Target' @c b@.
newtype Expr c b a = Expr (ReaderT (Target c b) Action a)
    deriving (Applicative, Functor, Monad)

instance Semigroup a => Semigroup (Expr c b a) where
    Expr x <> Expr y = Expr $ (<>) <$> x <*> y

-- TODO: The 'Semigroup a' constraint will at some point become redundant.
instance (Semigroup a, Monoid a) => Monoid (Expr c b a) where
    mempty  = pure mempty
    mappend = (<>)

-- | Expressions that compute a Boolean value.
type Predicate c b = Expr c b Bool

-- | Expressions that compute lists of arguments to be passed to builders.
type Args c b = Expr c b [String]

-- | Lift actions independent from the current build 'Target' into the 'Expr'
-- monad.
expr :: Action a -> Expr c b a
expr = Expr . lift

-- | Lift IO computations independent from the current build 'Target' into the
-- 'Expr' monad.
exprIO :: IO a -> Expr c b a
exprIO = Expr . liftIO

-- | Remove given elements from a list expression.
remove :: Eq a => [a] -> Expr c b [a] -> Expr c b [a]
remove xs e = filter (`notElem` xs) <$> e

-- | Add a single argument to 'Args'.
arg :: String -> Args c b
arg = pure . pure

-- | Values that can be converted to a 'Predicate'.
class ToPredicate p c b where
    toPredicate :: p -> Predicate c b

infixr 3 ?

-- | Apply a predicate to an expression.
(?) :: (Monoid a, ToPredicate p c b) => p -> Expr c b a -> Expr c b a
p ? e = do
    bool <- toPredicate p
    if bool then e else mempty

instance ToPredicate Bool c b where
    toPredicate = pure

instance ToPredicate p c b => ToPredicate (Action p) c b where
    toPredicate = toPredicate . expr

instance (c ~ c', b ~ b', ToPredicate p c' b') => ToPredicate (Expr c b p) c' b' where
    toPredicate p = toPredicate =<< p

-- | Interpret a given expression according to the given 'Target'.
interpret :: Target c b -> Expr c b a -> Action a
interpret target (Expr e) = runReaderT e target

-- | Interpret a given expression by looking only at the given 'Context'.
interpretInContext :: c -> Expr c b a -> Action a
interpretInContext c = interpret $ target c
    (error "contextOnlyTarget: builder not set")
    (error "contextOnlyTarget: inputs not set" )
    (error "contextOnlyTarget: outputs not set")

-- | Get the directory of build results.
getBuildRoot :: Expr c b FilePath
getBuildRoot = expr buildRoot

-- | Get the current build 'Context'.
getContext :: Expr c b c
getContext = Expr $ asks Target.context

-- | Get the 'Builder' for the current 'Target'.
getBuilder :: Expr c b b
getBuilder = Expr $ asks Target.builder

-- | Get the input files of the current 'Target'.
getInputs :: Expr c b [FilePath]
getInputs = Expr $ asks Target.inputs

-- | Run 'getInputs' and check that the result contains one input file only.
getInput :: (Show b, Show c) => Expr c b FilePath
getInput = Expr $ do
    target <- ask
    fromSingleton ("Exactly one input file expected in " ++ show target) <$>
        asks Target.inputs

-- | Get the files produced by the current 'Target'.
getOutputs :: Expr c b [FilePath]
getOutputs = Expr $ asks Target.outputs

-- | Run 'getOutputs' and check that the result contains one output file only.
getOutput :: (Show b, Show c) => Expr c b FilePath
getOutput = Expr $ do
    target <- ask
    fromSingleton ("Exactly one output file expected in " ++ show target) <$>
        asks Target.outputs

-- | Does any of the input files match a given pattern?
input :: FilePattern -> Predicate c b
input f = any (f ?==) <$> getInputs

-- | Does any of the input files match any of the given patterns?
inputs :: [FilePattern] -> Predicate c b
inputs = anyM input

-- | Does any of the output files match a given pattern?
output :: FilePattern -> Predicate c b
output f = any (f ?==) <$> getOutputs

-- | Does any of the output files match any of the given patterns?
outputs :: [FilePattern] -> Predicate c b
outputs = anyM output
