import re
from functools import partial, reduce

from funcparserlib.parser import some, a, many, skip, finished, forward_decl, oneplus
from funcparserlib.lexer import make_tokenizer, Token

from . import ast


def spaced_binop(items):
    return r'\s+(%s)\s+' % r'|'.join(r'(%s)' % i for i in items)


def distbinop(items):
    return r'\s+(%s)/' % r'|'.join(r'(%s)' % i for i in items)


def wrong_operators(items, unary=False):
    tmplt = [r'((^|\s+){0}(\s+|$))', r'({0}(\s+|$))']

    if not unary:
        tmplt = [tmplt[0], r'((^|\s+){0})', tmplt[1]]

    tmplt = r'|'.join(tmplt)

    return r'|'.join(tmplt.format(i) for i in items)

SPECS = [
    ('TEXT', (r'\s+/\s+',)),
    ('OP', (distbinop(['&&', '&', '~~', '~', '']),)),
    ('OP', (spaced_binop(['&&', '~~', '<<', '<-']),)),
    ('OP', (spaced_binop(['&', r'\^', r'\|', '~']),)),
    ('TEXT', (wrong_operators(['!!', '!'], True),)),
    ('TEXT', (wrong_operators(['->', '::', ':']),)),
    ('OP', (r'(!!)|!',)),
    ('OP', (r'(->)|(::)|:',)),
    # ('OP', (r'(^|(\s+))-',)),
    # ('OP', (r'(^|(\s+))@',)),
    ('OP', (r'[)(]',)),
    ('TEXT', (r'"[^"]*"',)),
    ('SPACE', (r'\s+',)),
    ('PLACEHOLDER', (r'#\{[A-Za-z_][\w_]*\}#',)),
    ('TEXT', (r'.+?', re.UNICODE)),
]

_tokenizer = make_tokenizer(SPECS)
_fallback_tokenizer = make_tokenizer([
    ('SPACE', (r'\s+',)),
    ('TEXT', (r'"[^"]*"',)),
    ('TEXT', (r'.+?', re.UNICODE)),
])


def strip_op(f):
    def wrap(*args, **kwargs):
        for token in f(*args, **kwargs):
            if token.type == 'OP':
                token.value = token.value.strip()
            yield token
    return wrap


@strip_op
def _tokenize(query, tokenizer):
    cur = None

    for token in tokenizer(query):
        if cur is not None and token.type == cur.type and token.type == 'TEXT':
            cur.value += token.value
            cur.end = token.end
        else:
            if cur is not None:
                yield cur

            cur = token

    if cur is not None:
        yield cur


def tokenize(query):
    return list(_tokenize(query, _tokenizer))


def tokenize_fallback(query):
    return list(_tokenize(query, _fallback_tokenizer))


