import { createHash } from 'crypto';
import { getBaseType } from '@graphql-codegen/plugin-helpers';
import { getRootTypes } from '@graphql-tools/utils';
import autoBind from 'auto-bind';
import { isInterfaceType, isListType, isNonNullType, isObjectType, isTypeSubTypeOf, isUnionType, Kind, SchemaMetaFieldDef, TypeMetaFieldDef, } from 'graphql';
import { DeclarationBlock, getFieldNames, getFieldNodeNameValue, getPossibleTypes, hasConditionalDirectives, hasIncrementalDeliveryDirectives, mergeSelectionSets, separateSelectionSet, } from './utils.js';
const operationTypes = ['Query', 'Mutation', 'Subscription'];
function isMetadataFieldName(name) {
    return ['__schema', '__type'].includes(name);
}
const metadataFieldMap = {
    __schema: SchemaMetaFieldDef,
    __type: TypeMetaFieldDef,
};
export class SelectionSetToObject {
    constructor(_processor, _scalars, _schema, _convertName, _getFragmentSuffix, _loadedFragments, _config, _parentSchemaType, _selectionSet) {
        this._processor = _processor;
        this._scalars = _scalars;
        this._schema = _schema;
        this._convertName = _convertName;
        this._getFragmentSuffix = _getFragmentSuffix;
        this._loadedFragments = _loadedFragments;
        this._config = _config;
        this._parentSchemaType = _parentSchemaType;
        this._selectionSet = _selectionSet;
        this._primitiveFields = [];
        this._primitiveAliasedFields = [];
        this._linksFields = [];
        this._queriedForTypename = false;
        autoBind(this);
    }
    createNext(parentSchemaType, selectionSet) {
        return new SelectionSetToObject(this._processor, this._scalars, this._schema, this._convertName.bind(this), this._getFragmentSuffix.bind(this), this._loadedFragments, this._config, parentSchemaType, selectionSet);
    }
    /**
     * traverse the inline fragment nodes recursively for collecting the selectionSets on each type
     */
    _collectInlineFragments(parentType, nodes, types) {
        if (isListType(parentType) || isNonNullType(parentType)) {
            return this._collectInlineFragments(parentType.ofType, nodes, types);
        }
        if (isObjectType(parentType)) {
            for (const node of nodes) {
                const typeOnSchema = node.typeCondition ? this._schema.getType(node.typeCondition.name.value) : parentType;
                const { fields, inlines, spreads } = separateSelectionSet(node.selectionSet.selections);
                const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
                const directives = node.directives || undefined;
                // When we collect the selection sets of inline fragments we need to
                // make sure directives on the inline fragments are stored in a way
                // that can be associated back to the fields in the fragment, to
                // support things like making those fields optional when deferring a
                // fragment (using @defer).
                const fieldsWithFragmentDirectives = fields.map(field => ({
                    ...field,
                    fragmentDirectives: field.fragmentDirectives || directives,
                }));
                if (isObjectType(typeOnSchema)) {
                    this._appendToTypeMap(types, typeOnSchema.name, fieldsWithFragmentDirectives);
                    this._appendToTypeMap(types, typeOnSchema.name, spreadsUsage[typeOnSchema.name]);
                    this._appendToTypeMap(types, typeOnSchema.name, directives);
                    this._collectInlineFragments(typeOnSchema, inlines, types);
                }
                else if (isInterfaceType(typeOnSchema) && parentType.getInterfaces().includes(typeOnSchema)) {
                    this._appendToTypeMap(types, parentType.name, fields);
                    this._appendToTypeMap(types, parentType.name, spreadsUsage[parentType.name]);
                    this._collectInlineFragments(typeOnSchema, inlines, types);
                }
            }
        }
        else if (isInterfaceType(parentType)) {
            const possibleTypes = getPossibleTypes(this._schema, parentType);
            for (const node of nodes) {
                const schemaType = node.typeCondition ? this._schema.getType(node.typeCondition.name.value) : parentType;
                const { fields, inlines, spreads } = separateSelectionSet(node.selectionSet.selections);
                const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
                if (isObjectType(schemaType) && possibleTypes.find(possibleType => possibleType.name === schemaType.name)) {
                    this._appendToTypeMap(types, schemaType.name, fields);
                    this._appendToTypeMap(types, schemaType.name, spreadsUsage[schemaType.name]);
                    this._collectInlineFragments(schemaType, inlines, types);
                }
                else if (isInterfaceType(schemaType) && schemaType.name === parentType.name) {
                    for (const possibleType of possibleTypes) {
                        this._appendToTypeMap(types, possibleType.name, fields);
                        this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
                        this._collectInlineFragments(schemaType, inlines, types);
                    }
                }
                else {
                    // it must be an interface type that is spread on an interface field
                    for (const possibleType of possibleTypes) {
                        if (!node.typeCondition) {
                            throw new Error('Invalid state. Expected type condition for interface spread on a interface field.');
                        }
                        const fragmentSpreadType = this._schema.getType(node.typeCondition.name.value);
                        // the field should only be added to the valid selections
                        // in case the possible type actually implements the given interface
                        if (isTypeSubTypeOf(this._schema, possibleType, fragmentSpreadType)) {
                            this._appendToTypeMap(types, possibleType.name, fields);
                            this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
                        }
                    }
                }
            }
        }
        else if (isUnionType(parentType)) {
            const possibleTypes = parentType.getTypes();
            for (const node of nodes) {
                const schemaType = node.typeCondition ? this._schema.getType(node.typeCondition.name.value) : parentType;
                const { fields, inlines, spreads } = separateSelectionSet(node.selectionSet.selections);
                const spreadsUsage = this.buildFragmentSpreadsUsage(spreads);
                if (isObjectType(schemaType) && possibleTypes.find(possibleType => possibleType.name === schemaType.name)) {
                    this._appendToTypeMap(types, schemaType.name, fields);
                    this._appendToTypeMap(types, schemaType.name, spreadsUsage[schemaType.name]);
                    this._collectInlineFragments(schemaType, inlines, types);
                }
                else if (isInterfaceType(schemaType)) {
                    const possibleInterfaceTypes = getPossibleTypes(this._schema, schemaType);
                    for (const possibleType of possibleTypes) {
                        if (possibleInterfaceTypes.find(possibleInterfaceType => possibleInterfaceType.name === possibleType.name)) {
                            this._appendToTypeMap(types, possibleType.name, fields);
                            this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
                            this._collectInlineFragments(schemaType, inlines, types);
                        }
                    }
                }
                else {
                    for (const possibleType of possibleTypes) {
                        this._appendToTypeMap(types, possibleType.name, fields);
                        this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]);
                    }
                }
            }
        }
    }
    _createInlineFragmentForFieldNodes(parentType, fieldNodes) {
        return {
            kind: Kind.INLINE_FRAGMENT,
            typeCondition: {
                kind: Kind.NAMED_TYPE,
                name: {
                    kind: Kind.NAME,
                    value: parentType.name,
                },
            },
            directives: [],
            selectionSet: {
                kind: Kind.SELECTION_SET,
                selections: fieldNodes,
            },
        };
    }
    /**
     * The `buildFragmentSpreadsUsage` method is used to collect fields from fragment spreads in the selection set.
     * It creates a record of fragment spread usages, which includes the fragment name, type name, and the selection nodes
     * inside the fragment.
     */
    buildFragmentSpreadsUsage(spreads) {
        const selectionNodesByTypeName = {};
        for (const spread of spreads) {
            const fragmentSpreadObject = this._loadedFragments.find(lf => lf.name === spread.name.value);
            if (fragmentSpreadObject) {
                const schemaType = this._schema.getType(fragmentSpreadObject.onType);
                const possibleTypesForFragment = getPossibleTypes(this._schema, schemaType);
                for (const possibleType of possibleTypesForFragment) {
                    const fragmentSuffix = this._getFragmentSuffix(spread.name.value);
                    const usage = this.buildFragmentTypeName(spread.name.value, fragmentSuffix, possibleTypesForFragment.length === 1 ? null : possibleType.name);
                    selectionNodesByTypeName[possibleType.name] ||= [];
                    selectionNodesByTypeName[possibleType.name].push({
                        fragmentName: spread.name.value,
                        typeName: usage,
                        onType: fragmentSpreadObject.onType,
                        selectionNodes: [...fragmentSpreadObject.node.selectionSet.selections],
                        fragmentDirectives: [...spread.directives],
                    });
                }
            }
        }
        return selectionNodesByTypeName;
    }
    flattenSelectionSet(selections, parentSchemaType) {
        const selectionNodesByTypeName = new Map();
        const inlineFragmentSelections = [];
        const fieldNodes = [];
        const fragmentSpreads = [];
        for (const selection of selections) {
            switch (selection.kind) {
                case Kind.FIELD:
                    fieldNodes.push(selection);
                    break;
                case Kind.INLINE_FRAGMENT:
                    inlineFragmentSelections.push(selection);
                    break;
                case Kind.FRAGMENT_SPREAD:
                    fragmentSpreads.push(selection);
                    break;
            }
        }
        if (fieldNodes.length) {
            inlineFragmentSelections.push(this._createInlineFragmentForFieldNodes(parentSchemaType ?? this._parentSchemaType, fieldNodes));
        }
        this._collectInlineFragments(parentSchemaType ?? this._parentSchemaType, inlineFragmentSelections, selectionNodesByTypeName);
        const fragmentsUsage = this.buildFragmentSpreadsUsage(fragmentSpreads);
        for (const [typeName, records] of Object.entries(fragmentsUsage)) {
            this._appendToTypeMap(selectionNodesByTypeName, typeName, records);
        }
        return selectionNodesByTypeName;
    }
    _appendToTypeMap(types, typeName, nodes) {
        if (!types.has(typeName)) {
            types.set(typeName, []);
        }
        if (nodes && nodes.length > 0) {
            types.get(typeName).push(...nodes);
        }
    }
    _buildGroupedSelections(parentName) {
        if (!this._selectionSet?.selections || this._selectionSet.selections.length === 0) {
            return { grouped: {}, mustAddEmptyObject: true, dependentTypes: [] };
        }
        const selectionNodesByTypeName = this.flattenSelectionSet(this._selectionSet.selections);
        // in case there is not a selection for each type, we need to add a empty type.
        let mustAddEmptyObject = false;
        const possibleTypes = getPossibleTypes(this._schema, this._parentSchemaType);
        const dependentTypes = [];
        if (!this._config.mergeFragmentTypes || this._config.inlineFragmentTypes === 'mask') {
            const grouped = possibleTypes.reduce((prev, type) => {
                const typeName = type.name;
                const schemaType = this._schema.getType(typeName);
                if (!isObjectType(schemaType)) {
                    throw new TypeError(`Invalid state! Schema type ${typeName} is not a valid GraphQL object!`);
                }
                const allNodes = selectionNodesByTypeName.get(typeName) || [];
                prev[typeName] ||= [];
                // incrementalNodes are the ones flagged with @defer, meaning they become nullable
                const { incrementalNodes, selectionNodes, fragmentSpreads } = allNodes.reduce((acc, node) => {
                    if ('fragmentDirectives' in node && hasIncrementalDeliveryDirectives(node.fragmentDirectives)) {
                        acc.incrementalNodes.push(node);
                    }
                    else {
                        acc.selectionNodes.push(node);
                    }
                    return acc;
                }, { selectionNodes: [], incrementalNodes: [], fragmentSpreads: [] });
                const { fields, dependentTypes: subDependentTypes } = this.buildSelectionSet(schemaType, selectionNodes, {
                    parentFieldName: this.buildParentFieldName(typeName, parentName),
                });
                const transformedSet = this.selectionSetStringFromFields(fields);
                if (transformedSet) {
                    prev[typeName].push(transformedSet);
                }
                dependentTypes.push(...subDependentTypes);
                if (!transformedSet && !fragmentSpreads.length) {
                    mustAddEmptyObject = true;
                }
                for (const incrementalNode of incrementalNodes) {
                    if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) {
                        const { fields: incrementalFields, dependentTypes: incrementalDependentTypes } = this.buildSelectionSet(schemaType, [incrementalNode], { unsetTypes: true, parentFieldName: parentName });
                        const incrementalSet = this.selectionSetStringFromFields(incrementalFields);
                        prev[typeName].push(incrementalSet);
                        dependentTypes.push(...incrementalDependentTypes);
                        continue;
                    }
                    const { fields: initialFields, dependentTypes: initialDependentTypes } = this.buildSelectionSet(schemaType, [incrementalNode], { parentFieldName: parentName });
                    const { fields: subsequentFields, dependentTypes: subsequentDependentTypes } = this.buildSelectionSet(schemaType, [incrementalNode], { unsetTypes: true, parentFieldName: parentName });
                    const initialSet = this.selectionSetStringFromFields(initialFields);
                    const subsequentSet = this.selectionSetStringFromFields(subsequentFields);
                    dependentTypes.push(...initialDependentTypes, ...subsequentDependentTypes);
                    prev[typeName].push({ union: [initialSet, subsequentSet] });
                }
                return prev;
            }, {});
            return { grouped, mustAddEmptyObject, dependentTypes };
        }
        // Accumulate a map of selected fields to the typenames that
        // share the exact same selected fields. When we find multiple
        // typenames with the same set of fields, we can collapse the
        // generated type to the selected fields and a string literal
        // union of the typenames.
        //
        // E.g. {
        //        __typename: "foo" | "bar";
        //        shared: string;
        //      }
        const grouped = possibleTypes.reduce((prev, type) => {
            const typeName = type.name;
            const schemaType = this._schema.getType(typeName);
            if (!isObjectType(schemaType)) {
                throw new TypeError(`Invalid state! Schema type ${typeName} is not a valid GraphQL object!`);
            }
            const selectionNodes = selectionNodesByTypeName.get(typeName) || [];
            const { typeInfo, fields, dependentTypes: subDependentTypes, } = this.buildSelectionSet(schemaType, selectionNodes, {
                parentFieldName: this.buildParentFieldName(typeName, parentName),
            });
            dependentTypes.push(...subDependentTypes);
            const key = this.selectionSetStringFromFields(fields);
            prev[key] = {
                fields,
                types: [...(prev[key]?.types ?? []), typeInfo || { name: '', type: type.name }].filter(Boolean),
            };
            return prev;
        }, {});
        // For every distinct set of fields, create the corresponding
        // string literal union of typenames.
        const compacted = Object.keys(grouped).reduce((acc, key) => {
            const typeNames = grouped[key].types.map(t => t.type);
            // Don't create very large string literal unions. TypeScript
            // will stop comparing some nested union types types when
            // they contain props with more than some number of string
            // literal union members (testing with TS 4.5 stops working
            // at 25 for a naive test case:
            // https://www.typescriptlang.org/play?ts=4.5.4&ssl=29&ssc=10&pln=29&pc=1#code/C4TwDgpgBAKg9nAMgQwE4HNoF4BQV9QA+UA3ngRQJYB21EqAXDsQEQCMLzULATJ6wGZ+3ACzCWAVnEA2cQHZxADnEBOcWwAM6jl3Z9dbIQbEGpB2QYUHlBtbp5b7O1j30ujLky7Os4wABb0nAC+ODigkFAAQlBYUOT4xGQUVLT0TKzO3G7cHqLiPtwWrFasNqx2mY6ZWXrqeexe3GyF7MXNpc3lzZXZ1dm1ruI8DTxNvGahFEkJKTR0jLMpRNx+gaicy6E4APQ7AALAAM4AtJTo1HCoEDgANhDAUMgMsAgoGNikwQDcdw9QACMXjE4shfmEItAAGI0bCzGbLfDzdIGYbiBrjVrtFidFjdFi9dj9di1Ng5dgNNjjFrqbFsXFsfFsQkOYaDckjYbjNZBHDbPaHU7nS7XP6PZBsF4wuixL6-e6PAGS6KyiXfIA
            const max_types = 20;
            for (let i = 0; i < typeNames.length; i += max_types) {
                const selectedTypes = typeNames.slice(i, i + max_types);
                const typenameUnion = grouped[key].types[0].name
                    ? this._processor.transformTypenameField(selectedTypes.join(' | '), grouped[key].types[0].name)
                    : [];
                const transformedSet = this.selectionSetStringFromFields([...typenameUnion, ...grouped[key].fields]);
                // The keys here will be used to generate intermediary
                // fragment names. To avoid blowing up the type name on large
                // unions, calculate a stable hash here instead.
                //
                // Also use fragment hashing if skipTypename is true, since we
                // then don't have a typename for naming the fragment.
                acc[selectedTypes.length <= 3
                    ? // Remove quote marks to produce a valid type name
                        selectedTypes.map(t => t.replace(/'/g, '')).join('_')
                    : createHash('sha256')
                        .update(selectedTypes.join() || transformedSet || '')
                        // Remove invalid characters to produce a valid type name
                        .digest('base64')
                        .replace(/[=+/]/g, '')] = [transformedSet];
            }
            return acc;
        }, {});
        return { grouped: compacted, mustAddEmptyObject, dependentTypes };
    }
    selectionSetStringFromFields(fields) {
        const allStrings = fields.filter((f) => typeof f === 'string');
        const allObjects = fields
            .filter((f) => typeof f !== 'string')
            .map(t => `${t.name}: ${t.type}`);
        const mergedObjects = allObjects.length ? this._processor.buildFieldsIntoObject(allObjects) : null;
        const transformedSet = this._processor.buildSelectionSetFromStrings([...allStrings, mergedObjects].filter(Boolean));
        return transformedSet;
    }
    buildSelectionSet(parentSchemaType, selectionNodes, options) {
        const primitiveFields = new Map();
        const primitiveAliasFields = new Map();
        const linkFieldSelectionSets = new Map();
        let requireTypename = false;
        // usages via fragment typescript type
        const fragmentsSpreadUsages = [];
        // ensure we mutate no function params
        selectionNodes = [...selectionNodes];
        let inlineFragmentConditional = false;
        for (const selectionNode of selectionNodes) {
            if ('kind' in selectionNode) {
                if (selectionNode.kind === 'Field') {
                    if (selectionNode.selectionSet) {
                        let selectedField = null;
                        const fields = parentSchemaType.getFields();
                        selectedField = fields[selectionNode.name.value];
                        if (isMetadataFieldName(selectionNode.name.value)) {
                            selectedField = metadataFieldMap[selectionNode.name.value];
                        }
                        if (!selectedField) {
                            continue;
                        }
                        const fieldName = getFieldNodeNameValue(selectionNode);
                        let linkFieldNode = linkFieldSelectionSets.get(fieldName);
                        if (linkFieldNode) {
                            linkFieldNode = {
                                ...linkFieldNode,
                                field: {
                                    ...linkFieldNode.field,
                                    selectionSet: mergeSelectionSets(linkFieldNode.field.selectionSet, selectionNode.selectionSet),
                                },
                            };
                        }
                        else {
                            linkFieldNode = {
                                selectedFieldType: selectedField.type,
                                field: selectionNode,
                            };
                        }
                        linkFieldSelectionSets.set(fieldName, linkFieldNode);
                    }
                    else if (selectionNode.alias) {
                        primitiveAliasFields.set(selectionNode.alias.value, selectionNode);
                    }
                    else if (selectionNode.name.value === '__typename') {
                        requireTypename = true;
                    }
                    else {
                        primitiveFields.set(selectionNode.name.value, selectionNode);
                    }
                }
                else if (selectionNode.kind === 'Directive') {
                    if (['skip', 'include'].includes(selectionNode?.name?.value)) {
                        inlineFragmentConditional = true;
                    }
                }
                else {
                    throw new TypeError('Unexpected type.');
                }
                continue;
            }
            if (this._config.inlineFragmentTypes === 'combine' || this._config.inlineFragmentTypes === 'mask') {
                fragmentsSpreadUsages.push(selectionNode.typeName);
                continue;
            }
            // Handle Fragment Spreads by generating inline types.
            const fragmentType = this._schema.getType(selectionNode.onType);
            if (fragmentType == null) {
                throw new TypeError(`Unexpected error: Type ${selectionNode.onType} does not exist within schema.`);
            }
            if (parentSchemaType.name === selectionNode.onType ||
                parentSchemaType.getInterfaces().find(iinterface => iinterface.name === selectionNode.onType) != null ||
                (isUnionType(fragmentType) &&
                    fragmentType.getTypes().find(objectType => objectType.name === parentSchemaType.name))) {
                // also process fields from fragment that apply for this parentType
                const flatten = this.flattenSelectionSet(selectionNode.selectionNodes, parentSchemaType);
                const typeNodes = flatten.get(parentSchemaType.name) ?? [];
                selectionNodes.push(...typeNodes);
                for (const iinterface of parentSchemaType.getInterfaces()) {
                    const typeNodes = flatten.get(iinterface.name) ?? [];
                    selectionNodes.push(...typeNodes);
                }
            }
        }
        const linkFields = [];
        const linkFieldsInterfaces = [];
        for (const { field, selectedFieldType } of linkFieldSelectionSets.values()) {
            const realSelectedFieldType = getBaseType(selectedFieldType);
            const selectionSet = this.createNext(realSelectedFieldType, field.selectionSet);
            const fieldName = field.alias?.value ?? field.name.value;
            const selectionSetObjects = selectionSet.transformSelectionSet(options.parentFieldName ? `${options.parentFieldName}_${fieldName}` : fieldName);
            linkFieldsInterfaces.push(...selectionSetObjects.dependentTypes);
            const isConditional = hasConditionalDirectives(field) || inlineFragmentConditional;
            const isOptional = options.unsetTypes;
            linkFields.push({
                alias: field.alias
                    ? this._processor.config.formatNamedField(field.alias.value, selectedFieldType, isConditional, isOptional)
                    : undefined,
                name: this._processor.config.formatNamedField(field.name.value, selectedFieldType, isConditional, isOptional),
                type: realSelectedFieldType.name,
                selectionSet: this._processor.config.wrapTypeWithModifiers(selectionSetObjects.mergedTypeString.split(`\n`).join(`\n  `), selectedFieldType),
            });
        }
        const typeInfoField = this.buildTypeNameField(parentSchemaType, this._config.nonOptionalTypename, this._config.addTypename, requireTypename, this._config.skipTypeNameForRoot);
        const transformed = [
            // Only add the typename field if we're not merging fragment
            // types. If we are merging, we need to wait until we know all
            // the involved typenames.
            ...(typeInfoField && (!this._config.mergeFragmentTypes || this._config.inlineFragmentTypes === 'mask')
                ? this._processor.transformTypenameField(typeInfoField.type, typeInfoField.name)
                : []),
            ...this._processor.transformPrimitiveFields(parentSchemaType, Array.from(primitiveFields.values()).map(field => ({
                isConditional: hasConditionalDirectives(field),
                fieldName: field.name.value,
            })), options.unsetTypes),
            ...this._processor.transformAliasesPrimitiveFields(parentSchemaType, Array.from(primitiveAliasFields.values()).map(field => ({
                alias: field.alias.value,
                fieldName: field.name.value,
                isConditional: hasConditionalDirectives(field),
            })), options.unsetTypes),
            ...this._processor.transformLinkFields(linkFields, options.unsetTypes),
        ].filter(Boolean);
        const allStrings = transformed.filter(t => typeof t === 'string');
        const allObjectsMerged = transformed
            .filter(t => typeof t !== 'string')
            .map((t) => `${t.name}: ${t.type}`);
        let mergedObjectsAsString = null;
        if (allObjectsMerged.length > 0) {
            mergedObjectsAsString = this._processor.buildFieldsIntoObject(allObjectsMerged);
        }
        const fields = [...allStrings, mergedObjectsAsString].filter(Boolean);
        if (fragmentsSpreadUsages.length) {
            if (this._config.inlineFragmentTypes === 'combine') {
                fields.push(...fragmentsSpreadUsages);
            }
            else if (this._config.inlineFragmentTypes === 'mask') {
                fields.push(`{ ' $fragmentRefs'?: { ${fragmentsSpreadUsages
                    .map(name => `'${name}': ${options.unsetTypes ? `Incremental<${name}>` : name}`)
                    .join(`;`)} } }`);
            }
        }
        return { typeInfo: typeInfoField, fields, dependentTypes: linkFieldsInterfaces };
    }
    buildTypeNameField(type, nonOptionalTypename = this._config.nonOptionalTypename, addTypename = this._config.addTypename, queriedForTypename = this._queriedForTypename, skipTypeNameForRoot = this._config.skipTypeNameForRoot) {
        const rootTypes = getRootTypes(this._schema);
        if (rootTypes.has(type) && skipTypeNameForRoot && !queriedForTypename) {
            return null;
        }
        if (nonOptionalTypename || addTypename || queriedForTypename) {
            const optionalTypename = !queriedForTypename && !nonOptionalTypename;
            return {
                name: `${this._processor.config.formatNamedField('__typename')}${optionalTypename ? '?' : ''}`,
                type: `'${type.name}'`,
            };
        }
        return null;
    }
    getUnknownType() {
        return 'never';
    }
    getEmptyObjectType() {
        return `{}`;
    }
    getEmptyObjectTypeString(mustAddEmptyObject) {
        return mustAddEmptyObject ? this.getEmptyObjectType() : ``;
    }
    transformSelectionSet(fieldName) {
        const possibleTypesList = getPossibleTypes(this._schema, this._parentSchemaType);
        const possibleTypes = possibleTypesList.map(v => v.name).sort();
        const fieldSelections = [
            ...getFieldNames({ selections: this._selectionSet.selections, loadedFragments: this._loadedFragments }),
        ].sort();
        // Optimization: Do not create new dependentTypes if fragment typename exists in cache
        // 2-layer cache: LOC => Field Selection Type Combination => cachedTypeString
        const objMap = this._processor.typeCache.get(this._selectionSet.loc) ?? new Map();
        this._processor.typeCache.set(this._selectionSet.loc, objMap);
        const cacheHashKey = `${fieldSelections.join(',')} @ ${possibleTypes.join('|')}`;
        const [cachedTypeString] = objMap.get(cacheHashKey) ?? [];
        if (cachedTypeString) {
            // reuse previously generated type, as it is identical
            return {
                mergedTypeString: cachedTypeString,
                // there are no new dependent types, as this is a nth use of the same type
                dependentTypes: [],
            };
        }
        const result = this.transformSelectionSetUncached(fieldName);
        objMap.set(cacheHashKey, [result.mergedTypeString, fieldName]);
        if (this._selectionSet.loc) {
            this._processor.typeCache.set(this._selectionSet.loc, objMap);
        }
        return result;
    }
    transformSelectionSetUncached(fieldName) {
        const { grouped, mustAddEmptyObject, dependentTypes: subDependentTypes } = this._buildGroupedSelections(fieldName);
        // This might happen in case we have an interface, that is being queries, without any GraphQL
        // "type" that implements it. It will lead to a runtime error, but we aim to try to reflect that in
        // build time as well.
        if (Object.keys(grouped).length === 0) {
            return {
                mergedTypeString: this.getUnknownType(),
                dependentTypes: subDependentTypes,
            };
        }
        const dependentTypes = Object.keys(grouped)
            .map(typeName => {
            const relevant = grouped[typeName].filter(Boolean);
            return relevant.map(objDefinition => {
                const name = fieldName ? `${fieldName}_${typeName}` : typeName;
                return {
                    name,
                    content: typeof objDefinition === 'string' ? objDefinition : objDefinition.union.join(' | '),
                    isUnionType: !!(typeof objDefinition !== 'string' && objDefinition.union.length > 1),
                };
            });
        })
            .filter(pairs => pairs.length > 0);
        const typeParts = [
            ...dependentTypes.map(pair => pair
                .map(({ name, content, isUnionType }) => 
            // unions need to be wrapped, as intersections have higher precedence
            this._config.extractAllFieldsToTypes ? name : isUnionType ? `(${content})` : content)
                .join(' & ')),
            this.getEmptyObjectTypeString(mustAddEmptyObject),
        ].filter(Boolean);
        const content = typeParts.join(' | ');
        if (typeParts.length > 1 && this._config.extractAllFieldsToTypes) {
            return {
                mergedTypeString: fieldName,
                dependentTypes: [
                    ...subDependentTypes,
                    ...dependentTypes.flat(1),
                    { name: fieldName, content, isUnionType: true },
                ],
            };
        }
        return {
            mergedTypeString: content,
            dependentTypes: [...subDependentTypes, ...dependentTypes.flat(1)],
            isUnionType: typeParts.length > 1,
        };
    }
    transformFragmentSelectionSetToTypes(fragmentName, fragmentSuffix, declarationBlockConfig) {
        const mergedTypeString = this.buildFragmentTypeName(fragmentName, fragmentSuffix);
        const { grouped, dependentTypes } = this._buildGroupedSelections(mergedTypeString);
        const subTypes = Object.keys(grouped).flatMap(typeName => {
            const possibleFields = grouped[typeName].filter(Boolean);
            const declarationName = this.buildFragmentTypeName(fragmentName, fragmentSuffix, typeName);
            if (possibleFields.length === 0) {
                if (!this._config.addTypename) {
                    return [{ name: declarationName, content: this.getEmptyObjectType() }];
                }
                return [];
            }
            const flatFields = possibleFields.map(selectionObject => {
                if (typeof selectionObject === 'string')
                    return { value: selectionObject };
                return { value: selectionObject.union.join(' | '), isUnionType: true };
            });
            const content = flatFields.length > 1
                ? flatFields.map(({ value, isUnionType }) => (isUnionType ? `(${value})` : value)).join(' & ')
                : flatFields.map(({ value }) => value).join(' & ');
            return {
                name: declarationName,
                content,
                isUnionType: false,
            };
        });
        const fragmentMaskPartial = this._config.inlineFragmentTypes === 'mask' ? ` & { ' $fragmentName'?: '${mergedTypeString}' }` : '';
        // TODO: unify with line 308 from base-documents-visitor
        const dependentTypesContent = this._config.extractAllFieldsToTypes
            ? dependentTypes.map(i => new DeclarationBlock(declarationBlockConfig)
                .export(true)
                .asKind('type')
                .withName(i.name)
                .withContent(i.content).string)
            : [];
        if (subTypes.length === 1) {
            return [
                ...dependentTypesContent,
                new DeclarationBlock(declarationBlockConfig)
                    .export()
                    .asKind('type')
                    .withName(mergedTypeString)
                    .withContent(subTypes[0].content + fragmentMaskPartial).string,
            ].join('\n');
        }
        return [
            ...dependentTypesContent,
            ...subTypes.map(t => new DeclarationBlock(declarationBlockConfig)
                .export(this._config.exportFragmentSpreadSubTypes)
                .asKind('type')
                .withName(t.name)
                .withContent(`${t.content}${this._config.inlineFragmentTypes === 'mask' ? ` & { ' $fragmentName'?: '${t.name}' }` : ''}`).string),
            new DeclarationBlock(declarationBlockConfig)
                .export()
                .asKind('type')
                .withName(mergedTypeString)
                .withContent(subTypes.map(t => t.name).join(' | ')).string,
        ].join('\n');
    }
    buildFragmentTypeName(name, suffix, typeName = '') {
        return this._convertName(name, {
            useTypesPrefix: true,
            suffix: typeName && suffix ? `_${typeName}_${suffix}` : typeName ? `_${typeName}` : suffix,
        });
    }
    buildParentFieldName(typeName, parentName) {
        // queries/mutations/fragments are guaranteed to be unique type names,
        // so we can skip affixing the field names with typeName
        return operationTypes.includes(typeName) ? parentName : `${parentName}_${typeName}`;
    }
}
