{-# LANGUAGE OverloadedStrings #-}

module Text.Liquid.Parser where

import           Prelude                    hiding (and, null, or, takeWhile)

import           Control.Applicative
import           Control.Lens               (Prism', prism')
import           Data.Attoparsec.Combinator (lookAhead)
import           Data.Attoparsec.Text
import           Data.Char                  (isAlpha)
import           Data.List.NonEmpty         (NonEmpty (..), nonEmpty)
import           Data.Scientific            (toBoundedInteger)
import           Data.Semigroup             hiding (option)
import           Data.Text                  (Text)
import qualified Data.Text                  as T
import           Text.Liquid.Helpers
import           Text.Liquid.Tokens
import           Text.Liquid.Types


-- | Match middle parser, around explicit start and end parsers
between
  :: Parser b -- ^ open tag parser
  -> Parser b -- ^ close tag parser
  -> Parser a -- ^ match middle parser
  -> Parser a
between open close p = do
  _ <- open
  x <- p
  (close *> return x) <|> fail "Tag or output statement incomplete"

-- | Match parser between whitespace
stripped
  :: Parser a
  -> Parser a
stripped =
  between skipSpace skipSpace

-- | Match given parser for a tag
tag
  :: Parser a
  -> Parser a
tag p =
  between tagStart tagEnd (stripped p)

-- | Match given parser for output block
outputTag
  :: Parser a
  -> Parser a
outputTag p =
  between outputStart outputEnd (stripped p)

-- | Match given tag name (e.g. for, case) with following parser
tagWith
  :: Parser a -- ^ initial tag type, e.g. for
  -> Parser b -- ^ follow on parser, e.g. variable
  -> Parser b
tagWith tg p =
  tag $ tg *> skipSpace >> p

-- | Convert match into text
mapT
  :: Parser [Char]
  -> Parser Text
mapT =
  fmap T.pack

-- | Match variables (without indices, including underscore or hash)
var :: Parser Text
var = mapT $ many1 $ letter <|> satisfy (inClass "_-")

-- | Parse a positive integer within square brackets, e.g. "[123]", NOT "[123.1]"
parseBoxedInt :: Parser Int
parseBoxedInt = do
    sc <- between oBr cBr scientific
    case toBoundedInteger sc of
      Just i  -> if i >= 0 then return i else err
      Nothing -> err
  where err = fail "invalid variable (array) index, expecting a positive integer"

-- | Parse a variable section with an optional indexing
--   An array index MUST be preceded by an object index
--   ...hence Maybe do comprehension
varIndexSection :: Parser (NonEmpty VarIndex)
varIndexSection = do
  vs <- sepBy var dot
  i  <- many parseBoxedInt
  brokenChar <- oBr <|> return '~'
  let ixs = do obs <- (nonEmpty (ObjectIndex <$> vs))
               Just obs <> (nonEmpty (ArrayIndex <$> i))
  if brokenChar == '[' then (fail "invalid array index - ill-typed") else case ixs of
    Just nel -> return nel
    Nothing  -> fail "invalid var index section"

-- | Parse a variable
variable :: Parser Expr
variable = do
  sections <- sepBy1 varIndexSection dot
  return . Variable $ foldl1 (<>) sections

-- | e.g. raw tag, comment tag
rawBodyTag :: Parser Text -- ^ start tag matcher
           -> Parser Text -- ^ end tag matcher
           -> Parser Text
rawBodyTag s e =
  s >> skipSpace *> (mapT $ manyTill anyChar (skipSpace >> e))

-- | Match interior of raw tag
rawTag :: Parser Expr
rawTag = RawText <$> rawBodyTag (tag rawStart) (tag rawEnd)

-- | Match interior of comment tag
commentTag :: Parser Expr
commentTag = rawBodyTag (tag commentStart) (tag commentEnd) *> pure Noop

-- | Match any raw text upto a tag/output start or the end of the input
textPart :: Parser Expr
textPart = RawText <$> (mapT $ manyTill1 (satisfy $ notInClass "{%") terminator)
  where terminator = lookAhead $ tagStart <|> outputStart <|> (endOfInput *> pure T.empty)

-- | Force the first character to be valid, otherwise fail miserably
manyTill1 :: Alternative f => f a -> f b -> f [a]
manyTill1 p e = (:) <$> p <*> s
    where s = (e *> pure []) <|> ((:) <$> p <*> s)

-- | Match an Ord comparison operator
ordOperator :: Parser (Expr -> Expr -> Expr)
ordOperator =
  stripped $ eq *> pure Equal              <|>
             neq *> pure NotEqual          <|>
             gtEq *> pure GtEqual          <|>
             ltEq *> pure LtEqual          <|>
             (mapT . some $ gt) *> pure Gt <|>
             (mapT . some $ lt) *> pure Lt <|>
             contains *> pure Contains

-- | Match an or, and or contains predicate
ordCombinator :: Parser (Expr -> Expr -> Expr)
ordCombinator =
  stripped $ or  *> pure Or  <|>
             and *> pure And

-- | Match a quoted string
quoteString :: Parser Expr
quoteString = do
  skipSpace
  beginTick <- satisfy (inClass "\'\"")
  qs        <- mapT $ manyTill anyChar (char beginTick)
  return $ QuoteString qs

-- | Match a binary predicate, e.g. a.b >= b.name
binaryPredicate :: Parser Expr
binaryPredicate = do
  lhs <- quoteString                 <|>
         (Num <$> scientific)        <|>
         Null <$ (stripped null)     <|>
         Nil <$ (stripped nil)       <|>
         Falseth <$ (stripped false) <|>
         Trueth  <$ (stripped true)  <|>
         variable
  op  <- ordOperator
  rhs <- quoteString                 <|>
         (Num <$> scientific)        <|>
         Null <$ (stripped null)     <|>
         Nil <$ (stripped nil)       <|>
         Falseth <$ (stripped false) <|>
         Trueth  <$ (stripped true)  <|>
         variable
  return $ op lhs rhs

-- | Parse and evaluate truthiness
truthy :: Parser Expr
truthy =
  Null    <$ (stripped null)              <|>
  Nil     <$ (stripped nil)               <|>
  Falseth <$ (stripped false)             <|>
  Trueth  <$ (stripped true)              <|>
  (Truthy . Num <$> stripped scientific)  <|>
  (Truthy       <$> stripped quoteString) <|>
  (Truthy       <$> stripped variable)

-- | Match a binary predicate, e.g. a.b >= b.name or 'barry'
predicate :: Parser Expr
predicate = do
  bpl <- stripped binaryPredicate
  oc  <- ordCombinator
  bpr <- stripped binaryPredicate
  return $ oc bpl bpr

-- | Match any predicate clause
predicateClause :: Parser Expr
predicateClause =
  predicate <|> binaryPredicate <|> truthy

-- | Match an if clause
ifClause :: Parser Expr
ifClause = IfClause <$> tagWith ifStart predicateClause

-- | Match an ifkey clause
ifKeyClause :: Parser Expr
ifKeyClause = IfKeyClause <$> tagWith ifKeyStart variableOnly
  where variableOnly = do res <- eitherP precheck variable
                          case res of
                            Left _   -> fail "Only variables as ifkey args allowed"
                            Right ok -> return ok
        precheck     = Null    <$ (stripped null)             <|>
                       Nil     <$ (stripped nil)              <|>
                       Falseth <$ (stripped false)            <|>
                       Trueth  <$ (stripped true)             <|>
                       (Truthy . Num <$> stripped scientific) <|>
                       (Truthy       <$> stripped quoteString)

-- | Match an elsif clause
elsifClause :: Parser Expr
elsifClause = ElsIfClause <$> tagWith elsIf predicateClause

-- | Match an else clause
elseClause :: Parser Expr
elseClause = (tag els) *> pure Else

-- | Match the end of an if clause
endIfClause :: Parser Expr
endIfClause = (tag endIf) *> pure Noop

-- | Match a variable condition for a case clause
caseClause :: Parser Expr
caseClause = tagWith caseStart variable

-- | Match a when clause, part of a case pattern match block
whenClause :: Parser Expr
whenClause = tagWith when (quoteString <|> (Num <$> scientific))

-- | Match the end of a case pattern match block
endCaseClause :: Parser Expr
endCaseClause = (tag caseEnd) *> pure Noop

-- | Match a filter fn name
filterName :: Parser Text
filterName = mapT $ skipSpace *> manyTill1 letter terminator
  where terminator = colon              <|>
                    (skipSpace *> pipe) <|>
                    (lookAhead $ satisfy (not . isAlpha))

-- | Match the list of arguments for the filter fn
filterArgs :: Parser [Expr]
filterArgs = skipSpace *> sepBy numOrString comma
  where numOrString = skipSpace *> (Num <$> scientific) <|> quoteString

-- | Match a filter cell, fn and args
filterCell :: Parser Expr
filterCell = do
  fnName <- filterName
  args   <- filterArgs
  typeCheckFilter fnName args

-- | Type check the function args and check arity
typeCheckFilter :: Text
                -> [Expr]
                -> Parser Expr
typeCheckFilter "toUpper"             []                                   =
  return $ FilterCell "toUpper" []
typeCheckFilter "toUpper"             _                                    =
  fail "toUpper filter takes no arguments"
typeCheckFilter "toLower"             []                                   =
  return $ FilterCell "toLower" []
typeCheckFilter "toLower"             _                                    =
  fail "toLower filter takes no arguments"
typeCheckFilter "toTitle"             []                                   =
  return $ FilterCell "toTitle" []
typeCheckFilter "toTitle"             _                                    =
  fail "toTitle filter takes no arguments"
typeCheckFilter "replace"             a@[QuoteString _, QuoteString _]     =
  return $ FilterCell "replace"       a
typeCheckFilter "replace"             _                                    =
  fail "replace filter requires find, replace strings as args"
typeCheckFilter "first"               []                                   =
  return $ FilterCell "first" []
typeCheckFilter "first"               _                                    =
  fail "first filter takes no arguments"
typeCheckFilter "firstOrDefault"      a@(_:_)                              =
  return $ FilterCell "firstOrDefault" a
typeCheckFilter "firstOrDefault" _                                         =
  fail "firstOrDefault requires a single default parameter"
typeCheckFilter "last"                []                                   =
  return $ FilterCell "last" []
typeCheckFilter "last"           _                                         =
  fail "last filter takes no arguments"
typeCheckFilter "lastOrDefault"       a@(_:_)                              =
  return $ FilterCell "lastOrDefault" a
typeCheckFilter "lastOrDefault"       _                                    =
  fail "lastOrDefault requires a single default parameter"
typeCheckFilter "countElements"       []                                   =
  return $ FilterCell "countElements" []
typeCheckFilter "countElements"       _                                    =
  fail "countElements takes no arguments"
typeCheckFilter "renderWithSeparator" a@[QuoteString _]                    =
  return $ FilterCell "renderWithSeparator" a
typeCheckFilter "renderWithSeparator" _                                    =
  fail "renderWithSeparator requires a separator argument with which to intersperse the target array"
typeCheckFilter "toSentenceWithSeparator" a@[QuoteString _, QuoteString _] =
  return $ FilterCell "toSentenceWithSeparator" a
typeCheckFilter "toSentenceWithSeparator" _                                =
  fail "toSentenceWithSeparator requires a separator argument and last element separator"
typeCheckFilter l                     _                                    =
  fail $ (show l) ++ ": function isn't supported"

-- | Match multiple filter fns and args
filterCells :: Parser [Expr]
filterCells = many ((filterCell <* (skipSpace *> pipe)) <|> filterCell)

-- | Match a lhs and a block of filters with their args
filterBlock :: Parser Expr
filterBlock = do
  lhs   <- (quoteString <|> (skipSpace *> variable)) <* (skipSpace >> pipe)
  cells <- filterCells
  return $ Filter lhs cells

-- | Output block, a variable, indexed variable, number or filter block
output :: Parser Expr
output = Output <$> outputTag (filterBlock <|> quoteString <|> variable)

-- | If statement, optional elsif or else
ifLogic :: Parser Expr
ifLogic = do
  start  <- ifClause <|> ifKeyClause <|> elsifClause <|> elseClause
  iftrue <- TrueStatements <$>
            manyTill (output <|> textPart)
                     (lookAhead elsifClause <|>
                      lookAhead elseClause  <|>
                      lookAhead endIfClause)
  let sofar = IfLogic start iftrue
  (endIfClause *> pure sofar) <|> (IfLogic sofar <$> ifLogic)

-- | Case pattern match block
caseLogic :: Parser Expr
caseLogic = do
    start    <- caseClause
    patterns <- many1 whenBlock
    _        <- endCaseClause
    return $ CaseLogic start patterns
  where whenBlock = do
          pattern <- whenClause <|> elseClause
          iftrue  <- TrueStatements <$>
                     manyTill (output <|> textPart)
                              (lookAhead whenClause <|>
                               lookAhead elseClause <|>
                               lookAhead endCaseClause)
          return (pattern, iftrue)

-- | Parse any block type
block :: Parser Expr
block = choice [ ifLogic
               , caseLogic
               , rawTag
               , commentTag
               , output
               , textPart
               ] <?> "Block Parsing"

-- | Parse an entire template into chunks
templateParser :: Parser [Expr]
templateParser = manyTill1 (block <?> "Syntax Error") endOfInput

-- | Run the templateParser on input text, force partial results to terminate with Failure
parseTemplate :: Text
              -> IResult Text [Expr]
parseTemplate t =
  feed (parse templateParser t) T.empty

templateP :: Prism' Text [Expr]
templateP = prism' back forw
  where forw = maybeResult . parseTemplate
        back = mconcat . fmap renderExpr