/** @category Type Info */

import type { Maybe } from '../jsutils/Maybe.ts';

import type {
  ASTNode,
  DocumentNode,
  FragmentDefinitionNode,
  VariableDefinitionNode,
} from '../language/ast.ts';
import { isNode } from '../language/ast.ts';
import { Kind } from '../language/kinds.ts';
import type { ASTVisitor } from '../language/visitor.ts';
import { getEnterLeaveForKind } from '../language/visitor.ts';

import type {
  GraphQLArgument,
  GraphQLCompositeType,
  GraphQLEnumValue,
  GraphQLField,
  GraphQLInputField,
  GraphQLInputType,
  GraphQLOutputType,
  GraphQLType,
} from '../type/definition.ts';
import {
  getNamedType,
  getNullableType,
  isCompositeType,
  isEnumType,
  isInputObjectType,
  isInputType,
  isListType,
  isObjectType,
  isOutputType,
} from '../type/definition.ts';
import type { GraphQLDirective } from '../type/directives.ts';
import type { GraphQLSchema } from '../type/schema.ts';

import { typeFromAST } from './typeFromAST.ts';

/** @internal */
export interface FragmentSignature {
  readonly definition: FragmentDefinitionNode;
  readonly variableDefinitions: Map<string, VariableDefinitionNode>;
}

/**
 * TypeInfo is a utility class which, given a GraphQL schema, can keep track
 * of the current field and type definitions at any point in a GraphQL document
 * AST during a recursive descent by calling `enter(node)` and `leave(node)`.
 */
export class TypeInfo {
  private _schema: GraphQLSchema;
  private _typeStack: Array<Maybe<GraphQLOutputType>>;
  private _parentTypeStack: Array<Maybe<GraphQLCompositeType>>;
  private _inputTypeStack: Array<Maybe<GraphQLInputType>>;
  private _fieldDefStack: Array<Maybe<GraphQLField<unknown, unknown>>>;
  private _defaultValueStack: Array<unknown>;
  private _directive: Maybe<GraphQLDirective>;
  private _argument: Maybe<GraphQLArgument>;
  private _enumValue: Maybe<GraphQLEnumValue>;
  private _fragmentSignaturesByName: (
    fragmentName: string,
  ) => Maybe<FragmentSignature>;

  private _fragmentSignature: Maybe<FragmentSignature>;
  private _fragmentArgument: Maybe<VariableDefinitionNode>;

  /**
   * Creates a TypeInfo instance.
   * @param schema - Schema used for type lookups.
   * @param initialType - Optional type to use at the start of traversal.
   * @param fragmentSignatures - Fragment signatures available during traversal.
   * @example
   * ```ts
   * // Track field types during a visitWithTypeInfo traversal.
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema } from 'graphql/utilities';
   * import { TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * const seenTypes = [];
   *
   * visit(
   *   parse('{ greeting }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Field: () => {
   *       seenTypes.push(String(typeInfo.getType()));
   *     },
   *   }),
   * );
   *
   * seenTypes; // => ['String']
   * ```
   * @example
   * ```ts
   * // This variant starts from an initial type and supplies fragment signatures.
   * import { Kind, parse } from 'graphql/language';
   * import { buildSchema, TypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting(name: String): String
   *   }
   * `);
   * const fragmentDocument = parse(
   *   'fragment GreetingFields($name: String) on Query { greeting(name: $name) }',
   *   { experimentalFragmentArguments: true },
   * );
   * const fragmentDefinition = fragmentDocument.definitions[0];
   * const variableDefinition = fragmentDefinition.variableDefinitions[0];
   * const typeInfo = new TypeInfo(schema, schema.getQueryType(), (name) =>
   *   name === 'GreetingFields'
   *     ? {
   *         definition: fragmentDefinition,
   *         variableDefinitions: new Map([['name', variableDefinition]]),
   *       }
   *     : undefined,
   * );
   *
   * typeInfo.enter({
   *   kind: Kind.SELECTION_SET,
   *   selections: [],
   * });
   * typeInfo.enter({
   *   kind: Kind.FRAGMENT_SPREAD,
   *   name: { kind: Kind.NAME, value: 'GreetingFields' },
   *   arguments: [],
   *   directives: [],
   * });
   *
   * String(typeInfo.getParentType()); // => 'Query'
   * typeInfo.getFragmentSignature()?.definition.name.value; // => 'GreetingFields'
   * ```
   */
  constructor(
    schema: GraphQLSchema,
    /**
     * Initial type may be provided in rare cases to facilitate traversals
     * beginning somewhere other than documents.
     */
    initialType?: Maybe<GraphQLType>,
    fragmentSignatures?: Maybe<
      (fragmentName: string) => Maybe<FragmentSignature>
    >,
  ) {
    this._schema = schema;
    this._typeStack = [];
    this._parentTypeStack = [];
    this._inputTypeStack = [];
    this._fieldDefStack = [];
    this._defaultValueStack = [];
    this._directive = null;
    this._argument = null;
    this._enumValue = null;
    this._fragmentSignaturesByName = fragmentSignatures ?? (() => null);
    this._fragmentSignature = null;
    this._fragmentArgument = null;
    if (initialType) {
      if (isInputType(initialType)) {
        this._inputTypeStack.push(initialType);
      }
      if (isCompositeType(initialType)) {
        this._parentTypeStack.push(initialType);
      }
      if (isOutputType(initialType)) {
        this._typeStack.push(initialType);
      }
    }
  }

