
"""
    A Recursive Descent implementation of a lexical parser for TOML.

    Grammar:
    --------

    Newline -> NEWLINE
    Comment -> COMMENT Newline
    LineTerminator -> Comment | Newline
    Space -> WHITESPACE Space | WHITESPACE | EMPTY
    TableHeader -> Space [ Space TableHeaderName Space ] Space LineTerminator |
        Space [[ Space TableHeaderName Space ]] Space LineTerminator
    TableHeaderName -> STRING Space '.' Space TableHeaderName | STRING
    Atomic -> STRING | INTEGER | FLOAT | DATE | BOOLEAN

    Array -> '[' Space ArrayInternal Space ']' | '[' Space ArrayInternal Space LineTerminator Space ']'
    ArrayInternal -> LineTerminator Space ArrayInternal | Value Space ',' Space LineTerminator Space ArrayInternal |
        Value Space ',' Space ArrayInternal | LineTerminator | Value | EMPTY

    InlineTable -> '{' Space InlineTableInternal Space '}'
    InlineTableKeyValuePair = STRING Space '=' Space Value
    InlineTableInternal -> InlineTableKeyValuePair Space ',' Space InlineTableInternal |
        InlineTableKeyValuePair | Empty

    Value -> Atomic | InlineTable | Array
    KeyValuePair -> Space STRING Space '=' Space Value Space LineTerminator

    TableBody -> KeyValuePair TableBody | EmptyLine TableBody | EmptyLine | KeyValuePair

    EmptyLine -> Space LineTerminator
    FileEntry -> TableHeader | TableBody

    TOMLFileElements -> FileEntry TOMLFileElements | FileEntry | EmptyLine | EMPTY
"""

from prettytoml import tokens
from prettytoml.elements.array import ArrayElement
from prettytoml.elements.atomic import AtomicElement
from prettytoml.elements.inlinetable import InlineTableElement
from prettytoml.elements.metadata import NewlineElement, CommentElement, WhitespaceElement, PunctuationElement
from prettytoml.elements.table import TableElement
from prettytoml.elements.tableheader import TableHeaderElement

from prettytoml.parser.recdesc import capture_from
from prettytoml.parser.errors import ParsingError
from prettytoml.parser.tokenstream import TokenStream

"""
    Non-terminals are represented as functions which return (RESULT, pending_token_stream), or raise ParsingError.
"""


def token(token_type):
    def factory(ts):
        t = ts.head
        if t.type != token_type:
            raise ParsingError('Expected a token of type {}'.format(token_type))
        return t, ts.tail
    return factory


def newline_element(token_stream):
    """
    Returns NewlineElement, pending_token_stream or raises ParsingError.
    """
    captured = capture_from(token_stream).find(token(tokens.TYPE_NEWLINE))
    return NewlineElement(captured.value()), captured.pending_tokens


def comment_tokens(ts1):
    c1 = capture_from(ts1).find(token(tokens.TYPE_COMMENT)).and_find(token(tokens.TYPE_NEWLINE))
    return c1.value(), c1.pending_tokens


def comment_element(token_stream):
    """
    Returns CommentElement, pending_token_stream or raises ParsingError.
    """
    captured = capture_from(token_stream).find(comment_tokens)
    return CommentElement(captured.value()), captured.pending_tokens


def line_terminator_tokens(token_stream):
    captured = capture_from(token_stream).find(comment_tokens).or_find(token(tokens.TYPE_NEWLINE))
    return captured.value(), captured.pending_tokens


def line_terminator_element(token_stream):
    captured = capture_from(token_stream).find(comment_element).or_find(newline_element)
    return captured.value('Expected a comment or a newline')[0], captured.pending_tokens


def zero_or_more_tokens(token_type):

    def factory(token_stream):
        def more(ts):
            c = capture_from(ts).find(token(token_type)).and_find(zero_or_more_tokens(token_type))
            return c.value(), c.pending_tokens

        def two(ts):
            c = capture_from(ts).find(token(tokens.TYPE_WHITESPACE))
            return c.value(), c.pending

        def zero(ts):
            return tuple(), ts

        captured = capture_from(token_stream).find(more).or_find(two).or_find(zero)
        return captured.value(), captured.pending_tokens

    return factory


