{-# LANGUAGE NamedFieldPuns    #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators     #-}

module Data.Morpheus.Schema.Internal.RenderIntrospection
  ( Type
  , Field
  , InputValue
  , renderType
  , createObjectType
  ) where

import           Data.Morpheus.Schema.EnumValue    (EnumValue, createEnumValue)
import qualified Data.Morpheus.Schema.Field        as F (Field (..), createFieldWith)
import qualified Data.Morpheus.Schema.InputValue   as IN (InputValue (..), createInputValueWith)
import           Data.Morpheus.Schema.Type         (Type (..))
import           Data.Morpheus.Schema.TypeKind     (TypeKind (..))
import           Data.Morpheus.Types.Internal.Data (DataField (..), DataFullType (..), DataInputField, DataInputObject,
                                                    DataLeaf (..), DataOutputField, DataType (..), DataTypeKind (..),
                                                    DataTypeLib, DataTypeWrapper (..), DataUnion, kindOf,
                                                    lookupDataType)
import           Data.Text                         (Text)

type InputValue = IN.InputValue Type

type Result a = DataTypeLib -> Either String a

type Field = F.Field Type

renderType :: (Text, DataFullType) -> Result Type
renderType (name', Leaf leaf') = const $ pure $ typeFromLeaf (name', leaf')
renderType (name', InputObject iObject') = renderInputObject (name', iObject')
renderType (name', OutputObject object') = typeFromObject (name', object')
  where
    typeFromObject (key, DataType {typeData, typeDescription}) lib =
      createObjectType key typeDescription <$>
      (Just <$>
       traverse
         (`fieldFromObjectField` lib)
         (filter (not . fieldHidden . snd) typeData))
renderType (name', Union union') = const $ pure $ typeFromUnion (name', union')
renderType (name', InputUnion inpUnion') = renderInputUnion (name', inpUnion')

renderTypeKind :: DataTypeKind -> TypeKind
renderTypeKind KindScalar      = SCALAR
renderTypeKind KindObject      = OBJECT
renderTypeKind KindUnion       = UNION
renderTypeKind KindInputUnion  = INPUT_OBJECT
renderTypeKind KindEnum        = ENUM
renderTypeKind KindInputObject = INPUT_OBJECT
renderTypeKind KindList        = LIST
renderTypeKind KindNonNull     = NON_NULL

wrap :: DataField a -> Type -> Type
wrap field' = wrapRec (fieldTypeWrappers field')

wrapRec :: [DataTypeWrapper] -> Type -> Type
wrapRec xs type' = foldr wrapByTypeWrapper type' xs

wrapByTypeWrapper :: DataTypeWrapper -> Type -> Type
wrapByTypeWrapper ListType    = wrapAs LIST
wrapByTypeWrapper NonNullType = wrapAs NON_NULL

lookupKind :: Text -> Result DataTypeKind
lookupKind name lib =
  case lookupDataType name lib of
    Nothing    -> Left ""
    Just value -> Right (kindOf value)

fieldFromObjectField :: (Text, DataOutputField) -> Result Field
fieldFromObjectField (key, field'@DataField {fieldType, fieldArgs}) lib = do
  kind <- renderTypeKind <$> lookupKind fieldType lib
  F.createFieldWith key (wrap field' $ createType kind fieldType "" $ Just []) <$>
    traverse (`inputValueFromArg` lib) fieldArgs

typeFromLeaf :: (Text, DataLeaf) -> Type
typeFromLeaf (key, BaseScalar DataType {typeDescription}) =
  createLeafType SCALAR key typeDescription Nothing
typeFromLeaf (key, CustomScalar DataType {typeDescription}) =
  createLeafType SCALAR key typeDescription Nothing
typeFromLeaf (key, LeafEnum DataType {typeDescription, typeData}) =
  createLeafType ENUM key typeDescription (Just $ map createEnumValue typeData)

createLeafType :: TypeKind -> Text -> Text -> Maybe [EnumValue] -> Type
createLeafType kind' name' desc' enums' =
  Type
    { kind = kind'
    , name = Just name'
    , description = Just desc'
    , fields = const $ return Nothing
    , ofType = Nothing
    , interfaces = Nothing
    , possibleTypes = Nothing
    , enumValues = const $ return enums'
    , inputFields = Nothing
    }

typeFromUnion :: (Text, DataUnion) -> Type
typeFromUnion (name', DataType { typeData = fields'
                               , typeDescription = description'
                               }) =
  Type
    { kind = UNION
    , name = Just name'
    , description = Just description'
    , fields = const $ return Nothing
    , ofType = Nothing
    , interfaces = Nothing
    , possibleTypes =
        Just (map (\x -> createObjectType (fieldType x) "" $ Just []) fields')
    , enumValues = const $ return Nothing
    , inputFields = Nothing
    }

inputValueFromArg :: (Text, DataInputField) -> Result InputValue
inputValueFromArg (key, input) =
  fmap (IN.createInputValueWith key) . createInputObjectType input

createInputObjectType :: DataInputField -> Result Type
createInputObjectType field@DataField {fieldType} lib = do
  kind <- renderTypeKind <$> lookupKind fieldType lib
  pure $ wrap field $ createType kind fieldType "" $ Just []

renderInputObject :: (Text, DataInputObject) -> Result Type
renderInputObject (key, DataType {typeData, typeDescription}) lib = do
  fields <- traverse (`inputValueFromArg` lib) typeData
  pure $ createInputObject key typeDescription fields

renderInputUnion :: (Text, DataUnion) -> Result Type
renderInputUnion (key', DataType {typeData, typeDescription}) lib =
  createInputObject key' typeDescription <$> traverse createField typeData
  where
    createField field =
      IN.createInputValueWith (fieldName field) <$>
      createInputObjectType field lib

createObjectType :: Text -> Text -> Maybe [Field] -> Type
createObjectType name' desc' fields' =
  Type
    { kind = OBJECT
    , name = Just name'
    , description = Just desc'
    , fields = const $ return fields'
    , ofType = Nothing
    , interfaces = Just []
    , possibleTypes = Nothing
    , enumValues = const $ return Nothing
    , inputFields = Nothing
    }

createInputObject :: Text -> Text -> [InputValue] -> Type
createInputObject name' desc' fields' =
  Type
    { kind = INPUT_OBJECT
    , name = Just name'
    , description = Just desc'
    , fields = const $ return Nothing
    , ofType = Nothing
    , interfaces = Nothing
    , possibleTypes = Nothing
    , enumValues = const $ return Nothing
    , inputFields = Just fields'
    }

createType :: TypeKind -> Text -> Text -> Maybe [Field] -> Type
createType kind' name' desc' fields' =
  Type
    { kind = kind'
    , name = Just name'
    , description = Just desc'
    , fields = const $ return fields'
    , ofType = Nothing
    , interfaces = Nothing
    , possibleTypes = Nothing
    , enumValues = const $ return $ Just []
    , inputFields = Nothing
    }

wrapAs :: TypeKind -> Type -> Type
wrapAs kind' contentType =
  Type
    { kind = kind'
    , name = Nothing
    , description = Nothing
    , fields = const $ return Nothing
    , ofType = Just contentType
    , interfaces = Nothing
    , possibleTypes = Nothing
    , enumValues = const $ return Nothing
    , inputFields = Nothing
    }