  /**
   * Returns the value used by `Object.prototype.toString`.
   * @returns The built-in string tag for this object.
   */
  get [Symbol.toStringTag](): string {
    return 'TypeInfo';
  }

  /**
   * Returns the current output type at this point in traversal.
   * @returns The current output type, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     viewer: User
   *   }
   *
   *   type User {
   *     name: String
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * const fieldTypes = {};
   *
   * visit(
   *   parse('{ viewer { name } }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Field: (node) => {
   *       fieldTypes[node.name.value] = String(typeInfo.getType());
   *     },
   *   }),
   * );
   *
   * fieldTypes; // => { viewer: 'User', name: 'String' }
   * ```
   */
  getType(): Maybe<GraphQLOutputType> {
    return this._typeStack.at(-1);
  }

  /**
   * Returns the current parent composite type.
   * @returns The current parent composite type, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     viewer: User
   *   }
   *
   *   type User {
   *     name: String
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * const parentTypes = {};
   *
   * visit(
   *   parse('{ viewer { name } }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Field: (node) => {
   *       parentTypes[node.name.value] = String(typeInfo.getParentType());
   *     },
   *   }),
   * );
   *
   * parentTypes; // => { viewer: 'Query', name: 'User' }
   * ```
   */
  getParentType(): Maybe<GraphQLCompositeType> {
    return this._parentTypeStack.at(-1);
  }

  /**
   * Returns the current input type at this point in traversal.
   * @returns The current input type, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     reviews(stars: Int!, sort: Sort = NEWEST): [String]
   *   }
   *
   *   enum Sort {
   *     NEWEST
   *     OLDEST
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * const inputTypes = {};
   *
   * visit(
   *   parse('{ reviews(stars: 5, sort: OLDEST) }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Argument: (node) => {
   *       inputTypes[node.name.value] = String(typeInfo.getInputType());
   *     },
   *   }),
   * );
   *
   * inputTypes; // => { stars: 'Int!', sort: 'Sort' }
   * ```
   */
  getInputType(): Maybe<GraphQLInputType> {
    return this._inputTypeStack.at(-1);
  }

  // Note: continues to expose the closest enclosing valid input type if
  // traversal descends into syntax with no corresponding GraphQL input type.
  /**
   * Returns the parent input type for the current input position.
   * @returns The parent input type, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   input ReviewFilter {
   *     stars: Int!
   *   }
   *
   *   type Query {
   *     reviews(filter: ReviewFilter): [String]
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * const parentInputTypes = {};
   *
   * visit(
   *   parse('{ reviews(filter: { stars: 5 }) }'),
   *   visitWithTypeInfo(typeInfo, {
   *     ObjectField: (node) => {
   *       parentInputTypes[node.name.value] = String(typeInfo.getParentInputType());
   *     },
   *   }),
   * );
   *
   * parentInputTypes; // => { stars: 'ReviewFilter' }
   * ```
   */
  getParentInputType(): Maybe<GraphQLInputType> {
    return this._inputTypeStack.at(-2);
  }