def space_element(token_stream):
    captured = capture_from(token_stream).find(zero_or_more_tokens(tokens.TYPE_WHITESPACE))
    return WhitespaceElement([t for t in captured.value() if t]), captured.pending_tokens


def string_token(token_stream):
    captured = capture_from(token_stream).\
        find(token(tokens.TYPE_BARE_STRING)).\
        or_find(token(tokens.TYPE_STRING)).\
        or_find(token(tokens.TYPE_LITERAL_STRING)).\
        or_find(token(tokens.TYPE_MULTILINE_STRING)).\
        or_find(token(tokens.TYPE_MULTILINE_LITERAL_STRING))
    return captured.value('Expected a string'), captured.pending_tokens


def string_element(token_stream):
    captured = capture_from(token_stream).find(string_token)
    return AtomicElement(captured.value()), captured.pending_tokens


def table_header_name_tokens(token_stream):

    def one(ts):
        c = capture_from(ts).\
            find(string_token).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(token(tokens.TYPE_OPT_DOT)).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(table_header_name_tokens)
        return c.value(), c.pending_tokens

    captured = capture_from(token_stream).find(one).or_find(string_token)
    return captured.value(), captured.pending_tokens


def table_header_element(token_stream):

    def single(ts1):
        c1 = capture_from(ts1).\
            find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(token(tokens.TYPE_OP_SQUARE_LEFT_BRACKET)).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(table_header_name_tokens).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(token(tokens.TYPE_OP_SQUARE_RIGHT_BRACKET)).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(line_terminator_tokens)

        return c1.value(), c1.pending_tokens

    def double(ts2):
        c2 = capture_from(ts2).\
            find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(token(tokens.TYPE_OP_DOUBLE_SQUARE_LEFT_BRACKET)).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(table_header_name_tokens).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(token(tokens.TYPE_OP_DOUBLE_SQUARE_RIGHT_BRACKET)).\
            and_find(zero_or_more_tokens(tokens.TYPE_WHITESPACE)).\
            and_find(line_terminator_tokens)

        return c2.value(), c2.pending_tokens

    captured = capture_from(token_stream).find(single).or_find(double)
    return TableHeaderElement(captured.value()), captured.pending_tokens


def atomic_element(token_stream):
    captured = capture_from(token_stream).\
        find(string_token).\
        or_find(token(tokens.TYPE_INTEGER)).\
        or_find(token(tokens.TYPE_FLOAT)).\
        or_find(token(tokens.TYPE_DATE)).\
        or_find(token(tokens.TYPE_BOOLEAN))
    return AtomicElement(captured.value('Expected an atomic primitive value')), captured.pending_tokens


def punctuation_element(token_type):
    def factory(ts):
        c = capture_from(ts).find(token(token_type))
        return PunctuationElement(c.value('Expected the punctuation element: {}'.format(token_type))), c.pending_tokens
    return factory


def value(token_stream):
    captured = capture_from(token_stream).\
        find(atomic_element).\
        or_find(array_element).\
        or_find(inline_table_element)
    return captured.value('Expected a primitive value, array or an inline table'), captured.pending_tokens


def array_internal(ts):

    def zero(ts0):
        c = capture_from(ts0).\
            and_find(line_terminator_element).\
            and_find(space_element).\
            and_find(array_internal)
        return c.value(), c.pending_tokens

    def one(ts1):
        c = capture_from(ts1).\
            find(value).\
            and_find(space_element).\
            and_find(punctuation_element(tokens.TYPE_OP_COMMA)).\
            and_find(space_element).\
            and_find(line_terminator_element).\
            and_find(space_element).\
            and_find(array_internal)
        return c.value(), c.pending_tokens

    def two(ts2):
        c = capture_from(ts2).\
            find(value).\
            and_find(space_element).\
            and_find(punctuation_element(tokens.TYPE_OP_COMMA)).\
            and_find(space_element).\
            and_find(array_internal)
        return c.value(), c.pending_tokens

    def three(ts3):
        c = capture_from(ts3).\
            find(space_element).\
            and_find(line_terminator_element)
        return c.value(), c.pending_tokens

    captured = capture_from(ts).find(zero).or_find(one).or_find(two).or_find(three).or_find(value).or_empty()
    return captured.value(), captured.pending_tokens


