package graphql.util.querygenerator;

import graphql.schema.FieldCoordinates;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLFieldsContainer;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import graphql.schema.GraphQLTypeUtil;
import graphql.schema.GraphQLUnionType;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

class QueryGeneratorFieldSelection {
    private final QueryGeneratorOptions options;
    private final GraphQLSchema schema;

    private static final GraphQLObjectType EMPTY_OBJECT_TYPE = GraphQLObjectType.newObject()
            .name("Empty")
            .build();

    QueryGeneratorFieldSelection(GraphQLSchema schema, QueryGeneratorOptions options) {
        this.options = options;
        this.schema = schema;
    }

    FieldSelectionResult buildFields(GraphQLFieldsContainer fieldsContainer) {
        Queue<List<GraphQLFieldsContainer>> containersQueue = new LinkedList<>();
        containersQueue.add(Collections.singletonList(fieldsContainer));

        Queue<FieldSelection> fieldSelectionQueue = new LinkedList<>();
        FieldSelection root = new FieldSelection(fieldsContainer.getName(), new HashMap<>(), false);
        fieldSelectionQueue.add(root);

        Set<FieldCoordinates> visited = new HashSet<>();
        AtomicInteger totalFieldCount = new AtomicInteger(0);
        boolean reachedMaxFieldCount = false;

        while (!reachedMaxFieldCount &&!containersQueue.isEmpty()) {
            processContainers(containersQueue, fieldSelectionQueue, visited, totalFieldCount);

            if (totalFieldCount.get() >= options.getMaxFieldCount()) {
                reachedMaxFieldCount = true;
            }
        }

        return new FieldSelectionResult(
                root,
                totalFieldCount.get(),
                reachedMaxFieldCount
        );
    }

    private void processContainers(Queue<List<GraphQLFieldsContainer>> containersQueue,
                                   Queue<FieldSelection> fieldSelectionQueue,
                                   Set<FieldCoordinates> visited,
                                   AtomicInteger totalFieldCount) {
        List<GraphQLFieldsContainer> containers = containersQueue.poll();
        FieldSelection fieldSelection = fieldSelectionQueue.poll();

        for (GraphQLFieldsContainer container : Objects.requireNonNull(containers)) {
            if (!options.getFilterFieldContainerPredicate().test(container)) {
                continue;
            }

            for (GraphQLFieldDefinition fieldDef : container.getFieldDefinitions()) {
                if (!options.getFilterFieldDefinitionPredicate().test(fieldDef)) {
                    continue;
                }

                if (totalFieldCount.get() >= options.getMaxFieldCount()) {
                    break;
                }

                if (hasRequiredArgs(fieldDef)) {
                    continue;
                }

                FieldCoordinates fieldCoordinates = FieldCoordinates.coordinates(container, fieldDef.getName());

                if (visited.contains(fieldCoordinates)) {
                    continue;
                }

                processField(
                        container,
                        fieldDef,
                        Objects.requireNonNull(fieldSelection),
                        containersQueue,
                        fieldSelectionQueue,
                        fieldCoordinates,
                        visited,
                        totalFieldCount
                );
            }
        }
    }

    private void processField(GraphQLFieldsContainer container,
                              GraphQLFieldDefinition fieldDef,
                              FieldSelection fieldSelection,
                              Queue<List<GraphQLFieldsContainer>> containersQueue,
                              Queue<FieldSelection> fieldSelectionQueue,
                              FieldCoordinates fieldCoordinates,
                              Set<FieldCoordinates> visited,
                              AtomicInteger totalFieldCount) {

        GraphQLType unwrappedType = GraphQLTypeUtil.unwrapAll(fieldDef.getType());
        FieldSelection newFieldSelection = getFieldSelection(fieldDef, unwrappedType);

        fieldSelection.fieldsByContainer.computeIfAbsent(container.getName(), key -> new ArrayList<>()).add(newFieldSelection);

        fieldSelectionQueue.add(newFieldSelection);

        if (unwrappedType instanceof GraphQLInterfaceType) {
            visited.add(fieldCoordinates);
            GraphQLInterfaceType interfaceType = (GraphQLInterfaceType) unwrappedType;
            List<GraphQLFieldsContainer> possibleTypes = new ArrayList<>(schema.getImplementations(interfaceType));

            containersQueue.add(possibleTypes);
        } else if (unwrappedType instanceof GraphQLUnionType) {
            visited.add(fieldCoordinates);
            GraphQLUnionType unionType = (GraphQLUnionType) unwrappedType;
            List<GraphQLFieldsContainer> possibleTypes = unionType.getTypes().stream()
                    .filter(possibleType -> possibleType instanceof GraphQLFieldsContainer)
                    .map(possibleType -> (GraphQLFieldsContainer) possibleType)
                    .collect(Collectors.toList());

            containersQueue.add(possibleTypes);
        } else if (unwrappedType instanceof GraphQLFieldsContainer) {
            visited.add(fieldCoordinates);
            containersQueue.add(Collections.singletonList((GraphQLFieldsContainer) unwrappedType));
        } else {
            containersQueue.add(Collections.singletonList(EMPTY_OBJECT_TYPE));
        }

        totalFieldCount.incrementAndGet();
    }

    private static FieldSelection getFieldSelection(GraphQLFieldDefinition fieldDef, GraphQLType unwrappedType) {
        boolean typeNeedsClassifier = unwrappedType instanceof GraphQLUnionType || unwrappedType instanceof GraphQLInterfaceType;

        // TODO: This statement is kinda awful
        final FieldSelection newFieldSelection;

        if (typeNeedsClassifier) {
            newFieldSelection = new FieldSelection(fieldDef.getName(), new HashMap<>(), true);
        } else if (unwrappedType instanceof GraphQLFieldsContainer) {
            newFieldSelection = new FieldSelection(fieldDef.getName(), new HashMap<>(), false);
        } else {
            newFieldSelection = new FieldSelection(fieldDef.getName(), null, false);
        }
        return newFieldSelection;
    }

    private boolean hasRequiredArgs(GraphQLFieldDefinition fieldDefinition) {
        // TODO: Maybe provide a hook to allow callers to resolve required arguments
        return fieldDefinition.getArguments().stream()
                .anyMatch(arg -> GraphQLTypeUtil.isNonNull(arg.getType()) && !arg.hasSetDefaultValue());
    }

    static class FieldSelection {
        final String name;
        final boolean needsTypeClassifier;
        final Map<String, List<FieldSelection>> fieldsByContainer;

        public FieldSelection(String name, Map<String, List<FieldSelection>> fieldsByContainer, boolean needsTypeClassifier) {
            this.name = name;
            this.needsTypeClassifier = needsTypeClassifier;
            this.fieldsByContainer = fieldsByContainer;
        }
    }

    static class FieldSelectionResult {
        final FieldSelection rootFieldSelection;
        final Integer totalFieldCount;
        final Boolean reachedMaxFieldCount;

        FieldSelectionResult(FieldSelection rootFieldSelection, Integer totalFieldCount, Boolean reachedMaxFieldCount) {
            this.rootFieldSelection = rootFieldSelection;
            this.totalFieldCount = totalFieldCount;
            this.reachedMaxFieldCount = reachedMaxFieldCount;
        }
    }
}
