{-# LANGUAGE RecordWildCards, QuasiQuotes #-}

module Language.CMake.Parser
( fileParser
) where

import qualified Data.ByteString.Char8 as BS
import Control.Applicative
import Data.Char
import Data.Functor
import Data.Maybe
import Data.String
import Text.RawString.QQ
import Text.Trifecta

import Language.CMake.AST hiding(commandInvocation)

fileParser :: Parser File
fileParser = File <$> many fileElement <* eof

fileElement :: Parser FileElement
fileElement = try (many (try bracketComment <|> spaceNonLF) *> lineEnding $> NonCommandElement)
          <|> (CommandElement <$> commandInvocation) <* many spaceNonLF <* lineEnding
  where spaceNonLF = void $ satisfy $ \c -> isSpace c && c /= '\n'

lineEnding :: Parser ()
lineEnding = skipOptional lineComment >> void newline

commandInvocation :: Parser CommandInvocation
commandInvocation = do
  spaces
  commandId <- BS.pack <$> some ('A' ~~ 'Z' <|> 'a' ~~ 'z' <|> char '_' <|> '0' ~~ '9')
  spaces
  commandArgs <- between (char '(') (char ')') arguments
  pure CommandInvocation { .. }
  where (~~) = satisfyRange

arguments :: Parser [Argument]
arguments = do
  arg <- maybeToList <$> optional argument
  rest <- concat <$> many separatedArguments
  pure $ arg <> rest

separatedArguments :: Parser [Argument]
separatedArguments = maybeToList <$> try (some separation *> optional argument)
                 <|> many separation *> between (char '(') (char ')') arguments

separation :: Parser ()
separation = void space <|> lineEnding

argument :: Parser Argument
argument = bracketArgument <|> quotedArgument <|> unquotedArgument

bracketArgument :: Parser Argument
bracketArgument = do
  opening <- char '[' *> many (char '=') <* char '['
  let closing = string $ "]" <> opening <> "]"
  fromString <$> anyChar `manyTill` try closing

quotedArgument :: Parser Argument
quotedArgument = fromString . catMaybes <$> many quotedElement `surroundedBy` char '"'

quotedElement :: Parser (Maybe Char)
quotedElement = Just <$> (noneOf [r|\"|] <|> try escapeSequence)
            <|> (char '\\' *> newline $> Nothing)

unquotedArgument :: Parser Argument
unquotedArgument = fromString <$> some (satisfy unElem <|> escapeSequence)
  where unElem c = c `notElem` [r|()#\"|] && not (isSpace c)

escapeSequence :: Parser Char
escapeSequence = char '\\' >> (self <|> encoded)
  where
    self = oneOf [r|()#" \$@^;|]
    encoded = char 't' $> '\t'
          <|> char 'r' $> '\r'
          <|> char 'n' $> '\n'

bracketComment :: Parser ()
bracketComment = char '#' >> void bracketArgument

lineComment :: Parser ()
lineComment = char '#' >> void (many $ notChar '\n')