{-# LANGUAGE TupleSections #-}
module Puppet.Parser.Internal
where

import           XPrelude.Extra                   hiding (many, option, some, try)

import           Control.Monad.Combinators.Expr
import qualified Data.Char                        as Char
import qualified Data.List                        as List
import qualified Data.List.NonEmpty               as NE
import qualified Data.Maybe.Strict                as S
import qualified Data.Scientific                  as Scientific
import qualified Data.Text                        as Text
import qualified Data.Vector                      as V
import           Text.Megaparsec
import           Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer       as Lexer
import qualified Text.Regex.PCRE.ByteString.Utils as Regex

import           Puppet.Language
import           Puppet.Parser.Types

-- | Space consumer
sc :: Parser ()
sc = Lexer.space space1 (Lexer.skipLineComment "#") (Lexer.skipBlockComment "/*" "*/")

-- | Lexeme consumes spaces after the input parser
lexeme :: Parser a -> Parser a
lexeme = Lexer.lexeme sc

-- | Consumes a text then consumes spaces
symbol :: Text -> Parser ()
symbol = void . Lexer.symbol sc

-- | Consumes a character then consumes spaces
symbolic :: Char -> Parser ()
symbolic = lexeme . void . single

braces :: Parser a -> Parser a
braces = between (symbolic '{') (symbolic '}')

parens :: Parser a -> Parser a
parens = between (symbolic '(') (symbolic ')')

brackets :: Parser a -> Parser a
brackets = between (symbolic '[') (symbolic ']')

comma :: Parser ()
comma = symbolic ','

sepComma :: Parser a -> Parser [a]
sepComma p = p `sepEndBy` comma

sepComma1 :: Parser a -> Parser [a]
sepComma1 p = p `sepEndBy1` comma

-- | Parses an 'Expression'.
expression :: Parser Expression
expression = do
  expr <- makeExprParser (lexeme terminal) expressionTable
  ms <- optional $ do
    symbolic '?'
    let case_expr = do
          c <-     SelectorDefault <$ symbol "default" -- default case
               <|> SelectorType <$> try datatype
               <|> fmap SelectorValue
                     (   UVariableReference <$> variableReference
                     <|> UBoolean <$> puppetBool
                     <|> UUndef <$ symbol "undef"
                     <|> literalValue
                     <|> UInterpolable <$> interpolableString
                     <|> URegexp <$> termRegexp
                     )
          symbol "=>"
          e <- expression
          pure (c :!: e)
    cases <- braces (sepComma1 case_expr)
    pure (ConditionalValue expr (V.fromList cases))
  case ms of
    Nothing -> pure expr
    Just cv -> pure cv

stringLiteral' :: Parser Text
stringLiteral' = between (char '\'') (symbolic '\'') interior
  where
    interior = Text.pack . concat <$> many (some (noneOf ['\'', '\\']) <|> (char '\\' *> fmap escape anySingle))
    escape '\'' = "'"
    escape x    = ['\\',x]

identifier :: Parser (Tokens Text)
identifier = takeWhile1P Nothing isIdentifierChar

-- | Only Ascii, hyphens (-) are not allowed.
isIdentifierChar :: Char -> Bool
isIdentifierChar x = Char.isAsciiLower x || Char.isAsciiUpper x || Char.isDigit x || (x == '_')

-- | Like 'indentifier' but hyphens (-) are allowed.
bareword :: Parser Text
bareword = Text.cons <$> satisfy Char.isAsciiLower <*> takeWhileP Nothing isBarewordChar
  where
    isBarewordChar :: Char -> Bool
    isBarewordChar x = isIdentifierChar x || (x == '-')

reserved :: Text -> Parser ()
reserved s =
  try $ do
    void (chunk s)
    notFollowedBy (satisfy isIdentifierChar)
    sc

qualif :: Parser Text -> Parser Text
qualif p = do
  header <- option "" (chunk "::")
  ( header <> ) . Text.intercalate "::" <$> p `sepBy1` chunk "::"

qualif1 :: Parser Text -> Parser Text
qualif1 p = do
  r <- qualif p
  unless ("::" `Text.isInfixOf` r) (fail "This parser is not qualified")
  pure r

-- | Consumes a var $foo and then spaces
variableReference :: Parser Text
variableReference = do
  v <- char '$' *> lexeme variableName
  when (Text.all Char.isDigit v) (fail "Can't assign fully numeric variables")
  pure v

