from ..compat import str_type
from .source import Source
from .error import LanguageError
from .lexer import Lexer, TokenKind, get_token_kind_desc, get_token_desc
from . import ast

__all__ = ['parse']


def parse(source, **kwargs):
    """Given a GraphQL source, parses it into a Document."""
    options = {'no_location': False, 'no_source': False}
    options.update(kwargs)
    source_obj = source
    if isinstance(source, str_type):
        source_obj = Source(source)
    parser = Parser(source_obj, options)
    return parse_document(parser)


class Parser(object):
    __slots__ = ['lexer', 'source', 'options', 'prev_end', 'token']

    def __init__(self, source, options):
        self.lexer = Lexer(source)
        self.source = source
        self.options = options
        self.prev_end = 0
        self.token = self.lexer.next_token()


class Loc(object):
    __slots__ = ['start', 'end', 'source']

    def __init__(self, start, end, source=None):
        self.start = start
        self.end = end
        self.source = source

    def __repr__(self):
        source = ' source={}'.format(self.source) if self.source else ''
        return '<Loc start={} end={}{}>'.format(self.start, self.end, source)

    def __eq__(self, other):
        return (
            isinstance(other, Loc) and
            self.start == other.start and
            self.end == other.end and
            self.source == other.source
        )


def loc(parser, start):
    """Returns a location object, used to identify the place in
    the source that created a given parsed object."""
    if parser.options['no_location']:
        return None

    if parser.options['no_source']:
        return Loc(start, parser.prev_end)

    return Loc(start, parser.prev_end, parser.source)


def advance(parser):
    """Moves the internal parser object to the next lexed token."""
    prev_end = parser.token.end
    parser.prev_end = prev_end
    parser.token = parser.lexer.next_token(prev_end)


def peek(parser, kind):
    """Determines if the next token is of a given kind"""
    return parser.token.kind == kind


def skip(parser, kind):
    """If the next token is of the given kind, return true after advancing
    the parser. Otherwise, do not change the parser state
    and return False."""
    match = parser.token.kind == kind
    if match:
        advance(parser)
    return match


def expect(parser, kind):
    """If the next token is of the given kind, return that token after
    advancing the parser. Otherwise, do not change the parser state and
    return False."""
    token = parser.token
    if token.kind == kind:
        advance(parser)
        return token
    raise LanguageError(
        parser.source,
        token.start,
        'Expected {}, found {}'.format(
            get_token_kind_desc(kind),
            get_token_desc(token)
        )
    )


def expect_keyword(parser, value):
    """If the next token is a keyword with the given value, return that
    token after advancing the parser. Otherwise, do not change the parser
    state and return False."""
    token = parser.token
    if token.kind == TokenKind.NAME and token.value == value:
        advance(parser)
        return token
    raise LanguageError(
        parser.source,
        token.start,
        'Expected "{}", found {}'.format(value, get_token_desc(token))
    )


def unexpected(parser, at_token=None):
    """Helper function for creating an error when an unexpected lexed token
    is encountered."""
    token = at_token or parser.token
    return LanguageError(
        parser.source,
        token.start,
        'Unexpected {}'.format(get_token_desc(token))
    )


def any(parser, open_kind, parse_fn, close_kind):
    """Returns a possibly empty list of parse nodes, determined by
    the parse_fn. This list begins with a lex token of openKind
    and ends with a lex token of closeKind. Advances the parser
    to the next lex token after the closing token."""
    expect(parser, open_kind)
    nodes = []
    while not skip(parser, close_kind):
        nodes.append(parse_fn(parser))
    return nodes


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


def parse_name(parser):
    """Converts a name lex token into a name parse node."""
    token = expect(parser, TokenKind.NAME)
    return ast.Name(
        value=token.value,
        loc=loc(parser, token.start)
    )


# Implements the parsing rules in the Document section.

def parse_document(parser):
    start = parser.token.start
    definitions = []
    while True:
        if peek(parser, TokenKind.BRACE_L):
            definitions.append(parse_operation_definition(parser))
        elif peek(parser, TokenKind.NAME):
            if parser.token.value in ('query', 'mutation'):
                definitions.append(parse_operation_definition(parser))
            elif parser.token.value == 'fragment':
                definitions.append(parse_fragment_definition(parser))
            else:
                raise unexpected(parser)
        else:
            raise unexpected(parser)
        if skip(parser, TokenKind.EOF):
            break
    return ast.Document(
        definitions=definitions,
        loc=loc(parser, start)
    )


# Implements the parsing rules in the Operations section.

def parse_operation_definition(parser):
    start = parser.token.start
    if peek(parser, TokenKind.BRACE_L):
        return ast.OperationDefinition(
            operation='query',
            name=None,
            variable_definitions=None,
            directives=[],
            selection_set=parse_selection_set(parser),
            loc=loc(parser, start)
        )
    operation_token = expect(parser, TokenKind.NAME)
    operation = operation_token.value
    return ast.OperationDefinition(
        operation=operation,
        name=parse_name(parser),
        variable_definitions=parse_variable_definitions(parser),
        directives=parse_directives(parser),
        selection_set=parse_selection_set(parser),
        loc=loc(parser, start)
    )


def parse_variable_definitions(parser):
    if peek(parser, TokenKind.PAREN_L):
        return many(
            parser,
            TokenKind.PAREN_L,
            parse_variable_definition,
            TokenKind.PAREN_R
        )
    return []


def parse_variable_definition(parser):
    start = parser.token.start
    variable = parse_variable(parser)
    type = (expect(parser, TokenKind.COLON), parse_type(parser))[1]
    if skip(parser, TokenKind.EQUALS):
        default_value = parse_value(parser, True)
    else:
        default_value = None
    return ast.VariableDefinition(
        variable=variable,
        type=type,
        default_value=default_value,
        loc=loc(parser, start)
    )


def parse_variable(parser):
    start = parser.token.start
    expect(parser, TokenKind.DOLLAR)
    return ast.Variable(
        name=parse_name(parser),
        loc=loc(parser, start)
    )


def parse_selection_set(parser):
    start = parser.token.start
    return ast.SelectionSet(