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

module Data.Morpheus.Rendering.RenderIntrospection
  ( render
  , createObjectType
  ) where

import           Control.Monad.Fail                 (MonadFail)
import           Data.Semigroup                     ((<>))
import           Data.Text                          (Text, unpack)

import           Data.Morpheus.Schema.Schema

-- Morpheus
import           Data.Morpheus.Schema.TypeKind      (TypeKind (..))
import           Data.Morpheus.Types.Internal.Data  (DataField (..), DataObject, DataTyCon (..), DataType (..),
                                                     DataTypeKind (..), DataTypeLib, DataTypeWrapper (..), DataUnion,
                                                     TypeAlias (..), kindOf, lookupDataType, toGQLWrapper)
import           Data.Morpheus.Types.Internal.Value (convertToJSONName)

constRes :: Applicative m => a -> b -> m a
constRes = const . pure

type Result m a = DataTypeLib -> m a

class RenderSchema a b where
  render :: (Monad m, MonadFail m) => (Text, a) -> DataTypeLib -> m (b m)

instance RenderSchema DataType S__Type where
  render (key, DataScalar DataTyCon {typeDescription})  =
    constRes $ createLeafType SCALAR key typeDescription Nothing
  render (key, DataEnum DataTyCon {typeDescription, typeData})  =
    constRes $ createLeafType ENUM key typeDescription (Just $ map createEnumValue typeData)
  render (name, DataInputObject iObject) = renderInputObject (name, iObject)
  render (name, DataObject object') = typeFromObject (name, object')
    where
      typeFromObject (key, DataTyCon {typeData, typeDescription}) lib =
        createObjectType key typeDescription <$>
        (Just <$> traverse (`render` lib) (filter (not . fieldHidden . snd) typeData))
  render (name, DataUnion union') = const $ pure $ typeFromUnion (name, union')
  render (name, DataInputUnion inpUnion') = renderInputUnion (name, inpUnion')

instance RenderSchema DataField S__Field where
  render (key, field@DataField {fieldType = TypeAlias {aliasTyCon}, fieldArgs}) lib = do
    kind <- renderTypeKind <$> lookupKind aliasTyCon lib
    createFieldWith key (wrap field $ createType kind aliasTyCon Nothing $ Just []) <$>
      traverse (`inputValueFromArg` lib) fieldArgs

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 :: Monad m => DataField -> S__Type m -> S__Type m
wrap DataField {fieldType = TypeAlias {aliasWrappers}} typ = foldr wrapByTypeWrapper typ (toGQLWrapper aliasWrappers)

wrapByTypeWrapper :: Monad m => DataTypeWrapper -> S__Type m -> S__Type m
wrapByTypeWrapper ListType    = wrapAs LIST
wrapByTypeWrapper NonNullType = wrapAs NON_NULL

lookupKind :: (Monad m, MonadFail m) => Text -> Result m DataTypeKind
lookupKind name lib =
  case lookupDataType name lib of
    Nothing    -> fail $ unpack ("Kind Not Found: " <> name)
    Just value -> pure (kindOf value)

inputValueFromArg :: (Monad m,MonadFail m) => (Text, DataField) -> Result m (S__InputValue m)
inputValueFromArg (key, input) = fmap (createInputValueWith key) . createInputObjectType input

createInputObjectType :: (Monad m, MonadFail m) => DataField -> Result m (S__Type m)
createInputObjectType field@DataField {fieldType = TypeAlias {aliasTyCon}} lib = do
  kind <- renderTypeKind <$> lookupKind aliasTyCon lib
  pure $ wrap field $ createType kind aliasTyCon Nothing $ Just []

renderInputObject :: (Monad m, MonadFail m) => (Text, DataObject) -> Result m (S__Type m)
renderInputObject (key, DataTyCon {typeData, typeDescription}) lib = do
  fields <- traverse (`inputValueFromArg` lib) typeData
  pure $ createInputObject key typeDescription fields