  /**
   * Returns the current field definition.
   * @returns The current field definition, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * let fieldName;
   *
   * visit(
   *   parse('{ greeting }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Field: () => {
   *       fieldName = typeInfo.getFieldDef()?.name;
   *     },
   *   }),
   * );
   *
   * fieldName; // => 'greeting'
   * ```
   */
  getFieldDef(): Maybe<GraphQLField<unknown, unknown>> {
    return this._fieldDefStack.at(-1);
  }

  /**
   * Returns the default input representation for the current input position.
   * @returns The current default input, if one is available.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     reviews(limit: Int = 10): [String]
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * let defaultLimit;
   *
   * visit(
   *   parse('{ reviews(limit: 5) }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Argument: () => {
   *       defaultLimit = typeInfo.getDefaultValue();
   *     },
   *   }),
   * );
   *
   * defaultLimit; // => { literal: { kind: 'IntValue', value: '10' } }
   * ```
   */
  getDefaultValue(): unknown {
    return this._defaultValueStack.at(-1);
  }

  /**
   * Returns the current directive definition.
   * @returns The current directive definition, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * let directiveName;
   *
   * visit(
   *   parse('{ greeting @include(if: true) }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Directive: () => {
   *       directiveName = typeInfo.getDirective()?.name;
   *     },
   *   }),
   * );
   *
   * directiveName; // => 'include'
   * ```
   */
  getDirective(): Maybe<GraphQLDirective> {
    return this._directive;
  }

  /**
   * Returns the current argument definition.
   * @returns The current argument definition, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     reviews(limit: Int = 10): [String]
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * let argumentName;
   *
   * visit(
   *   parse('{ reviews(limit: 5) }'),
   *   visitWithTypeInfo(typeInfo, {
   *     Argument: () => {
   *       argumentName = typeInfo.getArgument()?.name;
   *     },
   *   }),
   * );
   *
   * argumentName; // => 'limit'
   * ```
   */
  getArgument(): Maybe<GraphQLArgument> {
    return this._argument;
  }

  /**
   * Returns the current fragment signature.
   * @returns The fragment signature for the current fragment definition.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const document = parse(
   *   `
   *     {
   *       ...GreetingFields
   *     }
   *
   *     fragment GreetingFields on Query {
   *       greeting
   *     }
   *   `,
   *   { experimentalFragmentArguments: true },
   * );
   * const typeInfo = new TypeInfo(schema);
   * let fragmentName;
   *
   * visit(
   *   document,
   *   visitWithTypeInfo(typeInfo, {
   *     FragmentSpread: () => {
   *       fragmentName = typeInfo.getFragmentSignature()?.definition.name.value;
   *     },
   *   }),
   * );
   *
   * fragmentName; // => 'GreetingFields'
   * ```
   */
  getFragmentSignature(): Maybe<FragmentSignature> {
    return this._fragmentSignature;
  }

  /**
   * Returns the function used to look up fragment signatures by name.
   * @returns A function that maps fragment names to fragment signatures.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const document = parse(
   *   `
   *     {
   *       ...GreetingFields
   *     }
   *
   *     fragment GreetingFields on Query {
   *       greeting
   *     }
   *   `,
   *   { experimentalFragmentArguments: true },
   * );
   * const typeInfo = new TypeInfo(schema);
   * let fragmentName;
   *
   * visit(
   *   document,
   *   visitWithTypeInfo(typeInfo, {
   *     Document: () => {
   *       const getFragmentSignature = typeInfo.getFragmentSignatureByName();
   *       fragmentName =
   *         getFragmentSignature('GreetingFields')?.definition.name.value;
   *     },
   *   }),
   * );
   *
   * fragmentName; // => 'GreetingFields'
   * ```
   */
  getFragmentSignatureByName(): (
    fragmentName: string,
  ) => Maybe<FragmentSignature> {
    return this._fragmentSignaturesByName;
  }

