from typing import Callable, Dict, List, Optional, Union, TypeVar, cast
from functools import partial

from .ast import (
    ArgumentNode,
    BooleanValueNode,
    DefinitionNode,
    DirectiveDefinitionNode,
    DirectiveNode,
    DocumentNode,
    EnumTypeDefinitionNode,
    EnumTypeExtensionNode,
    EnumValueDefinitionNode,
    EnumValueNode,
    FieldDefinitionNode,
    FieldNode,
    FloatValueNode,
    FragmentDefinitionNode,
    FragmentSpreadNode,
    InlineFragmentNode,
    InputObjectTypeDefinitionNode,
    InputObjectTypeExtensionNode,
    InputValueDefinitionNode,
    IntValueNode,
    InterfaceTypeDefinitionNode,
    InterfaceTypeExtensionNode,
    ListTypeNode,
    ListValueNode,
    Location,
    NameNode,
    NamedTypeNode,
    NonNullTypeNode,
    NullValueNode,
    ObjectFieldNode,
    ObjectTypeDefinitionNode,
    ObjectTypeExtensionNode,
    ObjectValueNode,
    OperationDefinitionNode,
    OperationType,
    OperationTypeDefinitionNode,
    ScalarTypeDefinitionNode,
    ScalarTypeExtensionNode,
    SchemaDefinitionNode,
    SchemaExtensionNode,
    SelectionNode,
    SelectionSetNode,
    StringValueNode,
    TypeNode,
    TypeSystemDefinitionNode,
    TypeSystemExtensionNode,
    UnionTypeDefinitionNode,
    UnionTypeExtensionNode,
    ValueNode,
    VariableDefinitionNode,
    VariableNode,
)
from .directive_locations import DirectiveLocation
from .ast import Token
from .lexer import Lexer, is_punctuator_token_kind
from .source import Source
from .token_kind import TokenKind
from ..error import GraphQLError, GraphQLSyntaxError
from ..pyutils import inspect

__all__ = ["parse", "parse_type", "parse_value"]

T = TypeVar("T")

SourceType = Union[Source, str]


def parse(
    source: SourceType,
    no_location: bool = False,
    experimental_fragment_variables: bool = False,
) -> DocumentNode:
    """Given a GraphQL source, parse it into a Document.

    Throws GraphQLError if a syntax error is encountered.

    By default, the parser creates AST nodes that know the location in the source that
    they correspond to. The ``no_location`` option disables that behavior for
    performance or testing.

    Experimental features:

    If ``experimental_fragment_variables`` is set to ``True``, the parser will
    understand and parse variable definitions contained in a fragment definition.
    They'll be represented in the
    :attr:`~graphql.language.FragmentDefinitionNode.variable_definitions` field
    of the :class:`~graphql.language.FragmentDefinitionNode`.

    The syntax is identical to normal, query-defined variables. For example::

        fragment A($var: Boolean = false) on T  {
          ...
        }
    """
    parser = Parser(
        source,
        no_location=no_location,
        experimental_fragment_variables=experimental_fragment_variables,
    )
    return parser.parse_document()


def parse_value(
    source: SourceType,
    no_location: bool = False,
    experimental_fragment_variables: bool = False,
) -> ValueNode:
    """Parse the AST for a given string containing a GraphQL value.

    Throws GraphQLError if a syntax error is encountered.

    This is useful within tools that operate upon GraphQL Values directly and in
    isolation of complete GraphQL documents.

    Consider providing the results to the utility function:
    :func:`~graphql.value_from_ast`.
    """
    parser = Parser(
        source,
        no_location=no_location,
        experimental_fragment_variables=experimental_fragment_variables,
    )
    parser.expect_token(TokenKind.SOF)
    value = parser.parse_value_literal(False)
    parser.expect_token(TokenKind.EOF)
    return value


def parse_type(
    source: SourceType,
    no_location: bool = False,
    experimental_fragment_variables: bool = False,
) -> TypeNode:
    """Parse the AST for a given string containing a GraphQL Type.

    Throws GraphQLError if a syntax error is encountered.

    This is useful within tools that operate upon GraphQL Types directly and
    in isolation of complete GraphQL documents.

    Consider providing the results to the utility function:
    :func:`~graphql.value_from_ast`.
    """
    parser = Parser(
        source,
        no_location=no_location,
        experimental_fragment_variables=experimental_fragment_variables,
    )
    parser.expect_token(TokenKind.SOF)
    type_ = parser.parse_type_reference()
    parser.expect_token(TokenKind.EOF)
    return type_


