{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}

module Data.Medea.Parser.Primitive
  ( Identifier (..),
    MedeaString (..),
    Natural,
    PrimTypeIdentifier (..),
    ReservedIdentifier (..),
    identFromReserved,
    isReserved,
    isStartIdent,
    parseIdentifier,
    parseKeyVal,
    parseLine,
    parseNatural,
    parseReserved,
    parseString,
    tryPrimType,
  )
where

import Control.Monad (replicateM_, when)
import qualified Data.ByteString as BS
import Data.Char (isControl, isDigit, isSeparator)
import Data.Hashable (Hashable (..))
import Data.Maybe (isJust)
import Data.Medea.JSONType (JSONType (..))
import Data.Medea.Parser.Types (MedeaParser, ParseError (..))
import Data.Text (Text, head, pack, unpack)
import Data.Text.Encoding (encodeUtf8)
import Text.Megaparsec
  ( customFailure,
    manyTill,
    takeWhile1P,
  )
import Text.Megaparsec.Char (char, eol)
import Text.Megaparsec.Char.Lexer (charLiteral)
import Prelude hiding (head)

-- Identifier
newtype Identifier = Identifier {toText :: Text}
  deriving newtype (Eq, Ord, Show)

parseIdentifier :: MedeaParser Identifier
parseIdentifier = do
  ident <- takeWhile1P (Just "Non-separator") (not . isSeparatorOrControl)
  checkedConstruct Identifier ident

data ReservedIdentifier
  = RSchema
  | RStart
  | RType
  | RStringValues
  | RProperties
  | RPropertyName
  | RPropertySchema
  | RAdditionalPropertiesAllowed
  | RAdditionalPropertySchema
  | ROptionalProperty
  | RMinLength
  | RMaxLength
  | RElementType
  | RTuple
  | RArray
  | RBoolean
  | RNull
  | RNumber
  | RObject
  | RString
  deriving stock (Eq, Show)

fromReserved :: ReservedIdentifier -> Text
fromReserved RSchema = "$schema"
fromReserved RStart = "$start"
fromReserved RType = "$type"
fromReserved RStringValues = "$string-values"
fromReserved RProperties = "$properties"
fromReserved RPropertyName = "$property-name"
fromReserved RPropertySchema = "$property-schema"
fromReserved RAdditionalPropertiesAllowed = "$additional-properties-allowed"
fromReserved RAdditionalPropertySchema = "$additional-property-schema"
fromReserved ROptionalProperty = "$optional-property"
fromReserved RMinLength = "$min-length"
fromReserved RMaxLength = "$max-length"
fromReserved RElementType = "$element-type"
fromReserved RTuple = "$tuple"
fromReserved RArray = "$array"
fromReserved RBoolean = "$boolean"
fromReserved RNull = "$null"
fromReserved RNumber = "$number"
fromReserved RObject = "$object"
fromReserved RString = "$string"

identFromReserved :: ReservedIdentifier -> Identifier
identFromReserved = Identifier . fromReserved

tryReserved :: Text -> Maybe ReservedIdentifier
tryReserved "$schema" = Just RSchema
tryReserved "$start" = Just RStart
tryReserved "$type" = Just RType
tryReserved "$string-values" = Just RStringValues
tryReserved "$properties" = Just RProperties
tryReserved "$property-name" = Just RPropertyName
tryReserved "$property-schema" = Just RPropertySchema
tryReserved "$additional-properties-allowed" = Just RAdditionalPropertiesAllowed
tryReserved "$additional-property-schema" = Just RAdditionalPropertySchema
tryReserved "$optional-property" = Just ROptionalProperty
tryReserved "$min-length" = Just RMinLength
tryReserved "$max-length" = Just RMaxLength
tryReserved "$element-type" = Just RElementType
tryReserved "$tuple" = Just RTuple
tryReserved "$array" = Just RArray
tryReserved "$boolean" = Just RBoolean
tryReserved "$null" = Just RNull
tryReserved "$number" = Just RNumber
tryReserved "$object" = Just RObject
tryReserved "$string" = Just RString
tryReserved _ = Nothing

parseReserved :: ReservedIdentifier -> MedeaParser Identifier
parseReserved reserved = do
  ident <- takeWhile1P Nothing (not . isSeparatorOrControl)
  let reservedText = fromReserved reserved
  when (ident /= reservedText) $ customFailure . ExpectedReservedIdentifier $ reservedText
  checkedConstruct Identifier ident

newtype PrimTypeIdentifier = PrimTypeIdentifier {typeOf :: JSONType}
  deriving newtype (Eq)

tryPrimType :: Identifier -> Maybe PrimTypeIdentifier
tryPrimType (Identifier ident) = tryReserved ident >>= reservedToPrim

reservedToPrim :: ReservedIdentifier -> Maybe PrimTypeIdentifier
reservedToPrim RNull = Just . PrimTypeIdentifier $ JSONNull
reservedToPrim RBoolean = Just . PrimTypeIdentifier $ JSONBoolean
reservedToPrim RObject = Just . PrimTypeIdentifier $ JSONObject
reservedToPrim RArray = Just . PrimTypeIdentifier $ JSONArray
reservedToPrim RNumber = Just . PrimTypeIdentifier $ JSONNumber
reservedToPrim RString = Just . PrimTypeIdentifier $ JSONString
reservedToPrim _ = Nothing

isReserved :: Identifier -> Bool
isReserved = isJust . tryReserved . toText

isStartIdent :: Identifier -> Bool
isStartIdent = (== Just RStart) . tryReserved . toText

-- Natural Number
type Natural = Word

parseNatural :: MedeaParser Natural
parseNatural = do
  digits <- takeWhile1P (Just "digits") isDigit
  when (head digits == '0')
    $ customFailure . LeadingZero
    $ digits
  pure . read . unpack $ digits

-- String
newtype MedeaString = MedeaString {unwrap :: Text}
  deriving newtype (Eq, Ord, Show, Hashable)

parseString :: MedeaParser MedeaString
parseString = do
  string <- char '"' *> manyTill charLiteral (char '"')
  pure . MedeaString . pack $ string

{-# INLINE parseLine #-}
parseLine :: Int -> MedeaParser a -> MedeaParser a
parseLine spaces p = replicateM_ spaces (char ' ') *> p <* eol

parseKeyVal :: ReservedIdentifier -> MedeaParser a -> MedeaParser a
parseKeyVal key = (parseReserved key *> char ' ' *>)

-- Helpers
checkedConstruct ::
  (Text -> a) -> Text -> MedeaParser a
checkedConstruct f t =
  if (> 32) . BS.length . encodeUtf8 $ t
    then customFailure . IdentifierTooLong $ t
    else pure . f $ t

isSeparatorOrControl :: Char -> Bool
isSeparatorOrControl c = isSeparator c || isControl c
