module Text.Strapped.Parser
  ( parseTemplate
  ) where

import Control.Applicative ((<*>))
import Control.Monad
import Data.Monoid
import qualified Data.Text.Lazy as T
import Blaze.ByteString.Builder as B
import Blaze.ByteString.Builder.Char.Utf8 as B
import Text.Parsec
import Text.Parsec.String
import qualified Text.Parsec.Token as P
import Text.Parsec.Language (emptyDef)
import Text.Strapped.Types


tagStart :: GenParser Char st String
tagStart = string "{$"

tagEnd :: GenParser Char st String
tagEnd = string "$}"

wordString :: GenParser Char st String
wordString = many1 $ oneOf "_" <|> alphaNum

pathString :: GenParser Char st String
pathString = many1 $ oneOf "_./" <|> alphaNum

peekChar :: Char -> GenParser Char st ()
peekChar = void . try . lookAhead . char

peekTag = void . try . lookAhead . tag

tryTag :: GenParser Char st a -> GenParser Char st ()
tryTag = void . try . tag

tag :: GenParser Char st a -> GenParser Char st a
tag p = between (tagStart >> spaces) (spaces >> tagEnd) p <?> "Tag"

parseFloat :: GenParser Char st Double
parseFloat = do sign <- option 1 (do s <- oneOf "+-"
                                     return $ if s == '-' then-1.0 else 1.0)
                x    <- P.float $ P.makeTokenParser emptyDef
                return $ sign * x

parseInt :: GenParser Char st Integer
parseInt = do sign <- option 1 (do s <- oneOf "+-"
                                   return $ if s == '-' then-1 else 1)
              x    <- P.integer $ P.makeTokenParser emptyDef
              return $ sign * x

parseContent :: GenParser Char st a -> GenParser Char st [ParsedPiece]
parseContent end = do
  decls <- many (try $ spaces >> parseDecl)
  spaces
  extends <- optionMaybe (try $ spaces >> parseInherits)
  case (extends) of
    Just (e, epos) -> do
      includes <- manyTill parseIsIgnoreSpace end
      return $ (decls) ++ [ParsedPiece (Inherits e includes) epos]
    _     -> do
      ps <- manyTill parsePiece end
      return $ decls ++ ps
  where parseIsIgnoreSpace = do {spaces; b <- parseIsBlock; spaces; return b}

parseBlock :: GenParser Char st ParsedPiece
parseBlock = do
  pos <- getPosition
  blockName <- tag (string "block" >> spaces >> wordString) <?> "Block tag"
  blockContent <- parseContent (tryTag $ string "endblock") 
  return $ ParsedPiece (BlockPiece blockName blockContent) pos

parseRaw :: GenParser Char st ParsedPiece
parseRaw = do
  pos <- getPosition
  tag (string "raw") <?> "Raw tag"
  c <- anyChar
  s <- manyTill anyChar (tryTag (string "endraw"))
  return $ ParsedPiece (StaticPiece (B.fromString $ c:s)) pos

parseComment :: GenParser Char st ParsedPiece
parseComment = do
  pos <- getPosition
  tag (string "comment") <?> "Comment tag"
  c <- anyChar
  s <- manyTill anyChar (tryTag (string "endcomment"))
  return $ ParsedPiece (StaticPiece mempty) pos

parseIf :: GenParser Char st ParsedPiece
parseIf = do
  pos <- getPosition
  exp <- (tagStart >> spaces >> string "if" >> spaces >> parseExpression (try $ spaces >> tagEnd)) <?> "If tag"
  positive <- parseContent ((peekTag $ string "endif") <|> (tryTag $ string "else"))
  negative <- parseContent (tryTag $ string "endif")
  return $ ParsedPiece (IfPiece exp positive negative) pos

parseFor :: GenParser Char st ParsedPiece
parseFor = do
  pos <- getPosition
  (newVarName, exp) <- (tagStart >> spaces >> string "for" >> argParser) <?> "For tag"
  blockContent <- parseContent (tryTag $ string "endfor") 
  return $ ParsedPiece (ForPiece newVarName exp blockContent) pos
  where argParser = do 
        spaces
        v <- wordString
        spaces >> (string "in") >> spaces
        func <- parseExpression (try $ spaces >> tagEnd)
        return (v, func)

