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

from .ast import (
    ArgumentNode,
    BooleanValueNode,
    ConstArgumentNode,
    ConstDirectiveNode,
    ConstValueNode,
    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,
    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, is_source
from .token_kind import TokenKind
from ..error import GraphQLError, GraphQLSyntaxError

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

T = TypeVar("T")

SourceType = Union[Source, str]


def parse(
    source: SourceType,
    no_location: bool = False,
    allow_legacy_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.

    Legacy feature (will be removed in v3.3):

    If ``allow_legacy_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,
        allow_legacy_fragment_variables=allow_legacy_fragment_variables,
    )
    return parser.parse_document()


def parse_value(
    source: SourceType,
    no_location: bool = False,
    allow_legacy_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.utilities.value_from_ast`.
    """
    parser = Parser(
        source,
        no_location=no_location,
        allow_legacy_fragment_variables=allow_legacy_fragment_variables,
    )
    parser.expect_token(TokenKind.SOF)
    value = parser.parse_value_literal(False)
    parser.expect_token(TokenKind.EOF)
    return value


def parse_const_value(
    source: SourceType,
    no_location: bool = False,
    allow_legacy_fragment_variables: bool = False,
) -> ConstValueNode:
    """Parse the AST for a given string containing a GraphQL constant value.

    Similar to parse_value, but raises a arse error if it encounters a variable.
    The return type will be a constant value.
    """
    parser = Parser(
        source,
        no_location=no_location,
        allow_legacy_fragment_variables=allow_legacy_fragment_variables,
    )
    parser.expect_token(TokenKind.SOF)
    value = parser.parse_const_value_literal()
    parser.expect_token(TokenKind.EOF)
    return value


def parse_type(
    source: SourceType,
    no_location: bool = False,
    allow_legacy_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.utilities.value_from_ast`.
    """
    parser = Parser(
        source,
        no_location=no_location,
        allow_legacy_fragment_variables=allow_legacy_fragment_variables,
    )
    parser.expect_token(TokenKind.SOF)
    type_ = parser.parse_type_reference()
    parser.expect_token(TokenKind.EOF)
    return type_


class Parser:
    """GraphQL AST parser.

    This class is exported only to assist people in implementing their own parsers
    without duplicating too much code and should be used only as last resort for cases
    such as experimental syntax or if certain features couldn't be contributed upstream.

    It's still part of the internal API and is versioned, so any changes to it are never
    considered breaking changes. If you still need to support multiple versions of the
    library, please use the `__version_info__` variable for version detection.
    """

    _lexer: Lexer
    _no_Location: bool
    _allow_legacy_fragment_variables: bool

    def __init__(
        self,
        source: SourceType,
        no_location: bool = False,
        allow_legacy_fragment_variables: bool = False,
    ):
        source = (
            cast(Source, source) if is_source(source) else Source(cast(str, source))
        )

        self._lexer = Lexer(source)
        self._no_location = no_location
        self._allow_legacy_fragment_variables = allow_legacy_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_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",
    }

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

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

        ExecutableDefinition: OperationDefinition or FragmentDefinition

        TypeSystemDefinition: SchemaDefinition, TypeDefinition or DirectiveDefinition

        TypeDefinition: ScalarTypeDefinition, ObjectTypeDefinition,
            InterfaceTypeDefinition, UnionTypeDefinition,
            EnumTypeDefinition or InputObjectTypeDefinition
        """
        if self.peek(TokenKind.BRACE_L):
            return self.parse_operation_definition()

        # Many definitions begin with a description and require a lookahead.
        has_description = self.peek_description()
        keyword_token = (
            self._lexer.lookahead() if has_description else self._lexer.token
        )

        if keyword_token.kind is TokenKind.NAME:
            token_name = cast(str, keyword_token.value)
            method_name = self._parse_type_system_definition_method_names.get(
                token_name
            )
            if method_name:
                return getattr(self, f"parse_{method_name}")()

            if has_description:
                raise GraphQLSyntaxError(
                    self._lexer.source,
                    self._lexer.token.start,
                    "Unexpected description,"
                    " descriptions are supported only on type definitions.",
                )

            method_name = self._parse_other_definition_method_names.get(token_name)
            if method_name:
                return getattr(self, f"parse_{method_name}")()

        raise self.unexpected(keyword_token)

    # 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_const_value_literal()
            if self.expect_optional_token(TokenKind.EQUALS)
            else None,
            directives=self.parse_const_directives(),
            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
        item = cast(Callable[[], ArgumentNode], item)
        return self.optional_many(TokenKind.PAREN_L, item, TokenKind.PAREN_R)

    def parse_argument(self, is_const: bool = False) -> ArgumentNode:
        """Argument[Const]: Name : Value[?Const]"""
        start = self._lexer.token
        name = self.parse_name()

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

    def parse_const_argument(self) -> ConstArgumentNode:
        """Argument[Const]: Name : Value[Const]"""
        return cast(ConstArgumentNode, self.parse_argument(True))

    # 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")
        # Legacy support for defining variables within fragments changes
        # the grammar of FragmentDefinition
        if self._allow_legacy_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 is_const:
            variable_token = self.expect_token(TokenKind.DOLLAR)
            token = self._lexer.token
            if token.kind is TokenKind.NAME:
                var_name = token.value
                raise GraphQLSyntaxError(
                    self._lexer.source,
                    variable_token.start,
                    f"Unexpected variable '${var_name}' in constant value.",
                )
            raise self.unexpected(variable_token)
        return self.parse_variable()

    def parse_const_value_literal(self) -> ConstValueNode:
        return cast(ConstValueNode, self.parse_value_literal(True))

    # 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_const_directives(self) -> List[ConstDirectiveNode]:
        return cast(List[ConstDirectiveNode], self.parse_directives(True))

    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
        type_: TypeNode
        if self.expect_optional_token(TokenKind.BRACKET_L):
            inner_type = self.parse_type_reference()
            self.expect_token(TokenKind.BRACKET_R)
            type_ = ListTypeNode(type=inner_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_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_const_directives()
        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_const_directives()
        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_const_directives()
        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"""
        return (
            self.delimited_many(TokenKind.AMP, self.parse_named_type)
            if self.expect_optional_keyword("implements")
            else []
        )

    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_const_directives()
        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_const_value_literal()
            if self.expect_optional_token(TokenKind.EQUALS)
            else None
        )
        directives = self.parse_const_directives()
        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_const_directives()
        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_const_directives()
        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"""
        return (
            self.delimited_many(TokenKind.PIPE, self.parse_named_type)
            if self.expect_optional_token(TokenKind.EQUALS)
            else []
        )

    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_const_directives()
        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_enum_value_name()
        directives = self.parse_const_directives()
        return EnumValueDefinitionNode(
            description=description,
            name=name,
            directives=directives,
            loc=self.loc(start),
        )

    def parse_enum_value_name(self) -> NameNode:
        """EnumValue: Name but not ``true``, ``false`` or ``null``"""
        if self._lexer.token.value in ("true", "false", "null"):
            raise GraphQLSyntaxError(
                self._lexer.source,
                self._lexer.token.start,
                f"{get_token_desc(self._lexer.token)} is reserved"
                " and cannot be used for an enum value.",
            )
        return self.parse_name()

    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_const_directives()
        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_const_directives()
        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_const_directives()
        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_const_directives()
        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_const_directives()
        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_const_directives()
        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_const_directives()
        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_const_directives()
        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"""
        return self.delimited_many(TokenKind.PIPE, self.parse_directive_location)

    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) -> bool:
        """Expect the next token optionally to be of the given kind.

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

        return False

    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
        expect_optional_token = partial(self.expect_optional_token, close_kind)
        while not expect_optional_token():
            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
            expect_optional_token = partial(self.expect_optional_token, close_kind)
            while not expect_optional_token():
                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
        expect_optional_token = partial(self.expect_optional_token, close_kind)
        while not expect_optional_token():
            append(parse_fn())
        return nodes

    def delimited_many(
        self, delimiter_kind: TokenKind, parse_fn: Callable[[], T]
    ) -> List[T]:
        """Fetch many delimited nodes.

        Returns a non-empty list of parse nodes, determined by the ``parse_fn``. This
        list may begin with a lex token of ``delimiter_kind`` followed by items
        separated by lex tokens of ``delimiter_kind``. Advances the parser to the next
        lex token after the last item in the list.
        """
        expect_optional_token = partial(self.expect_optional_token, delimiter_kind)
        expect_optional_token()
        nodes: List[T] = []
        append = nodes.append
        while True:
            append(parse_fn())
            if not expect_optional_token():
                break
        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