  /**
   * Returns the current fragment argument definition.
   * @returns The variable definition for the current fragment argument.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting(name: String): String
   *   }
   * `);
   * const document = parse(
   *   `
   *     {
   *       ...GreetingFields(name: "Ada")
   *     }
   *
   *     fragment GreetingFields($name: String) on Query {
   *       greeting(name: $name)
   *     }
   *   `,
   *   { experimentalFragmentArguments: true },
   * );
   * const typeInfo = new TypeInfo(schema);
   * let argumentName;
   *
   * visit(
   *   document,
   *   visitWithTypeInfo(typeInfo, {
   *     FragmentArgument: () => {
   *       argumentName = typeInfo.getFragmentArgument()?.variable.name.value;
   *     },
   *   }),
   * );
   *
   * argumentName; // => 'name'
   * ```
   */
  getFragmentArgument(): Maybe<VariableDefinitionNode> {
    return this._fragmentArgument;
  }

  /**
   * Returns the current enum value definition.
   * @returns The current enum value definition, if known.
   * @example
   * ```ts
   * import { parse, visit } from 'graphql/language';
   * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   enum Sort {
   *     NEWEST
   *     OLDEST
   *   }
   *
   *   type Query {
   *     reviews(sort: Sort = NEWEST): [String]
   *   }
   * `);
   * const typeInfo = new TypeInfo(schema);
   * let enumValueName;
   *
   * visit(
   *   parse('{ reviews(sort: OLDEST) }'),
   *   visitWithTypeInfo(typeInfo, {
   *     EnumValue: () => {
   *       enumValueName = typeInfo.getEnumValue()?.name;
   *     },
   *   }),
   * );
   *
   * enumValueName; // => 'OLDEST'
   * ```
   */
  getEnumValue(): Maybe<GraphQLEnumValue> {
    return this._enumValue;
  }