parseDecl :: GenParser Char st ParsedPiece
parseDecl = do {spaces; decl <- parserDecl; spaces; return decl} <?> "Let tag"
  where parserDecl = do
          pos <- getPosition
          tagStart >> spaces
          string "let" >> spaces
          varName <- wordString
          spaces >> string "=" >> spaces
          func <- parseExpression (try $ spaces >> tagEnd)
          return $ ParsedPiece (Decl varName func) pos
          
parseIsBlock = do
      blockName <- tag (string "isblock" >> spaces >> wordString) <?> "Isblock tag"
      blockContent <- parseContent (tryTag $ string "endisblock") 
      return (blockName, blockContent)

parseInclude :: GenParser Char st ParsedPiece
parseInclude = do
  pos <- getPosition
  tag (parserInclude pos) <?> "Include tag"
  where parserInclude pos = do
                string "include" >> spaces
                includeName <- pathString
                return $ ParsedPiece (Include includeName) pos

parseInherits = do {pos <- getPosition; mtag <- tag (string "inherits" >> spaces >> pathString); return (mtag, pos)} <?> "Inherits tag"

parseFunc ::  GenParser Char st ParsedPiece
parseFunc = parserFunc <?> "Call tag"
  where parserFunc = do
          pos <- getPosition
          string "${" >> spaces
          exp <- parseExpression (try $ spaces >> string "}")
          return $ ParsedPiece (FuncPiece exp) pos

parseExpression ::  GenParser Char st a -> GenParser Char st ParsedExpression
parseExpression end = manyPart <?> "Expression"
  where parseGroup = try parens <|> parseAtomic
        parseAtomic  = do
            pos <- getPosition
            exp <- try parseList <|> 
                   try (parseString '\"') <|> 
                   try (parseString '\'') <|> 
                   try (parseFloat >>= (return . LiteralExpression . LitDouble)) <|> 
                   try (parseInt >>= (return . LiteralExpression . LitInteger)) <|>
                   try (parseBool >>= (return . LiteralExpression . LitBool)) <|>
                   literal
            return $ ParsedExpression exp pos
        parens = (string "(" >> spaces) >> parseExpression (try $ spaces >> string ")")
        parseList = between (string "[" >> spaces) 
                            (spaces >> string "]") 
                            (sepBy (spaces >> parseGroup) (string ",")) 
                    >>= (return . ListExpression)
        manyPart = do
          pos <- getPosition
          pieces <- manyTill (spaces >> parseGroup) end
          return $ ParsedExpression (Multipart pieces) pos
        parseString esc = parseStringContents esc >>= (return . LiteralExpression . LitText . T.pack)
        parseBool = (try $ string "True" >> return True) <|> (try $ string "False" >> return False)
        literal = wordString >>= (return . LookupExpression)

parseStringContents ::  Char -> GenParser Char st String
parseStringContents esc = between (char esc) (char esc) (many chars)
    where chars = (try escaped) <|> noneOf [esc]
          escaped = char '\\' >> choice (zipWith escapedChar codes replacements)
          escapedChar code replacement = char code >> return replacement
          codes        = ['b',  'n',  'f',  'r',  't',  '\\', '\"', '\'', '/']
          replacements = ['\b', '\n', '\f', '\r', '\t', '\\', '\"', '\'', '/']

parseStatic :: GenParser Char st ParsedPiece
parseStatic = do
  pos <- getPosition
  c <- anyChar
  s <- manyTill anyChar (peekChar '{' <|> peekChar '$' <|> eof)
  return $ ParsedPiece (StaticPiece (B.fromString $ c:s)) pos

parseNonStatic :: GenParser Char st ParsedPiece
parseNonStatic =  try parseComment
              <|> try parseRaw
              <|> try parseBlock
              <|> try parseIf
              <|> try parseFor
              <|> try parseInclude
              <|> parseFunc
              
parsePiece :: GenParser Char st ParsedPiece
parsePiece = (try parseNonStatic <|> parseStatic)

parsePieces :: GenParser Char st [ParsedPiece]
parsePieces = parseContent eof

parseToTemplate :: GenParser Char st Template
parseToTemplate = (parseContent eof) >>= (return . Template)

-- | Take a template body and a template name and return either an error or a
--   renderable template.

parseTemplate :: String -> String -> Either ParseError Template
parseTemplate s tmplN = parse parseToTemplate tmplN s