{-# LANGUAGE RecordWildCards, FlexibleContexts #-}
module ParserGen.ParseQuote
    ( getDecls
    , getDatatypes
    , getRepackers
    ) where

import Control.Applicative hiding (many, (<|>), optional)
import Control.Monad (unless, (>=>))
import Data.Char (chr)
import Data.List (isPrefixOf)
import Language.Haskell.TH as TH
import System.Directory (getCurrentDirectory)
import System.FilePath.Posix ((</>), takeDirectory)
import Text.Parsec hiding (spaces)
import Text.Parsec.Pos

import ParserGen.Types

type ParserQ = ParsecT String () Q

getDecls :: FilePath -> Q [Decl]
getDecls = getTemplate >=> parseDecls

getDatatypes :: FilePath -> Q [Datatype]
getDatatypes = fmap (fst . unzipDecls) . getDecls

getRepackers :: FilePath -> Q [Repacker]
getRepackers = fmap (snd . unzipDecls) . getDecls

getTemplate :: FilePath -> Q (SourcePos, String)
getTemplate templateName = do
    filename <- loc_filename <$> location
    pwd <- runIO $ getCurrentDirectory
    let templatePath = (takeDirectory $ pwd </> filename) </> templateName
    body <- runIO $ readFile templatePath
    return (newPos templateName 1 1, body)

parseInQ :: ParserQ v -> (SourcePos, String) -> Q v
parseInQ p (pos, s) = do
    parseResult <- runParserT (inPosition p) () "" s
    case parseResult of
        Right v  -> return v
        Left err -> fail $ show err
  where
    inPosition :: ParserQ v -> ParserQ v
    inPosition p' = do
        setPosition pos
        val <- p'
        eof
        return val

parseDecls :: (SourcePos, String) -> Q [Decl]
parseDecls = parseInQ $ many1 $
    (DatatypeDecl <$> datatypeParser) <|>
    (RepackerDecl <$> repackerParser)

datatypeParser :: ParserQ Datatype
datatypeParser = do
    _           <- optional endofline
    typeName    <- identifier
    _           <- endofline
    typeConstrs <- many1 constrParser
    _           <- many endofline

    return Datatype {..}

spaces :: Stream s m Char => ParsecT s u m ()
spaces = skipMany1 (oneOf "\t ")

constrParser :: ParserQ DataConstructor
constrParser = do
    _            <- try (string "  " <?> "constructor padding")
    constrName   <- identifier
    constrPrefix <- optionMaybe (try $ spaces *> prefix)
    _            <- endofline
    constrFields <- many1 constFieldParser

    return DataConstructor {..}

repeatFactor :: ParserQ Int
repeatFactor = try (decimal <* char 'x') <?> "repetition factor"

constFieldParser :: ParserQ DataField
constFieldParser = do
    _           <- try (string "    ") <?> "field padding"
    fieldRepeat <- optionMaybe (try $ repeatFactor <* spaces)
    fieldName   <- fieldNameParser
    _           <- spaces
    fieldStrict <- try (char '!' *> return True <* spaces) <|> return False
    fieldType   <- typeParser
    _           <- spaces
    signed      <- option False (True <$ char '+')
    fieldWidth  <- decimal <?> "field width spec"
    fieldParser <- fieldParserParser signed
    _           <- endofline
    return DataField {..}

fieldNameParser :: ParserQ (Maybe String)
fieldNameParser =
    (Just <$> identifier) <|> (Nothing <$ (char '_' <* identifier))

typeParser :: ParserQ Type
typeParser = (singleWord <|> multiWord) <?> "field type"
  where
    singleWord = (TH.ConT . TH.mkName) <$> ((:) <$> letter <*> many alphaNum)
    multiWord  = error "multiWord is not yet implemented"

fieldParserParser :: Bool -> ParserQ ParserType
fieldParserParser signed =
    (if signed then pure SignedParser else fail "signed parser") <|>
    (CustomParser    <$> try (spaces *> customParser))           <|>
    (HardcodedString <$> try (spaces *> hardcodedString))        <|>
    (pure UnsignedParser)

customParser :: ParserQ Exp
customParser = singleWord <?> "custom parser"
  where
    singleWord = (TH.VarE . TH.mkName) <$>
        ((:) <$> lower <*> many1 (noneOf "( )\t\n"))

hardcodedString :: ParserQ String
hardcodedString =
    between (char '"') (char '"') (many1 $ escapedChar <|> notQuote) <?>
    "hardcoded string"
  where
    escapedChar = char '\\' *> (special <|> hex <|> dec)

    special :: ParserQ Char
    special = do
            c <- oneOf "nt\"\\"
            return $ case c of
                'n' -> '\n'
                't' -> '\t'
                v   -> v -- unescape for \" and \\

    hex :: ParserQ Char
    hex = char 'x' *> ((chr . read  . ("0x"++)) <$> many1 hexDigit)

    dec :: ParserQ Char
    dec = chr <$> decimal

    notQuote = noneOf ['"']

repackerParser :: ParserQ Repacker
repackerParser = Repacker
    <$> parseRepackerName
    <*  spaces
    <*> identifier
    <*  endofline
    <*> many1 parseRepackerField
    <*  many endofline

parseRepackerName :: ParserQ String
parseRepackerName = do
    name <- many1 alphaNum 
    unless ("repackerFor" `isPrefixOf` name) $ fail $
        "Repacker name must start with \"repackerFor\": " ++ name
    return name

parseRepackerField :: ParserQ RepackerField
parseRepackerField = RepackerField
    <$  (try (string "  ") <?> "repacker field padding")
    <*> identifier
    <*> optionMaybe (spaces *> customParser)
    <*  endofline

decimal :: ParserQ Int
decimal = read <$> many1 digit

identifier :: ParserQ String
identifier = ((:) <$> upper <*> many alphaNum)

prefix :: ParserQ String
prefix = ((:) <$> lower <*> many alphaNum)

endofline :: ParserQ [Char]
endofline = many1
    (try $ many (oneOf "\t ")  *> (option "" $ try comment) *> char '\n') <?>
    "end of line"
  where
    comment = string "--" *> many1 (noneOf "\n")