{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Data.Morpheus.Parsing.Internal.Terms
  ( name,
    variable,
    varName,
    ignoredTokens,
    parseString,
    collection,
    setOf,
    uniqTuple,
    uniqTupleOpt,
    parseTypeCondition,
    spreadLiteral,
    parseAlias,
    sepByAnd,
    parseName,
    parseType,
    keyword,
    optDescription,
    optionalCollection,
    parseTypeName,
    pipe,
    brackets,
    equal,
    colon,
    at,
    symbol,
  )
where

import Data.ByteString.Lazy.Internal (ByteString)
import Data.Mergeable.IsMap (FromList)
import Data.Morpheus.Ext.Result (GQLResult)
import Data.Morpheus.Internal.Utils
  ( Empty (..),
    KeyOf,
    fromElems,
    fromLBS,
  )
import Data.Morpheus.Parsing.Internal.Internal
  ( Parser,
    Position,
    getLocation,
  )
import Data.Morpheus.Parsing.Internal.SourceText
  ( ignoredTokens,
    ignoredTokens1,
    parseStringBS,
  )
import Data.Morpheus.Types.Internal.AST
  ( Description,
    FieldName,
    Ref (..),
    TypeName,
    TypeRef (..),
    TypeWrapper (..),
    packName,
  )
import qualified Data.Morpheus.Types.Internal.AST as AST
import Data.Morpheus.Types.Internal.AST.Name (Name)
import Relude hiding (ByteString, empty, many)
import Text.Megaparsec
  ( between,
    label,
    sepBy,
    sepBy1,
    sepEndBy,
    takeWhile1P,
    takeWhileP,
    try,
    (<?>),
  )
import Text.Megaparsec.Byte
  ( char,
    string,
  )

-- ':'
#define COLON 58
-- '@'
#define AT 64
-- '='
#define EQUAL 61
-- '|'
#define PIPE 124
-- '$'
#define DOLLAR 36
-- '&'
#define AMPERSAND 38
-- '_'
#define UNDERSCORE 95
-- '!'
#define BANG 33

#define CHAR_A 65

#define CHAR_Z 90

#define CHAR_a 97

#define CHAR_z 122

#define DIGIT_0 48

#define DIGIT_9 57

symbol :: Word8 -> Parser ()
symbol x = char x *> ignoredTokens
{-# INLINE symbol #-}

colon :: Parser ()
colon = symbol COLON
{-# INLINE colon #-}

at :: Parser ()
at = symbol AT
{-# INLINE at #-}

equal :: Parser ()
equal = symbol EQUAL
{-# INLINE equal #-}

pipe :: Parser a -> Parser [a]
pipe x = optional (symbol PIPE) *> (x `sepBy1` symbol PIPE)
{-# INLINE pipe #-}

-- parens : '()'
parens :: Parser a -> Parser a
parens = between (symbol 40) (symbol 41)
{-# INLINE parens #-}

-- braces: {}
braces :: Parser a -> Parser a
braces = between (symbol 123) (symbol 125)
{-# INLINE braces #-}

-- brackets: []
brackets :: Parser a -> Parser a
brackets = between (symbol 91) (symbol 93)
{-# INLINE brackets #-}

-- 2.1.9 Names
-- https://spec.graphql.org/draft/#Name
-- Name
name :: Parser AST.Token
name =
  label "Name"
    $ fromLBS
    <$> do
      (<>) <$> takeWhile1P Nothing isStartChar <*> takeWhileP Nothing isContinueChar
    <* ignoredTokens
  where
    isStartChar x =
      (x >= CHAR_a && x <= CHAR_z)
        || (x >= CHAR_A && x <= CHAR_Z)
        || x
        == UNDERSCORE
    {-# INLINE isStartChar #-}
    isContinueChar x =
      isStartChar x
        || (x >= DIGIT_0 && x <= DIGIT_9) -- digit
    {-# INLINE isContinueChar #-}
{-# INLINE name #-}

parseName :: Parser (Name t)
parseName = packName <$> name
{-# INLINE parseName #-}

parseTypeName :: Parser TypeName
parseTypeName = packName <$> name
{-# INLINE parseTypeName #-}

keyword :: ByteString -> Parser ()
keyword x = string x *> ignoredTokens1
{-# INLINE keyword #-}

varName :: Parser FieldName
varName = symbol DOLLAR *> parseName <* ignoredTokens
{-# INLINE varName #-}

-- Variable : https://graphql.github.io/graphql-spec/June2018/#Variable
--
-- Variable :  $Name
--
variable :: Parser (Ref FieldName)
variable =
  label "variable"
    $ flip Ref
    <$> getLocation
    <*> varName
{-# INLINE variable #-}

-- Descriptions: https://graphql.github.io/graphql-spec/June2018/#Description
--
-- Description:
--   StringValue
optDescription :: Parser (Maybe Description)
optDescription = optional parseString
{-# INLINE optDescription #-}

parseString :: Parser AST.Token
parseString = label "String" $ fromLBS <$> parseStringBS
{-# INLINE parseString #-}

------------------------------------------------------------------------
sepByAnd :: Parser a -> Parser [a]
sepByAnd entry = entry `sepBy` (optional (symbol AMPERSAND) *> ignoredTokens)
{-# INLINE sepByAnd #-}

-----------------------------
collection :: Parser a -> Parser [a]
collection entry = braces (entry `sepEndBy` ignoredTokens)
{-# INLINE collection #-}

setOf :: (FromList GQLResult map k a, KeyOf k a) => Parser a -> Parser (map k a)
setOf = collection >=> lift . fromElems
{-# INLINE setOf #-}

optionalCollection :: (Empty c) => Parser c -> Parser c
optionalCollection x = x <|> pure empty
{-# INLINE optionalCollection #-}

parseNonNull :: Parser Bool
parseNonNull = (symbol BANG $> True) <|> pure False
{-# INLINE parseNonNull #-}

uniqTuple :: (FromList GQLResult map k a, KeyOf k a) => Parser a -> Parser (map k a)
uniqTuple parser =
  label "Tuple"
    $ parens
      (parser `sepBy` ignoredTokens <?> "empty Tuple value!")
    >>= lift
    . fromElems
{-# INLINE uniqTuple #-}

uniqTupleOpt ::
  ( FromList GQLResult map k a,
    Empty (map k a),
    KeyOf k a
  ) =>
  Parser a ->
  Parser (map k a)
uniqTupleOpt x = uniqTuple x <|> pure empty
{-# INLINE uniqTupleOpt #-}

-- Type Conditions: https://graphql.github.io/graphql-spec/June2018/#sec-Type-Conditions
--
--  TypeCondition:
--    on NamedType
--
parseTypeCondition :: Parser TypeName
parseTypeCondition = keyword "on" *> parseTypeName
{-# INLINE parseTypeCondition #-}

spreadLiteral :: Parser Position
spreadLiteral = getLocation <* string "..." <* ignoredTokens
{-# INLINE spreadLiteral #-}

-- Field Alias : https://graphql.github.io/graphql-spec/June2018/#sec-Field-Alias
-- Alias
--  Name:
parseAlias :: Parser (Maybe FieldName)
parseAlias = try (optional alias) <|> pure Nothing
  where
    alias = label "alias" (parseName <* colon)
{-# INLINE parseAlias #-}

parseType :: Parser TypeRef
parseType = uncurry TypeRef <$> (unwrapped <|> wrapped)
  where
    unwrapped :: Parser (TypeName, TypeWrapper)
    unwrapped = (,) <$> parseTypeName <*> (BaseType <$> parseNonNull)
    {-# INLINE unwrapped #-}
    ----------------------------------------------
    wrapped :: Parser (TypeName, TypeWrapper)
    wrapped = do
      (typename, wrapper) <- brackets (unwrapped <|> wrapped)
      isRequired <- parseNonNull
      pure (typename, TypeList wrapper isRequired)
    {-# INLINE wrapped #-}
{-# INLINE parseType #-}
