{-# LANGUAGE LambdaCase #-}
module Trasa.TH.Parse where

import qualified Data.List.NonEmpty as NE
import qualified Data.Set as S
import Data.Bifunctor (first)
import Language.Haskell.TH (Name,mkName)
import Control.Applicative ((<|>))
import Control.Monad (void)
import Data.Void (Void)
import qualified Text.Megaparsec as MP
import qualified Text.Megaparsec.Char.Lexer as L

import Trasa.TH.Types
import Trasa.TH.Lexer

import Debug.Trace

type Parser = MP.Parsec (MP.ErrorFancy Void) Stream

wrongToken :: a -> S.Set (MP.ErrorItem a)
wrongToken t = S.singleton (MP.Tokens (t NE.:| []))

space :: Parser ()
space = flip MP.token (wrongToken $ LexemeSpace 0) $ \case
  LexemeSpace _ -> Just ()
  other -> Nothing

optionalSpace :: Parser ()
optionalSpace = void (MP.optional space)

string :: Parser String
string = flip MP.token (wrongToken (LexemeString 0 "")) $ \case
  LexemeString _ str -> Just str
  other -> Nothing

name :: Parser Name
name = fmap mkName string

match :: Lexeme -> Parser ()
match lexeme = flip MP.token (wrongToken lexeme) $ \other ->
  if lexeme == other
  then Just ()
  else Nothing

matchChar :: ReservedChar -> Parser ()
matchChar = match . LexemeChar

newline :: Parser ()
newline = matchChar ReservedCharNewline

colon :: Parser ()
colon = matchChar ReservedCharColon

slash :: Parser ()
slash = matchChar ReservedCharSlash

questionMark :: Parser ()
questionMark = matchChar ReservedCharQuestionMark

ampersand :: Parser ()
ampersand = matchChar ReservedCharAmpersand

equal :: Parser ()
equal = matchChar ReservedCharEqual

bracket :: Parser a -> Parser a
bracket = MP.between (matchChar ReservedCharOpenBracket) (matchChar ReservedCharCloseBracket)

comma :: Parser ()
comma = matchChar ReservedCharComma

capture :: Parser (CaptureRep Name)
capture =
  fmap MatchRep string <|>
  fmap CaptureRep (colon *> name)

query :: Parser [QueryRep Name]
query = MP.sepBy (QueryRep <$> string <*> paramRep) ampersand
  where
    paramRep = MP.choice [ fmap OptionalRep optional, fmap ListRep list, pure FlagRep ]
    optional = MP.try (equal *> name)
    list = equal *> bracket name

list :: Parser a -> Parser [a]
list val = bracket (MP.sepBy val (optionalSpace *> comma <* optionalSpace))

response :: Parser (NE.NonEmpty Name)
response = list name >>= \case
  [] -> fail "Response requires at least one response type in the list"
  (n : ns) -> pure (n NE.:| ns)

routeRep :: Parser (RouteRep Name)
routeRep = do
  optionalSpace
  routeId <- string
  space
  method <- string
  space
  slash
  caps <- MP.sepBy capture slash
  qrys <- questionMark *> query <|> return []
  space
  req  <- list name
  space
  res  <- response
  optionalSpace
  newline
  return (RouteRep routeId method caps qrys req res)

routesRep :: Parser (RoutesRep Name)
routesRep = do
  optionalSpace
  void (MP.optional newline)
  optionalSpace
  match (LexemeSymbol ReservedSymbolDataType)
  colon
  optionalSpace
  dataType <- string
  newline
  routes <- MP.many routeRep
  return (RoutesRep dataType routes)

parseRoutesRep :: String -> Either String (RoutesRep Name)
parseRoutesRep str = do
  tokens <- first MP.errorBundlePretty (MP.parse stream "" str)
  first MP.errorBundlePretty (MP.parse routesRep "" (traceShowId tokens))