def get_parser():
    # полезные функции
    unarg = lambda f: lambda x: f(*x)     # распаковывает аргументы
    tokval = lambda tok: tok.value                  # значение токена
    makeop = lambda f: lambda tok: f      # функция создания операции

    word_re = re.compile(r'[a-zA-Z0-9_]+')

    space = skip(some(lambda tok: tok.type == 'SPACE'))
    word = (
        some(lambda tok: tok.type == 'TEXT' and word_re.match(tokval(tok)))
        >> (lambda tok: ast.Keyword(tokval(tok)))
    )

    def is_integer(tok):
        value = tokval(tok)
        if value.isdigit():
            return True
        try:
            int(value)
            return True
        except ValueError:
            return False

    integer = some(lambda tok: tok.type == 'TEXT' and is_integer(tok))
    text = some(lambda tok: tok.type == 'TEXT')
    ordered_text = some(lambda tok: tokval(tok)[0] == tokval(tok)[-1] == '"')
    placeholder = some(lambda tok: tok.type == 'PLACEHOLDER') >> (
        lambda tok: ast.Placeholder(tokval(tok)[2:-2]))

    # простые бинарные операторы
    op = lambda s: a(Token('OP', s))
    op_ = lambda s: skip(op(s))

    andnot = op('~') >> makeop(ast.AndNot)
    andnot_doc = op('~~') >> makeop(ast.AndNotDoc)

    and_ = op('&') >> makeop(ast.And)
    space_op = (
        some(lambda tok: tok.type == 'SPACE') >>
        (lambda tok: Token('OP', ' ')) >> makeop(ast.AndSoft)
    )
    and_doc = op('&&') >> makeop(ast.AndDoc)
    or_doc = op('|') >> makeop(ast.Or)
    syn_op = op('^') >> makeop(ast.Synonym)  # синоним
    restr_doc = op('<<') >> makeop(ast.Constr)
    refine = op('<-') >> makeop(ast.Refine)  # уточнение; он же буст

    # не обрамляется пробелами
    cmp_rarr = op('->') >> makeop(ast.Rarr)  # ранжирующий поиск в зоне zone->123
    attrsearch = op(':') >> makeop(ast.AttrSearch)
    reverse_freq = op('::') >> makeop(ast.Weight)

    # простые унарные ператоры
    exact_word = op('!') >> makeop(ast.ExactWord)
    exact_lemma = op('!!') >> makeop(ast.ExactLemma)
    # exclud_un = op('-')

    # фасеты
    # facet = op_('@') + word + op_(':') + many(word + op_(',')) + word

    # оператор расстояния
    signed_number = lambda sign, value: tokval(sign) + tokval(value)

    neg_number = op('-') + integer >> unarg(signed_number)
    pos_number = op('+') + integer >> unarg(signed_number)
    number = neg_number | pos_number | (integer >> tokval)

    distance_simple = number
    distance_full = op_('(') + number + space + number + op_(')')
    distance = (distance_full | distance_simple) + space

    def makedistop(ast_func):
        @unarg
        def inner(op, dist):
            if isinstance(dist, str):
                if dist.startswith('+') or dist.startswith('-'):
                    # /+1 -> /(+1 +1); /-1 -> /(-1 -1)
                    start, stop = int(dist), int(dist)
                else:
                    # /1 -> /(-1, +1)
                    start, stop = -int(dist), int(dist)
            else:
                start, stop = map(int, dist)

            assert start <= stop
            return partial(ast_func, (start, stop))

        return inner

    and_dist = op('&/') + distance >> makedistop(ast.AndDist)
    space_dist = op('/') + distance >> makedistop(ast.SimpleAndDist)
    andnot_dist = op('~/') + distance >> makedistop(ast.AndNotDist)
    andnot_doc_dist = op('~~/') + distance >> makedistop(ast.AndNotDocDist)
    and_doc_dist = op('&&/') + distance >> makedistop(ast.AndDocDist)

    # функции свертки в дерево
    @unarg
    def eval(argument, lst):
        return reduce(lambda arg1, tpl: tpl[0](arg1, tpl[1]), lst, argument)

    def eval_expr(args):
        if isinstance(args, tuple) and len(args) == 3:
            arg1, f, arg2 = args
            return f(arg1, arg2)
        elif isinstance(args, tuple) and len(args) == 2:
            f, arg = args
            return f(arg)
        else:
            return args

    # определяем что такое выражение, учитываем приоритет операций
    expr = forward_decl()

    # нулевой уровень
    text_simple = oneplus(text) >> (
        lambda toks: ast.Text(''.join(map(tokval, toks)))
    )
    text_ordered = ordered_text >> (
        lambda tok: ast.OrderedText(tokval(tok)[1:-1])
    )

    base_expr = (
        text_ordered | text_simple | placeholder | (op_('(') + expr + op_(')'))
    )

    # Нижний уровень задание веса word::34545
    to_weight = lambda tok: ast.WeightValue(tokval(tok))
    weighted_expr = (
        (base_expr + reverse_freq + (integer >> to_weight)) | base_expr
    ) >> eval_expr

    # поиск в зоне или поиск атриббута zone:ololo level 2
    # Ранжирующий поиск в зонах zone->a
    zone_attr_search_or_rarr = (
        (word + (attrsearch | cmp_rarr) + weighted_expr) | weighted_expr
    ) >> eval_expr

    # унарные операторы ! !! level 3
    unary_expr = (
        ((exact_word | exact_lemma) + zone_attr_search_or_rarr) |
        zone_attr_search_or_rarr
    ) >> eval_expr

    # level 4
    # оператор "И" внутри предложения с расстоянием: &/(1 2) или /(1 2)
    dist_and_sent_expr = (
        unary_expr + many((and_dist | space_dist) + unary_expr)
    ) >> eval

    # level 5
    # Оператор И: & оператор "синоним": ^ и
    # опетатор илсключения внутри предложения с расстоянием:  ~/(-1 1)
    and_or_syn_or_exc_doc = and_ | syn_op | andnot_dist
    and_sent_or_dist_andnot_expr = dist_and_sent_expr + many(
        and_or_syn_or_exc_doc + dist_and_sent_expr) >> eval

    # оператор исключения внутри предложения ~ level 6
    andnot_sent_expr = (
        and_sent_or_dist_andnot_expr + many(andnot + and_sent_or_dist_andnot_expr)
    ) >> eval

    # level 7
    # Оператор "И" в виде простого пробела: ' ' или
    # И из документа с расстроянием &&/(-1 1)
    space_or_and_doc_dist = space_op | and_doc_dist
    phrase_or_and_doc_dist_exp = (
        andnot_sent_expr + many(space_or_and_doc_dist + andnot_sent_expr)
    ) >> eval

    # level 8
    # опетатор И для всего документа: &&
    # оператор исключения из документа с расстоянием: ~~/(-1 1)
    and_dor_or_andnot_doc_dist = and_doc | andnot_doc_dist
    and_doc_or_andnot_doc_dist_expr = (
        phrase_or_and_doc_dist_exp +
        many(and_dor_or_andnot_doc_dist + phrase_or_and_doc_dist_exp)
    ) >> eval

    # оператор исключения из документа, без расстояния: ~~ level 9
    andnot_doc_expr = (and_doc_or_andnot_doc_dist_expr + many(
        andnot_doc + and_doc_or_andnot_doc_dist_expr)
    ) >> eval

    # оператор ограничения: << level 10
    restr_doc_expr = (
        andnot_doc_expr + many(restr_doc + andnot_doc_expr)
    ) >> eval

    # оператор ИЛИ level 11
    or_expr = restr_doc_expr + many(or_doc + restr_doc_expr) >> eval

    # оператор refine или в народе буст <- level 12
    refine_expr = or_expr + many(refine + or_expr) >> eval

    # закругляемся
    expr.define(refine_expr)

    return expr + skip(finished)


parse = get_parser().parse
