{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE NoImplicitPrelude #-}

module Data.Morpheus.Types.Internal.AST.Value
  ( Value (..),
    ScalarValue (..),
    Object,
    replaceValue,
    decodeScientific,
    RawValue,
    ValidValue,
    RawObject,
    ValidObject,
    Variable (..),
    ResolvedValue,
    ResolvedObject,
    VariableContent (..),
    ObjectEntry (..),
    VariableDefinitions,
  )
where

-- MORPHEUS
import qualified Data.Aeson as A
  ( FromJSON (..),
    ToJSON (..),
    Value (..),
    object,
    pairs,
    (.=),
  )
import Data.Foldable (foldr')
import Data.Mergeable
  ( NameCollision (..),
    OrdMap,
  )
import Data.Morpheus.Internal.Utils
  ( KeyOf (..),
    toAssoc,
    unsafeFromList,
  )
import Data.Morpheus.Rendering.RenderGQL
  ( RenderGQL (..),
    fromText,
    render,
    renderInputSeq,
    space,
  )
import Data.Morpheus.Types.Internal.AST.Base
  ( Position,
    Ref (..),
  )
import Data.Morpheus.Types.Internal.AST.Error
  ( GQLError,
    Msg (..),
    at,
  )
import Data.Morpheus.Types.Internal.AST.Name
  ( FieldName,
    TypeName,
    packName,
    unpackName,
  )
import Data.Morpheus.Types.Internal.AST.Stage
  ( CONST,
    CONST_OR_VALID,
    RAW,
    Stage,
    VALID,
  )
import Data.Morpheus.Types.Internal.AST.Type (TypeRef (..))
import Data.Scientific
  ( Scientific,
    floatingOrInteger,
  )
import qualified Data.Text as T
import qualified Data.Vector as V
import Instances.TH.Lift ()
import Language.Haskell.TH.Syntax (Lift (..))
import Relude

-- | Primitive Values for GQLScalar: 'Int', 'Float', 'String', 'Boolean'.
-- for performance reason type 'Text' represents GraphQl 'String' value
data ScalarValue
  = Int Int
  | Float Double
  | String Text
  | Boolean Bool
  | Value A.Value
  deriving (Show, Eq, Generic, Lift)

instance IsString ScalarValue where
  fromString = String . T.pack

instance RenderGQL ScalarValue where
  renderGQL (Int x) = renderGQL x
  renderGQL (Float x) = renderGQL x
  renderGQL (String x) = renderGQL x
  renderGQL (Boolean x) = renderGQL x
  renderGQL (Value x) = renderGQL x

instance A.ToJSON ScalarValue where
  toJSON (Float x) = A.toJSON x
  toJSON (Int x) = A.toJSON x
  toJSON (Boolean x) = A.toJSON x
  toJSON (String x) = A.toJSON x
  toJSON (Value x) = A.toJSON x

instance A.FromJSON ScalarValue where
  parseJSON (A.Bool v) = pure $ Boolean v
  parseJSON (A.Number v) = pure $ decodeScientific v
  parseJSON (A.String v) = pure $ String v
  parseJSON notScalar = fail $ "Expected Scalar got :" <> show notScalar

data VariableContent (stage :: Stage) where
  DefaultValue :: Maybe ResolvedValue -> VariableContent CONST
  ValidVariableValue :: {validVarContent :: ValidValue} -> VariableContent VALID

instance Lift (VariableContent a) where
  lift (DefaultValue x) = [|DefaultValue x|]
  lift (ValidVariableValue x) = [|ValidVariableValue x|]

#if MIN_VERSION_template_haskell(2,16,0)
  liftTyped (DefaultValue x) = [||DefaultValue x||]
  liftTyped (ValidVariableValue x) = [||ValidVariableValue x||]
#endif
deriving instance Show (VariableContent a)

deriving instance Eq (VariableContent a)

data Variable (stage :: Stage) = Variable
  { variablePosition :: Position,
    variableName :: FieldName,
    variableType :: TypeRef,
    variableValue :: VariableContent (CONST_OR_VALID stage)
  }
  deriving (Show, Eq, Lift)

instance KeyOf FieldName (Variable s) where
  keyOf = variableName

instance NameCollision GQLError (Variable s) where
  nameCollision Variable {variableName, variablePosition} =
    ("There can Be only One Variable Named " <> msg variableName)
      `at` variablePosition

type VariableDefinitions s = OrdMap FieldName (Variable s)

data Value (stage :: Stage) where
  ResolvedVariable :: Ref FieldName -> Variable VALID -> Value CONST
  VariableValue :: Ref FieldName -> Value RAW
  Object :: Object stage -> Value stage
  List :: [Value stage] -> Value stage
  Enum :: TypeName -> Value stage
  Scalar :: ScalarValue -> Value stage
  Null :: Value stage

instance IsString (Value stage) where
  fromString = Scalar . fromString

deriving instance Show (Value a)

deriving instance Eq (Value s)

data ObjectEntry (s :: Stage) = ObjectEntry
  { entryName :: FieldName,
    entryValue :: Value s
  }
  deriving (Eq, Show)

instance RenderGQL (ObjectEntry a) where
  renderGQL (ObjectEntry name value) = fromText (unpackName name) <> ": " <> renderGQL value

instance NameCollision GQLError (ObjectEntry s) where
  nameCollision ObjectEntry {entryName} =
    "There can Be only One field Named "
      <> msg entryName

instance KeyOf FieldName (ObjectEntry s) where
  keyOf = entryName

type Object a = OrdMap FieldName (ObjectEntry a)

type ValidObject = Object VALID

type RawObject = Object RAW

type ResolvedObject = Object CONST

type RawValue = Value RAW

type ValidValue = Value VALID

type ResolvedValue = Value CONST

deriving instance Lift (Value a)

deriving instance Lift (ObjectEntry a)

instance RenderGQL (Value a) where
  renderGQL (ResolvedVariable Ref {refName} _) = "$" <> renderGQL refName
  renderGQL (VariableValue Ref {refName}) = "$" <> renderGQL refName <> " "
  renderGQL Null = "null"
  renderGQL (Enum x) = renderGQL x
  renderGQL (Scalar x) = renderGQL x
  renderGQL (Object xs) = "{" <> entries <> "}"
    where
      entries
        | null xs = ""
        | otherwise = space <> renderInputSeq (toList xs) <> space
  renderGQL (List list) = "[" <> renderInputSeq list <> "]"

instance Msg (Value a) where
  msg = msg . render

instance A.ToJSON (Value a) where
  toJSON (ResolvedVariable _ Variable {variableValue = ValidVariableValue x}) =
    A.toJSON x
  toJSON (VariableValue Ref {refName}) =
    A.String $ "($ref:" <> unpackName refName <> ")"
  toJSON Null = A.Null
  toJSON (Enum x) = A.String (unpackName x)
  toJSON (Scalar x) = A.toJSON x
  toJSON (List x) = A.toJSON x
  toJSON (Object fields) = A.object $ fmap toEntry (toList fields)
    where
      toEntry (ObjectEntry name value) = unpackName name A..= A.toJSON value

  -------------------------------------------
  toEncoding (ResolvedVariable _ Variable {variableValue = ValidVariableValue x}) =
    A.toEncoding x
  toEncoding (VariableValue Ref {refName}) =
    A.toEncoding $ "($ref:" <> refName <> ")"
  toEncoding Null = A.toEncoding A.Null
  toEncoding (Enum x) = A.toEncoding x
  toEncoding (Scalar x) = A.toEncoding x
  toEncoding (List x) = A.toEncoding x
  toEncoding (Object ordMap) = A.pairs $ renderSeries encodeField (toList ordMap)
    where
      encodeField (ObjectEntry key value) = unpackName key A..= value

-- fixes GHC 8.2.2, which can't deduce (Semigroup p) from context (Monoid p)
renderSeries :: (Semigroup p, Monoid p) => (e -> p) -> [e] -> p
renderSeries _ [] = mempty
renderSeries f (x : xs) = foldr' (\e es -> es <> f e) (f x) xs

decodeScientific :: Scientific -> ScalarValue
decodeScientific v = case floatingOrInteger v of
  Left float -> Float float
  Right int -> Int int

replaceValue :: A.Value -> Value a
replaceValue (A.Bool v) = mkBoolean v
replaceValue (A.Number v) = Scalar $ decodeScientific v
replaceValue (A.String v) = mkString v
replaceValue (A.Object v) =
  mkObject
    $ fmap
      (bimap packName replaceValue)
      (toAssoc v)
replaceValue (A.Array li) = List (fmap replaceValue (V.toList li))
replaceValue A.Null = Null

instance A.FromJSON (Value a) where
  parseJSON = pure . replaceValue

-- DEFAULT VALUES

mkBoolean :: Bool -> Value s
mkBoolean = Scalar . Boolean

mkString :: Text -> Value s
mkString = Scalar . String

mkObject :: [(FieldName, Value s)] -> Value s
mkObject = Object . unsafeFromList . fmap toEntry
  where
    toEntry (key, value) = (key, ObjectEntry key value)

instance Hashable (Value a) where
  hashWithSalt s (Object x) = hashWithSalt s (0 :: Int, toList x)
  hashWithSalt s (List x) = hashWithSalt s (1 :: Int, x)
  hashWithSalt s (Enum x) = hashWithSalt s (2 :: Int, x)
  hashWithSalt s (Scalar x) = hashWithSalt s (3 :: Int, show x :: String)
  hashWithSalt s Null = hashWithSalt s (4 :: Int)
  hashWithSalt s (ResolvedVariable _ _) = hashWithSalt s (5 :: Int)
  hashWithSalt s (VariableValue Ref {refName}) = hashWithSalt s (6 :: Int, refName)

instance Hashable (ObjectEntry s) where
  hashWithSalt s (ObjectEntry name value) = hashWithSalt s (name, value)
