{-# LANGUAGE FlexibleContexts #-}
module Language.JS.Parser where

import Control.Applicative ((<|>))
import Control.Monad (liftM2)
import qualified Text.Parsec as P
import Language.JS.Types
import Language.JS.Common
import Language.JS.Operators

-- | Identifier name.
identifierName =
  liftM2 (++) (P.many (P.oneOf "_$")) (P.many1 P.alphaNum)

-- | Parse identifier (no reserved words).
identifier =
  P.try (LI <$> (do i <- identifierName
                    case i `elem` reservedWords of
                      True -> P.unexpected "reserved word"
                      _    -> return i)) P.<?> "[identifier]"

-- | Parse numeric literal.
-- Here we don't distinguish between kinds.
numericLiteral =
  LN <$> (binN <|> octN <|> hexN <|> decN)
  P.<?> "[number-literal]"
  where prefix p = do
          i <- P.char '0'
          x <- P.oneOf p
          return [i, x]
        combine = liftM2 (++)
        hexN = combine (P.try (prefix "xX")) (P.many1 (P.oneOf "0123456789abcdefABCDEF"))
        octN = combine (P.try (prefix "oO")) (P.many1 (P.oneOf "01234567"))
        binN = combine (P.try (prefix "bB")) (P.many1 (P.oneOf "01"))
        decN = do
          lead <- P.many1 P.digit
          fraction <- liftM2 (:) (P.char '.') (P.many P.digit) <|> return ""
          expo <- expoN
          return (lead ++ fraction ++ expo)
        expoN = liftM2 (:) (P.oneOf "eE") (P.many P.digit) <|> return ""

-- | Parse boolean literal.
booleanLiteral =
  P.try (boolA "true" <|> boolA "false")
  P.<?> "[boolean]"
  where boolA = fmap (LB . toHask) . keywordB
        toHask s | s == "true" = True
                 | otherwise = False

-- | this identifier
thisIdent =
  const LThis <$> keywordB "this" P.<?> "[this]"

-- | null identifier
nullIdent =
  const LNull <$> keywordB "null" P.<?> "[null]"

-- | Parse string literal.
stringLiteral =
  buildExpression LS '\"' withoutNewLineAllowed
  <|> buildExpression LS '\'' withoutNewLineAllowed
  <|> LTS <$> (P.char '`' *> templateString "" [])
  P.<?> "[string-literal]"
  where
    withoutNewLineAllowed e = P.many (P.satisfy (\c -> c /= '\n' && c /= e))
    buildExpression ctor wc p = ctor <$> P.try (P.between (P.char wc) (P.char wc) (p wc))

-- | Parse template strings.
templateString str ls = (do
  t <- P.anyToken
  case t of
    '$' -> do
      e <- TExpression <$> braces (expressionNonEmpty True)
      let s' = if length str > 0 then [TString str, e] else [e]
      templateString "" (ls ++ s')
    '`' -> return (ls ++ (if length str > 0 then [TString str] else []))
    _   -> templateString (str ++ [t]) ls) <|> return ls

-- | Parse regular expression literal.
regexLiteral =
  let re = (P.string "/" >> return "") <|>
           (do es <- P.char '\\' -- escaped char
               t <- P.anyToken
               n <- re
               return (es:t:n)) <|>
           (liftM2 (:) P.anyToken re)
  in RegExp <$> ((P.char '/') *> re) <*> P.many (P.oneOf "mgi")

-- | Parse elision (aka ',' without a value on array).
elision =
  const Elision <$> keywordB ","
  P.<?> "elision"

-- | Parse many items on a array declaration.
arrayItems ls =
  (lexeme (elision <|> item) >>= \x -> arrayItems (ls ++ [x])) <|> return ls
  where item = checkSpread Spread (expressionNonEmpty False) <* P.optional (P.char ',')

-- | Parse array literal.
arrayLiteral =
  P.try (LA <$> brackets (betweenSpaces (arrayItems [])))
  P.<?> "[array]"

-- | key and/or value property pair.
objectBinds = do
  sp <- P.optionMaybe (keywordB "...")
  case sp of
    Just _ -> OPI . Spread <$> literals
    Nothing -> (OPM <$> (asyncMethodDef <|> classGetSetMethodDef <|> propertyMethodDef)) <|> (do
      k <- identifier
      x <- P.try (P.lookAhead (P.oneOf ",:}"))
      case x of
        ':' -> P.try (OPKV k <$> (lexeme (P.char ':') *> lexeme (checkSpread Spread (expressionNonEmpty False)) P.<?> "[object-value-expression]"))
        _ -> return (OPI k))

-- | Parse object literal.
-- objectLiteral :: P.ParsecT s u m Expression
objectLiteral =
  LO <$> lexeme (braces (P.sepBy (betweenSpaces objectBinds) (P.char ',')))
  P.<?> "[object-literal]"

-- | Parse parenthesis expression.
parensExpression =
  LP <$> parens (betweenSpaces (expressionNonEmpty True))
  P.<?> "[parenthesis]"

-- | Check for spread operation before parse 'p'.
checkSpread ctor p =
  do i <- P.optionMaybe (keywordB "...")
     case i of
       Just _ -> ctor <$> p
       Nothing -> p

-- | Parse used by function declarations.
formalParameter =
  betweenSpaces bindExpression
  P.<?> "[formal-parameters]"

-- | Parse function declaration
functionDeclaration =
  keywordB "function" *>
    (Function <$> lexeme (P.optionMaybe identifier)
              <*> lexeme (parens (commaSep formalParameter))
              <*> lexeme (SBlock <$> braces (betweenSpaces (P.many (lexeme statements)))))
  P.<?> "[function]"

-- | Prase arrow function declaration.
arrowFunctionDeclaration =
  P.try (Arrow <$> (lexeme (parens manyParams <|> singleParam) <* keywordB "=>")
               <*> blockOrStatements)
  P.<?> "[arrow-function]"
  where singleParam = Left <$> bindVar
        manyParams = Right <$> commaSep formalParameter

-- | Parse any kind of funcion declaration (function or arrow function).
functionExpression =
  arrowFunctionDeclaration <|> functionDeclaration

-- | Parse property method of a class or object literal.
propertyMethodDef =
  P.try (PropertyMethod <$> lexeme identifier
                        <*> lexeme (parens (commaSep formalParameter))
                        <*> (SBlock <$> braces (whiteSpaces *> P.many (lexeme statements))))
  P.<?> "[class-method-definition]"

-- | Parse a static property of a class.
classStaticDef =
  lexeme (keywordB "static") *>
    (ClassStatic <$> (propertyMethodDef <|> classPropertyDef))

-- | Parse a getter or setter method.
classGetSetMethodDef =
  (keywordB "set" *> (ClassSetMethod <$> propertyMethodDef)) <|>
  (keywordB "get" *> (ClassGetMethod <$> propertyMethodDef))
  P.<?> "[class-get-set-definition]"

-- | Check for a async property method.
asyncMethodDef =
  keywordB "async" *> (Async <$> propertyMethodDef)
  P.<?> "[async-definition]"

-- | Parse a class property definition.
classPropertyDef =
  P.try (ClassProperty <$> (lexeme identifier <* P.char '=' <* whiteSpaces)
                       <*> lexeme (expressionNonEmpty False))
  P.<?> "[class-property]"

-- | Parse a class declaration.
classDeclaration =
  keywordB "class" *> (Class <$> (lexeme (P.optionMaybe identifier))
                             <*> P.optionMaybe (keywordB "extends" *> lexeme identifier)
                             <*> (SBlock <$> braces (whiteSpaces *> classBlock)))
  P.<?> "[class-expression]"
  where classBlock = P.many (lexeme (toStatement <$> classBlockDecls))
        classBlockDecls = (classPropertyDef
                            <|> asyncMethodDef
                            <|> classStaticDef
                            <|> classGetSetMethodDef
                            <|> propertyMethodDef)

-- | Dot member.
dotMember p =
  Dot p <$> (lexeme (P.char '.') *> identifier)
  P.<?> "[dot-expression]"

-- | Array like accessor.
accessor p =
  Acc p <$> brackets (betweenSpaces (expressionNonEmpty True))
  P.<?> "[array-expression]"

-- | Function call.
functionCall p =
  FCall p <$> lexeme (parens (commaSep (whiteSpaces *> expressionNonEmpty False)))
  P.<?> "[function-call]"

-- | new
newIdent =
  const Nothing <$> keywordB "new" P.<?> "[new]"

-- | Parse member expression.
memberExpression (Just p) =
  (do dt <- (functionCall p <|> dotMember p <|> accessor p) P.<?> "[member-expression]"
      memberExpression (Just dt)) <|> return p
memberExpression Nothing =
  (New <$> expressions) P.<?> "[new-expression]"

-- | Parse literals.
literals =
  thisIdent
  <|> nullIdent
  <|> booleanLiteral
  <|> stringLiteral
  <|> arrayLiteral
  <|> objectLiteral
  <|> regexLiteral
  <|> numericLiteral
  <|> identifier

-- | Parse primary expressions.
primaryExpression =
  literals
  <|> functionDeclaration
  <|> classDeclaration
  <|> parensExpression

-- | Check for maybe semi.
-- TODO: There are some rules for expression termination...need to check that.
maybeSemi =
  P.optional (P.char ';')

-- | Parse a empty expression.
emptyExpression =
  (const Empty) <$> (P.char ';')
  P.<?> "[empty-expressions]"

-- | Parse rules for left hand side expression.
leftHandSideExpression =
  (newIdent <|> (Just <$> lexeme primaryExpression)) >>= memberExpression
  P.<?> "left-hand-side-expression"

-- | Parse expressions.
expressions =
  emptyExpression <|> expressionNonEmpty True P.<?> "[expressions]"

-- | Parse single line comment.
comment =
  P.try (Comment <$> (P.string "//" *> P.many (P.satisfy (\c -> c /= '\n'))))

-- | Parse multiline comment.
multilineComment =
  P.try (MultilineComment <$> (P.between (P.string "/*") (P.string "*/") (P.many P.anyToken)))

-- | Parse comment like an expression.
commentExpression =
  comment <|> multilineComment

-- | Parse expressions excluding emptyExpression.
expressionNonEmpty notComma =
  commentExpression
  <|> functionExpression
  <|> classDeclaration
  <|> (operationExpression notComma (expressionNonEmpty notComma) leftHandSideExpression)
  <|> primaryExpression
  P.<?> "[non-empty-expressions]"

-- | Convert a expression into a statement.
toStatement :: Expression -> Statement
toStatement (Function (Just (LI a)) b c) = SF a b c
toStatement (Class (Just (LI a)) b c)    = SC a b c
toStatement a                            = SExp a

-- Statements

-- | Parse import namespace clauses.
importNamespaceClause =
  Namespace <$> ((keywordB "*" *> keywordB "as") *> identifier)

-- | Parse import bind clauses.
importBindClause =
  BindNames <$> braces (commaSep (betweenSpaces identifier))

-- | Parse default clauses.
importDefaultNameClause =
  DefaultName <$> lexeme identifier

-- | Parse import clauses excluding namespace clause.
importManyClauses =
  commaSep1 (whiteSpaces *> (importBindClause <|> importDefaultNameClause))

-- | Parse all import clauses.
importClauses =
  (Left <$> importNamespaceClause) <|>
  (Right <$> importManyClauses)

-- | Parse import file statement.
importFileStatement =
  SImportFile <$> lexeme stringLiteral

-- | Parse import statement.
importStatement =
  SImport <$> (lexeme importClauses <* keywordB "from") <*> lexeme stringLiteral

-- | Parse import statements.
importStatements =
  keywordB "import" *> (importStatement <|> importFileStatement)
  P.<?> "[import-statement]"

reexportStatement = P.try (SRExport <$> (lexeme (expressionNonEmpty False) <* keywordB "from") <*> lexeme stringLiteral)
exportDefaultStatement = keywordB "default" *> (SExportDefault <$> expressionNonEmpty False)
exportStatement = SExport <$> statements

-- | Parse export statements.
exportStatements = keywordB "export" *> (reexportStatement <|> exportDefaultStatement <|> exportStatement)
                   P.<?> "[export-statement]"

-- | Parse continue statement.
continueStatement =
  keywordB "continue" *> (SContinue <$> (P.optionMaybe identifier))
  P.<?> "[continue-statement]"

-- | Parse break statement.
breakStatement =
  keywordB "break" *> (SBreak <$> (P.optionMaybe identifier))
  P.<?> "[break-statement]"

-- | Parse block statement.
blockStatement allowedStmt =
  SBlock <$> P.try (braces (betweenSpaces (P.many allowedStmt)))
  P.<?> "[block-statement]"

blockOrStatements =
  SBlock <$> braces (whiteSpaces *> P.many (lexeme statements)) <|> statements

-- | Parse if statement.
ifStatement =
  keywordB "if" *> (SIf <$> (lexeme parensExpression)
                        <*> lexeme blockOrStatements
                        <*> P.optionMaybe (keywordB "else" *> blockOrStatements))
  P.<?> "[if-statement]"

-- | Parse catch part of try statement.
catchBlock =
  keywordB "catch" *> (SCatch <$> lexeme (P.optionMaybe parensExpression)
                              <*> blockStatement statements)
  P.<?> "[try/catch-statement]"

-- | Parse finally part of try statement.
finallyBlock =
  keywordB "finally" *> (SFinally <$> (blockStatement statements))
  P.<?> "[try/catch/finally-statement]"

-- | Parse try statement.
tryStatement =
  keywordB "try" *> (STry <$> lexeme (blockStatement statements)
                          <*> catchBlock
                          <*> P.optionMaybe finallyBlock)
  P.<?> "[try-statement]"

-- | Parse throw statement.
throwStatement =
  keywordB "throw" *> (SThrow <$> (expressionNonEmpty False))
  P.<?> "[throw-statement]"

-- | Parse return statement.
returnStatement =
  keywordB "return" *> (SReturn <$> expressions)
  P.<?> "[return-statement]"

bindVar =
  BindVar <$> lexeme identifier <*> P.optionMaybe (P.notFollowedBy (keywordB "=>") *> (lexeme (P.char '=') *> (expressionNonEmpty False)))
bindPatternDecl =
  BindPattern <$> (lexeme (objectLiteral <|> arrayLiteral)) <*> P.optionMaybe (lexeme (P.char '=') *> (expressionNonEmpty False))
bindSpread =
  BindRest <$> (keywordB "..." *> leftHandSideExpression)
bindExpression =
  (bindVar <|> bindPatternDecl <|> bindSpread) P.<?> "[var-binds]"

constVariableStatement =
  P.try (SVariable <$> (keywordB "const") <*> commaSep1 (betweenSpaces bindExpression))
notConstVariableStatement =
  P.try (SVariable <$> (keywordB "let" <|> keywordB "var") <*> commaSep1 (betweenSpaces bindExpression))

-- | Parse variable statement.
variableStatement =
  constVariableStatement <|> notConstVariableStatement P.<?> "[variable-statement]"

-- | Parse case clause switch statement.
caseClause =
  lexeme ((caseB <|> defaultCase) <* (P.char ':'))
  P.<?> "[switch/case-expression]"
  where defaultCase = const DefaultCase <$> (keywordB "default")
        caseB       = keywordB "case" *> (Case <$> literals)

-- | Parse case clause switch statement.
caseDeclaration =
  SCase <$> lexeme (P.many1 caseClause)
        <*> P.many (lexeme ((breakStatement <* maybeSemi) <|> statements))

-- | Parse switch statement.
switchStatement =
  keywordB "switch" *>
    (SSwitch <$> lexeme parensExpression
             <*> braces (betweenSpaces (P.many caseDeclaration)))
  P.<?> "[switch-statement]"

-- | Parse debugger statement.
debuggerStatement =
  const SDebugger <$> keywordB "debugger"
  P.<?> "[debugger-statement]"

-- | Parse breakable statement.
-- TODO: this parser can be improved to parse vaild javascript code
-- by passing to the break statement to subsequent statements.
breakableStatement =
  blockStatement ((breakStatement <* maybeSemi) <|> statements) <|> statements

-- | Parse while statement.
whileStatement =
  keywordB "while" *>
    (SWhile <$> lexeme (parens (P.many1 expressions))
            <*> breakableStatement)
  P.<?> "[while-statement]"

-- | parse do-while statement.
doWhileStatement =
  keywordB "do" *>
    (SDoWhile <$> lexeme breakableStatement
              <*> (keywordB "while" *> parens (P.many1 expressions)))
  P.<?> "[do/while-statement]"

forInVStyle =
  P.try (ForInV <$> lexeme (keywordB "let" <|> keywordB "const" <|> keywordB "var")
                <*> bindExpression
                <*> (keywordB "in" *> (expressionNonEmpty False)))
forOfVStyle =
  P.try (ForOfV <$> lexeme (keywordB "let" <|> keywordB "const" <|> keywordB "var")
                <*> bindExpression
                <*> (keywordB "of" *> expressionNonEmpty False ))
forInStyle =
  P.try (ForIn <$> bindExpression <*> (keywordB "in" *> expressionNonEmpty False))
forOfStyle =
  P.try (ForOf <$> bindExpression <*> (keywordB "of" *> expressionNonEmpty False))
forRegularStyle =
  ForRegular <$> P.try (P.optionMaybe bindExpression <* (P.char ';'))
             <*> P.try (P.optionMaybe (expressionNonEmpty True) <* (P.char ';'))
             <*> P.optionMaybe (expressionNonEmpty True)
forStyle =
  forInVStyle <|> forOfVStyle <|> forInStyle <|> forOfStyle <|> forRegularStyle P.<?> "[for-style]"

-- | Parse for statement.
forStatement =
  SFor <$> lexeme (keywordB "for" *> (parens forStyle)) <*> breakableStatement

-- | Parse iteration statements (for, white, do/while).
iterationStatement = forStatement <|> whileStatement <|> doWhileStatement

-- | Parse with statement.
withStatement =
  keywordB "with" *> (SWith <$> lexeme (parens (expressionNonEmpty True))
                            <*> (SBlock <$> braces (whiteSpaces *> P.many (lexeme statements)) <|> statements))
  P.<?> "[with-statement]"

-- | Parse labelled statement.
labelledStatement =
  SLabel <$> P.try (lexeme (identifier <* P.char ':')) <*> statements
  P.<?> "[labelled-statement]"

-- | Parse statements.
statements = ((blockStatement statements
               <|> ifStatement
               <|> iterationStatement
               <|> debuggerStatement
               <|> labelledStatement
               <|> continueStatement
               <|> tryStatement
               <|> throwStatement
               <|> returnStatement
               <|> switchStatement
               <|> withStatement
               <|> variableStatement
               <|> fmap toStatement expressions) <* maybeSemi) P.<?> "[statements]"

-- | Parse all statements allowed to be on top level.
-- This helps to not allow import and export expressions
-- in any other part of the code.
topLevelStatements = importStatements <|> exportStatements <|> statements

-- | parser
parseJs = P.many (betweenSpaces (topLevelStatements <* maybeSemi))

-- | Parse a script with a filename.
parse = P.parse parseJs

-- | Parse a script from a file. Just for convinience.
parseFromFile filename = P.parse parseJs filename <$> readFile filename