package graphql.schema.idl;

import graphql.GraphQLError;
import graphql.Internal;
import graphql.language.Argument;
import graphql.language.DirectiveDefinition;
import graphql.language.EnumTypeDefinition;
import graphql.language.EnumValueDefinition;
import graphql.language.FieldDefinition;
import graphql.language.InputObjectTypeDefinition;
import graphql.language.InputObjectTypeExtensionDefinition;
import graphql.language.InputValueDefinition;
import graphql.language.InterfaceTypeDefinition;
import graphql.language.ObjectTypeDefinition;
import graphql.language.ScalarTypeDefinition;
import graphql.language.TypeDefinition;
import graphql.language.TypeName;
import graphql.language.UnionTypeDefinition;
import graphql.schema.idl.errors.MissingTypeError;
import graphql.schema.idl.errors.NonUniqueArgumentError;
import graphql.schema.idl.errors.NonUniqueNameError;
import graphql.schema.idl.errors.TypeExtensionEnumValueRedefinitionError;
import graphql.schema.idl.errors.TypeExtensionFieldRedefinitionError;
import graphql.schema.idl.errors.TypeExtensionMissingBaseTypeError;
import graphql.util.FpKit;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import static graphql.schema.idl.SchemaTypeChecker.checkNamedUniqueness;
import static graphql.util.FpKit.mergeFirst;

/**
 * A support class to help break up the large SchemaTypeChecker class.  This handles
 * the checking of "type extensions"
 */
@Internal
class SchemaTypeExtensionsChecker {

