import { BREAK, isSelectionNode, visit } from "graphql";
import { cacheSlot } from "@apollo/client/cache";
import { argumentsObjectFromField, buildQueryFromSelectionSet, createFragmentMap, getFragmentDefinitions, getMainDefinition, hasDirectives, isField, isInlineFragment, mergeDeep, mergeDeepArray, removeClientSetsFromDocument, resultKeyNameFromField, shouldInclude, } from "@apollo/client/utilities";
import { invariant } from "@apollo/client/utilities/invariant";
export class LocalState {
    cache;
    client;
    resolvers;
    fragmentMatcher;
    selectionsToResolveCache = new WeakMap();
    constructor({ cache, client, resolvers, fragmentMatcher, }) {
        this.cache = cache;
        if (client) {
            this.client = client;
        }
        if (resolvers) {
            this.addResolvers(resolvers);
        }
        if (fragmentMatcher) {
            this.setFragmentMatcher(fragmentMatcher);
        }
    }
    addResolvers(resolvers) {
        this.resolvers = this.resolvers || {};
        if (Array.isArray(resolvers)) {
            resolvers.forEach((resolverGroup) => {
                this.resolvers = mergeDeep(this.resolvers, resolverGroup);
            });
        }
        else {
            this.resolvers = mergeDeep(this.resolvers, resolvers);
        }
    }
    setResolvers(resolvers) {
        this.resolvers = {};
        this.addResolvers(resolvers);
    }
    getResolvers() {
        return this.resolvers || {};
    }
    // Run local client resolvers against the incoming query and remote data.
    // Locally resolved field values are merged with the incoming remote data,
    // and returned. Note that locally resolved fields will overwrite
    // remote data using the same field name.
    async runResolvers({ document, remoteResult, context, variables, onlyRunForcedResolvers = false, }) {
        if (document) {
            return this.resolveDocument(document, remoteResult.data, context, variables, this.fragmentMatcher, onlyRunForcedResolvers).then((localResult) => ({
                ...remoteResult,
                data: localResult.result,
            }));
        }
        return remoteResult;
    }
    setFragmentMatcher(fragmentMatcher) {
        this.fragmentMatcher = fragmentMatcher;
    }
    getFragmentMatcher() {
        return this.fragmentMatcher;
    }
    // Client queries contain everything in the incoming document (if a @client
    // directive is found).
    clientQuery(document) {
        if (hasDirectives(["client"], document)) {
            if (this.resolvers) {
                return document;
            }
        }
        return null;
    }
    // Server queries are stripped of all @client based selection sets.
    serverQuery(document) {
        return removeClientSetsFromDocument(document);
    }
    prepareContext(context) {
        const { cache } = this;
        return {
            ...context,
            cache,
            // Getting an entry's cache key is useful for local state resolvers.
            getCacheKey(obj) {
                return cache.identify(obj);
            },
        };
    }
    // To support `@client @export(as: "someVar")` syntax, we'll first resolve
    // @client @export fields locally, then pass the resolved values back to be
    // used alongside the original operation variables.
    async addExportedVariables(document, variables = {}, context = {}) {
        if (document) {
            return this.resolveDocument(document, this.buildRootValueFromCache(document, variables) || {}, this.prepareContext(context), variables).then((data) => ({
                ...variables,
                ...data.exportedVariables,
            }));
        }
        return {
            ...variables,
        };
    }
    shouldForceResolvers(document) {
        let forceResolvers = false;
        visit(document, {
            Directive: {
                enter(node) {
                    if (node.name.value === "client" && node.arguments) {
                        forceResolvers = node.arguments.some((arg) => arg.name.value === "always" &&
                            arg.value.kind === "BooleanValue" &&
                            arg.value.value === true);
                        if (forceResolvers) {
                            return BREAK;
                        }
                    }
                },
            },
        });
        return forceResolvers;
    }
    // Query the cache and return matching data.
    buildRootValueFromCache(document, variables) {
        return this.cache.diff({
            query: buildQueryFromSelectionSet(document),
            variables,
            returnPartialData: true,
            optimistic: false,
        }).result;
    }
    async resolveDocument(document, rootValue, context = {}, variables = {}, fragmentMatcher = () => true, onlyRunForcedResolvers = false) {
        const mainDefinition = getMainDefinition(document);
        const fragments = getFragmentDefinitions(document);
        const fragmentMap = createFragmentMap(fragments);
        const selectionsToResolve = this.collectSelectionsToResolve(mainDefinition, fragmentMap);
        const definitionOperation = mainDefinition.operation;
        const defaultOperationType = definitionOperation ?
            definitionOperation.charAt(0).toUpperCase() +
                definitionOperation.slice(1)
            : "Query";
        const { cache, client } = this;
        const execContext = {
            fragmentMap,
            context: {
                ...context,
                cache,
                client,
            },
            variables,
            fragmentMatcher,
            defaultOperationType,
            exportedVariables: {},
            selectionsToResolve,
            onlyRunForcedResolvers,
        };
        const isClientFieldDescendant = false;
        return this.resolveSelectionSet(mainDefinition.selectionSet, isClientFieldDescendant, rootValue, execContext).then((result) => ({
            result,
            exportedVariables: execContext.exportedVariables,
        }));
    }
    async resolveSelectionSet(selectionSet, isClientFieldDescendant, rootValue, execContext) {
        const { fragmentMap, context, variables } = execContext;
        const resultsToMerge = [rootValue];
        const execute = async (selection) => {
            if (!isClientFieldDescendant &&
                !execContext.selectionsToResolve.has(selection)) {
                // Skip selections without @client directives
                // (still processing if one of the ancestors or one of the child fields has @client directive)
                return;
            }
            if (!shouldInclude(selection, variables)) {
                // Skip this entirely.
                return;
            }
            if (isField(selection)) {
                return this.resolveField(selection, isClientFieldDescendant, rootValue, execContext).then((fieldResult) => {
                    if (typeof fieldResult !== "undefined") {
                        resultsToMerge.push({
                            [resultKeyNameFromField(selection)]: fieldResult,
                        });
                    }
                });
            }
            let fragment;
            if (isInlineFragment(selection)) {
                fragment = selection;
            }
            else {
                // This is a named fragment.
                fragment = fragmentMap[selection.name.value];
                invariant(fragment, 65, selection.name.value);
            }
            if (fragment && fragment.typeCondition) {
                const typeCondition = fragment.typeCondition.name.value;
                if (execContext.fragmentMatcher(rootValue, typeCondition, context)) {
                    return this.resolveSelectionSet(fragment.selectionSet, isClientFieldDescendant, rootValue, execContext).then((fragmentResult) => {
                        resultsToMerge.push(fragmentResult);
                    });
                }
            }
        };
        return Promise.all(selectionSet.selections.map(execute)).then(function () {
            return mergeDeepArray(resultsToMerge);
        });
    }
    async resolveField(field, isClientFieldDescendant, rootValue, execContext) {
        if (!rootValue) {
            return null;
        }
        const { variables } = execContext;
        const fieldName = field.name.value;
        const aliasedFieldName = resultKeyNameFromField(field);
        const aliasUsed = fieldName !== aliasedFieldName;
        const defaultResult = rootValue[aliasedFieldName] || rootValue[fieldName];
        let resultPromise = Promise.resolve(defaultResult);
        // Usually all local resolvers are run when passing through here, but
        // if we've specifically identified that we only want to run forced
        // resolvers (that is, resolvers for fields marked with
        // `@client(always: true)`), then we'll skip running non-forced resolvers.
        if (!execContext.onlyRunForcedResolvers ||
            this.shouldForceResolvers(field)) {
            const resolverType = rootValue.__typename || execContext.defaultOperationType;
            const resolverMap = this.resolvers && this.resolvers[resolverType];
            if (resolverMap) {
                const resolve = resolverMap[aliasUsed ? fieldName : aliasedFieldName];
                if (resolve) {
                    resultPromise = Promise.resolve(
                    // In case the resolve function accesses reactive variables,
                    // set cacheSlot to the current cache instance.
                    cacheSlot.withValue(this.cache, resolve, [
                        rootValue,
                        argumentsObjectFromField(field, variables),
                        execContext.context,
                        { field, fragmentMap: execContext.fragmentMap },
                    ]));
                }
            }
        }
        return resultPromise.then((result = defaultResult) => {
            // If an @export directive is associated with the current field, store
            // the `as` export variable name and current result for later use.
            if (field.directives) {
                field.directives.forEach((directive) => {
                    if (directive.name.value === "export" && directive.arguments) {
                        directive.arguments.forEach((arg) => {
                            if (arg.name.value === "as" && arg.value.kind === "StringValue") {
                                execContext.exportedVariables[arg.value.value] = result;
                            }
                        });
                    }
                });
            }
            // Handle all scalar types here.
            if (!field.selectionSet) {
                return result;
            }
            // From here down, the field has a selection set, which means it's trying
            // to query a GraphQLObjectType.
            if (result == null) {
                // Basically any field in a GraphQL response can be null, or missing
                return result;
            }
            const isClientField = field.directives?.some((d) => d.name.value === "client") ?? false;
            if (Array.isArray(result)) {
                return this.resolveSubSelectedArray(field, isClientFieldDescendant || isClientField, result, execContext);
            }
            // Returned value is an object, and the query has a sub-selection. Recurse.
            if (field.selectionSet) {
                return this.resolveSelectionSet(field.selectionSet, isClientFieldDescendant || isClientField, result, execContext);
            }
        });
    }
    resolveSubSelectedArray(field, isClientFieldDescendant, result, execContext) {
        return Promise.all(result.map((item) => {
            if (item === null) {
                return null;
            }
            // This is a nested array, recurse.
            if (Array.isArray(item)) {
                return this.resolveSubSelectedArray(field, isClientFieldDescendant, item, execContext);
            }
            // This is an object, run the selection set on it.
            if (field.selectionSet) {
                return this.resolveSelectionSet(field.selectionSet, isClientFieldDescendant, item, execContext);
            }
        }));
    }
    // Collect selection nodes on paths from document root down to all @client directives.
    // This function takes into account transitive fragment spreads.
    // Complexity equals to a single `visit` over the full document.
    collectSelectionsToResolve(mainDefinition, fragmentMap) {
        const isSingleASTNode = (node) => !Array.isArray(node);
        const selectionsToResolveCache = this.selectionsToResolveCache;
        function collectByDefinition(definitionNode) {
            if (!selectionsToResolveCache.has(definitionNode)) {
                const matches = new Set();
                selectionsToResolveCache.set(definitionNode, matches);
                visit(definitionNode, {
                    Directive(node, _, __, ___, ancestors) {
                        if (node.name.value === "client") {
                            ancestors.forEach((node) => {
                                if (isSingleASTNode(node) && isSelectionNode(node)) {
                                    matches.add(node);
                                }
                            });
                        }
                    },
                    FragmentSpread(spread, _, __, ___, ancestors) {
                        const fragment = fragmentMap[spread.name.value];
                        invariant(fragment, 66, spread.name.value);
                        const fragmentSelections = collectByDefinition(fragment);
                        if (fragmentSelections.size > 0) {
                            // Fragment for this spread contains @client directive (either directly or transitively)
                            // Collect selection nodes on paths from the root down to fields with the @client directive
                            ancestors.forEach((node) => {
                                if (isSingleASTNode(node) && isSelectionNode(node)) {
                                    matches.add(node);
                                }
                            });
                            matches.add(spread);
                            fragmentSelections.forEach((selection) => {
                                matches.add(selection);
                            });
                        }
                    },
                });
            }
            return selectionsToResolveCache.get(definitionNode);
        }
        return collectByDefinition(mainDefinition);
    }
}
//# sourceMappingURL=LocalState.js.map
