-- |
-- Module      :  Text.Mustache.Compile
-- Copyright   :  © 2016–present Stack Builders
-- License     :  BSD 3 clause
--
-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
-- Stability   :  experimental
-- Portability :  portable
--
-- Mustache 'Template' creation from file or a 'Text' value. You don't
-- usually need to import the module, because "Text.Mustache" re-exports
-- everything you may need, import that module instead.
module Text.Mustache.Compile
  ( compileMustacheDir,
    compileMustacheDir',
    getMustacheFilesInDir,
    getMustacheFilesInDir',
    isMustacheFile,
    compileMustacheFile,
    compileMustacheText,
  )
where

import Control.Exception
import Control.Monad.Except
import qualified Data.Map as M
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Void
import System.Directory
import qualified System.FilePath as F
import Text.Megaparsec
import Text.Mustache.Parser
import Text.Mustache.Type

-- | Compile all templates in specified directory and select one. Template
-- files should have the extension @mustache@, (e.g. @foo.mustache@) to be
-- recognized. This function /does not/ scan the directory recursively.
--
-- Note that each template\/partial will get an identifier which consists of
-- the name of corresponding template file with extension @.mustache@
-- dropped. This is important for e.g. selecting active template after
-- loading (the first argument).
--
-- The action can throw 'MustacheParserException' and the same exceptions as
-- 'getDirectoryContents', and 'T.readFile'.
--
-- > compileMustacheDir = complieMustacheDir' isMustacheFile
compileMustacheDir ::
  MonadIO m =>
  -- | Which template to select after compiling
  PName ->
  -- | Directory with templates
  FilePath ->
  -- | The resulting template
  m Template
compileMustacheDir = compileMustacheDir' isMustacheFile

-- | The same as 'compileMustacheDir', but allows using a custom predicate
-- for template selection.
--
-- @since 1.2.0
compileMustacheDir' ::
  MonadIO m =>
  -- | Template selection predicate
  (FilePath -> Bool) ->
  -- | Which template to select after compiling
  PName ->
  -- | Directory with templates
  FilePath ->
  -- | The resulting template
  m Template
compileMustacheDir' predicate pname path =
  getMustacheFilesInDir' predicate path
    >>= fmap selectKey . foldM f (Template undefined M.empty)
  where
    selectKey t = t {templateActual = pname}
    f (Template _ old) fp = do
      Template _ new <- compileMustacheFile fp
      return (Template undefined (M.union new old))

-- | Return a list of templates found in given directory. The returned paths
-- are absolute.
--
-- @since 0.2.2
getMustacheFilesInDir ::
  MonadIO m =>
  -- | Directory with templates
  FilePath ->
  m [FilePath]
getMustacheFilesInDir = getMustacheFilesInDir' isMustacheFile

-- | Return a list of templates found via a predicate in given directory.
-- The returned paths are absolute.
--
-- @since 1.2.0
getMustacheFilesInDir' ::
  MonadIO m =>
  -- | Mustache file selection predicate
  (FilePath -> Bool) ->
  -- | Directory with templates
  FilePath ->
  m [FilePath]
getMustacheFilesInDir' predicate path =
  liftIO $
    getDirectoryContents path
      >>= filterM f . fmap (F.combine path)
      >>= mapM makeAbsolute
  where
    f p = (&& predicate p) <$> doesFileExist p

-- | The default Mustache file predicate.
--
-- @since 1.2.0
isMustacheFile :: FilePath -> Bool
isMustacheFile path = F.takeExtension path == ".mustache"

-- | Compile a single Mustache template and select it.
--
-- The action can throw 'MustacheParserException' and the same exceptions as
-- 'T.readFile'.
compileMustacheFile ::
  MonadIO m =>
  -- | Location of the file
  FilePath ->
  m Template
compileMustacheFile path = liftIO $ do
  input <- T.readFile path
  withException (compile input)
  where
    pname = pathToPName path
    compile = fmap (Template pname . M.singleton pname) . parseMustache path

-- | Compile Mustache template from a lazy 'Text' value. The cache will
-- contain only this template named according to given 'PName'.
compileMustacheText ::
  -- | How to name the template?
  PName ->
  -- | The template to compile
  Text ->
  -- | The result
  Either (ParseErrorBundle Text Void) Template
compileMustacheText pname txt =
  Template pname . M.singleton pname <$> parseMustache "" txt

----------------------------------------------------------------------------
-- Helpers

-- | Build a 'PName' from given 'FilePath'.
pathToPName :: FilePath -> PName
pathToPName = PName . T.pack . F.takeBaseName

-- | Throw 'MustacheException' if argument is 'Left' or return the result
-- inside 'Right'.
withException ::
  -- | Value to process
  Either (ParseErrorBundle Text Void) Template ->
  -- | The result
  IO Template
withException = either (throwIO . MustacheParserException) return