    void checkTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry) {
        Map<String, DirectiveDefinition> directiveDefinitionMap = typeRegistry.getDirectiveDefinitions();
        checkObjectTypeExtensions(errors, typeRegistry, directiveDefinitionMap);
        checkInterfaceTypeExtensions(errors, typeRegistry, directiveDefinitionMap);
        checkUnionTypeExtensions(errors, typeRegistry, directiveDefinitionMap);
        checkEnumTypeExtensions(errors, typeRegistry, directiveDefinitionMap);
        checkScalarTypeExtensions(errors, typeRegistry, directiveDefinitionMap);
        checkInputObjectTypeExtensions(errors, typeRegistry, directiveDefinitionMap);
    }


    /*
     * Object type extensions have the potential to be invalid if incorrectly defined.
     *
     * The named type must already be defined and must be an Object type.
     * The fields of an Object type extension must have unique names; no two fields may share the same name.
     * Any fields of an Object type extension must not be already defined on the original Object type.
     * Any directives provided must not already apply to the original Object type.
     * Any interfaces provided must not be already implemented by the original Object type.
     * The resulting extended object type must be a super-set of all interfaces it implements.
     */
    private void checkObjectTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, Map<String, DirectiveDefinition> directiveDefinitionMap) {
        typeRegistry.objectTypeExtensions()
                .forEach((name, extensions) -> {
                            checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, ObjectTypeDefinition.class);

                            extensions.forEach(extension -> {
                                List<FieldDefinition> fieldDefinitions = extension.getFieldDefinitions();
                                // field unique ness
                                checkNamedUniqueness(errors, extension.getFieldDefinitions(), FieldDefinition::getName,
                                        (namedField, fieldDef) -> new NonUniqueNameError(extension, fieldDef));

                                // field arg unique ness
                                extension.getFieldDefinitions().forEach(fld -> checkNamedUniqueness(errors, fld.getInputValueDefinitions(), InputValueDefinition::getName,
                                        (namedField, inputValueDefinition) -> new NonUniqueArgumentError(extension, fld, name)));

                                // directive checks
                                fieldDefinitions.forEach(fld -> fld.getDirectives().forEach(directive ->
                                        checkNamedUniqueness(errors, directive.getArguments(), Argument::getName,
                                                (argumentName, argument) -> new NonUniqueArgumentError(extension, fld, argumentName))));

                                // then check for field re-defs from the base type
                                ObjectTypeDefinition baseTypeDef = typeRegistry.getTypeOrNull(extension.getName(), ObjectTypeDefinition.class);
                                if (baseTypeDef != null) {
                                    checkForFieldRedefinition(errors, extension, fieldDefinitions, baseTypeDef.getFieldDefinitions());
                                }
                            });

                            // fields must be unique within a type extension
                            checkForTypeExtensionFieldUniqueness(
                                    errors,
                                    extensions,
                                    ObjectTypeDefinition::getFieldDefinitions
                            );

                        }
                );
    }

    /*
     * Interface type extensions have the potential to be invalid if incorrectly defined.
     *
     * The named type must already be defined and must be an Interface type.
     * The fields of an Interface type extension must have unique names; no two fields may share the same name.
     * Any fields of an Interface type extension must not be already defined on the original Interface type.
     * Any Object type which implemented the original Interface type must also be a super-set of the fields of the Interface type extension (which may be due to Object type extension).
     * Any directives provided must not already apply to the original Interface type.
     */
    private void checkInterfaceTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, Map<String, DirectiveDefinition> directiveDefinitionMap) {
        typeRegistry.interfaceTypeExtensions()
                .forEach((name, extensions) -> {
                    checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, InterfaceTypeDefinition.class);

                    extensions.forEach(extension -> {
                        List<FieldDefinition> fieldDefinitions = extension.getFieldDefinitions();
                        // field unique ness
                        checkNamedUniqueness(errors, extension.getFieldDefinitions(), FieldDefinition::getName,
                                (namedField, fieldDef) -> new NonUniqueNameError(extension, fieldDef));

                        // field arg unique ness
                        extension.getFieldDefinitions().forEach(fld -> checkNamedUniqueness(errors, fld.getInputValueDefinitions(), InputValueDefinition::getName,
                                (namedField, inputValueDefinition) -> new NonUniqueArgumentError(extension, fld, name)));

                        // directive checks
                        fieldDefinitions.forEach(fld -> fld.getDirectives().forEach(directive ->
                                checkNamedUniqueness(errors, directive.getArguments(), Argument::getName,
                                        (argumentName, argument) -> new NonUniqueArgumentError(extension, fld, argumentName))));

                        //
                        // then check for field re-defs from the base type
                        InterfaceTypeDefinition baseTypeDef = typeRegistry.getTypeOrNull(extension.getName(), InterfaceTypeDefinition.class);
                        if (baseTypeDef != null) {
                            checkForFieldRedefinition(errors, extension, fieldDefinitions, baseTypeDef.getFieldDefinitions());
                        }
                    });
                    // fields must be unique within a type extension
                    checkForTypeExtensionFieldUniqueness(
                            errors,
                            extensions,
                            InterfaceTypeDefinition::getFieldDefinitions
                    );
                });
    }

    /*
     * Union type extensions have the potential to be invalid if incorrectly defined.
     *
     * The named type must already be defined and must be a Union type.
     * The member types of a Union type extension must all be Object base types; Scalar, Interface and Union types must not be member types of a Union. Similarly, wrapping types must not be member types of a Union.
     * All member types of a Union type extension must be unique.
     * All member types of a Union type extension must not already be a member of the original Union type.
     * Any directives provided must not already apply to the original Union type.
     */
    private void checkUnionTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, Map<String, DirectiveDefinition> directiveDefinitionMap) {
        typeRegistry.unionTypeExtensions()
                .forEach((name, extensions) -> {
                    checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, UnionTypeDefinition.class);

                    extensions.forEach(extension -> {
                        List<TypeName> memberTypes = extension.getMemberTypes().stream()
                                .map(t -> TypeInfo.typeInfo(t).getTypeName()).collect(Collectors.toList());

                        checkNamedUniqueness(errors, memberTypes, TypeName::getName,
                                (namedMember, memberType) -> new NonUniqueNameError(extension, namedMember));

                        memberTypes.forEach(
                                memberType -> {
                                    ObjectTypeDefinition unionTypeDefinition = typeRegistry.getTypeOrNull(memberType, ObjectTypeDefinition.class);
                                    if (unionTypeDefinition == null) {
                                        errors.add(new MissingTypeError("union member", extension, memberType));
                                    }
                                }
                        );
                    });
                });
    }

    /*
     * Enum type extensions have the potential to be invalid if incorrectly defined.
     *
     * The named type must already be defined and must be an Enum type.
     * All values of an Enum type extension must be unique.
     * All values of an Enum type extension must not already be a value of the original Enum.
     * Any directives provided must not already apply to the original Enum type.
     */
    private void checkEnumTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, Map<String, DirectiveDefinition> directiveDefinitionMap) {
        typeRegistry.enumTypeExtensions()
                .forEach((name, extensions) -> {
                    checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, EnumTypeDefinition.class);

                    extensions.forEach(extension -> {
                        // field unique ness
                        List<EnumValueDefinition> enumValueDefinitions = extension.getEnumValueDefinitions();
                        checkNamedUniqueness(errors, enumValueDefinitions, EnumValueDefinition::getName,
                                (namedField, enumValue) -> new NonUniqueNameError(extension, enumValue));

                        //
                        // then check for field re-defs from the base type
                        EnumTypeDefinition baseTypeDef = typeRegistry.getTypeOrNull(extension.getName(), EnumTypeDefinition.class);
                        if (baseTypeDef != null) {
                            checkForEnumValueRedefinition(errors, extension, enumValueDefinitions, baseTypeDef.getEnumValueDefinitions());
                        }

                    });

                    checkForTypeExtensionEnumFieldUniqueness(errors, extensions, EnumTypeDefinition::getEnumValueDefinitions);
                });
    }

    /*
     * Scalar type extensions have the potential to be invalid if incorrectly defined.
     *
     * The named type must already be defined and must be a Scalar type.
     * Any directives provided must not already apply to the original Scalar type.
     */

    private void checkScalarTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, Map<String, DirectiveDefinition> directiveDefinitionMap) {
        typeRegistry.scalarTypeExtensions()
                .forEach((name, extensions) -> {
                    checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, ScalarTypeDefinition.class);
                });

    }

    /*
     * Input object type extensions have the potential to be invalid if incorrectly defined.
     *
     * The named type must already be defined and must be a Input Object type.
     * All fields of an Input Object type extension must have unique names.
     * All fields of an Input Object type extension must not already be a field of the original Input Object.
     * Any directives provided must not already apply to the original Input Object type.
     */
    private void checkInputObjectTypeExtensions(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, Map<String, DirectiveDefinition> directiveDefinitionMap) {
        typeRegistry.inputObjectTypeExtensions()
                .forEach((name, extensions) -> {
                    checkTypeExtensionHasCorrespondingType(errors, typeRegistry, name, extensions, InputObjectTypeDefinition.class);
                    // field redefinitions
                    extensions.forEach(extension -> {
                        List<InputValueDefinition> inputValueDefinitions = extension.getInputValueDefinitions();
                        // field unique ness
                        checkNamedUniqueness(errors, inputValueDefinitions, InputValueDefinition::getName,
                                (namedField, fieldDef) -> new NonUniqueNameError(extension, fieldDef));

                        // directive checks
                        inputValueDefinitions.forEach(fld -> fld.getDirectives().forEach(directive ->
                                checkNamedUniqueness(errors, directive.getArguments(), Argument::getName,
                                        (argumentName, argument) -> new NonUniqueArgumentError(extension, fld, argumentName))));
                        //
                        // then check for field re-defs from the base type
                        InputObjectTypeDefinition baseTypeDef = typeRegistry.getTypeOrNull(extension.getName(), InputObjectTypeDefinition.class);
                        if (baseTypeDef != null) {
                            checkForInputValueRedefinition(errors, extension, inputValueDefinitions, baseTypeDef.getInputValueDefinitions());
                        }
                    });
                    //
                    // fields must be unique within a type extension
                    checkForTypeExtensionInputFieldUniqueness(
                            errors,
                            extensions,
                            InputObjectTypeDefinition::getInputValueDefinitions
                    );

                });
    }


    private void checkTypeExtensionHasCorrespondingType(List<GraphQLError> errors, TypeDefinitionRegistry typeRegistry, String name, List<? extends TypeDefinition<?>> extTypeList, Class<? extends TypeDefinition<?>> targetClass) {
        TypeDefinition<?> extensionDefinition = extTypeList.get(0);
        TypeDefinition<?> typeDefinition = typeRegistry.getTypeOrNull(TypeName.newTypeName().name(name).build(), targetClass);
        if (typeDefinition == null) {
            errors.add(new TypeExtensionMissingBaseTypeError(extensionDefinition));
        }
    }

    private void checkForFieldRedefinition(List<GraphQLError> errors, TypeDefinition<?> typeDefinition, List<FieldDefinition> fieldDefinitions, List<FieldDefinition> referenceFieldDefinitions) {

        Map<String, FieldDefinition> referenceMap = FpKit.getByName(referenceFieldDefinitions, FieldDefinition::getName, mergeFirst());

        fieldDefinitions.forEach(fld -> {
            if (referenceMap.containsKey(fld.getName())) {
                errors.add(new TypeExtensionFieldRedefinitionError(typeDefinition, fld));
            }
        });
    }

    private void checkForInputValueRedefinition(List<GraphQLError> errors, InputObjectTypeExtensionDefinition typeDefinition, List<InputValueDefinition> inputValueDefinitions, List<InputValueDefinition> referenceInputValues) {
        Map<String, InputValueDefinition> referenceMap = FpKit.getByName(referenceInputValues, InputValueDefinition::getName, mergeFirst());

        inputValueDefinitions.forEach(fld -> {
            if (referenceMap.containsKey(fld.getName())) {
                errors.add(new TypeExtensionFieldRedefinitionError(typeDefinition, fld));
            }
        });
    }

    private void checkForEnumValueRedefinition(List<GraphQLError> errors, TypeDefinition<?> typeDefinition, List<EnumValueDefinition> enumValueDefinitions, List<EnumValueDefinition> referenceEnumValueDefinitions) {

        Map<String, EnumValueDefinition> referenceMap = FpKit.getByName(referenceEnumValueDefinitions, EnumValueDefinition::getName, mergeFirst());

        enumValueDefinitions.forEach(fld -> {
            if (referenceMap.containsKey(fld.getName())) {
                errors.add(new TypeExtensionEnumValueRedefinitionError(typeDefinition, fld));
            }
        });
    }

    private <T extends TypeDefinition<?>> void checkForTypeExtensionFieldUniqueness(
            List<GraphQLError> errors,
            List<T> extensions,
            Function<T, List<FieldDefinition>> getFieldDefinitionsFunc
    ) {
        Set<String> seenFields = new HashSet<>();

        for (T extension : extensions) {
            for (FieldDefinition field : getFieldDefinitionsFunc.apply(extension)) {
                if (seenFields.contains(field.getName())) {
                    errors.add(new TypeExtensionFieldRedefinitionError(extension, field));
                } else {
                    seenFields.add(field.getName());
                }
            }
        }
    }

    private <T extends TypeDefinition<?>> void checkForTypeExtensionInputFieldUniqueness(
            List<GraphQLError> errors,
            List<T> extensions,
            Function<T, List<InputValueDefinition>> getFieldDefinitionsFunc
    ) {
        Set<String> seenFields = new HashSet<>();

        for (T extension : extensions) {
            for (InputValueDefinition field : getFieldDefinitionsFunc.apply(extension)) {
                if (seenFields.contains(field.getName())) {
                    errors.add(new TypeExtensionFieldRedefinitionError(extension, field));
                } else {
                    seenFields.add(field.getName());
                }
            }
        }
    }

    private <T extends TypeDefinition<?>> void checkForTypeExtensionEnumFieldUniqueness(
            List<GraphQLError> errors,
            List<T> extensions,
            Function<T, List<EnumValueDefinition>> getFieldDefinitionsFunc
    ) {
        Set<String> seenFields = new HashSet<>();

        for (T extension : extensions) {
            for (EnumValueDefinition field : getFieldDefinitionsFunc.apply(extension)) {
                if (seenFields.contains(field.getName())) {
                    errors.add(new TypeExtensionEnumValueRedefinitionError(extension, field));
                } else {
                    seenFields.add(field.getName());
                }
            }
        }
    }
}