  /**
   * Updates this TypeInfo instance for an entered AST node.
   * @param node - AST node being entered.
   * @returns Nothing.
   * @example
   * ```ts
   * import { Kind, parse } from 'graphql/language';
   * import { buildSchema, TypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const document = parse('{ greeting }');
   * const operation = document.definitions[0];
   * const selectionSet = operation.selectionSet;
   * const field = selectionSet.selections[0];
   * const typeInfo = new TypeInfo(schema);
   *
   * typeInfo.enter(operation);
   * typeInfo.enter(selectionSet);
   * typeInfo.enter(field);
   *
   * field.kind; // => Kind.FIELD
   * typeInfo.getParentType()?.name; // => 'Query'
   * String(typeInfo.getType()); // => 'String'
   * ```
   */
  enter(node: ASTNode): void {
    const schema = this._schema;
    // Note: many of the types below are explicitly typed as "unknown" to drop
    // any assumptions of a valid schema to ensure runtime types are properly
    // checked before continuing since TypeInfo is used as part of validation
    // which occurs before guarantees of schema and document validity.
    switch (node.kind) {
      case Kind.DOCUMENT: {
        const fragmentSignatures = getFragmentSignatures(node);
        this._fragmentSignaturesByName = (fragmentName: string) =>
          fragmentSignatures.get(fragmentName);
        break;
      }
      case Kind.SELECTION_SET: {
        const namedType: unknown = getNamedType(this.getType());
        this._parentTypeStack.push(
          isCompositeType(namedType) ? namedType : undefined,
        );
        break;
      }
      case Kind.FIELD: {
        const parentType = this.getParentType();
        let fieldDef;
        let fieldType: unknown;
        if (parentType) {
          fieldDef = schema.getField(parentType, node.name.value);
          if (fieldDef) {
            fieldType = fieldDef.type;
          }
        }
        this._fieldDefStack.push(fieldDef);
        this._typeStack.push(isOutputType(fieldType) ? fieldType : undefined);
        break;
      }
      case Kind.DIRECTIVE:
        this._directive = schema.getDirective(node.name.value);
        break;
      case Kind.OPERATION_DEFINITION: {
        const rootType = schema.getRootType(node.operation);
        this._typeStack.push(isObjectType(rootType) ? rootType : undefined);
        break;
      }
      case Kind.FRAGMENT_SPREAD: {
        this._fragmentSignature = this.getFragmentSignatureByName()(
          node.name.value,
        );
        break;
      }
      case Kind.INLINE_FRAGMENT:
      case Kind.FRAGMENT_DEFINITION: {
        const typeConditionAST = node.typeCondition;
        const outputType: unknown = typeConditionAST
          ? typeFromAST(schema, typeConditionAST)
          : getNamedType(this.getType());
        this._typeStack.push(isOutputType(outputType) ? outputType : undefined);
        break;
      }
      case Kind.VARIABLE_DEFINITION: {
        const inputType: unknown = typeFromAST(schema, node.type);
        this._inputTypeStack.push(
          isInputType(inputType) ? inputType : undefined,
        );
        break;
      }
      case Kind.ARGUMENT: {
        let argDef;
        let argType: unknown;
        const fieldOrDirective = this.getDirective() ?? this.getFieldDef();
        if (fieldOrDirective) {
          argDef = fieldOrDirective.args.find(
            (arg) => arg.name === node.name.value,
          );
          if (argDef) {
            argType = argDef.type;
          }
        }
        this._argument = argDef;
        this._defaultValueStack.push(
          argDef?.default ?? argDef?.defaultValue ?? undefined,
        );
        this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
        break;
      }
      case Kind.FRAGMENT_ARGUMENT: {
        const fragmentSignature = this.getFragmentSignature();
        const argDef = fragmentSignature?.variableDefinitions.get(
          node.name.value,
        );
        this._fragmentArgument = argDef;
        let argType: unknown;
        if (argDef) {
          argType = typeFromAST(this._schema, argDef.type);
        }
        this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
        break;
      }
      case Kind.LIST: {
        const listType: unknown = getNullableType(this.getInputType());
        const itemType: unknown = isListType(listType)
          ? listType.ofType
          : undefined;
        // List positions never have a default value.
        this._defaultValueStack.push(undefined);
        this._inputTypeStack.push(isInputType(itemType) ? itemType : undefined);
        break;
      }
      case Kind.OBJECT_FIELD: {
        const objectType: unknown = getNamedType(this.getInputType());
        let inputFieldType: GraphQLInputType | undefined;
        let inputField: GraphQLInputField | undefined;
        if (isInputObjectType(objectType)) {
          inputField = objectType.getFields()[node.name.value];
          if (inputField != null) {
            inputFieldType = inputField.type;
          }
        }
        this._defaultValueStack.push(
          inputField?.default ?? inputField?.defaultValue ?? undefined,
        );
        this._inputTypeStack.push(
          isInputType(inputFieldType) ? inputFieldType : undefined,
        );
        break;
      }
      case Kind.ENUM: {
        const enumType: unknown = getNamedType(this.getInputType());
        let enumValue;
        if (isEnumType(enumType)) {
          enumValue = enumType.getValue(node.value);
        }
        this._enumValue = enumValue;
        break;
      }
      default:
      // Ignore other nodes
    }
  }

