{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}

module Data.Morpheus.Parsing.Internal.Terms
  ( token
  , qualifier
  , variable
  , spaceAndComments
  , spaceAndComments1
  , pipeLiteral
  -------------
  , setOf
  , parseTypeCondition
  , spreadLiteral
  , parseNonNull
  , parseMaybeTuple
  , parseAssignment
  , parseWrappedType
  , litEquals
  , litAssignment
  , parseTuple
  , parseAlias
  , sepByAnd
  , parseName
  , parseType
  , keyword
  , operator
  , optDescription
  , parseNegativeSign
  )
where

import           Data.Functor                   ( ($>) )
import           Data.Text                      ( Text
                                                , pack
                                                , strip
                                                )
import           Text.Megaparsec                ( between
                                                , label
                                                , try
                                                , many
                                                , manyTill
                                                , optional
                                                , sepBy
                                                , sepEndBy
                                                , skipMany
                                                , skipManyTill
                                                , try
                                                , (<?>)
                                                , (<|>)
                                                )
import           Text.Megaparsec.Char           ( char
                                                , digitChar
                                                , letterChar
                                                , newline
                                                , printChar
                                                , space
                                                , space1
                                                , string
                                                )

-- MORPHEUS
import           Data.Morpheus.Parsing.Internal.Internal
                                                ( Parser
                                                , Position
                                                , getLocation
                                                )
import           Data.Morpheus.Types.Internal.AST
                                                ( DataTypeWrapper(..)
                                                , Key
                                                , Description
                                                , Name
                                                , toHSWrappers
                                                , convertToHaskellName
                                                , Ref(..)
                                                , TypeRef(..)
                                                )


-- Name : https://graphql.github.io/graphql-spec/June2018/#sec-Names
--
-- Name :: /[_A-Za-z][_0-9A-Za-z]*/
--

parseNegativeSign :: Parser Bool
parseNegativeSign = (char '-' $> True <* spaceAndComments) <|> pure False

parseName :: Parser Name
parseName = token

keyword :: Key -> Parser ()
keyword word = string word *> space1 *> spaceAndComments

operator :: Char -> Parser ()
operator x = char x *> spaceAndComments

-- LITERALS
setLiteral :: Parser [a] -> Parser [a]
setLiteral =
  between (char '{' *> spaceAndComments) (char '}' *> spaceAndComments)

pipeLiteral :: Parser ()
pipeLiteral = char '|' *> spaceAndComments

litEquals :: Parser ()
litEquals = char '=' *> spaceAndComments

litAssignment :: Parser ()
litAssignment = char ':' *> spaceAndComments

-- PRIMITIVE
------------------------------------
token :: Parser Text
token = label "token" $ do
  firstChar <- letterChar <|> char '_'
  restToken <- many $ letterChar <|> char '_' <|> digitChar
  spaceAndComments
  return $ convertToHaskellName $ pack $ firstChar : restToken

qualifier :: Parser (Text, Position)
qualifier = label "qualifier" $ do
  position <- getLocation
  value    <- token
  return (value, position)


-- Variable : https://graphql.github.io/graphql-spec/June2018/#Variable
--
-- Variable :  $Name
--
variable :: Parser Ref
variable = label "variable" $ do
  refPosition <- getLocation
  _           <- char '$'
  refName     <- token
  spaceAndComments
  pure $ Ref { refName, refPosition }

spaceAndComments1 :: Parser ()
spaceAndComments1 = space1 *> spaceAndComments

-- Descriptions: https://graphql.github.io/graphql-spec/June2018/#Description
--
-- Description:
--   StringValue
-- TODO: should support """ and "
--
optDescription :: Parser (Maybe Description)
optDescription = optional parseDescription

parseDescription :: Parser Text
parseDescription =
  strip . pack <$> (blockDescription <|> singleLine) <* spaceAndComments
 where
  blockDescription =
    blockQuotes
      *> manyTill (printChar <|> newline) blockQuotes
      <* spaceAndComments
    where blockQuotes = string "\"\"\""
  ----------------------------
  singleLine =
    stringQuote *> manyTill printChar stringQuote <* spaceAndComments
    where stringQuote = char '"'

-- Ignored Tokens : https://graphql.github.io/graphql-spec/June2018/#sec-Source-Text.Ignored-Tokens
--  Ignored:
--    UnicodeBOM
--    WhiteSpace
--    LineTerminator
--    Comment
--    Comma
-- TODO: implement as in specification
spaceAndComments :: Parser ()
spaceAndComments = ignoredTokens

ignoredTokens :: Parser ()
ignoredTokens =
  label "IgnoredTokens" $ space *> skipMany inlineComment *> space
  where inlineComment = char '#' *> skipManyTill printChar newline *> space
    ------------------------------------------------------------------------

-- COMPLEX
sepByAnd :: Parser a -> Parser [a]
sepByAnd entry = entry `sepBy` (char '&' *> spaceAndComments)

-----------------------------
setOf :: Parser a -> Parser [a]
setOf entry = setLiteral (entry `sepEndBy` many (char ',' *> spaceAndComments))

parseNonNull :: Parser [DataTypeWrapper]
parseNonNull = do
  wrapper <- (char '!' $> [NonNullType]) <|> pure []
  spaceAndComments
  return wrapper

parseMaybeTuple :: Parser a -> Parser [a]
parseMaybeTuple parser = parseTuple parser <|> pure []

parseTuple :: Parser a -> Parser [a]
parseTuple parser = label "Tuple" $ between
  (char '(' *> spaceAndComments)
  (char ')' *> spaceAndComments)
  (parser `sepBy` (many (char ',') *> spaceAndComments) <?> "empty Tuple value!"
  )

parseAssignment :: (Show a, Show b) => Parser a -> Parser b -> Parser (a, b)
parseAssignment nameParser valueParser = label "assignment" $ do
  name' <- nameParser
  litAssignment
  value' <- valueParser
  pure (name', value')

-- Type Conditions: https://graphql.github.io/graphql-spec/June2018/#sec-Type-Conditions
--
--  TypeCondition:
--    on NamedType
--
parseTypeCondition :: Parser Text
parseTypeCondition = do
  _ <- string "on"
  space1
  token

spreadLiteral :: Parser Position
spreadLiteral = do
  index <- getLocation
  _     <- string "..."
  space
  return index

parseWrappedType :: Parser ([DataTypeWrapper], Text)
parseWrappedType = (unwrapped <|> wrapped) <* spaceAndComments
 where
  unwrapped :: Parser ([DataTypeWrapper], Text)
  unwrapped = ([], ) <$> token <* spaceAndComments
  ----------------------------------------------
  wrapped :: Parser ([DataTypeWrapper], Text)
  wrapped = between
    (char '[' *> spaceAndComments)
    (char ']' *> spaceAndComments)
    (do
      (wrappers, name) <- unwrapped <|> wrapped
      nonNull'         <- parseNonNull
      return ((ListType : nonNull') ++ wrappers, name)
    )

-- Field Alias : https://graphql.github.io/graphql-spec/June2018/#sec-Field-Alias
-- Alias
--  Name:
parseAlias :: Parser (Maybe Key)
parseAlias = try (optional alias) <|> pure Nothing
  where alias = label "alias" $ token <* char ':' <* spaceAndComments


parseType :: Parser TypeRef
parseType = do
  (wrappers, typeConName) <- parseWrappedType
  nonNull                 <- parseNonNull
  pure TypeRef { typeConName
               , typeArgs     = Nothing
               , typeWrappers = toHSWrappers $ nonNull ++ wrappers
               }