renderInputUnion :: (Monad m, MonadFail m) => (Text, DataUnion) -> Result m (S__Type m)
renderInputUnion (key', DataTyCon {typeData, typeDescription}) lib =
  createInputObject key' typeDescription <$> traverse createField typeData
  where
    createField field = createInputValueWith (fieldName field) <$> createInputObjectType field lib

createLeafType :: Monad m => TypeKind -> Text -> Maybe Text -> Maybe [S__EnumValue m] -> S__Type m
createLeafType kind name description enums =
  S__Type
    { s__TypeKind = constRes kind
    , s__TypeName = constRes $ Just name
    , s__TypeDescription = constRes description
    , s__TypeFields = constRes Nothing
    , s__TypeOfType = constRes Nothing
    , s__TypeInterfaces = constRes Nothing
    , s__TypePossibleTypes = constRes Nothing
    , s__TypeEnumValues = constRes enums
    , s__TypeInputFields = constRes Nothing
    }

typeFromUnion :: Monad m => (Text, DataUnion) -> S__Type m
typeFromUnion (name, DataTyCon {typeData, typeDescription}) =
  S__Type
    { s__TypeKind = constRes UNION
    , s__TypeName = constRes $ Just name
    , s__TypeDescription = constRes typeDescription
    , s__TypeFields = constRes Nothing
    , s__TypeOfType = constRes Nothing
    , s__TypeInterfaces = constRes Nothing
    , s__TypePossibleTypes =
        constRes $ Just (map (\x -> createObjectType (aliasTyCon $ fieldType x) Nothing $ Just []) typeData)
    , s__TypeEnumValues = constRes Nothing
    , s__TypeInputFields = constRes Nothing
    }

createObjectType :: Monad m => Text -> Maybe Text -> Maybe [S__Field m] -> S__Type m
createObjectType name description fields =
  S__Type
    { s__TypeKind = constRes OBJECT
    , s__TypeName = constRes $ Just name
    , s__TypeDescription = constRes description
    , s__TypeFields = constRes fields
    , s__TypeOfType = constRes Nothing
    , s__TypeInterfaces = constRes $ Just []
    , s__TypePossibleTypes = constRes Nothing
    , s__TypeEnumValues = constRes Nothing
    , s__TypeInputFields = constRes Nothing
    }

createInputObject :: Monad m => Text -> Maybe Text -> [S__InputValue m] -> S__Type m
createInputObject name description fields =
  S__Type
    { s__TypeKind = constRes INPUT_OBJECT
    , s__TypeName = constRes $ Just name
    , s__TypeDescription = constRes description
    , s__TypeFields = constRes Nothing
    , s__TypeOfType = constRes Nothing
    , s__TypeInterfaces = constRes Nothing
    , s__TypePossibleTypes = constRes Nothing
    , s__TypeEnumValues = constRes Nothing
    , s__TypeInputFields = constRes $ Just fields
    }

createType :: Monad m => TypeKind -> Text -> Maybe Text -> Maybe [S__Field m] -> S__Type m
createType kind name description fields' =
  S__Type
    { s__TypeKind = constRes kind
    , s__TypeName = constRes $ Just name
    , s__TypeDescription = constRes description
    , s__TypeFields = constRes fields'
    , s__TypeOfType = constRes Nothing
    , s__TypeInterfaces = constRes Nothing
    , s__TypePossibleTypes = constRes Nothing
    , s__TypeEnumValues = constRes $ Just []
    , s__TypeInputFields = constRes Nothing
    }

wrapAs :: Monad m => TypeKind -> S__Type m -> S__Type m
wrapAs kind contentType =
  S__Type
    { s__TypeKind = constRes kind
    , s__TypeName = constRes Nothing
    , s__TypeDescription = constRes Nothing
    , s__TypeFields = constRes Nothing
    , s__TypeOfType = constRes $ Just contentType
    , s__TypeInterfaces = constRes Nothing
    , s__TypePossibleTypes = constRes Nothing
    , s__TypeEnumValues = constRes Nothing
    , s__TypeInputFields = constRes Nothing
    }

createFieldWith :: Monad m => Text -> S__Type m -> [S__InputValue m] -> S__Field m
createFieldWith _name fieldType fieldArgs =
  S__Field
    { s__FieldName = constRes (convertToJSONName _name)
    , s__FieldDescription = constRes Nothing
    , s__FieldArgs = constRes fieldArgs
    , s__FieldType' = constRes fieldType
    , s__FieldIsDeprecated = constRes False
    , s__FieldDeprecationReason = constRes Nothing
    }

createInputValueWith :: Monad m => Text -> S__Type m -> S__InputValue m
createInputValueWith name ivType =
  S__InputValue
    { s__InputValueName = constRes (convertToJSONName name)
    , s__InputValueDescription = constRes Nothing
    , s__InputValueType' = constRes ivType
    , s__InputValueDefaultValue = constRes Nothing
    }

createEnumValue :: Monad m => Text -> S__EnumValue m
createEnumValue name =
  S__EnumValue
    { s__EnumValueName = constRes name
    , s__EnumValueDescription = constRes Nothing
    , s__EnumValueIsDeprecated = constRes False
    , s__EnumValueDeprecationReason = constRes Nothing
    }