{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}

module Data.Morpheus.Validation.Query.Variable
  ( resolveOperationVariables
  )
where


import qualified Data.Map                      as M
                                                ( lookup )
import           Data.Maybe                     ( maybe )
import           Data.Semigroup                 ( (<>) )

--- MORPHEUS
import           Data.Morpheus.Error.Variable   ( uninitializedVariable)
import           Data.Morpheus.Types.Internal.AST
                                                ( DefaultValue
                                                , Operation(..)
                                                , Variable(..)
                                                , VariableDefinitions
                                                , Fragment(..)
                                                , Argument(..)
                                                , Selection(..)
                                                , SelectionContent(..)
                                                , SelectionSet
                                                , Ref(..)
                                                , TypeDefinition
                                                , Variables
                                                , Value(..)
                                                , ValidValue
                                                , RawValue
                                                , ResolvedValue
                                                , VALID
                                                , RAW
                                                , VariableContent(..)
                                                , isNullable
                                                , TypeRef(..)
                                                , VALIDATION_MODE(..)
                                                , ObjectEntry(..)
                                                )
import           Data.Morpheus.Types.Internal.Operation
                                                ( Listable(..)
                                                , Failure(..)
                                                )
import           Data.Morpheus.Types.Internal.Validation
                                                ( BaseValidator
                                                , askSchema
                                                , askFragments
                                                , selectKnown
                                                , constraint
                                                , Constraint(..)
                                                , withScopePosition
                                                , startInput
                                                , InputSource(..)
                                                , checkUnused
                                                )
import           Data.Morpheus.Validation.Internal.Value
                                                ( validateInput )

class ExploreRefs a where
  exploreRefs :: a -> [Ref]

instance ExploreRefs RawValue where
  exploreRefs (VariableValue ref   ) = [ref]
  exploreRefs (Object        fields) = concatMap (exploreRefs . entryValue) fields
  exploreRefs (List          ls    ) = concatMap exploreRefs ls
  exploreRefs _                      = []

instance ExploreRefs (Argument RAW) where
  exploreRefs = exploreRefs . argumentValue

mapSelection :: (Selection RAW -> BaseValidator [b]) -> SelectionSet RAW -> BaseValidator [b]
mapSelection f = fmap concat . traverse f

allVariableRefs :: [SelectionSet RAW] -> BaseValidator [Ref]
allVariableRefs = fmap concat . traverse (mapSelection searchRefs)
 where
  -- | search used variables in every arguments
  searchRefs :: Selection RAW -> BaseValidator [Ref]
  searchRefs Selection { selectionArguments, selectionContent = SelectionField }
    = return $ concatMap exploreRefs selectionArguments
  searchRefs Selection { selectionArguments, selectionContent = SelectionSet selSet }
    = getArgs <$> mapSelection searchRefs selSet
   where
    getArgs :: [Ref] -> [Ref]
    getArgs x = concatMap exploreRefs selectionArguments <> x
  searchRefs (InlineFragment Fragment { fragmentSelection })
    = mapSelection searchRefs fragmentSelection
  searchRefs (Spread reference)
    = askFragments
      >>= selectKnown reference
      >>= mapSelection searchRefs
      .   fragmentSelection

resolveOperationVariables
  :: Variables
  -> VALIDATION_MODE
  -> Operation RAW
  -> BaseValidator (VariableDefinitions VALID)
resolveOperationVariables
    root
    validationMode
    Operation
      { operationSelection
      , operationArguments
      }
  = checkUnusedVariables *>
    traverse (lookupAndValidateValueOnBody root validationMode) operationArguments
 where
  checkUnusedVariables :: BaseValidator ()
  checkUnusedVariables = do
    uses <- allVariableRefs [operationSelection]
    checkUnused uses (toList operationArguments)

lookupAndValidateValueOnBody
  :: Variables
  -> VALIDATION_MODE
  -> Variable RAW
  -> BaseValidator (Variable VALID)
lookupAndValidateValueOnBody
  bodyVariables
  validationMode
  var@Variable {
      variableName,
      variableType,
      variablePosition,
      variableValue = DefaultValue defaultValue
    }
  = withScopePosition variablePosition
      $ toVariable
      <$> ( askSchema
          >>= selectKnown (Ref (typeConName variableType) variablePosition)
          >>= constraint INPUT var
          >>= checkType getVariable defaultValue
        )
 where
  toVariable x = var { variableValue = ValidVariableValue x }
  getVariable :: Maybe ResolvedValue
  getVariable = M.lookup variableName bodyVariables
  ------------------------------------------------------------------
  -- checkType :: 
  checkType
    :: Maybe ResolvedValue
    -> DefaultValue
    -> TypeDefinition
    -> BaseValidator ValidValue
  checkType (Just variable) Nothing varType = validator varType variable
  checkType (Just variable) (Just defValue) varType =
    validator varType defValue >> validator varType variable
  checkType Nothing (Just defValue) varType = validator varType defValue
  checkType Nothing Nothing varType
    | validationMode /= WITHOUT_VARIABLES && not (isNullable variableType)
    = failure $ uninitializedVariable var
    | otherwise
    = returnNull
   where
    returnNull =
      maybe (pure Null) (validator varType) (M.lookup variableName bodyVariables)
  -----------------------------------------------------------------------------------------------
  validator :: TypeDefinition -> ResolvedValue -> BaseValidator ValidValue
  validator varType varValue
    = startInput (SourceVariable var)
        $ validateInput
          (typeWrappers variableType)
          varType
          (ObjectEntry variableName varValue)