    {-# LANGUAGE TypeSynonymInstances #-}
    {-
    Copyright (C) 2009-2010 John MacFarlane <jgm@berkeley.edu>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
    -}

    {- |
       Module      : Text.Pandoc.Templates
       Copyright   : Copyright (C) 2009-2010 John MacFarlane
       License     : GNU GPL, version 2 or above

       Maintainer  : John MacFarlane <jgm@berkeley.edu>
       Stability   : alpha
       Portability : portable

    A simple templating system with variable substitution and conditionals.
    Example:

    > renderTemplate [("name","Sam"),("salary","50,000")] $
    >    "Hi, $name$.  $if(salary)$You make $$$salary$.$else$No salary data.$endif$"
    > "Hi, John.  You make $50,000."

    A slot for an interpolated variable is a variable name surrounded
    by dollar signs.  To include a literal @$@ in your template, use
    @$$@.  Variable names must begin with a letter and can contain letters,
    numbers, @_@, and @-@.

    The value of a variable will be indented to the same level as the
    variable.

    A conditional begins with @$if(variable_name)$@ and ends with @$endif$@.
    It may optionally contain an @$else$@ section.  The if section is
    used if @variable_name@ has a non-null value, otherwise the else section
    is used.

    Conditional keywords should not be indented, or unexpected spacing
    problems may occur.

    If a variable name is associated with multiple values in the association
    list passed to 'renderTemplate', you may use the @$for$@ keyword to
    iterate over them:

    > renderTemplate [("name","Sam"),("name","Joe")] $
    >   "$for(name)$\nHi, $name$.\n$endfor$"
    > "Hi, Sam.\nHi, Joe."

    You may optionally specify separators using @$sep$@:

    > renderTemplate [("name","Sam"),("name","Joe"),("name","Lynn")] $
    >   "Hi, $for(name)$$name$$sep$, $endfor$"
    > "Hi, Sam, Joe, Lynn."
    -}

    module Text.Pandoc.Templates ( renderTemplate
                                 , TemplateTarget
                                 , getDefaultTemplate ) where

    import Text.ParserCombinators.Parsec
    import Control.Monad (liftM, when, forM)
    import System.FilePath
    import Data.List (intercalate, intersperse)
    import Text.PrettyPrint (text, Doc)
    import Text.XHtml (primHtml, Html)
    import Data.ByteString.Lazy.UTF8 (ByteString, fromString)
    import Text.Pandoc.Shared (readDataFile)
    import qualified Control.Exception.Extensible as E (try, IOException)

    -- | Get default template for the specified writer.
    getDefaultTemplate :: (Maybe FilePath) -- ^ User data directory to search first
                       -> String           -- ^ Name of writer
                       -> IO (Either E.IOException String)
    getDefaultTemplate _ "native" = return $ Right ""
    getDefaultTemplate user "odt" = getDefaultTemplate user "opendocument"
    getDefaultTemplate user writer = do
      let format = takeWhile (/='+') writer  -- strip off "+lhs" if present
      let fname = "templates" </> format  <.> "template"
      E.try $ readDataFile user fname

    data TemplateState = TemplateState Int [(String,String)]

    adjustPosition :: String -> GenParser Char TemplateState String
    adjustPosition str = do
      let lastline = takeWhile (/= '\n') $ reverse str
      updateState $ \(TemplateState pos x) ->
        if str == lastline
           then TemplateState (pos + length lastline) x
           else TemplateState (length lastline) x
      return str

    class TemplateTarget a where
      toTarget :: String -> a

    instance TemplateTarget String where
      toTarget = id

    instance TemplateTarget ByteString where
      toTarget = fromString

    instance TemplateTarget Html where
      toTarget = primHtml

    instance TemplateTarget Doc where
      toTarget = text

    -- | Renders a template
    renderTemplate :: TemplateTarget a
                   => [(String,String)]  -- ^ Assoc. list of values for variables
                   -> String             -- ^ Template
                   -> a
    renderTemplate vals templ =
      case runParser (do x <- parseTemplate; eof; return x) (TemplateState 0 vals) "template" templ of
           Left e        -> error $ show e
           Right r       -> toTarget $ concat r

    reservedWords :: [String]
    reservedWords = ["else","endif","for","endfor","sep"]

    parseTemplate :: GenParser Char TemplateState [String]
    parseTemplate =
      many $ (plaintext <|> escapedDollar <|> conditional <|> for <|> variable)
               >>= adjustPosition

    plaintext :: GenParser Char TemplateState String
    plaintext = many1 $ noneOf "$"

    escapedDollar :: GenParser Char TemplateState String
    escapedDollar = try $ string "$$" >> return "$"

    skipEndline :: GenParser Char st ()
    skipEndline = try $ skipMany (oneOf " \t") >> newline >> return ()

    conditional :: GenParser Char TemplateState String
    conditional = try $ do
      TemplateState pos vars <- getState
      string "$if("
      id' <- ident
      string ")$"
      -- if newline after the "if", then a newline after "endif" will be swallowed
      multiline <- option False $ try $ skipEndline >> return True
      ifContents <- liftM concat parseTemplate
      -- reset state for else block
      setState $ TemplateState pos vars
      elseContents <- option "" $ do try (string "$else$")
                                     when multiline $ optional skipEndline
                                     liftM concat parseTemplate
      string "$endif$"
      when multiline $ optional skipEndline
      let conditionSatisfied = case lookup id' vars of
                                    Nothing -> False
                                    Just "" -> False
                                    Just _  -> True
      return $ if conditionSatisfied
                  then ifContents
                  else elseContents

    for :: GenParser Char TemplateState String
    for = try $ do
      TemplateState pos vars <- getState
      string "$for("
      id' <- ident
      string ")$"
      -- if newline after the "for", then a newline after "endfor" will be swallowed
      multiline <- option False $ try $ skipEndline >> return True
      let matches = filter (\(k,_) -> k == id') vars
      let indent = replicate pos ' '
      contents <- forM matches $ \m -> do
                          updateState $ \(TemplateState p v) -> TemplateState p (m:v)
                          raw <- liftM concat $ lookAhead parseTemplate
                          return $ intercalate ('\n':indent) $ lines $ raw ++ "\n"
      parseTemplate
      sep <- option "" $ do try (string "$sep$")
                            when multiline $ optional skipEndline
                            liftM concat parseTemplate
      string "$endfor$"
      when multiline $ optional skipEndline
      setState $ TemplateState pos vars
      return $ concat $ intersperse sep contents

    ident :: GenParser Char TemplateState String
    ident = do
      first <- letter
      rest <- many (alphaNum <|> oneOf "_-")
      let id' = first : rest
      if id' `elem` reservedWords
         then pzero
         else return id'

    variable :: GenParser Char TemplateState String
    variable = try $ do
      char '$'
      id' <- ident
      char '$'
      TemplateState pos vars <- getState
      let indent = replicate pos ' '
      return $ case lookup id' vars of
                 Just val  -> intercalate ('\n' : indent) $ lines val
                 Nothing   -> ""