{-# LANGUAGE TupleSections          #-}
{-# LANGUAGE ConstraintKinds        #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE DefaultSignatures      #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE NamedFieldPuns         #-}
{-# LANGUAGE OverloadedStrings      #-}
{-# LANGUAGE PolyKinds              #-}
{-# LANGUAGE RankNTypes             #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TypeApplications       #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE UndecidableInstances   #-}
{-# LANGUAGE RecordWildCards        #-}

module Data.Morpheus.Execution.Server.Introspect
  ( TypeUpdater
  , Introspect(..)
  , IntrospectRep(..)
  , IntroCon
  , updateLib
  , buildType
  , introspectObjectFields
  , TypeScope(..)
  )
where

import           Data.Map                       ( Map )
import           Data.Proxy                     ( Proxy(..) )
import           Data.Set                       ( Set )
import           Data.Text                      ( Text
                                                , pack
                                                )
import           GHC.Generics
import           Data.Semigroup                 ( (<>) )
import           Data.List                      ( partition )

-- MORPHEUS
import           Data.Morpheus.Error.Utils      ( globalErrorMessage )
import           Data.Morpheus.Error.Schema     ( nameCollisionError )
import           Data.Morpheus.Execution.Server.Generics.EnumRep
                                                ( EnumRep(..) )
import           Data.Morpheus.Kind             ( Context(..)
                                                , ENUM
                                                , GQL_KIND
                                                , SCALAR
                                                , OUTPUT
                                                , INPUT
                                                )
import           Data.Morpheus.Types.Types      ( MapKind
                                                , Pair
                                                )
import           Data.Morpheus.Types.GQLScalar  ( GQLScalar(..) )
import           Data.Morpheus.Types.GQLType    ( GQLType(..) )
import           Data.Morpheus.Types.Internal.Resolving
                                                ( Failure(..)
                                                , resolveUpdates
                                                , Resolver
                                                )
import           Data.Morpheus.Types.Internal.AST
                                                ( Name
                                                , ArgumentsDefinition(..)
                                                , Meta(..)
                                                , FieldDefinition(..)
                                                , TypeContent(..)
                                                , TypeDefinition(..)
                                                , Key
                                                , createAlias
                                                , defineType
                                                , isTypeDefined
                                                , toListField
                                                , toNullableField
                                                , createEnumValue
                                                , TypeUpdater
                                                , DataFingerprint(..)
                                                , DataUnion
                                                , FieldsDefinition(..)
                                                , InputFieldsDefinition(..)
                                                , TypeRef(..)
                                                , Message
                                                , unsafeFromFields
                                                )
import           Data.Morpheus.Types.Internal.Operation
                                                ( Empty(..)
                                                , Singleton(..)
                                                )

type IntroCon a = (GQLType a, IntrospectRep (CUSTOM a) a)


-- |  Generates internal GraphQL Schema for query validation and introspection rendering
class Introspect a where
  isObject :: proxy a -> Bool
  default isObject :: GQLType a => proxy a -> Bool
  isObject _ = isObjectKind (Proxy @a)
  field :: proxy a -> Text -> FieldDefinition
  introspect :: proxy a -> TypeUpdater
  -----------------------------------------------
  default field :: GQLType a =>
    proxy a -> Text -> FieldDefinition
  field _ = buildField (Proxy @a) NoArguments

instance {-# OVERLAPPABLE #-} (GQLType a, IntrospectKind (KIND a) a) => Introspect a where
  introspect _ = introspectKind (Context :: Context (KIND a) a)

-- Maybe
instance Introspect a => Introspect (Maybe a) where
  isObject _ = False
  field _ = toNullableField . field (Proxy @a)
  introspect _ = introspect (Proxy @a)

-- List
instance Introspect a => Introspect [a] where
  isObject _ = False
  field _ = toListField . field (Proxy @a)
  introspect _ = introspect (Proxy @a)

-- Tuple
instance Introspect (Pair k v) => Introspect (k, v) where
  isObject _ = True
  field _ = field (Proxy @(Pair k v))
  introspect _ = introspect (Proxy @(Pair k v))

-- Set
instance Introspect [a] => Introspect (Set a) where
  isObject _ = False
  field _ = field (Proxy @[a])
  introspect _ = introspect (Proxy @[a])

-- Map
instance Introspect (MapKind k v Maybe) => Introspect (Map k v) where
  isObject _ = True
  field _ = field (Proxy @(MapKind k v Maybe))
  introspect _ = introspect (Proxy @(MapKind k v Maybe))

-- Resolver : a -> Resolver b
instance (GQLType b, IntrospectRep 'False a, Introspect b) => Introspect (a -> m b) where
  isObject _ = False
  field _ name = fieldObj { fieldArgs }
   where
    fieldObj  = field (Proxy @b) name
    fieldArgs = ArgumentsDefinition Nothing $ unFieldsDefinition $ fst  $ introspectObjectFields
      (Proxy :: Proxy 'False)
      (__typeName (Proxy @b), OutputType, Proxy @a)
  introspect _ typeLib = resolveUpdates typeLib
                                        (introspect (Proxy @b) : inputs)
   where
    name = "Arguments for " <> __typeName (Proxy @b)
    inputs :: [TypeUpdater]
    inputs =
      snd $ introspectObjectFields (Proxy :: Proxy 'False) (name, InputType, Proxy @a)

--  GQL Resolver b, MUTATION, SUBSCRIPTION, QUERY
instance (GQLType b, Introspect b) => Introspect (Resolver fo e m b) where
  isObject _ = False
  field _ = field (Proxy @b)
  introspect _ = introspect (Proxy @b)


-- | Introspect With specific Kind: 'kind': object, scalar, enum ...
class IntrospectKind (kind :: GQL_KIND) a where
  introspectKind :: Context kind a -> TypeUpdater -- Generates internal GraphQL Schema

-- SCALAR
instance (GQLType a, GQLScalar a) => IntrospectKind SCALAR a where
  introspectKind _ = updateLib scalarType [] (Proxy @a)
    where scalarType = buildType $ DataScalar $ scalarValidator (Proxy @a)

-- ENUM
instance (GQL_TYPE a, EnumRep (Rep a)) => IntrospectKind ENUM a where
  introspectKind _ = updateLib enumType [] (Proxy @a)
   where
    enumType =
      buildType $ DataEnum $ map createEnumValue $ enumTags (Proxy @(Rep a))

instance (GQL_TYPE a, IntrospectRep (CUSTOM a) a) => IntrospectKind INPUT a where
  introspectKind _ = derivingData (Proxy @a) InputType

instance (GQL_TYPE a, IntrospectRep (CUSTOM a) a) => IntrospectKind OUTPUT a where
  introspectKind _ = derivingData (Proxy @a) OutputType

derivingData
  :: forall a
   . (GQLType a, IntrospectRep (CUSTOM a) a)
  => Proxy a
  -> TypeScope
  -> TypeUpdater
derivingData _ scope = updateLib (buildType datatypeContent) updates (Proxy @a)
 where
  (datatypeContent, updates) = introspectRep
    (Proxy @(CUSTOM a))
    (Proxy @a, scope, baseName, baseFingerprint)
  baseName        = __typeName (Proxy @a)
  baseFingerprint = __typeFingerprint (Proxy @a)

type GQL_TYPE a = (Generic a, GQLType a)

fromInput :: InputFieldsDefinition -> FieldsDefinition
fromInput = FieldsDefinition . unInputFieldsDefinition

toInput :: FieldsDefinition -> InputFieldsDefinition
toInput = InputFieldsDefinition . unFieldsDefinition

introspectObjectFields
  :: IntrospectRep custom a
  => proxy1 (custom :: Bool)
  -> (Name, TypeScope, proxy2 a)
  -> (FieldsDefinition, [TypeUpdater])
introspectObjectFields p1 (name, scope, proxy) = withObject
  (introspectRep p1 (proxy, scope, "", DataFingerprint "" []))
 where
  withObject (DataObject     {objectFields}, ts) = (objectFields, ts)
  withObject (DataInputObject x, ts) = (fromInput x, ts)
  withObject _ = (empty, [introspectFailure (name <> " should have only one nonempty constructor")])

introspectFailure :: Message -> TypeUpdater
introspectFailure = const . failure . globalErrorMessage . ("invalid schema: " <>)

-- Object Fields
class IntrospectRep (custom :: Bool) a where
  introspectRep :: proxy1 custom -> ( proxy2 a,TypeScope,Name,DataFingerprint) -> (TypeContent, [TypeUpdater])

instance (TypeRep (Rep a) , Generic a) => IntrospectRep 'False a where
  introspectRep _ (_, scope, name, fing) =
    derivingDataContent (Proxy @a) (name, fing) scope

buildField :: GQLType a => Proxy a -> ArgumentsDefinition -> Text -> FieldDefinition
buildField proxy fieldArgs fieldName = FieldDefinition
  { fieldType     = createAlias $ __typeName proxy
  , fieldMeta     = Nothing
  , ..
  }

buildType :: GQLType a => TypeContent -> Proxy a -> TypeDefinition
buildType typeContent proxy = TypeDefinition
  { typeName        = __typeName proxy
  , typeFingerprint = __typeFingerprint proxy
  , typeMeta        = Just Meta { metaDescription = description proxy
                                , metaDirectives  = []
                                }
  , typeContent
  }

updateLib
  :: GQLType a
  => (Proxy a -> TypeDefinition)
  -> [TypeUpdater]
  -> Proxy a
  -> TypeUpdater
updateLib typeBuilder stack proxy lib =
  case isTypeDefined (__typeName proxy) lib of
    Nothing -> resolveUpdates
      (defineType (typeBuilder proxy) lib)
      stack
    Just fingerprint' | fingerprint' == __typeFingerprint proxy -> return lib
    -- throw error if 2 different types has same name
    Just _ -> failure $ nameCollisionError (__typeName proxy)


-- NEW AUTOMATIC DERIVATION SYSTEM

data ConsRep = ConsRep {
  consName :: Key,
  consIsRecord :: Bool,
  consFields :: [FieldRep]
}

data FieldRep = FieldRep {
  fieldTypeName :: Name,
  fieldData :: FieldDefinition,
  fieldTypeUpdater :: TypeUpdater,
  fieldIsObject :: Bool
}

data ResRep = ResRep {
  enumCons :: [Name],
  unionRef :: [Name],
  unionRecordRep :: [ConsRep ]
}

isEmpty :: ConsRep -> Bool
isEmpty ConsRep { consFields = [] } = True
isEmpty _                           = False

isUnionRef :: Name -> ConsRep  -> Bool
isUnionRef baseName ConsRep { consName, consFields = [FieldRep { fieldIsObject = True, fieldTypeName }] }
  = consName == baseName <> fieldTypeName
isUnionRef _ _ = False

setFieldNames :: ConsRep -> ConsRep
setFieldNames cons@ConsRep { consFields } = cons
  { consFields = zipWith setFieldName ([0 ..] :: [Int]) consFields
  }
 where
  setFieldName i fieldR@FieldRep { fieldData = fieldD } = fieldR { fieldData = fieldD { fieldName } }
    where fieldName = "_" <> pack (show i)

analyseRep :: Name -> [ConsRep] -> ResRep
analyseRep baseName cons = ResRep
  { enumCons       = map consName enumRep
  , unionRef       = map fieldTypeName $ concatMap consFields unionRefRep
  , unionRecordRep = unionRecordRep <> map setFieldNames anyonimousUnionRep
  }
 where
  (enumRep       , left1             ) = partition isEmpty cons
  (unionRefRep   , left2             ) = partition (isUnionRef baseName) left1
  (unionRecordRep, anyonimousUnionRep) = partition consIsRecord left2

derivingDataContent
  :: forall a
   . (Generic a, TypeRep (Rep a))
  => Proxy a
  -> (Name, DataFingerprint)
  -> TypeScope
  -> (TypeContent, [TypeUpdater])
derivingDataContent _ (baseName, baseFingerprint) scope =
  builder $ typeRep $ Proxy @(Rep a)
 where
  builder [ConsRep { consFields }] = buildObject scope consFields
  builder cons                     = genericUnion scope cons
   where
    genericUnion InputType = buildInputUnion (baseName, baseFingerprint)
    genericUnion OutputType =
      buildUnionType (baseName, baseFingerprint) DataUnion (DataObject [])


buildInputUnion
  :: (Name, DataFingerprint) -> [ConsRep ] -> (TypeContent, [TypeUpdater])
buildInputUnion (baseName, baseFingerprint) cons = datatype
  (analyseRep baseName cons)
 where
  datatype ResRep { unionRef = [], unionRecordRep = [], enumCons } =
    (DataEnum (map createEnumValue enumCons), types)
  datatype ResRep { unionRef, unionRecordRep, enumCons } =
    (DataInputUnion typeMembers, types <> unionTypes)
   where
    typeMembers =
      map (, True) (unionRef <> unionMembers) <> map (, False) enumCons
    (unionMembers, unionTypes) =
      buildUnions (DataInputObject . toInput) baseFingerprint unionRecordRep
  types = map fieldTypeUpdater $ concatMap consFields cons

buildUnionType
  :: (Name, DataFingerprint)
  -> (DataUnion -> TypeContent)
  -> (FieldsDefinition -> TypeContent)
  -> [ConsRep]
  -> (TypeContent, [TypeUpdater])
buildUnionType (baseName, baseFingerprint) wrapUnion wrapObject cons = datatype
  (analyseRep baseName cons)
 where
  datatype ResRep { unionRef = [], unionRecordRep = [], enumCons } =
    (DataEnum (map createEnumValue enumCons), types)
  datatype ResRep { unionRef, unionRecordRep, enumCons } =
    (wrapUnion typeMembers, types <> enumTypes <> unionTypes)
   where
    typeMembers = unionRef <> enumMembers <> unionMembers
    (enumMembers, enumTypes) =
      buildUnionEnum wrapObject baseName baseFingerprint enumCons
    (unionMembers, unionTypes) =
      buildUnions wrapObject baseFingerprint unionRecordRep
  types = map fieldTypeUpdater $ concatMap consFields cons


buildObject :: TypeScope -> [FieldRep] -> (TypeContent, [TypeUpdater])
buildObject isOutput consFields = (wrapWith fields, types)
 where
  (fields, types) = buildDataObject consFields
  wrapWith | isOutput == OutputType = DataObject []
           | otherwise              = DataInputObject . toInput

buildDataObject :: [FieldRep] -> (FieldsDefinition , [TypeUpdater])
buildDataObject consFields = (fields, types)
 where
  fields = unsafeFromFields $ map fieldData consFields
  types  = map fieldTypeUpdater consFields

buildUnions
  :: (FieldsDefinition -> TypeContent)
  -> DataFingerprint
  -> [ConsRep]
  -> ([Name], [TypeUpdater])
buildUnions wrapObject baseFingerprint cons = (members, map buildURecType cons)
 where
  buildURecType consRep = pure . defineType
      (buildUnionRecord wrapObject baseFingerprint consRep)
  members = map consName cons

buildUnionRecord
  :: (FieldsDefinition -> TypeContent) -> DataFingerprint -> ConsRep -> TypeDefinition
buildUnionRecord wrapObject typeFingerprint ConsRep { consName, consFields } = TypeDefinition
    { typeName        = consName
    , typeFingerprint
    , typeMeta        = Nothing
    , typeContent     = wrapObject $ unsafeFromFields $ map fieldData consFields
    }

buildUnionEnum
  :: (FieldsDefinition -> TypeContent)
  -> Name
  -> DataFingerprint
  -> [Name]
  -> ([Name], [TypeUpdater])
buildUnionEnum wrapObject baseName baseFingerprint enums = (members, updates)
 where
  members | null enums = []
          | otherwise  = [enumTypeWrapperName]
  enumTypeName        = baseName <> "Enum"
  enumTypeWrapperName = enumTypeName <> "Object"
  -------------------------
  updates :: [TypeUpdater]
  updates
    | null enums
    = []
    | otherwise
    = [ buildEnumObject wrapObject
                        enumTypeWrapperName
                        baseFingerprint
                        enumTypeName
      , buildEnum enumTypeName baseFingerprint enums
      ]

buildEnum :: Name -> DataFingerprint -> [Name] -> TypeUpdater
buildEnum typeName typeFingerprint tags = pure . defineType
  TypeDefinition
    { typeMeta        = Nothing
    , typeContent     = DataEnum $ map createEnumValue tags
    , ..
    }

buildEnumObject
  :: (FieldsDefinition -> TypeContent)
  -> Name
  -> DataFingerprint
  -> Name
  -> TypeUpdater
buildEnumObject wrapObject typeName typeFingerprint enumTypeName =
  pure . defineType
    TypeDefinition
      { typeName
      , typeFingerprint
      , typeMeta        = Nothing
      , typeContent     = wrapObject $ singleton
          FieldDefinition
            { fieldName  = "enum"
            , fieldArgs  = NoArguments
            , fieldType  = createAlias enumTypeName
            , fieldMeta  = Nothing
            }
      }

data TypeScope = InputType | OutputType deriving (Show,Eq,Ord)

--  GENERIC UNION
class TypeRep f where
  typeRep :: Proxy f -> [ConsRep]

instance TypeRep f => TypeRep (M1 D d f) where
  typeRep _ = typeRep (Proxy @f)

-- | recursion for Object types, both of them : 'INPUT_OBJECT' and 'OBJECT'
instance (TypeRep a, TypeRep b) => TypeRep (a :+: b) where
  typeRep _ = typeRep (Proxy @a) <> typeRep (Proxy @b)

instance (ConRep f, Constructor c) => TypeRep (M1 C c f) where
  typeRep _ =
    [ ConsRep { consName     = pack $ conName (undefined :: (M1 C c f a))
              , consFields   = conRep (Proxy @f)
              , consIsRecord = conIsRecord (undefined :: (M1 C c f a))
              }
    ]

class ConRep f where
    conRep :: Proxy f -> [FieldRep]

-- | recursion for Object types, both of them : 'UNION' and 'INPUT_UNION'
instance (ConRep  a, ConRep  b) => ConRep  (a :*: b) where
  conRep _ = conRep (Proxy @a) <> conRep (Proxy @b)

instance (Selector s, Introspect a) => ConRep (M1 S s (Rec0 a)) where
  conRep _ =
    [ FieldRep { fieldTypeName    = typeConName $ fieldType fieldData
               , fieldData        = fieldData
               , fieldTypeUpdater = introspect (Proxy @a)
               , fieldIsObject    = isObject (Proxy @a)
               }
    ]
   where
    name      = pack $ selName (undefined :: M1 S s (Rec0 ()) ())
    fieldData = field (Proxy @a) name

instance ConRep U1 where
  conRep _ = []