  /**
   * Updates this TypeInfo instance for a left AST node.
   * @param node - AST node being entered.
   * @returns Nothing.
   * @example
   * ```ts
   * import { parse } from 'graphql/language';
   * import { buildSchema, TypeInfo } from 'graphql/utilities';
   *
   * const schema = buildSchema(`
   *   type Query {
   *     greeting: String
   *   }
   * `);
   * const document = parse('{ greeting }');
   * const operation = document.definitions[0];
   * const selectionSet = operation.selectionSet;
   * const field = selectionSet.selections[0];
   * const typeInfo = new TypeInfo(schema);
   *
   * typeInfo.enter(operation);
   * typeInfo.enter(selectionSet);
   * typeInfo.enter(field);
   * String(typeInfo.getType()); // => 'String'
   *
   * typeInfo.leave(field);
   * typeInfo.getType(); // => undefined
   * ```
   */
  leave(node: ASTNode): void {
    switch (node.kind) {
      case Kind.DOCUMENT:
        this._fragmentSignaturesByName = () => null;
        break;
      case Kind.SELECTION_SET:
        this._parentTypeStack.pop();
        break;
      case Kind.FIELD:
        this._fieldDefStack.pop();
        this._typeStack.pop();
        break;
      case Kind.DIRECTIVE:
        this._directive = null;
        break;
      case Kind.FRAGMENT_SPREAD:
        this._fragmentSignature = null;
        break;
      case Kind.OPERATION_DEFINITION:
      case Kind.INLINE_FRAGMENT:
      case Kind.FRAGMENT_DEFINITION:
        this._typeStack.pop();
        break;
      case Kind.VARIABLE_DEFINITION:
        this._inputTypeStack.pop();
        break;
      case Kind.ARGUMENT:
        this._argument = null;
        this._defaultValueStack.pop();
        this._inputTypeStack.pop();
        break;
      case Kind.FRAGMENT_ARGUMENT: {
        this._fragmentArgument = null;
        this._defaultValueStack.pop();
        this._inputTypeStack.pop();
        break;
      }
      case Kind.LIST:
      case Kind.OBJECT_FIELD:
        this._defaultValueStack.pop();
        this._inputTypeStack.pop();
        break;
      case Kind.ENUM:
        this._enumValue = null;
        break;
      default:
      // Ignore other nodes
    }
  }
}

function getFragmentSignatures(
  document: DocumentNode,
): Map<string, FragmentSignature> {
  const fragmentSignatures = new Map<string, FragmentSignature>();
  for (const definition of document.definitions) {
    if (definition.kind === Kind.FRAGMENT_DEFINITION) {
      const variableDefinitions = new Map<string, VariableDefinitionNode>();
      if (definition.variableDefinitions) {
        for (const varDef of definition.variableDefinitions) {
          variableDefinitions.set(varDef.variable.name.value, varDef);
        }
      }
      const signature = { definition, variableDefinitions };
      fragmentSignatures.set(definition.name.value, signature);
    }
  }
  return fragmentSignatures;
}

/**
 * Creates a new visitor instance which maintains a provided TypeInfo instance
 * along with visiting visitor.
 * @param typeInfo - TypeInfo instance to update during traversal.
 * @param visitor - Visitor callbacks to wrap with TypeInfo updates.
 * @returns A visitor that keeps TypeInfo in sync while delegating callbacks.
 * @example
 * ```ts
 * import { parse, visit } from 'graphql/language';
 * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities';
 *
 * const schema = buildSchema(`
 *   type Query {
 *     greeting: String
 *   }
 * `);
 * const typeInfo = new TypeInfo(schema);
 * const fields = [];
 *
 * visit(
 *   parse('{ greeting }'),
 *   visitWithTypeInfo(typeInfo, {
 *     Field: (node) => {
 *       fields.push({
 *         name: node.name.value,
 *         parentType: String(typeInfo.getParentType()),
 *         type: String(typeInfo.getType()),
 *       });
 *     },
 *   }),
 * );
 *
 * fields; // => [{ name: 'greeting', parentType: 'Query', type: 'String' }]
 * ```
 */
export function visitWithTypeInfo(
  typeInfo: TypeInfo,
  visitor: ASTVisitor,
): ASTVisitor {
  return {
    enter(...args) {
      const node = args[0];
      typeInfo.enter(node);
      const fn = getEnterLeaveForKind(visitor, node.kind).enter;
      if (fn) {
        const result = fn.apply(visitor, args);
        if (result !== undefined) {
          typeInfo.leave(node);
          if (isNode(result)) {
            typeInfo.enter(result);
          }
        }
        return result;
      }
    },
    leave(...args) {
      const node = args[0];
      const fn = getEnterLeaveForKind(visitor, node.kind).leave;
      let result;
      if (fn) {
        result = fn.apply(visitor, args);
      }
      typeInfo.leave(node);
      return result;
    },
  };
}