variableName :: Parser Text
variableName = qualif identifier

-- yay with reserved words
typeName :: Parser Text
typeName = className

className :: Parser Text
className = lexeme $ qualif $ genericModuleName False

funcName :: Parser Text
funcName = lexeme $ qualif $ genericModuleName False

moduleName :: Parser Text
moduleName = lexeme $ genericModuleName False

parameterName :: Parser Text
parameterName = moduleName

resourceNameRef :: Parser Text
resourceNameRef = lexeme $ qualif (genericModuleName True)

genericModuleName :: Bool -> Parser Text
genericModuleName isReference = do
  let acceptable x = Char.isAsciiLower x || Char.isDigit x || (x == '_')
      firstletter = if isReference
                      then fmap Char.toLower (satisfy Char.isAsciiUpper)
                      else satisfy Char.isAsciiLower
  (Text.cons) <$> firstletter <*> takeWhileP Nothing acceptable

-- | Variable expression
varExpression :: Parser Expression
varExpression = Terminal . UVariableReference <$> variableReference

-- | String interpolation
interpolableString :: Parser (Vector Expression)
interpolableString = V.fromList <$> between (char '"') (symbolic '"')
 ( many (interpolableVariableReference <|> doubleQuotedStringContent <|> fmap (Terminal . UString . Text.singleton) (char '$')) )
  where
    doubleQuotedStringContent = Terminal . UString . Text.pack . concat <$>
      some ((char '\\' *> fmap escaper anySingle) <|> some (noneOf [ '"', '\\', '$' ]))
    escaper :: Char -> String
    escaper 'n'  = "\n"
    escaper 't'  = "\t"
    escaper 'r'  = "\r"
    escaper '"'  = "\""
    escaper '\\' = "\\"
    escaper '$'  = "$"
    escaper x    = ['\\',x]
    -- this is specialized because we can't be "tokenized" here
    varname = Text.concat <$> some (chunk "::" <|> identifier)
    varexpr = Terminal . UVariableReference <$> varname
    indexchain =  makeExprParser varexpr [[Postfix indexLookupChain]] -- e.g: os['release']['major']
    interpolableVariableReference = do
      void (char '$')
      let fenced =    try (indexchain <* char '}')
                  <|> try (varexpr <* char '}')
                  <|> (expression <* char '}')
      (symbolic '{' *> fenced) <|> try varexpr <|> pure (Terminal (UString (Text.singleton '$')))

integerOrDouble :: Parser (Either Integer Double)
integerOrDouble = Left <$> hex <|> (either Right Left . Scientific.floatingOrInteger <$> Lexer.scientific)
  where
    hex = chunk "0x" *> Lexer.hexadecimal

puppetArray :: Parser UnresolvedValue
puppetArray = fmap (UArray . V.fromList) (brackets (sepComma expression)) <?> "Array"

puppetHash :: Parser UnresolvedValue
puppetHash = fmap (UHash . V.fromList) (braces (sepComma hashPart)) <?> "Hash"
  where
    hashPart = (:!:) <$> (expression <* symbol "=>") <*> expression

puppetBool :: Parser Bool
puppetBool =
      (reserved "true" >> pure True)
  <|> (reserved "false" >> pure False)
  <?> "Boolean"

resourceReferenceRaw :: Parser (Text, [Expression])
resourceReferenceRaw = do
  let restype_parser = qualif (genericModuleName True)
      resnames_parser = brackets (expression `sepBy1` comma)
  (,) <$> restype_parser <*> resnames_parser <?> "Resource reference"

resourceReference :: Parser UnresolvedValue
resourceReference = do
  (restype, resnames) <- resourceReferenceRaw
  pure $ UResourceReference restype $ case resnames of
           [x] -> x
           _   -> Terminal $ UArray (V.fromList resnames)

-- | Functions that have named that are not valid ...
specialFunctions :: Parser Text
specialFunctions =
      chunk "Integer"
  <|> chunk "Numeric"

-- The first argument defines if non-parenthesized arguments are acceptable
genFunctionCall :: Bool -> Parser (Text, Vector Expression)
genFunctionCall nonparens = do
  fname <- (specialFunctions <|> funcName) <?> "Function name"
  let
      -- first check if the function arg is not a qualified name (ex.: include foo::bar)
      -- if it is not, then we expect an expression
      qualif_param = (Terminal . UString) <$> qualif1 moduleName <* notFollowedBy (single '(') -- <* lookAhead (anySingleBut '(')
      func_arg expr =  try qualif_param <|> expr <?> "Function argument"
      terminalF = terminalG FunctionWithoutParens
      expressionF = makeExprParser (lexeme terminalF) expressionTable <?> "Function expression"
      withparens = parens (func_arg expression `sepEndBy` comma)
      withoutparens = if nonparens
                      then func_arg expressionF `sepEndBy1` comma
                      else fail "Not an argument list allowed with function without parentheses"
  args  <- withparens <|> withoutparens
  pure (fname, V.fromList args)

