{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE OverlappingInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE DeriveDataTypeable #-}
module Development.Cake3.Monad where

import Control.Applicative
import Control.Monad
import Control.Monad.State
import Control.Monad.Reader
import Control.Monad.Trans
import Control.Monad.Loc
import Data.Data
import Data.Typeable
import Data.Monoid
import Data.Maybe
import qualified Data.Map as M
import Data.Map(Map)
import qualified Data.Set as S
import Data.Set(Set)
import qualified Data.String as STR
import Data.List as L hiding (foldl')
import Data.Either
import Data.Foldable (Foldable(..), foldl')
import qualified Data.ByteString.Char8 as BS
import qualified Data.Foldable as F
import qualified Data.Traversable as F
import qualified Data.Text as T
import Development.Cake3.Types
import qualified System.IO as IO
import Text.Printf
import Text.QuasiMake

import Language.Haskell.TH.Quote
import Language.Haskell.TH hiding(unsafe)
import Language.Haskell.Meta (parseExp)

import System.FilePath.Wrapper


type Location = String

-- | MakeState describes the state of the EDSL synthesizers during the
-- the program execution.
data MakeState = MS {
    prebuilds :: Recipe
    -- ^ Prebuild commands. targets/prerequsites of the recipe are ignored,
    -- commands are executed before any target
  , postbuilds :: Recipe
    -- ^ Postbuild commands.
  , recipes :: Set Recipe
    -- ^ The set of recipes
  , sloc :: Location
    -- ^ Current location. FIXME: fix or remove
  , makeDeps :: Set File
    -- ^ Set of files which the Makefile depends on
  , placement :: [File]
    -- ^ Placement list is the order of targets to be placed in the output file
  , includes :: Set File
    -- ^ Set of files to include in the output file (Makefile specific thing)
  , errors :: String
    -- ^ Errors found so far
  , warnings :: String
    -- ^ Warnings found so far
  , outputFile :: File
  }

-- Oh, such a boilerplate
initialMakeState mf = MS defr defr mempty mempty mempty mempty mempty mempty mempty mf where
  defr = emptyRecipe "<internal>"

getPlacementPos :: Make Int
getPlacementPos = L.length <$> placement <$> get

addPlacement :: Int -> File -> Make ()
addPlacement pos r = modify $ \ms -> ms { placement = r`insertInto`(placement ms) } where
  insertInto x xs = let (h,t) = splitAt pos xs in h ++ (x:t)

addMakeDep :: File -> Make ()
addMakeDep f = modify (\ms -> ms { makeDeps = S.insert f (makeDeps ms) })

-- | Add prebuild command
prebuild, postbuild :: (MonadMake m) => CommandGen -> m ()
prebuild cmdg = liftMake $ do
  s <- get
  pb <- fst <$> runA' (prebuilds s) (shell cmdg)
  put s { prebuilds = pb }
postbuild cmdg = liftMake $ do
  s <- get
  pb <- fst <$> runA' (postbuilds s) (shell cmdg)
  put s { postbuilds = pb }

-- | Find recipes without targets
checkForEmptyTarget :: (Foldable f) => f Recipe -> String
checkForEmptyTarget rs = foldl' checker mempty rs where
  checker es r | S.null (rtgt r) = es++e
               | otherwise = es where
    e = printf "Error: Recipe without targets\n\t%s\n" (show r)

-- | Find recipes sharing a target
checkForTargetConflicts :: (Foldable f) => f Recipe -> String
checkForTargetConflicts rs = foldl' checker mempty (groupRecipes rs) where
  checker es rs | S.size rs > 1 = es++e
                | otherwise = es where
    e = printf "Error: Recipes share one or more targets\n\t%s\n" (show rs)


-- | A Monad providing access to MakeState. TODO: not mention IO here.
class (Monad m) => MonadMake m where
  liftMake :: (Make' IO) a -> m a

newtype Make' m a = Make { unMake :: (StateT MakeState m) a }
  deriving(Monad, Functor, Applicative, MonadState MakeState, MonadIO, MonadFix)

type Make a = Make' IO a

instance MonadMake (Make' IO) where
  liftMake = id

instance (MonadMake m) => MonadMake (A' m) where
  liftMake m = A' (lift (liftMake m))

instance (MonadMake m) => MonadMake (StateT s m) where
  liftMake = lift . liftMake


-- | Returns a MakeState
evalMake :: (Monad m) => File -> Make' m a -> m MakeState
evalMake mf mk = do
  ms <- flip execStateT (initialMakeState mf) (unMake mk)
  return ms {
    errors = checkForEmptyTarget (recipes ms) ++ checkForTargetConflicts (recipes ms)
  }

modifyLoc f = modify $ \ms -> ms { sloc = f (sloc ms) }

addRecipe :: Recipe -> Make ()
addRecipe r = modify $ \ms -> 
  let rs = recipes ms ; k = rtgt r
  in ms { recipes = (S.insert r (recipes ms)) }

getLoc :: Make String
getLoc = sloc <$> get

-- | Add 'include ...' directive to the final Makefile for each input file.
includeMakefile :: (Foldable t) => t File -> Make ()
includeMakefile fs = foldl' scan (return ()) fs where
  scan a f = do
    modify $ \ms -> ms {includes = S.insert f (includes ms)}
    return ()

instance (Monad m) => MonadLoc (Make' m) where
  withLoc l' (Make um) = Make $ do
    modifyLoc (\l -> l') >> um

-- | 'A' here stands for Action. It is a State monad carrying a Recipe as its
-- state.  Various monadic actions add targets, prerequisites and shell commands
-- to this recipe. After that, @rule@ function records it to the @MakeState@.
-- After the recording, no modification is allowed for this recipe.
newtype A' m a = A' { unA' :: StateT Recipe m a }
  deriving(Monad, Functor, Applicative, MonadState Recipe, MonadIO,MonadFix)

-- | Verison of Action monad with fixed parents
type A a = A' (Make' IO) a

-- | A class of monads providing access to the underlying A monad
class (Monad m, Monad t) => MonadAction t m | t -> m where
  liftAction :: A' m x -> t x

instance (Monad m) => MonadAction (A' m) m where
  liftAction = id

-- | Run the Action monad, using already existing Recipe as input.
runA' :: (Monad m) => Recipe -> A' m a -> m (Recipe, a)
runA' r act = do
  (a,r) <- runStateT (unA' act) r
  return (r,a)

-- | Create new empty recipe and run action on it.
runA :: (Monad m)
  => String -- ^ Location string (in the Cakefile.hs)
  -> A' m a -- ^ Recipe builder
  -> m (Recipe, a)
runA loc act = runA' (emptyRecipe loc) act

-- | Version of runA discarding the result of A's computation
runA_ :: (Monad m) => String -> A' m a -> m Recipe
runA_ loc act = runA loc act >>= return .fst

-- | Get a list of targets added so far
targets :: (Applicative m, Monad m) => A' m (Set File)
targets = rtgt <$> get

-- | Get a list of prerequisites added so far
prerequisites :: (Applicative m, Monad m) => A' m (Set File)
prerequisites = rsrc <$> get

-- | Mark the recipe as 'PHONY' i.e. claim that all it's targets are not real
-- files. Makefile-specific.
markPhony :: (Monad m) => A' m ()
markPhony = modify $ \r -> r { rflags = S.insert Phony (rflags r) }

-- | Mark the recipe as 'INTERMEDIATE' i.e. claim that all it's targets may be
-- removed after the build process. Makefile-specific.
markIntermediate :: (Monad m) => A' m ()
markIntermediate = modify $ \r -> r { rflags = S.insert Intermediate (rflags r) }

-- | Obtain the contents of a File. Note, that this generally means, that
-- Makefile should be regenerated each time the File is changed.
readFileForMake :: (MonadMake m)
  => File -- ^ File to read contents of
  -> m BS.ByteString
readFileForMake f = liftMake (addMakeDep f >> liftIO (BS.readFile (toFilePath f)))

-- | CommandGen is a recipe-builder packed in the newtype to prevent partial
-- expantion of it's commands
newtype CommandGen' m = CommandGen' { unCommand :: A' m Command }
type CommandGen = CommandGen' (Make' IO)

-- | Pack the command builder into a CommandGen
commandGen :: A Command -> CommandGen
commandGen mcmd = CommandGen' mcmd

-- | Modifie the recipe builder: ignore all the dependencies
ignoreDepends :: (Monad m) => A' m a -> A' m a
ignoreDepends action = do
  r <- get
  a <- action
  modify $ \r' -> r' { rsrc = rsrc r, rvars = rvars r }
  return a

-- | Apply the recipe builder to the current recipe state. Return the list of
-- targets of the current @Recipe@ under construction
shell :: (Monad m)
  => CommandGen' m -- ^ Command builder as returned by cmd quasi-quoter
  -> A' m [File]
shell cmdg = do
  line <- unCommand cmdg
  commands [line]
  r <- get
  return (S.toList (rtgt r))

-- | Version of @shell@ which doesn't track it's dependencies
unsafeShell :: (Monad m) => CommandGen' m -> A' m [File]
unsafeShell cmdg = ignoreDepends (shell cmdg)

-- | Simple wrapper for strings, a target for various typeclass instances.
newtype CakeString = CakeString String
  deriving(Show,Eq,Ord)

-- | An alias to CakeString constructor
string :: String -> CakeString
string = CakeString

-- | Class of things which may be referenced using '\@(expr)' syntax of the
-- quasi-quoted shell expressions.
class (Monad m) => RefOutput m x where
  -- | Register the output item, return it's shell-command representation. Files
  -- are rendered using space protection quotation, variables are wrapped into
  -- $(VAR) syntax, item lists are converted into space-separated lists.
  refOutput :: x -> A' m Command

instance (Monad m) => RefOutput m File where
  refOutput f = do
    modify $ \r -> r { rtgt = f `S.insert` (rtgt r)}
    return_file f

-- FIXME: inbetween will not notice if spaces are already exists
inbetween x mx = (concat`liftM`mx) >>= \l -> return (inbetween' x l) where
  inbetween' x [] = []
  inbetween' x [a] = [a]
  inbetween' x (a:as) = a:x:(inbetween' x as)

spacify l = (CmdStr " ") `inbetween` l

instance (Monad m) => RefOutput m [File] where
  refOutput xs = spacify $ mapM refOutput (xs)

instance (Monad m) => RefOutput m (Set File) where
  refOutput xs = refOutput (S.toList xs)

instance (RefOutput m x) => RefOutput m (Maybe x) where
  refOutput mx = case mx of
    Nothing -> return mempty
    Just x -> refOutput x

-- | Class of things which may be referenced using '\$(expr)' from inside
-- of quasy-quoted shell expressions
class (MonadAction a m) => RefInput a m x where
  -- | Register the input item, return it's shell-script representation
  refInput :: x -> a Command

instance (MonadAction a m) => RefInput a m File where
  refInput f = liftAction $ do
    modify $ \r -> r { rsrc = f `S.insert` (rsrc r)}
    return_file f

instance (MonadAction a m) => RefInput a m Recipe where
  refInput r = refInput (rtgt r)

instance (RefInput a m x) => RefInput a m [x] where
  refInput xs = spacify $ mapM refInput xs

instance (MonadAction a m) => RefInput a m (Set File) where
  refInput xs = refInput (S.toList xs)

instance (MonadIO a, RefInput a m x) => RefInput a m (IO x) where
  refInput mx = liftIO mx >>= refInput

instance (MonadAction a m, MonadMake a) => RefInput a m (Make Recipe) where
  refInput mr = liftMake mr >>= refInput

instance (RefInput a m x, MonadMake a) => RefInput a m (Make x) where
  refInput mx = liftMake mx >>= refInput

instance (RefInput a m x) => RefInput a m (Maybe x) where
  refInput mx = case mx of
    Nothing -> return mempty
    Just x -> refInput x

instance (MonadAction a m) => RefInput a m Variable where
  refInput v@(Variable n _) = liftAction $ do
    variables [v]
    return_text $ printf "$(%s)" n

instance (MonadAction a m) => RefInput a m CakeString where
  refInput v@(CakeString s) = do
    return_text s

-- | Add it's argument to the list of dependencies (prerequsites) of a current
-- recipe under construction
depend :: (RefInput a m x)
  => x -- ^ File or [File] or (Set File) or other form of dependency.
  -> a ()
depend x = refInput x >> return ()

-- | Declare that current recipe produces some producable item.
produce :: (RefOutput m x)
  => x -- ^ File or [File] or other form of target.
  -> A' m ()
produce x = refOutput x >> return ()

-- | Add variables to the list of variables referenced by the current recipe
variables :: (Foldable t, Monad m)
  => (t Variable) -- ^ A set of variables to depend the recipe on
  -> A' m ()
variables vs = modify (\r -> r { rvars = foldl' (\a v -> S.insert v a) (rvars r) vs } )

-- | Add commands to the list of commands of a current recipe under
-- construction. Warning: this function behaves like unsafeShell i.e. it doesn't
-- analyze the command text
commands :: (Monad m) => [Command] -> A' m ()
commands cmds = modify (\r -> r { rcmd = (rcmd r) ++ cmds } )

-- | Set the recipe's location in the Cakefile.hs
location :: (Monad m) => String -> A' m ()
location l  = modify (\r -> r { rloc = l } )

-- | Set additional flags
flags :: (Monad m) => Set Flag -> A' m ()
flags f = modify (\r -> r { rflags = (rflags r) `mappend` f } )

-- | Has effect of a function @QQ -> CommandGen@ where QQ is a string supporting
-- the following syntax:
--
-- * $(expr) evaluates to expr and adds it to the list of dependencies (prerequsites)
--
-- * \@(expr) evaluates to expr and adds it to the list of targets
--
-- * $$ and \@\@ evaluates to $ and \@
--
-- /Example/
--
-- > [cmd|gcc $flags -o @file|]
--
-- is equivalent to
--
-- >   return $ CommandGen $ do
-- >     s1 <- refInput "gcc "
-- >     s2 <- refInput (flags :: Variable)
-- >     s3 <- refInput " -o "
-- >     s4 <- refOutput (file :: File)
-- >     return (s1 ++ s2 ++ s3 ++ s4)
--
-- Later, this command may be examined or passed to the shell function to apply
-- it to the recipe
--
cmd :: QuasiQuoter
cmd = QuasiQuoter
  { quotePat  = undefined
  , quoteType = undefined
  , quoteDec  = undefined
  , quoteExp = \s -> appE [| \x -> CommandGen' x |] (qqact s)
  } where
    qqact s = 
      let chunks = flip map (getChunks (STR.fromString s)) $ \c ->
                     case c of
                       T t -> let t' = T.unpack t in [| return_text t' |]
                       E c t -> case parseExp (T.unpack t) of
                                  Left  e -> error e
                                  Right e -> case c of
                                    '$' -> appE [| refInput |] (return e)
                                    '@' -> appE [| refOutput |] (return e)
      in appE [| \l -> L.concat <$> (sequence l) |] (listE chunks)