package graphql.normalized.nf;

import com.google.common.collect.ImmutableListMultimap;
import graphql.Assert;
import graphql.ExperimentalApi;
import graphql.execution.MergedField;
import graphql.execution.ResultPath;
import graphql.execution.directives.QueryDirectives;
import graphql.language.Field;
import graphql.language.OperationDefinition;
import graphql.schema.FieldCoordinates;
import graphql.schema.GraphQLFieldsContainer;

import java.util.List;
import java.util.Map;

/**
 * A {@link NormalizedOperation} represent how the text of a graphql operation (sometimes known colloquially as a query)
 * will be executed at runtime according to the graphql specification.  It handles complex mechanisms like merging
 * duplicate fields into one and also detecting when the types of a given field may actually be for more than one possible object
 * type.
 * <p>
 * An operation consists of a list of {@link NormalizedField}s in a parent child hierarchy
 */
@ExperimentalApi
public class NormalizedOperation {
    private final OperationDefinition.Operation operation;
    private final String operationName;
    private final List<NormalizedField> rootFields;
    private final ImmutableListMultimap<Field, NormalizedField> fieldToNormalizedField;
    private final Map<NormalizedField, MergedField> normalizedFieldToMergedField;
    private final Map<NormalizedField, QueryDirectives> normalizedFieldToQueryDirectives;
    private final ImmutableListMultimap<FieldCoordinates, NormalizedField> coordinatesToNormalizedFields;
    private final int operationFieldCount;
    private final int operationDepth;

    public NormalizedOperation(
            OperationDefinition.Operation operation,
            String operationName,
            List<NormalizedField> rootFields,
            ImmutableListMultimap<Field, NormalizedField> fieldToNormalizedField,
            Map<NormalizedField, MergedField> normalizedFieldToMergedField,
            Map<NormalizedField, QueryDirectives> normalizedFieldToQueryDirectives,
            ImmutableListMultimap<FieldCoordinates, NormalizedField> coordinatesToNormalizedFields,
            int operationFieldCount,
            int operationDepth) {
        this.operation = operation;
        this.operationName = operationName;
        this.rootFields = rootFields;
        this.fieldToNormalizedField = fieldToNormalizedField;
        this.normalizedFieldToMergedField = normalizedFieldToMergedField;
        this.normalizedFieldToQueryDirectives = normalizedFieldToQueryDirectives;
        this.coordinatesToNormalizedFields = coordinatesToNormalizedFields;
        this.operationFieldCount = operationFieldCount;
        this.operationDepth = operationDepth;
    }

    /**
     * @return operation AST being executed
     */
    public OperationDefinition.Operation getOperation() {
        return operation;
    }

    /**
     * @return the operation name, which can be null
     */
    public String getOperationName() {
        return operationName;
    }

    /**
     * @return This returns how many {@link NormalizedField}s are in the operation.
     */
    public int getOperationFieldCount() {
        return operationFieldCount;
    }

    /**
     * @return This returns the depth of the operation
     */
    public int getOperationDepth() {
        return operationDepth;
    }

    /**
     * This multimap shows how a given {@link NormalizedField} maps to a one or more field coordinate in the schema
     *
     * @return a multimap of fields to schema field coordinates
     */
    public ImmutableListMultimap<FieldCoordinates, NormalizedField> getCoordinatesToNormalizedFields() {
        return coordinatesToNormalizedFields;
    }

    /**
     * @return a list of the top level {@link NormalizedField}s in this operation.
     */
    public List<NormalizedField> getRootFields() {
        return rootFields;
    }

    /**
     * This is a multimap and  the size of it reflects all the normalized fields in the operation
     *
     * @return an immutable list multimap of {@link Field} to {@link NormalizedField}
     */
    public ImmutableListMultimap<Field, NormalizedField> getFieldToNormalizedField() {
        return fieldToNormalizedField;
    }

    /**
     * Looks up one or more {@link NormalizedField}s given a {@link Field} AST element in the operation
     *
     * @param field the field to look up
     *
     * @return zero, one or more possible {@link NormalizedField}s that represent that field
     */
    public List<NormalizedField> getNormalizedFields(Field field) {
        return fieldToNormalizedField.get(field);
    }

    /**
     * @return a map of {@link NormalizedField} to {@link MergedField}s
     */
    public Map<NormalizedField, MergedField> getNormalizedFieldToMergedField() {
        return normalizedFieldToMergedField;
    }

    /**
     * Looks up the {@link MergedField} given a {@link NormalizedField}
     *
     * @param NormalizedField the field to use the key
     *
     * @return a {@link MergedField} or null if its not present
     */
    public MergedField getMergedField(NormalizedField NormalizedField) {
        return normalizedFieldToMergedField.get(NormalizedField);
    }

    /**
     * @return a map of {@link NormalizedField} to its {@link QueryDirectives}
     */
    public Map<NormalizedField, QueryDirectives> getNormalizedFieldToQueryDirectives() {
        return normalizedFieldToQueryDirectives;

    }

    /**
     * This looks up the {@link QueryDirectives} associated with the given {@link NormalizedField}
     *
     * @param NormalizedField the executable normalised field in question
     *
     * @return the fields query directives or null
     */
    public QueryDirectives getQueryDirectives(NormalizedField NormalizedField) {
        return normalizedFieldToQueryDirectives.get(NormalizedField);
    }

    /**
     * This will find a {@link NormalizedField} given a merged field and a result path.  If this does not find a field it will assert with an exception
     *
     * @param mergedField     the merged field
     * @param fieldsContainer the containing type of that field
     * @param resultPath      the result path in play
     *
     * @return the NormalizedField
     */
    public NormalizedField getNormalizedField(MergedField mergedField, GraphQLFieldsContainer fieldsContainer, ResultPath resultPath) {
        List<NormalizedField> NormalizedFields = fieldToNormalizedField.get(mergedField.getSingleField());
        List<String> keysOnlyPath = resultPath.getKeysOnly();
        for (NormalizedField NormalizedField : NormalizedFields) {
            if (NormalizedField.getListOfResultKeys().equals(keysOnlyPath)) {
                if (NormalizedField.getObjectTypeNames().contains(fieldsContainer.getName())) {
                    return NormalizedField;
                }
            }
        }
        return Assert.assertShouldNeverHappen("normalized field not found");
    }
}
