{-# OPTIONS_GHC -Wno-missing-export-lists #-}

{-# LANGUAGE FlexibleContexts #-}

module Burrito.Internal.Parse where

import qualified Burrito.Internal.Type.Character as Character
import qualified Burrito.Internal.Type.Digit as Digit
import qualified Burrito.Internal.Type.Expression as Expression
import qualified Burrito.Internal.Type.Field as Field
import qualified Burrito.Internal.Type.Literal as Literal
import qualified Burrito.Internal.Type.MaxLength as MaxLength
import qualified Burrito.Internal.Type.Modifier as Modifier
import qualified Burrito.Internal.Type.Name as Name
import qualified Burrito.Internal.Type.Operator as Operator
import qualified Burrito.Internal.Type.Template as Template
import qualified Burrito.Internal.Type.Token as Token
import qualified Burrito.Internal.Type.Variable as Variable
import qualified Data.Char as Char
import qualified Data.Ix as Ix
import qualified Data.List.NonEmpty as NonEmpty
import qualified Text.Parsec as Parsec
import qualified Text.Read as Read

-- | Attempts to parse a string as a URI template. If parsing fails, this will
-- return @Nothing@. Otherwise it will return @Just@ the parsed template.
--
-- Parsing will usually succeed, but it can fail if the input string contains
-- characters that are not valid in IRIs (like @^@) or if the input string
-- contains an invalid template expression (like @{!}@). To include characters
-- that aren't valid in IRIs, percent encode them (like @%5E@).
--
-- >>> parse "invalid template"
-- Nothing
-- >>> parse "valid-template"
-- Just (Template ...)
parse :: String -> Maybe Template.Template
parse = either (const Nothing) Just . Parsec.parse template ""

template :: Parsec.Stream s m Char => Parsec.ParsecT s u m Template.Template
template = Template.Template <$> Parsec.many token <* Parsec.eof

token :: Parsec.Stream s m Char => Parsec.ParsecT s u m Token.Token
token = choice (Token.Expression <$> expression) (Token.Literal <$> literal)

choice
  :: Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a -> Parsec.ParsecT s u m a
choice = (Parsec.<|>)

expression
  :: Parsec.Stream s m Char => Parsec.ParsecT s u m Expression.Expression
expression =
  Parsec.between (Parsec.char '{') (Parsec.char '}')
    $ Expression.Expression
    <$> operator
    <*> sepBy1 variable (Parsec.char ',')

operator :: Parsec.Stream s m Char => Parsec.ParsecT s u m Operator.Operator
operator = Parsec.option Operator.None $ Parsec.choice
  [ Operator.Ampersand <$ Parsec.char '&'
  , Operator.FullStop <$ Parsec.char '.'
  , Operator.NumberSign <$ Parsec.char '#'
  , Operator.PlusSign <$ Parsec.char '+'
  , Operator.QuestionMark <$ Parsec.char '?'
  , Operator.Semicolon <$ Parsec.char ';'
  , Operator.Solidus <$ Parsec.char '/'
  ]

sepBy1
  :: Parsec.ParsecT s u m a
  -> Parsec.ParsecT s u m x
  -> Parsec.ParsecT s u m (NonEmpty.NonEmpty a)
sepBy1 p s = (NonEmpty.:|) <$> p <*> Parsec.many (s *> p)

variable :: Parsec.Stream s m Char => Parsec.ParsecT s u m Variable.Variable
variable = Variable.Variable <$> name <*> modifier

name :: Parsec.Stream s m Char => Parsec.ParsecT s u m Name.Name
name = Name.Name <$> sepBy1 field (Parsec.char '.')

field :: Parsec.Stream s m Char => Parsec.ParsecT s u m Field.Field
field = Field.Field <$> nonEmpty fieldCharacter

nonEmpty
  :: Parsec.ParsecT s u m a -> Parsec.ParsecT s u m (NonEmpty.NonEmpty a)
nonEmpty p = (NonEmpty.:|) <$> p <*> Parsec.many p

fieldCharacter
  :: Parsec.Stream s m Char
  => Parsec.ParsecT s u m (Character.Character Field.Field)
fieldCharacter = choice encodedCharacter (unencodedCharacter isFieldCharacter)

encodedCharacter
  :: Parsec.Stream s m Char => Parsec.ParsecT s u m (Character.Character tag)
encodedCharacter = Parsec.char '%' >> Character.Encoded <$> digit <*> digit

digit :: Parsec.Stream s m Char => Parsec.ParsecT s u m Digit.Digit
digit = do
  x <- Parsec.satisfy Char.isHexDigit
  maybe (fail "invalid Digit") pure $ Digit.fromChar x

unencodedCharacter
  :: Parsec.Stream s m Char
  => (Char -> Bool)
  -> Parsec.ParsecT s u m (Character.Character tag)
unencodedCharacter = fmap Character.Unencoded . Parsec.satisfy

isFieldCharacter :: Char -> Bool
isFieldCharacter x = case x of
  '_' -> True
  _ -> Char.isAsciiUpper x || Char.isAsciiLower x || Char.isDigit x

modifier :: Parsec.Stream s m Char => Parsec.ParsecT s u m Modifier.Modifier
modifier = Parsec.option Modifier.None $ Parsec.choice
  [ Modifier.Asterisk <$ Parsec.char '*'
  , Parsec.char ':' >> Modifier.Colon <$> maxLength
  ]

maxLength :: Parsec.Stream s m Char => Parsec.ParsecT s u m MaxLength.MaxLength
maxLength = do
  x <- Parsec.satisfy $ Ix.inRange ('1', '9')
  xs <- Parsec.many $ Parsec.satisfy Char.isDigit
  n <- maybe (fail "invalid MaxLength") pure . Read.readMaybe $ x : xs
  if isMaxLength n
    then pure $ MaxLength.MaxLength n
    else fail "invalid MaxLength"

isMaxLength :: Int -> Bool
isMaxLength = Ix.inRange (1, 9999)

literal :: Parsec.Stream s m Char => Parsec.ParsecT s u m Literal.Literal
literal = Literal.Literal <$> nonEmpty literalCharacter

literalCharacter
  :: Parsec.Stream s m Char
  => Parsec.ParsecT s u m (Character.Character Literal.Literal)
literalCharacter =
  choice encodedCharacter (unencodedCharacter isLiteralCharacter)

isLiteralCharacter :: Char -> Bool
isLiteralCharacter x = case x of
  ' ' -> False
  '"' -> False
  '\'' -> False
  '%' -> False
  '<' -> False
  '>' -> False
  '\\' -> False
  '^' -> False
  '`' -> False
  '{' -> False
  '|' -> False
  '}' -> False
  _ ->
    Ix.inRange ('\x20', '\x7e') x
      || Ix.inRange ('\xa0', '\xd7ff') x
      || Ix.inRange ('\xe000', '\xf8ff') x
      || Ix.inRange ('\xf900', '\xfdcf') x
      || Ix.inRange ('\xfdf0', '\xffef') x
      || Ix.inRange ('\x10000', '\x1fffd') x
      || Ix.inRange ('\x20000', '\x2fffd') x
      || Ix.inRange ('\x30000', '\x3fffd') x
      || Ix.inRange ('\x40000', '\x4fffd') x
      || Ix.inRange ('\x50000', '\x5fffd') x
      || Ix.inRange ('\x60000', '\x6fffd') x
      || Ix.inRange ('\x70000', '\x7fffd') x
      || Ix.inRange ('\x80000', '\x8fffd') x
      || Ix.inRange ('\x90000', '\x9fffd') x
      || Ix.inRange ('\xa0000', '\xafffd') x
      || Ix.inRange ('\xb0000', '\xbfffd') x
      || Ix.inRange ('\xc0000', '\xcfffd') x
      || Ix.inRange ('\xd0000', '\xdfffd') x
      || Ix.inRange ('\xe1000', '\xefffd') x
      || Ix.inRange ('\xf0000', '\xffffd') x
      || Ix.inRange ('\x100000', '\x10fffd') x