literalValue :: Parser UnresolvedValue
literalValue = lexeme (fmap UString stringLiteral' <|> fmap UString bareword <|> fmap UNumber numericalvalue <?> "Literal Value")
  where
    signed :: Num n => Parser (n -> n)
    signed = (negate <$ char '-') <|> pure (\x -> x)
    numericalvalue = ((,) <$> signed <*> integerOrDouble) >>= \case
      (s, Left x)  -> pure (s (fromIntegral x))
      (s, Right y) -> pure (s (Scientific.fromFloatDigits y))

data TerminalMode
  = FunctionWithoutParens
  | StandardMode

-- this is a hack for functions :(
terminalG :: TerminalMode -> Parser Expression
terminalG mode =
      parens expression
  <|> fmap (Terminal . UInterpolable) interpolableString
  <|> (Terminal UUndef <$ reserved "undef")
  <|> fmap (Terminal . URegexp) termRegexp
  <|> varExpression
  <|> fmap Terminal puppetArray
  <|> fmap Terminal puppetHash
  <|> fmap (Terminal . UBoolean) puppetBool
  <|> case mode of
        FunctionWithoutParens -> remaining
        StandardMode          -> lambda <|> remaining
 where
   lambda = fmap Terminal (fmap UHOLambdaCall (try lambdaCall) <|> try funcCall)
   remaining = fmap (Terminal . UDataType) datatype
           <|> fmap Terminal resourceReference
           <|> fmap Terminal literalValue
   funcCall :: Parser UnresolvedValue
   funcCall = uncurry UFunctionCall <$> genFunctionCall False

regexp :: Parser Text
regexp = do
  void (single '/')
  Text.pack . concat <$> many ( do { void (char '\\') ; x <- anySingle; return ['\\', x] } <|> some (noneOf [ '/', '\\' ]) )
      <* symbolic '/'

compileRegexp :: Text -> Parser CompRegex
compileRegexp p = case Regex.compile' Regex.compBlank Regex.execBlank (encodeUtf8 p) of
  Right r -> pure $ CompRegex p r
  Left ms -> fail ("Can't parse regexp /" <> Text.unpack p <> "/ : " ++ show ms)

termRegexp :: Parser CompRegex
termRegexp = regexp >>= compileRegexp

terminal :: Parser Expression
terminal = terminalG StandardMode

expressionTable :: [[Operator Parser Expression]]
expressionTable = [ [ Postfix indexLookupChain ] -- http://stackoverflow.com/questions/10475337/parsec-expr-repeated-prefix-postfix-operator-not-supported
                  , [ Prefix ( symbolic '-'   *> pure Negate           ) ]
                  , [ Prefix ( symbolic '!'   *> pure Not              ) ]
                  , [ InfixL ( symbolic '.'   *> pure FunctionApplication ) ]
                  , [ InfixL ( reserved "in"  *> pure Contains         ) ]
                  , [ InfixL ( symbolic '/'   *> pure Division         )
                    , InfixL ( symbolic '*'   *> pure Multiplication   )
                    ]
                  , [ InfixL ( symbolic '+'   *> pure Addition     )
                    , InfixL ( symbolic '-'   *> pure Substraction )
                    ]
                  , [ InfixL ( symbol "<<"    *> pure LeftShift  )
                    , InfixL ( symbol ">>"    *> pure RightShift )
                    ]
                  , [ InfixL ( symbol "=="    *> pure Equal     )
                    , InfixL ( symbol "!="    *> pure Different )
                    ]
                  , [ InfixL ( symbol "=~"    *> pure RegexMatch    )
                    , InfixL ( symbol "!~"    *> pure NotRegexMatch )
                    ]
                  , [ InfixL ( symbol ">="    *> pure MoreEqualThan )
                    , InfixL ( symbol "<="    *> pure LessEqualThan )
                    , InfixL ( symbol ">"     *> pure MoreThan      )
                    , InfixL ( symbol "<"     *> pure LessThan      )
                    ]
                  , [ InfixL ( reserved "and" *> pure And )
                    , InfixL ( reserved "or"  *> pure Or  )
                    ]
                  ]

-- | Postfix of a chain of lookup indexes such as "['release']['major']"
indexLookupChain :: Parser (Expression -> Expression)
indexLookupChain = List.foldr1 (flip (.)) <$> some checkLookup
  where
    checkLookup = flip Lookup <$> brackets expression

stringExpression :: Parser Expression
stringExpression =
      (Terminal . UInterpolable) <$> interpolableString
  <|> (reserved "undef" $> Terminal UUndef)
  <|> (Terminal . UBoolean) <$> puppetBool
  <|> varExpression
  <|> Terminal <$> literalValue

-- | a = b = 0
chainedVariableReferences :: Parser [Text]
chainedVariableReferences = do
  h <- variableReference
  t <- many (try next)
  pure (h:t)
  where
    next = symbolic '=' *> variableReference <* lookAhead (single '=' *> space1)

varAssign :: Parser VarAssignDecl
varAssign = do
  p <- getSourcePos
  mt <- optional datatype
  vs <- chainedVariableReferences
  void $ symbolic '='
  expr <- expression
  pe <- getSourcePos
  pure (VarAssignDecl mt vs expr (p :!: pe))

nodeDecl :: Parser [NodeDecl]
nodeDecl = do
  p <- getSourcePos
  reserved "node"
  let toString (UString s) = s
      toString (UNumber n) = scientific2text n
      toString _           = panic "Can't happen at nodeDecl"
      nodename = (reserved "default" >> pure NodeDefault) <|> fmap (NodeName . toString) literalValue
  ns <- (fmap NodeMatch termRegexp <|> nodename) `sepBy1` comma
  inheritance <- option S.Nothing (fmap S.Just (reserved "inherits" *> nodename))
  st <- braces statementList
  pe <- getSourcePos
  pure [NodeDecl n st inheritance (p :!: pe) | n <- ns]

defineDecl :: Parser DefineDecl
defineDecl = do
  p <- getSourcePos
  reserved "define"
  name <- typeName
  -- TODO check native type
  params <- option V.empty puppetClassParameters
  st <- braces statementList
  pe <- getSourcePos
  pure (DefineDecl name params st (p :!: pe))

puppetClassParameters :: Parser Parameters
puppetClassParameters = V.fromList <$> parens (sepComma var)
  where
    toStrictMaybe (Just x) = S.Just x
    toStrictMaybe Nothing  = S.Nothing
    var :: Parser (Pair (Pair Text (S.Maybe UDataType)) (S.Maybe Expression))
    var = do
      tp <- toStrictMaybe <$> optional datatype
      n  <- variableReference
      df <- toStrictMaybe <$> optional (symbolic '=' *> expression)
      pure (n :!: tp :!: df)

puppetIfStyleCondition :: Parser (Pair Expression (Vector Statement))
puppetIfStyleCondition = (:!:) <$> expression <*> braces statementList

unlessCondition :: Parser ConditionalDecl
unlessCondition = do
  p <- getSourcePos
  reserved "unless"
  (cond :!: stmts) <- puppetIfStyleCondition
  pe <- getSourcePos
  pure (ConditionalDecl (V.singleton (Not cond :!: stmts)) (p :!: pe))

ifCondition :: Parser ConditionalDecl
ifCondition = do
  p <- getSourcePos
  reserved "if"
  maincond <- puppetIfStyleCondition
  others   <- many (reserved "elsif" *> puppetIfStyleCondition)
  elsecond <- option V.empty (reserved "else" *> braces statementList)
  let ec = if V.null elsecond
               then []
               else [Terminal (UBoolean True) :!: elsecond]
  pe <- getSourcePos
  pure (ConditionalDecl (V.fromList (maincond : others ++ ec)) (p :!: pe))

caseCondition :: Parser ConditionalDecl
caseCondition = do
  let puppetRegexpCase = Terminal . URegexp <$> termRegexp
      defaultCase = Terminal (UBoolean True) <$ reserved "default"
      matchesToExpression e (x, stmts) = f x :!: stmts
        where f = case x of
                    (Terminal (UBoolean _)) -> identity
                    (Terminal (URegexp _))  -> RegexMatch e
                    _                       -> Equal e
      cases = do
        matches <- (puppetRegexpCase <|> defaultCase <|> expression) `sepBy1` comma
        void $ symbolic ':'
        stmts <- braces statementList
        pure $ map (,stmts) matches
  p <- getSourcePos
  reserved "case"
  expr1 <- expression
  condlist <- concat <$> braces (some cases)
  pe <- getSourcePos
  pure (ConditionalDecl (V.fromList (map (matchesToExpression expr1) condlist)) (p :!: pe) )

data OperatorChain a
  = OperatorChain a LinkType (OperatorChain a)
  | EndOfChain a

instance Foldable OperatorChain where
  foldMap f (EndOfChain x)         = f x
  foldMap f (OperatorChain a _ nx) = f a <> foldMap f nx

operatorChainStatement :: OperatorChain a -> a
operatorChainStatement (OperatorChain a _ _) = a
operatorChainStatement (EndOfChain x)        = x

zipChain :: OperatorChain a -> [ ( a, a, LinkType ) ]
zipChain (OperatorChain a d nx) = (a, operatorChainStatement nx, d) : zipChain nx
zipChain (EndOfChain _)         = []

depOperator :: Parser LinkType
depOperator =
      (RBefore <$ symbol "->")
  <|> (RNotify <$ symbol "~>")

assignment :: Parser AttributeDecl
assignment =
      (AttributeDecl <$> lexeme key <*> arrowOp  <*> expression)
  <|> (AttributeWildcard <$> (symbolic '*' *> symbol "=>" *> expression))
  where
    key = bareword <?> "Assignment key"
    arrowOp =
          (AssignArrow <$ symbol "=>")
      <|> (AppendArrow <$ symbol "+>")

-- | Resource Collector
resCollDecl :: Position -> Text -> Parser ResCollDecl
resCollDecl p restype = do
  openchev <- some (char '<')
  when (length openchev > 2) (fail "Too many brackets")
  void $ symbolic '|'
  e <- option AlwaysTrue searchExpression
  void (char '|')
  void (count (length openchev) (char '>'))
  sc
  overrides <- option [] $ braces (sepComma assignment)
  let collectortype = if length openchev == 1
                          then Collector
                          else ExportedCollector
  pe <- getSourcePos
  pure (ResCollDecl collectortype restype e (V.fromList overrides) (p :!: pe) )
  where
    searchExpression :: Parser SearchExpression
    searchExpression =
      let searchTable :: [[Operator Parser SearchExpression]]
          searchTable = [ [ InfixL ( reserved "and" *> pure AndSearch )
                          , InfixL ( reserved "or"  *> pure OrSearch  )
                          ] ]
          searchterm = parens searchExpression <|> check
          check = do
            attrib <- parameterName
            opr    <- (EqualitySearch <$ symbol "==")
                  <|> (NonEqualitySearch <$ symbol "!=")
            term   <- stringExpression
            pure (opr attrib term)
      in makeExprParser (lexeme searchterm) searchTable

classDecl :: Parser ClassDecl
classDecl = do
  p <- getSourcePos
  reserved "class"
  ClassDecl <$> className
            <*> option V.empty puppetClassParameters
            <*> option S.Nothing (fmap S.Just (reserved "inherits" *> className))
            <*> braces statementList
            <*> ( (p :!:) <$> getSourcePos )

mainFuncDecl :: Parser MainFuncDecl
mainFuncDecl = do
  p <- getSourcePos
  (fname, args) <- genFunctionCall True
  pe <- getSourcePos
  pure (MainFuncDecl fname args (p :!: pe))

hoLambdaDecl :: Parser HigherOrderLambdaDecl
hoLambdaDecl = do
  p <- getSourcePos
  fc <- lambdaCall
  pe <- getSourcePos
  pure (HigherOrderLambdaDecl fc (p :!: pe))

dotLambdaDecl :: Parser HigherOrderLambdaDecl
dotLambdaDecl = do
  p <- getSourcePos
  ex <- expression
  pe <- getSourcePos
  hf <- case ex of
    FunctionApplication e (Terminal (UHOLambdaCall hf)) -> do
      unless (null (hf ^. hoLambdaExpr)) (fail "Can't call a function with . and ()")
      pure (hf & hoLambdaExpr .~ V.singleton e)
    Terminal (UHOLambdaCall hf) -> do
      when (null (hf ^. hoLambdaExpr)) (fail "This function needs data to operate on")
      pure hf
    _ -> fail "A method chained by dots."
  pure (HigherOrderLambdaDecl hf (p :!: pe))


resDefaultDecl :: Parser ResDefaultDecl
resDefaultDecl = do
  p <- getSourcePos
  rnd  <- resourceNameRef
  let assignmentList = V.fromList <$> sepComma1 assignment
  asl <- braces assignmentList
  pe <- getSourcePos
  pure (ResDefaultDecl rnd asl (p :!: pe))

resOverrideDecl :: Parser [ResOverrideDecl]
resOverrideDecl = do
  p <- getSourcePos
  restype  <- resourceNameRef
  names <- brackets (expression `sepBy1` comma) <?> "Resource reference values"
  assignments <- V.fromList <$> braces (sepComma assignment)
  pe <- getSourcePos
  pure [ ResOverrideDecl restype n assignments (p :!: pe) | n <- names ]

arrayof :: Parser p -> Parser [p]
arrayof p = symbolic '[' *> sepBy p comma <* symbolic ']'

-- | Heterogeneous chain (interleaving resource declarations with
-- resource references) needs to be supported:
--
--    class { 'docker::service': } ->
--    Class['docker']
chainableResources :: Parser [Statement]
chainableResources = do
  let withresname = do
        p <- getSourcePos
        restype  <- resourceNameRef
        lookAhead anySingle >>= \case
          '[' -> do
              resnames <- brackets (expression `sepBy1` comma)
              pe <- getSourcePos
              pure (ChainResRefr restype resnames (p :!: pe))
          _ -> ChainResColl <$> resCollDecl p restype
  let oneresource = pure <$> try withresname <|> map ChainResDecl <$> resDeclGroup
  chain <- parseRelationships (oneresource <|> concat <$> arrayof oneresource)
  let relations = do
        (g1, g2, lt) <- zipChain chain
        (rt1, rn1, _   :!: pe1) <- concatMap extractResRef g1
        (rt2, rn2, ps2 :!: _  ) <- concatMap extractResRef g2
        pure (DepDecl (rt1 :!: rn1) (rt2 :!: rn2) lt (pe1 :!: ps2))
  pure $ map DependencyDeclaration relations <> (chain ^.. folded . folded . to extractChainStatement . folded)
  where
    extractResRef :: ChainableRes -> [(Text, Expression, PPosition)]
    extractResRef (ChainResColl _)                      = []
    extractResRef (ChainResDecl (ResDecl rt rn _ _ pp)) = [(rt,rn,pp)]
    extractResRef (ChainResRefr rt rns pp)              = [(rt,rn,pp) | rn <- rns]

    extractChainStatement :: ChainableRes -> [Statement]
    extractChainStatement (ChainResColl r) = [ResourceCollectionDeclaration r]
    extractChainStatement (ChainResDecl d) = [ResourceDeclaration d]
    extractChainStatement ChainResRefr{}   = []

    parseRelationships :: Parser a -> Parser (OperatorChain a)
    parseRelationships p = do
      g <- p
      o <- optional depOperator
      case o of
        Just o' -> OperatorChain g o' <$> parseRelationships p
        Nothing -> pure (EndOfChain g)

    resDeclGroup :: Parser [ResDecl]
    resDeclGroup = do
      let resourceName = expression
          resourceDeclaration = do
            p <- getSourcePos
            names <- brackets (sepComma1 resourceName) <|> fmap pure resourceName
            void $ symbolic ':'
            vals  <- fmap V.fromList (sepComma assignment)
            pe <- getSourcePos
            pure [(n, vals, p :!: pe) | n <- names ]
          groupDeclaration = (,) <$> many (char '@') <*> typeName <* symbolic '{'
      (virts, rtype) <- try groupDeclaration -- for matching reasons, this gets a try until the opening brace
      let sep = symbolic ';' <|> comma
      x <- resourceDeclaration `sepEndBy1` sep
      void $ symbolic '}'
      virtuality <- case virts of
        ""   -> pure Normal
        "@"  -> pure Virtual
        "@@" -> pure Exported
        _    -> fail "Invalid virtuality"
      return [ ResDecl rtype rname conts virtuality pos | (rname, conts, pos) <- concat x ]

statement :: Parser [Statement]
statement =
      (pure . HigherOrderLambdaDeclaration <$> try dotLambdaDecl)
  <|> (pure . VarAssignmentDeclaration <$> varAssign)
  <|> (map NodeDeclaration <$> nodeDecl)
  <|> (pure . DefineDeclaration <$> defineDecl)
  <|> (pure . ConditionalDeclaration <$> unlessCondition)
  <|> (pure . ConditionalDeclaration <$> ifCondition)
  <|> (pure . ConditionalDeclaration <$> caseCondition)
  <|> (pure . ResourceDefaultDeclaration <$> try resDefaultDecl)
  <|> (map ResourceOverrideDeclaration <$> try resOverrideDecl)
  <|> chainableResources
  <|> (pure . ClassDeclaration <$> classDecl)
  <|> (pure . HigherOrderLambdaDeclaration <$> try hoLambdaDecl)
  <|> (pure . MainFunctionDeclaration <$> mainFuncDecl)
  <?> "Statement"

datatype :: Parser UDataType
datatype =
      dtString
  <|> dtInteger
  <|> dtFloat
  <|> dtNumeric
  <|> (UDTBoolean <$ reserved "Boolean")
  <|> (UDTScalar <$ reserved "Scalar")
  <|> (UDTData <$ reserved "Data")
  <|> (UDTAny <$ reserved "Any")
  <|> (UDTCollection <$ reserved "Collection")
  <|> dtArray
  <|> dtHash
  <|> (UDTUndef <$ reserved "Undef")
  <|> (reserved "Optional" *> (UDTOptional <$> brackets datatype))
  <|> (UNotUndef <$ reserved "NotUndef")
  <|> (reserved "Variant" *> (UDTVariant . NE.fromList <$> brackets (datatype `sepBy1` symbolic ',')))
  <|> (reserved "Regexp" *> (UDTRegexp <$> optional (brackets termRegexp)))
  -- while all the other cases are straightforward, it seems that the
  -- following syntax is a valid regexp for puppet:
  --   '^dqsqsdqs$'
  -- instead of:
  --   /^dqsqsdqs$/
  --
  -- That is the reason there is a "quotedRegexp" case
  <|> (reserved "Pattern" *> (UDTPattern . NE.fromList <$> brackets ( (termRegexp <|> quotedRegexp) `sepBy1` symbolic ',')))
  <|> (reserved "Enum" *> (UDTEnum . NE.fromList <$> brackets (expression `sepBy1` symbolic ',')))
  <|> dtExternal
  <?> "UDataType"
  where
    quotedRegexp = stringLiteral' >>= compileRegexp
    integer = integerOrDouble >>= either (return . fromIntegral) (\d -> fail ("Integer value expected, instead of " ++ show d))
    float = either fromIntegral identity <$> integerOrDouble
    dtArgs str def parseArgs = do
      void $ reserved str
      fromMaybe def <$> optional (brackets parseArgs)
    dtbounded s constructor parser = dtArgs s (constructor Nothing Nothing) $ do
      lst <- parser `sepBy1` symbolic ','
      case lst of
        [minlen]        -> return $ constructor (Just minlen) Nothing
        [minlen,maxlen] -> return $ constructor (Just minlen) (Just maxlen)
        _               -> fail ("Too many arguments to datatype " ++ Text.unpack s)
    dtString = dtbounded "String" UDTString integer
    dtInteger = dtbounded "Integer" UDTInteger integer
    dtFloat = dtbounded "Float" UDTFloat float
    dtNumeric = dtbounded "Numeric" (\ma mb -> UDTVariant (UDTFloat ma mb :| [UDTInteger (truncate <$> ma) (truncate <$> mb)])) float
    dtArray = do
      reserved "Array"
      ml <- optional $ brackets $ do
        tp <- datatype
        rst <- optional (symbolic ',' *> integer `sepBy1` symbolic ',')
        return (tp, rst)
      case ml of
        Nothing                 -> return (UDTArray UDTData 0 Nothing)
        Just (t, Nothing)       -> return (UDTArray t 0 Nothing)
        Just (t, Just [mi])     -> return (UDTArray t mi Nothing)
        Just (t, Just [mi, mx]) -> return (UDTArray t mi (Just mx))
        Just (_, Just _)        -> fail "Too many arguments to datatype Array"
    dtHash = do
      reserved "Hash"
      ml <- optional $ brackets $ do
        tk <- datatype
        symbolic ','
        tv <- datatype
        rst <- optional (symbolic ',' *> integer `sepBy1` symbolic ',')
        return (tk, tv, rst)
      case ml of
        Nothing                      -> return (UDTHash UDTScalar UDTData 0 Nothing)
        Just (tk, tv, Nothing)       -> return (UDTHash tk tv 0 Nothing)
        Just (tk, tv, Just [mi])     -> return (UDTHash tk tv mi Nothing)
        Just (tk, tv, Just [mi, mx]) -> return (UDTHash tk tv mi (Just mx))
        Just (_, _, Just _)          -> fail "Too many arguments to datatype Hash"
    dtExternal =
      choice [ reserved "Stdlib::Absolutepath" $> UDTData
             , reserved "Stdlib::Base32" $> UDTData
             , reserved "Stdlib::Base64" $> UDTData
             , reserved "Stdlib::Compat::Absolute_path" $> UDTData
             , reserved "Stdlib::Compat::Array" $> UDTData
             , reserved "Stdlib::Compat::Bool" $> UDTData
             , reserved "Stdlib::Compat::Float" $> UDTData
             , reserved "Stdlib::Compat::Hash" $> UDTData
             , reserved "Stdlib::Compat::Integer" $> UDTData
             , reserved "Stdlib::Compat::Ip_address" $> UDTData
             , reserved "Stdlib::Compat::Ipv4" $> UDTData
             , reserved "Stdlib::Compat::Ipv6" $> UDTData
             , reserved "Stdlib::Compat::Numeric" $> UDTData
             , reserved "Stdlib::Compat::String" $> UDTData
             , reserved "Stdlib::Ensure::Service" $> UDTData
             , reserved "Stdlib::Filemode" $> UDTData
             , reserved "Stdlib::Filesource" $> UDTData
             , reserved "Stdlib::Fqdn" $> UDTData
             , reserved "Stdlib::Host" $> UDTData
             , reserved "Stdlib::HTTPSUrl" $> UDTData
             , reserved "Stdlib::HTTPUrl" $> UDTData
             , reserved "Stdlib::IP::Address::Nosubnet" $> UDTData
             , reserved "Stdlib::Ip_address" $> UDTData
             , reserved "Stdlib::IP::Address" $> UDTData
             , reserved "Stdlib::IP::Address::V4::CIDR" $> UDTData
             , reserved "Stdlib::IP::Address::V4::Nosubnet" $> UDTData
             , reserved "Stdlib::IP::Address::V4" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Alternative" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Compressed" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Full" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Nosubnet::Alternative" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Nosubnet::Compressed" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Nosubnet::Full" $> UDTData
             , reserved "Stdlib::IP::Address::V6::Nosubnet" $> UDTData
             , reserved "Stdlib::IP::Address::V6" $> UDTData
             , reserved "Stdlib::Ipv4" $> UDTData
             , reserved "Stdlib::Ipv6" $> UDTData
             , reserved "Stdlib::MAC" $> UDTData
             , reserved "Stdlib::Port::Privileged" $> UDTData
             , reserved "Stdlib::Port" $> UDTData
             , reserved "Stdlib::Port::Unprivileged" $> UDTData
             , reserved "Stdlib::Unixpath" $> UDTData
             , reserved "Stdlib::Windowspath" $> UDTData
             , reserved "Nginx::ErrorLogSeverity" $> UDTData
             , reserved "Jenkins::Tunnel" $> UDTData
             , reserved "Systemd::Unit" $> UDTData
             , reserved "Systemd::ServiceLimits" $> UDTData
             , reserved "Systemd::Dropin" $> UDTData
             , reserved "Systemd::JournaldSettings" $> UDTData
             ]

statementList :: Parser (Vector Statement)
statementList = V.fromList . concat <$> many statement

lambdaCall :: Parser HOLambdaCall
lambdaCall = do
  let tostrict (Just x) = S.Just x
      tostrict Nothing  = S.Nothing
  HOLambdaCall <$> lambFunc
               <*> parameters
               <*> lambParams
               <*> (symbolic '{' *> fmap (V.fromList . concat) (many (try statement)))
               <*> fmap tostrict (optional expression) <* symbolic '}'
  where
    parameters :: Parser (V.Vector Expression)
    parameters = maybe V.empty V.fromList <$> optional (parens (expression `sepBy` comma))
    lambFunc :: Parser LambdaFunc
    lambFunc = LambdaFunc <$> moduleName
    lambParams :: Parser LambdaParameters
    lambParams = between (symbolic '|') (symbolic '|') hp
      where
        lambdaParameter :: Parser LambdaParameter
        lambdaParameter = LambdaParam <$> optional datatype <*> lexeme (char '$' *> identifier)
        hp = V.fromList <$> lambdaParameter `sepBy1` comma