def array_element(token_stream):

    def one(ts1):
        ca = capture_from(ts1).\
            find(punctuation_element(tokens.TYPE_OP_SQUARE_LEFT_BRACKET)).\
            and_find(space_element).\
            and_find(array_internal).\
            and_find(space_element).\
            and_find(punctuation_element(tokens.TYPE_OP_SQUARE_RIGHT_BRACKET))
        return ca.value(), ca.pending_tokens

    def two(ts2):
        ca = capture_from(ts2).\
            find(punctuation_element(tokens.TYPE_OP_SQUARE_LEFT_BRACKET)).\
            and_find(space_element).\
            and_find(array_internal).\
            and_find(space_element).\
            and_find(line_terminator_element).\
            and_find(space_element).\
            and_find(punctuation_element(tokens.TYPE_OP_SQUARE_RIGHT_BRACKET))
        return ca.value(), ca.pending_tokens

    captured = capture_from(token_stream).find(one).or_find(two)
    return ArrayElement(captured.value()), captured.pending_tokens


def inline_table_element(token_stream):

    # InlineTableElement -> '{' Space InlineTableInternal Space '}'
    # InlineTableKeyValuePair = STRING Space '=' Space Value
    # InlineTableInternal -> InlineTableKeyValuePair Space ',' Space InlineTableInternal |
    #     InlineTableKeyValuePair | Empty

    def key_value(ts):
        ca = capture_from(ts).\
            find(string_element).\
            and_find(space_element).\
            and_find(punctuation_element(tokens.TYPE_OP_ASSIGNMENT)).\
            and_find(space_element).\
            and_find(value)
        return ca.value(), ca.pending_tokens

    def internal(ts):
        def one(ts1):
            c1 = capture_from(ts1).\
                find(key_value).\
                and_find(space_element).\
                and_find(punctuation_element(tokens.TYPE_OP_COMMA)).\
                and_find(space_element).\
                and_find(internal)
            return c1.value(), c1.pending_tokens

        c = capture_from(ts).find(one).or_find(key_value).or_empty()
        return c.value(), c.pending_tokens

    captured = capture_from(token_stream).\
        find(punctuation_element(tokens.TYPE_OP_CURLY_LEFT_BRACKET)).\
        and_find(space_element).\
        and_find(internal).\
        and_find(space_element).\
        and_find(punctuation_element(tokens.TYPE_OP_CURLY_RIGHT_BRACKET))

    return InlineTableElement(captured.value()), captured.pending_tokens


def key_value_pair(token_stream):
    captured = capture_from(token_stream).\
        find(space_element).\
        and_find(string_element).\
        and_find(space_element).\
        and_find(punctuation_element(tokens.TYPE_OP_ASSIGNMENT)).\
        and_find(space_element).\
        and_find(value).\
        and_find(space_element).\
        and_find(line_terminator_element)
    return captured.value(), captured.pending_tokens


def table_body_elements(token_stream):

    # TableBody -> KeyValuePair TableBody | EmptyLine TableBody | EmptyLine | KeyValuePair

    def one(ts1):
        c = capture_from(ts1).\
            find(key_value_pair).\
            and_find(table_body_elements)
        return c.value(), c.pending_tokens

    def two(ts2):
        c = capture_from(ts2).\
            find(empty_line_elements).\
            and_find(table_body_elements)
        return c.value(), c.pending_tokens

    captured = capture_from(token_stream).\
        find(one).\
        or_find(two).\
        or_find(empty_line_elements).\
        or_find(key_value_pair)

    return captured.value(), captured.pending_tokens


def table_body_element(token_stream):
    captured = capture_from(token_stream).find(table_body_elements)
    return TableElement(captured.value()), captured.pending_tokens


def empty_line_tokens(ts1):
    c1 = capture_from(ts1).find(space_element).and_find(line_terminator_element)
    return c1.value(), c1.pending_tokens


def empty_line_elements(token_stream):
    captured = capture_from(token_stream).find(empty_line_tokens)
    return captured.value(), captured.pending_tokens


def file_entry_element(token_stream):
    captured = capture_from(token_stream).find(table_header_element).\
        or_find(table_body_element)
    return captured.value(), captured.pending_tokens


def toml_file_elements(token_stream):

    def one(ts1):
        c1 = capture_from(ts1).find(file_entry_element).and_find(toml_file_elements)
        return c1.value(), c1.pending_tokens

    captured = capture_from(token_stream).find(one).or_find(file_entry_element).or_empty()
    return captured.value(), captured.pending_tokens
