content: {-|
  Module      : $Header$
  Description : Basic functions for dealing with mustache templates.
  Copyright   : (c) Justus Adam, 2015
  License     : BSD3
  Maintainer  : dev@justus.science
  Stability   : experimental
  Portability : POSIX
  -}
  {-# OPTIONS_GHC -fno-warn-missing-fields #-}
  module Text.Mustache.Compile
    ( automaticCompile
    , localAutomaticCompile
    , TemplateCache
    , compileTemplateWithCache
    , compileTemplate
    , cacheFromList
    , getPartials
    , mustache
    , embedTemplate
    , embedSingleTemplate
    ) where
  
  
  import           Control.Arrow ( (&&&) )
  import           Control.Monad ( (<=<), filterM, foldM )
  import           Control.Monad.Except
                     ( ExceptT (..), MonadError (..), runExceptT )
  import           Control.Monad.State
                     ( MonadState (..), MonadTrans (..), StateT, evalStateT
                     , modify
                     )
  import           Data.Bool ( bool )
  import qualified Data.HashMap.Strict as HM
  import           Data.Text ( Text )
  import qualified Data.Text as T
  import qualified Data.Text.IO as TIO
  import           Language.Haskell.TH
                     ( Exp, Loc, Q, loc_filename, loc_start, location )
  import           Language.Haskell.TH.Quote
                     ( QuasiQuoter (..), quoteExp )
  import qualified Language.Haskell.TH.Syntax as THS
  import           System.Directory ( doesFileExist, makeAbsolute )
  import           System.FilePath ( (</>) )
  import           Text.Mustache.Parser ( parse )
  import           Text.Mustache.Types
                     ( STree, Template (..), TemplateCache, Node (..) )
  import           Text.Parsec.Error ( Message (..), ParseError, newErrorMessage )
  import           Text.Parsec.Pos ( initialPos )
  import           Text.Printf ( printf )
  
  
  {-|
    Compiles a mustache template provided by name including the mentioned partials.
  
    The same can be done manually using 'getFile', 'mustacheParser' and 'getPartials'.
  
    This function also ensures each partial is only compiled once even though it may
    be included by other partials including itself.
  
    A reference to the included template will be found in each including templates
    'partials' section.
  -}
  automaticCompile :: [FilePath] -> FilePath -> IO (Either ParseError Template)
  automaticCompile searchSpace = compileTemplateWithCache searchSpace mempty
  
  
  -- | Compile the template with the search space set to only the current directory
  localAutomaticCompile :: FilePath -> IO (Either ParseError Template)
  localAutomaticCompile = automaticCompile ["."]
  
  
  {-|
    Compile a mustache template providing a list of precompiled templates that do
    not have to be recompiled.
  -}
  compileTemplateWithCache :: [FilePath]
                           -> TemplateCache
                           -> FilePath
                           -> IO (Either ParseError Template)
  compileTemplateWithCache searchSpace templates initName =
    runExceptT $ evalStateT (compile' initName) $ flattenPartials templates
    where
      compile' :: FilePath
               -> StateT
                  (HM.HashMap String Template)
                  (ExceptT ParseError IO)
                  Template
      compile' name' = do
        templates' <- get
        case HM.lookup name' templates' of
          Just template -> pure template
          Nothing -> do
            rawSource <- lift $ getFile searchSpace name'
            compiled@(Template { ast = mSTree }) <-
              lift $ ExceptT . pure $ compileTemplate name' rawSource
  
            foldM
              (\st@(Template { partials = p }) partialName -> do
                nt <- compile' partialName
                modify (HM.insert partialName nt)
                pure (st { partials = HM.insert partialName nt p })
              )
              compiled
              (getPartials mSTree)
  
  
  -- | Flatten a list of Templates into a single 'TemplateCache'
  cacheFromList :: [Template] -> TemplateCache
  cacheFromList = flattenPartials . HM.fromList . fmap (name &&& id)
  
  
  -- | Compiles a 'Template' directly from 'Text' without checking for missing partials.
  -- the result will be a 'Template' with an empty 'partials' cache.
  compileTemplate :: String -> Text -> Either ParseError Template
  compileTemplate name' = fmap (flip (Template name') mempty) . parse name'
  
  
  {-|
    Find the names of all included partials in a mustache STree.
  
    Same as @join . fmap getPartials'@
  -}
  getPartials :: STree -> [FilePath]
  getPartials = (getPartials' =<<)
  
  
  {-|
    Find partials in a single Node
  -}
  getPartials' :: Node Text -> [FilePath]
  getPartials' (Partial _ p) = pure p
  getPartials' (Section _ n) = getPartials n
  getPartials' (InvertedSection _ n) = getPartials n
  getPartials' _                     = mempty
  
  
  flattenPartials :: TemplateCache -> TemplateCache
  flattenPartials m = HM.foldrWithKey (HM.insertWith (\_ b -> b)) m m
  
  
  {-|
    @getFile searchSpace file@ iteratively searches all directories in
    @searchSpace@ for a @file@ returning it if found or raising an error if none
    of the directories contain the file.
  
    This trows 'ParseError's to be compatible with the internal Either Monad of
    'compileTemplateWithCache'.
  -}
  getFile :: [FilePath] -> FilePath -> ExceptT ParseError IO Text
  getFile [] fp = throwError $ fileNotFound fp
  getFile (templateDir : xs) fp =
    lift (doesFileExist filePath) >>=
      bool
        (getFile xs fp)
        (lift $ TIO.readFile filePath)
    where
      filePath = templateDir </> fp
  
  
  -- |
  -- Compile a mustache 'Template' at compile time. Usage:
  --
  -- > {-# LANGUAGE QuasiQuotes #-}
  -- > import Text.Mustache.Compile (mustache)
  -- >
  -- > foo :: Template
  -- > foo = [mustache|This is my inline {{ template }} created at compile time|]
  --
  -- Partials are not supported in the QuasiQuoter
  mustache :: QuasiQuoter
  mustache = QuasiQuoter {quoteExp = \unprocessedTemplate -> do
    l <- location
    compileTemplateTH (fileAndLine l) unprocessedTemplate }
  
  
  -- |
  -- Compile a mustache 'Template' at compile time providing a search space for
  -- any partials. Usage:
  --
  -- > {-# LANGUAGE TemplateHaskell #-}
  -- > import Text.Mustache.Compile (embedTemplate)
  -- >
  -- > foo :: Template
  -- > foo = $(embedTemplate ["dir", "dir/partials"] "file.mustache")
  --
  embedTemplate :: [FilePath] -> FilePath -> Q Exp
  embedTemplate searchSpace filename = do
    template <- either (fail . ("Parse error in mustache template: " ++) . show) pure =<< THS.runIO (automaticCompile searchSpace filename)
    let possiblePaths = do
          fname <- (filename:) . HM.keys . partials $ template
          path <- searchSpace
          pure $ path </> fname
    mapM_ addDependentRelativeFile =<< THS.runIO (filterM doesFileExist possiblePaths)
    THS.lift template
  
  
  -- |
  -- Compile a mustache 'Template' at compile time. Usage:
  --
  -- > {-# LANGUAGE TemplateHaskell #-}
  -- > import Text.Mustache.Compile (embedSingleTemplate)
  -- >
  -- > foo :: Template
  -- > foo = $(embedSingleTemplate "dir/file.mustache")
  --
  -- Partials are not supported in embedSingleTemplate
  embedSingleTemplate :: FilePath -> Q Exp
  embedSingleTemplate filePath = do
    addDependentRelativeFile filePath
    compileTemplateTH filePath =<< THS.runIO (readFile filePath)
  
  
  fileAndLine :: Loc -> String
  fileAndLine loc = loc_filename loc ++ ":" ++ (show . fst . loc_start $ loc)
  
  
  compileTemplateTH :: String -> String -> Q Exp
  compileTemplateTH filename unprocessed =
    either
      (fail . ("Parse error in mustache template: " ++) . show)
      THS.lift
      (compileTemplate filename (T.pack unprocessed))
  
  
  addDependentRelativeFile :: FilePath -> Q ()
  addDependentRelativeFile = THS.qAddDependentFile <=< THS.runIO . makeAbsolute
  
  
  -- ERRORS
  fileNotFound :: FilePath -> ParseError
  fileNotFound fp = newErrorMessage
    (Message $ printf "Template file '%s' not found" fp)
    (initialPos fp)
  