class Parser:

    _lexer: Lexer
    _no_Location: bool
    _experimental_fragment_variables: bool

    def __init__(
        self,
        source: SourceType,
        no_location: bool = False,
        experimental_fragment_variables: bool = False,
    ):
        if isinstance(source, str):
            source = Source(source)
        elif not isinstance(source, Source):
            raise TypeError(f"Must provide Source. Received: {inspect(source)}.")
        self._lexer = Lexer(source)
        self._no_location = no_location
        self._experimental_fragment_variables = experimental_fragment_variables

    def parse_name(self) -> NameNode:
        """Convert a name lex token into a name parse node."""
        token = self.expect_token(TokenKind.NAME)
        return NameNode(value=token.value, loc=self.loc(token))

    # Implement the parsing rules in the Document section.

    def parse_document(self) -> DocumentNode:
        """Document: Definition+"""
        start = self._lexer.token
        return DocumentNode(
            definitions=self.many(TokenKind.SOF, self.parse_definition, TokenKind.EOF),
            loc=self.loc(start),
        )

    _parse_definition_method_names: Dict[str, str] = {
        **dict.fromkeys(("query", "mutation", "subscription"), "operation_definition"),
        "fragment": "fragment_definition",
        **dict.fromkeys(
            (
                "schema",
                "scalar",
                "type",
                "interface",
                "union",
                "enum",
                "input",
                "directive",
            ),
            "type_system_definition",
        ),
        "extend": "type_system_extension",
    }

    def parse_definition(self) -> DefinitionNode:
        """Definition: ExecutableDefinition or TypeSystemDefinition/Extension

        ExecutableDefinition: OperationDefinition or FragmentDefinition
        """
        if self.peek(TokenKind.NAME):
            method_name = self._parse_definition_method_names.get(
                cast(str, self._lexer.token.value)
            )
            if method_name:
                return getattr(self, f"parse_{method_name}")()
        elif self.peek(TokenKind.BRACE_L):
            return self.parse_operation_definition()
        elif self.peek_description():
            return self.parse_type_system_definition()
        raise self.unexpected()

    _parse_executable_definition_method_names: Dict[str, str] = {
        **dict.fromkeys(("query", "mutation", "subscription"), "operation_definition"),
        **dict.fromkeys(("fragment",), "fragment_definition"),
    }

    # Implement the parsing rules in the Operations section.

    def parse_operation_definition(self) -> OperationDefinitionNode:
        """OperationDefinition"""
        start = self._lexer.token
        if self.peek(TokenKind.BRACE_L):
            return OperationDefinitionNode(
                operation=OperationType.QUERY,
                name=None,
                variable_definitions=[],
                directives=[],
                selection_set=self.parse_selection_set(),
                loc=self.loc(start),
            )
        operation = self.parse_operation_type()
        name = self.parse_name() if self.peek(TokenKind.NAME) else None
        return OperationDefinitionNode(
            operation=operation,
            name=name,
            variable_definitions=self.parse_variable_definitions(),
            directives=self.parse_directives(False),
            selection_set=self.parse_selection_set(),
            loc=self.loc(start),
        )

    def parse_operation_type(self) -> OperationType:
        """OperationType: one of query mutation subscription"""
        operation_token = self.expect_token(TokenKind.NAME)
        try:
            return OperationType(operation_token.value)
        except ValueError:
            raise self.unexpected(operation_token)

    def parse_variable_definitions(self) -> List[VariableDefinitionNode]:
        """VariableDefinitions: (VariableDefinition+)"""
        return self.optional_many(
            TokenKind.PAREN_L, self.parse_variable_definition, TokenKind.PAREN_R
        )

    def parse_variable_definition(self) -> VariableDefinitionNode:
        """VariableDefinition: Variable: Type DefaultValue? Directives[Const]?"""
        start = self._lexer.token
        return VariableDefinitionNode(
            variable=self.parse_variable(),
            type=self.expect_token(TokenKind.COLON) and self.parse_type_reference(),
            default_value=self.parse_value_literal(True)
            if self.expect_optional_token(TokenKind.EQUALS)
            else None,
            directives=self.parse_directives(True),
            loc=self.loc(start),
        )

    def parse_variable(self) -> VariableNode:
        """Variable: $Name"""
        start = self._lexer.token
        self.expect_token(TokenKind.DOLLAR)
        return VariableNode(name=self.parse_name(), loc=self.loc(start))

    def parse_selection_set(self) -> SelectionSetNode:
        """SelectionSet: {Selection+}"""
        start = self._lexer.token
        return SelectionSetNode(
            selections=self.many(
                TokenKind.BRACE_L, self.parse_selection, TokenKind.BRACE_R
            ),
            loc=self.loc(start),
        )

    def parse_selection(self) -> SelectionNode:
        """Selection: Field or FragmentSpread or InlineFragment"""
        return (
            self.parse_fragment if self.peek(TokenKind.SPREAD) else self.parse_field
        )()

    def parse_field(self) -> FieldNode:
        """Field: Alias? Name Arguments? Directives? SelectionSet?"""
        start = self._lexer.token
        name_or_alias = self.parse_name()
        if self.expect_optional_token(TokenKind.COLON):
            alias: Optional[NameNode] = name_or_alias
            name = self.parse_name()
        else:
            alias = None
            name = name_or_alias
        return FieldNode(
            alias=alias,
            name=name,
            arguments=self.parse_arguments(False),
            directives=self.parse_directives(False),
            selection_set=self.parse_selection_set()
            if self.peek(TokenKind.BRACE_L)
            else None,
            loc=self.loc(start),
        )

    def parse_arguments(self, is_const: bool) -> List[ArgumentNode]:
        """Arguments[Const]: (Argument[?Const]+)"""
        item = self.parse_const_argument if is_const else self.parse_argument
        return self.optional_many(TokenKind.PAREN_L, item, TokenKind.PAREN_R)

    def parse_argument(self) -> ArgumentNode:
        """Argument: Name : Value"""
        start = self._lexer.token
        name = self.parse_name()

        self.expect_token(TokenKind.COLON)
        return ArgumentNode(
            name=name, value=self.parse_value_literal(False), loc=self.loc(start)
        )

    def parse_const_argument(self) -> ArgumentNode:
        """Argument[Const]: Name : Value[?Const]"""
        start = self._lexer.token
        return ArgumentNode(
            name=self.parse_name(),
            value=self.expect_token(TokenKind.COLON) and self.parse_value_literal(True),
            loc=self.loc(start),
        )

    # Implement the parsing rules in the Fragments section.

    def parse_fragment(self) -> Union[FragmentSpreadNode, InlineFragmentNode]:
        """Corresponds to both FragmentSpread and InlineFragment in the spec.

        FragmentSpread: ... FragmentName Directives?
        InlineFragment: ... TypeCondition? Directives? SelectionSet
        """
        start = self._lexer.token
        self.expect_token(TokenKind.SPREAD)

        has_type_condition = self.expect_optional_keyword("on")
        if not has_type_condition and self.peek(TokenKind.NAME):
            return FragmentSpreadNode(
                name=self.parse_fragment_name(),
                directives=self.parse_directives(False),
                loc=self.loc(start),
            )
        return InlineFragmentNode(
            type_condition=self.parse_named_type() if has_type_condition else None,
            directives=self.parse_directives(False),
            selection_set=self.parse_selection_set(),
            loc=self.loc(start),
        )

    def parse_fragment_definition(self) -> FragmentDefinitionNode:
        """FragmentDefinition"""
        start = self._lexer.token
        self.expect_keyword("fragment")
        # Experimental support for defining variables within fragments changes
        # the grammar of FragmentDefinition
        if self._experimental_fragment_variables:
            return FragmentDefinitionNode(
                name=self.parse_fragment_name(),
                variable_definitions=self.parse_variable_definitions(),
                type_condition=self.parse_type_condition(),
                directives=self.parse_directives(False),
                selection_set=self.parse_selection_set(),
                loc=self.loc(start),
            )
        return FragmentDefinitionNode(
            name=self.parse_fragment_name(),
            type_condition=self.parse_type_condition(),
            directives=self.parse_directives(False),
            selection_set=self.parse_selection_set(),
            loc=self.loc(start),
        )

    def parse_fragment_name(self) -> NameNode:
        """FragmentName: Name but not ``on``"""
        if self._lexer.token.value == "on":
            raise self.unexpected()
        return self.parse_name()

    def parse_type_condition(self) -> NamedTypeNode:
        """TypeCondition: NamedType"""
        self.expect_keyword("on")
        return self.parse_named_type()

    # Implement the parsing rules in the Values section.

    _parse_value_literal_method_names: Dict[TokenKind, str] = {
        TokenKind.BRACKET_L: "list",
        TokenKind.BRACE_L: "object",
        TokenKind.INT: "int",
        TokenKind.FLOAT: "float",
        TokenKind.STRING: "string_literal",
        TokenKind.BLOCK_STRING: "string_literal",
        TokenKind.NAME: "named_values",
        TokenKind.DOLLAR: "variable_value",
    }

    def parse_value_literal(self, is_const: bool) -> ValueNode:
        method_name = self._parse_value_literal_method_names.get(self._lexer.token.kind)
        if method_name:  # pragma: no cover
            return getattr(self, f"parse_{method_name}")(is_const)
        raise self.unexpected()  # pragma: no cover

    def parse_string_literal(self, _is_const: bool = False) -> StringValueNode:
        token = self._lexer.token
        self._lexer.advance()
        return StringValueNode(
            value=token.value,
            block=token.kind == TokenKind.BLOCK_STRING,
            loc=self.loc(token),
        )

    def parse_list(self, is_const: bool) -> ListValueNode:
        """ListValue[Const]"""
        start = self._lexer.token
        item = partial(self.parse_value_literal, is_const)
        # noinspection PyTypeChecker
        return ListValueNode(
            values=self.any(TokenKind.BRACKET_L, item, TokenKind.BRACKET_R),
            loc=self.loc(start),
        )

    def parse_object_field(self, is_const: bool) -> ObjectFieldNode:
        start = self._lexer.token
        name = self.parse_name()
        self.expect_token(TokenKind.COLON)

        return ObjectFieldNode(
            name=name, value=self.parse_value_literal(is_const), loc=self.loc(start)
        )

    def parse_object(self, is_const: bool) -> ObjectValueNode:
        """ObjectValue[Const]"""
        start = self._lexer.token
        item = partial(self.parse_object_field, is_const)
        return ObjectValueNode(
            fields=self.any(TokenKind.BRACE_L, item, TokenKind.BRACE_R),
            loc=self.loc(start),
        )

    def parse_int(self, _is_const: bool = False) -> IntValueNode:
        token = self._lexer.token
        self._lexer.advance()
        return IntValueNode(value=token.value, loc=self.loc(token))

    def parse_float(self, _is_const: bool = False) -> FloatValueNode:
        token = self._lexer.token
        self._lexer.advance()
        return FloatValueNode(value=token.value, loc=self.loc(token))

    def parse_named_values(self, _is_const: bool = False) -> ValueNode:
        token = self._lexer.token
        value = token.value
        self._lexer.advance()
        if value == "true":
            return BooleanValueNode(value=True, loc=self.loc(token))
        if value == "false":
            return BooleanValueNode(value=False, loc=self.loc(token))
        if value == "null":
            return NullValueNode(loc=self.loc(token))
        return EnumValueNode(value=value, loc=self.loc(token))

    def parse_variable_value(self, is_const: bool) -> VariableNode:
        if not is_const:
            return self.parse_variable()
        raise self.unexpected()

    # Implement the parsing rules in the Directives section.

    def parse_directives(self, is_const: bool) -> List[DirectiveNode]:
        """Directives[Const]: Directive[?Const]+"""
        directives: List[DirectiveNode] = []
        append = directives.append
        while self.peek(TokenKind.AT):
            append(self.parse_directive(is_const))
        return directives

    def parse_directive(self, is_const: bool) -> DirectiveNode:
        """Directive[Const]: @ Name Arguments[?Const]?"""
        start = self._lexer.token
        self.expect_token(TokenKind.AT)
        return DirectiveNode(
            name=self.parse_name(),
            arguments=self.parse_arguments(is_const),
            loc=self.loc(start),
        )

    # Implement the parsing rules in the Types section.

    def parse_type_reference(self) -> TypeNode:
        """Type: NamedType or ListType or NonNullType"""
        start = self._lexer.token
        if self.expect_optional_token(TokenKind.BRACKET_L):
            type_ = self.parse_type_reference()
            self.expect_token(TokenKind.BRACKET_R)
            type_ = ListTypeNode(type=type_, loc=self.loc(start))
        else:
            type_ = self.parse_named_type()
        if self.expect_optional_token(TokenKind.BANG):
            return NonNullTypeNode(type=type_, loc=self.loc(start))
        return type_

    def parse_named_type(self) -> NamedTypeNode:
        """NamedType: Name"""
        start = self._lexer.token
        return NamedTypeNode(name=self.parse_name(), loc=self.loc(start))

    # Implement the parsing rules in the Type Definition section.

    _parse_type_system_definition_method_names: Dict[str, str] = {
        "schema": "schema_definition",
        "scalar": "scalar_type_definition",
        "type": "object_type_definition",
        "interface": "interface_type_definition",
        "union": "union_type_definition",
        "enum": "enum_type_definition",
        "input": "input_object_type_definition",
        "directive": "directive_definition",
    }

    def parse_type_system_definition(self) -> TypeSystemDefinitionNode:
        """TypeSystemDefinition"""
        # Many definitions begin with a description and require a lookahead.
        keyword_token = (
            self._lexer.lookahead() if self.peek_description() else self._lexer.token
        )
        method_name = self._parse_type_system_definition_method_names.get(
            cast(str, keyword_token.value)
        )
        if method_name:
            return getattr(self, f"parse_{method_name}")()
        raise self.unexpected(keyword_token)

    _parse_type_extension_method_names: Dict[str, str] = {
        "schema": "schema_extension",
        "scalar": "scalar_type_extension",
        "type": "object_type_extension",
        "interface": "interface_type_extension",
        "union": "union_type_extension",
        "enum": "enum_type_extension",
        "input": "input_object_type_extension",
    }

    def parse_type_system_extension(self) -> TypeSystemExtensionNode:
        """TypeSystemExtension"""
        keyword_token = self._lexer.lookahead()
        if keyword_token.kind == TokenKind.NAME:
            method_name = self._parse_type_extension_method_names.get(
                cast(str, keyword_token.value)
            )
            if method_name:  # pragma: no cover
                return getattr(self, f"parse_{method_name}")()
        raise self.unexpected(keyword_token)

    def peek_description(self) -> bool:
        return self.peek(TokenKind.STRING) or self.peek(TokenKind.BLOCK_STRING)

    def parse_description(self) -> Optional[StringValueNode]:
        """Description: StringValue"""
        if self.peek_description():
            return self.parse_string_literal()
        return None

    def parse_schema_definition(self) -> SchemaDefinitionNode:
        """SchemaDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("schema")
        directives = self.parse_directives(True)
        operation_types = self.many(
            TokenKind.BRACE_L, self.parse_operation_type_definition, TokenKind.BRACE_R
        )
        return SchemaDefinitionNode(
            description=description,
            directives=directives,
            operation_types=operation_types,
            loc=self.loc(start),
        )

    def parse_operation_type_definition(self) -> OperationTypeDefinitionNode:
        """OperationTypeDefinition: OperationType : NamedType"""
        start = self._lexer.token
        operation = self.parse_operation_type()
        self.expect_token(TokenKind.COLON)
        type_ = self.parse_named_type()
        return OperationTypeDefinitionNode(
            operation=operation, type=type_, loc=self.loc(start)
        )

    def parse_scalar_type_definition(self) -> ScalarTypeDefinitionNode:
        """ScalarTypeDefinition: Description? scalar Name Directives[Const]?"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("scalar")
        name = self.parse_name()
        directives = self.parse_directives(True)
        return ScalarTypeDefinitionNode(
            description=description,
            name=name,
            directives=directives,
            loc=self.loc(start),
        )

    def parse_object_type_definition(self) -> ObjectTypeDefinitionNode:
        """ObjectTypeDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("type")
        name = self.parse_name()
        interfaces = self.parse_implements_interfaces()
        directives = self.parse_directives(True)
        fields = self.parse_fields_definition()
        return ObjectTypeDefinitionNode(
            description=description,
            name=name,
            interfaces=interfaces,
            directives=directives,
            fields=fields,
            loc=self.loc(start),
        )

    def parse_implements_interfaces(self) -> List[NamedTypeNode]:
        """ImplementsInterfaces"""
        types: List[NamedTypeNode] = []
        if self.expect_optional_keyword("implements"):
            # optional leading ampersand
            self.expect_optional_token(TokenKind.AMP)
            append = types.append
            while True:
                append(self.parse_named_type())
                if not self.expect_optional_token(TokenKind.AMP):
                    break
        return types

    def parse_fields_definition(self) -> List[FieldDefinitionNode]:
        """FieldsDefinition: {FieldDefinition+}"""
        return self.optional_many(
            TokenKind.BRACE_L, self.parse_field_definition, TokenKind.BRACE_R
        )

    def parse_field_definition(self) -> FieldDefinitionNode:
        """FieldDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        name = self.parse_name()
        args = self.parse_argument_defs()
        self.expect_token(TokenKind.COLON)
        type_ = self.parse_type_reference()
        directives = self.parse_directives(True)
        return FieldDefinitionNode(
            description=description,
            name=name,
            arguments=args,
            type=type_,
            directives=directives,
            loc=self.loc(start),
        )

    def parse_argument_defs(self) -> List[InputValueDefinitionNode]:
        """ArgumentsDefinition: (InputValueDefinition+)"""
        return self.optional_many(
            TokenKind.PAREN_L, self.parse_input_value_def, TokenKind.PAREN_R
        )

    def parse_input_value_def(self) -> InputValueDefinitionNode:
        """InputValueDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        name = self.parse_name()
        self.expect_token(TokenKind.COLON)
        type_ = self.parse_type_reference()
        default_value = (
            self.parse_value_literal(True)
            if self.expect_optional_token(TokenKind.EQUALS)
            else None
        )
        directives = self.parse_directives(True)
        return InputValueDefinitionNode(
            description=description,
            name=name,
            type=type_,
            default_value=default_value,
            directives=directives,
            loc=self.loc(start),
        )

    def parse_interface_type_definition(self) -> InterfaceTypeDefinitionNode:
        """InterfaceTypeDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("interface")
        name = self.parse_name()
        interfaces = self.parse_implements_interfaces()
        directives = self.parse_directives(True)
        fields = self.parse_fields_definition()
        return InterfaceTypeDefinitionNode(
            description=description,
            name=name,
            interfaces=interfaces,
            directives=directives,
            fields=fields,
            loc=self.loc(start),
        )

    def parse_union_type_definition(self) -> UnionTypeDefinitionNode:
        """UnionTypeDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("union")
        name = self.parse_name()
        directives = self.parse_directives(True)
        types = self.parse_union_member_types()
        return UnionTypeDefinitionNode(
            description=description,
            name=name,
            directives=directives,
            types=types,
            loc=self.loc(start),
        )

    def parse_union_member_types(self) -> List[NamedTypeNode]:
        """UnionMemberTypes"""
        types: List[NamedTypeNode] = []
        if self.expect_optional_token(TokenKind.EQUALS):
            # optional leading pipe
            self.expect_optional_token(TokenKind.PIPE)
            append = types.append
            while True:
                append(self.parse_named_type())
                if not self.expect_optional_token(TokenKind.PIPE):
                    break
        return types

    def parse_enum_type_definition(self) -> EnumTypeDefinitionNode:
        """UnionTypeDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("enum")
        name = self.parse_name()
        directives = self.parse_directives(True)
        values = self.parse_enum_values_definition()
        return EnumTypeDefinitionNode(
            description=description,
            name=name,
            directives=directives,
            values=values,
            loc=self.loc(start),
        )

    def parse_enum_values_definition(self) -> List[EnumValueDefinitionNode]:
        """EnumValuesDefinition: {EnumValueDefinition+}"""
        return self.optional_many(
            TokenKind.BRACE_L, self.parse_enum_value_definition, TokenKind.BRACE_R
        )

    def parse_enum_value_definition(self) -> EnumValueDefinitionNode:
        """EnumValueDefinition: Description? EnumValue Directives[Const]?"""
        start = self._lexer.token
        description = self.parse_description()
        name = self.parse_name()
        directives = self.parse_directives(True)
        return EnumValueDefinitionNode(
            description=description,
            name=name,
            directives=directives,
            loc=self.loc(start),
        )

    def parse_input_object_type_definition(self) -> InputObjectTypeDefinitionNode:
        """InputObjectTypeDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("input")
        name = self.parse_name()
        directives = self.parse_directives(True)
        fields = self.parse_input_fields_definition()
        return InputObjectTypeDefinitionNode(
            description=description,
            name=name,
            directives=directives,
            fields=fields,
            loc=self.loc(start),
        )

    def parse_input_fields_definition(self) -> List[InputValueDefinitionNode]:
        """InputFieldsDefinition: {InputValueDefinition+}"""
        return self.optional_many(
            TokenKind.BRACE_L, self.parse_input_value_def, TokenKind.BRACE_R
        )

    def parse_schema_extension(self) -> SchemaExtensionNode:
        """SchemaExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("schema")
        directives = self.parse_directives(True)
        operation_types = self.optional_many(
            TokenKind.BRACE_L, self.parse_operation_type_definition, TokenKind.BRACE_R
        )
        if not directives and not operation_types:
            raise self.unexpected()
        return SchemaExtensionNode(
            directives=directives, operation_types=operation_types, loc=self.loc(start)
        )

    def parse_scalar_type_extension(self) -> ScalarTypeExtensionNode:
        """ScalarTypeExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("scalar")
        name = self.parse_name()
        directives = self.parse_directives(True)
        if not directives:
            raise self.unexpected()
        return ScalarTypeExtensionNode(
            name=name, directives=directives, loc=self.loc(start)
        )

    def parse_object_type_extension(self) -> ObjectTypeExtensionNode:
        """ObjectTypeExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("type")
        name = self.parse_name()
        interfaces = self.parse_implements_interfaces()
        directives = self.parse_directives(True)
        fields = self.parse_fields_definition()
        if not (interfaces or directives or fields):
            raise self.unexpected()
        return ObjectTypeExtensionNode(
            name=name,
            interfaces=interfaces,
            directives=directives,
            fields=fields,
            loc=self.loc(start),
        )

    def parse_interface_type_extension(self) -> InterfaceTypeExtensionNode:
        """InterfaceTypeExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("interface")
        name = self.parse_name()
        interfaces = self.parse_implements_interfaces()
        directives = self.parse_directives(True)
        fields = self.parse_fields_definition()
        if not (interfaces or directives or fields):
            raise self.unexpected()
        return InterfaceTypeExtensionNode(
            name=name,
            interfaces=interfaces,
            directives=directives,
            fields=fields,
            loc=self.loc(start),
        )

    def parse_union_type_extension(self) -> UnionTypeExtensionNode:
        """UnionTypeExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("union")
        name = self.parse_name()
        directives = self.parse_directives(True)
        types = self.parse_union_member_types()
        if not (directives or types):
            raise self.unexpected()
        return UnionTypeExtensionNode(
            name=name, directives=directives, types=types, loc=self.loc(start)
        )

    def parse_enum_type_extension(self) -> EnumTypeExtensionNode:
        """EnumTypeExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("enum")
        name = self.parse_name()
        directives = self.parse_directives(True)
        values = self.parse_enum_values_definition()
        if not (directives or values):
            raise self.unexpected()
        return EnumTypeExtensionNode(
            name=name, directives=directives, values=values, loc=self.loc(start)
        )

    def parse_input_object_type_extension(self) -> InputObjectTypeExtensionNode:
        """InputObjectTypeExtension"""
        start = self._lexer.token
        self.expect_keyword("extend")
        self.expect_keyword("input")
        name = self.parse_name()
        directives = self.parse_directives(True)
        fields = self.parse_input_fields_definition()
        if not (directives or fields):
            raise self.unexpected()
        return InputObjectTypeExtensionNode(
            name=name, directives=directives, fields=fields, loc=self.loc(start)
        )

    def parse_directive_definition(self) -> DirectiveDefinitionNode:
        """DirectiveDefinition"""
        start = self._lexer.token
        description = self.parse_description()
        self.expect_keyword("directive")
        self.expect_token(TokenKind.AT)
        name = self.parse_name()
        args = self.parse_argument_defs()
        repeatable = self.expect_optional_keyword("repeatable")
        self.expect_keyword("on")
        locations = self.parse_directive_locations()
        return DirectiveDefinitionNode(
            description=description,
            name=name,
            arguments=args,
            repeatable=repeatable,
            locations=locations,
            loc=self.loc(start),
        )

    def parse_directive_locations(self) -> List[NameNode]:
        """DirectiveLocations"""
        # optional leading pipe
        self.expect_optional_token(TokenKind.PIPE)
        locations: List[NameNode] = []
        append = locations.append
        while True:
            append(self.parse_directive_location())
            if not self.expect_optional_token(TokenKind.PIPE):
                break
        return locations

    def parse_directive_location(self) -> NameNode:
        """DirectiveLocation"""
        start = self._lexer.token
        name = self.parse_name()
        if name.value in DirectiveLocation.__members__:
            return name
        raise self.unexpected(start)

    # Core parsing utility functions

    def loc(self, start_token: Token) -> Optional[Location]:
        """Return a location object.

        Used to identify the place in the source that created a given parsed object.
        """
        if not self._no_location:
            end_token = self._lexer.last_token
            source = self._lexer.source
            return Location(start_token, end_token, source)
        return None

    def peek(self, kind: TokenKind) -> bool:
        """Determine if the next token is of a given kind"""
        return self._lexer.token.kind == kind

    def expect_token(self, kind: TokenKind) -> Token:
        """Expect the next token to be of the given kind.

        If the next token is of the given kind, return that token after advancing
        the lexer. Otherwise, do not change the parser state and throw an error.
        """
        token = self._lexer.token
        if token.kind == kind:
            self._lexer.advance()
            return token

        raise GraphQLSyntaxError(
            self._lexer.source,
            token.start,
            f"Expected {get_token_kind_desc(kind)}, found {get_token_desc(token)}.",
        )

    def expect_optional_token(self, kind: TokenKind) -> Optional[Token]:
        """Expect the next token optionally to be of the given kind.

        If the next token is of the given kind, return that token after advancing
        the lexer. Otherwise, do not change the parser state and return None.
        """
        token = self._lexer.token
        if token.kind == kind:
            self._lexer.advance()
            return token

        return None

    def expect_keyword(self, value: str) -> None:
        """Expect the next token to be a given keyword.

        If the next token is a given keyword, advance the lexer.
        Otherwise, do not change the parser state and throw an error.
        """
        token = self._lexer.token
        if token.kind == TokenKind.NAME and token.value == value:
            self._lexer.advance()
        else:
            raise GraphQLSyntaxError(
                self._lexer.source,
                token.start,
                f"Expected '{value}', found {get_token_desc(token)}.",
            )

    def expect_optional_keyword(self, value: str) -> bool:
        """Expect the next token optionally to be a given keyword.

        If the next token is a given keyword, return True after advancing the lexer.
        Otherwise, do not change the parser state and return False.
        """
        token = self._lexer.token
        if token.kind == TokenKind.NAME and token.value == value:
            self._lexer.advance()
            return True

        return False

    def unexpected(self, at_token: Optional[Token] = None) -> GraphQLError:
        """Create an error when an unexpected lexed token is encountered."""
        token = at_token or self._lexer.token
        return GraphQLSyntaxError(
            self._lexer.source, token.start, f"Unexpected {get_token_desc(token)}."
        )

    def any(
        self, open_kind: TokenKind, parse_fn: Callable[[], T], close_kind: TokenKind
    ) -> List[T]:
        """Fetch any matching nodes, possibly none.

        Returns a possibly empty list of parse nodes, determined by the ``parse_fn``.
        This list begins with a lex token of ``open_kind`` and ends with a lex token of
        ``close_kind``. Advances the parser to the next lex token after the closing
        token.
        """
        self.expect_token(open_kind)
        nodes: List[T] = []
        append = nodes.append
        while not self.expect_optional_token(close_kind):
            append(parse_fn())
        return nodes

    def optional_many(
        self, open_kind: TokenKind, parse_fn: Callable[[], T], close_kind: TokenKind
    ) -> List[T]:
        """Fetch matching nodes, maybe none.

        Returns a list of parse nodes, determined by the ``parse_fn``. It can be empty
        only if the open token is missing, otherwise it will always return a non-empty
        list that begins with a lex token of ``open_kind`` and ends with a lex token of
        ``close_kind``. Advances the parser to the next lex token after the closing
        token.
        """
        if self.expect_optional_token(open_kind):
            nodes = [parse_fn()]
            append = nodes.append
            while not self.expect_optional_token(close_kind):
                append(parse_fn())
            return nodes
        return []

    def many(
        self, open_kind: TokenKind, parse_fn: Callable[[], T], close_kind: TokenKind
    ) -> List[T]:
        """Fetch matching nodes, at least one.

        Returns a non-empty list of parse nodes, determined by the ``parse_fn``.
        This list begins with a lex token of ``open_kind`` and ends with a lex token of
        ``close_kind``. Advances the parser to the next lex token after the closing
        token.
        """
        self.expect_token(open_kind)
        nodes = [parse_fn()]
        append = nodes.append
        while not self.expect_optional_token(close_kind):
            append(parse_fn())
        return nodes


def get_token_desc(token: Token) -> str:
    """Describe a token as a string for debugging."""
    value = token.value
    return get_token_kind_desc(token.kind) + (
        f" '{value}'" if value is not None else ""
    )


def get_token_kind_desc(kind: TokenKind) -> str:
    """Describe a token kind as a string for debugging."""
    return f"'{kind.value}'" if is_punctuator_token_kind(kind) else kind.value
