#-*- coding: utf-8 -*-
u"""
сокращенная и адаптированная копия:
- https://github.yandex-team.ru/raw/custom-solutions/constructor/master/interester/highlighter/word_utils.py
- https://github.yandex-team.ru/raw/custom-solutions/constructor/master/interester/highlighter/reports/search_queries.py

автор:simplylizz
"""

import itertools
import functools
import re
import types

from advq.generation.common.queries import query_parser
from clemmer import stopwords
import clemmer
import _hiliter

def _cached_method(method):
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        key = args + tuple(sorted(kwargs.items()))

        if not hasattr(self, '_caches'):
            self._caches = {}
        if method.__name__ not in self._caches:
            self._caches[method.__name__] = {}
        cache = self._caches[method.__name__]

        if key not in cache:
            res = method(self, *args, **kwargs)
            if isinstance(res, (types.GeneratorType, list)):
                res = tuple(res)
            cache[key] = res

        return cache[key]

    return wrapper


class WordHelper(object):
    def __init__(self):
        self.stop_word_checker = stopwords.StopWordChecker()
        self.query_parser = query_parser.Parser(
            query_parser.get_direct_parser_rules,
        )

    @_cached_method
    def _get_words(self, phrase, raw=False):
        """Возвращает пары: (слово, флаг зафиксированности слова)"""
        phrase = phrase.lower()

        if raw:
            # План таков:
            # 1. сплитим всё по пробелам,
            # 2. разбиваем каждый кусок с помощью split_raw_word, тем
            # самым разбив по всем нетривиальным разделителям,
            # 3. очищаем результат от "мусора" - всего,
            # кроме \w\d\. (возможно это опасно)

            words = itertools.chain(
                *(query_parser.split_raw_word(w) for w in phrase.split()))

            for w in words:
                w = re.sub(r'[^\w\d\.]+', '', w, 0, re.U)
                if w:
                    yield w, False
            return

        # Парсер падает если слово начинается с точки. Это невалидная
        # конструкция с точки зрения Директа, но были случаи когда такие
        # фразы приезжали. В качестве фикса - удаляем точки в начале
        # слов.
        phrase = re.sub(
            # начинается на "начало строки" или пробельные символы,
            # потом точка,
            # потом не пробельные символы или конец строки
            r'(?:^|\s)\.([^\s|$])',
            r' \1',
            phrase,
        )

        # от BS приезжает слово "а`элита", парсер на нём падает
        # интерфейс директа заменяет обратную кавычку на одинарную, вот
        # и мы...
        phrase = phrase.replace('`', "'")

        def _really_get_words(node, fixed=False):
            if not fixed and isinstance(node, query_parser.Node):
                if node.type in {"quoted_words", "fixed_word", "plus_word",
                                 "sq_brackets"}:
                    fixed = True
                elif node.type == "words_num":
                    # ~0 эквивалентно фразе в кавычках; кажется BS
                    # поддерживает только ~0, но на всякий случай проверяем
                    assert node.children[1] == '0'
                    fixed = True

            if isinstance(node, list):
                for n in node:
                    if isinstance(n, query_parser.Node):
                        for w in _really_get_words(n, fixed=fixed):
                            yield w
            elif node.type in ("fixed_word", "plus_word"):
                for child in node.children:
                    yield child, fixed
            elif node.type == "raw_word":
                for child in node.children:
                    yield child, fixed
            elif node.type == "minus":
                return
            else:
                for n in node.children:
                    if isinstance(n, query_parser.Node):
                        for w in _really_get_words(n, fixed=fixed):
                            yield w

        for w in _really_get_words(self.query_parser(phrase)):
            yield w

    @_cached_method
    def clean_phrase(
            self, phrase, remove_stopwords=True, unique=False, raw=False,
    ):
        """
        Remove any minuswords, operators, stopwords (if s-wrods aren't
        fixed with "+" operator and related flag was setted) and all
        non-alphanum symbols.
        Also convert phrase to lower case, without this stopwords check
        could give false negative result.
        unique - вернуть только уникальные слова с учётом пересечения их
            лемм/словоформ если слово не зафиксировано, если
            зафиксировано - без учёта.
        raw - флаг означающий обычный текст, не в формате
            директа/BS/etc. Для разбивки на слова используется
            advq7.query_parser.split_raw_word, парсер не нужен, да и
            может упасть на чем-то, что ему покажется неправильным
            использованием оператора (например, воскл. знак после
            слова).
        """
        if not phrase:
            return phrase

        words = []

        try:
            wg = list(self._get_words(phrase, raw=raw))
        except Exception as exc:
            raise RuntimeError(
                u"failed to parse '%s', original error: %s" % (phrase, exc))

        seen = set()
        for word, fixed in wg:
            if fixed or not remove_stopwords or not self.is_stop_word(word):
                if unique:
                    if fixed:
                        if word not in seen:
                            seen.add(word)
                            words.append(word)
                    else:
                        lemmas = self.get_lemmas(word)
                        if all(l not in seen for l in lemmas):
                            words.append(word)
                        # на всякий случай добавляем все леммы, вдруг в
                        # seen только часть
                        seen.update(lemmas)
                else:
                    words.append(word)

        clean_phrase = " ".join(words)

        return clean_phrase

    def get_hl_nhl(self, query, ad_phrase, debug=False):
        u"""
        Split query on two lists: highlighted and not highlighted words.
        Order isn't preserved.
        >>> get_hl_nhl(
        ...     u'мама мыла раму смотреть без регистрации +и sms "в сиэтле"',
        ...     u'Моем мылом полы (без регистрации и sms)! в Seatle',
        ... ) == (
        ...     [u'регистрации', u'и', u'sms', u'в', u'мыла', u'сиэтле'],
        ...     [u'мама', u'раму', u'смотреть'],
        ... )
        True
        """
        # WARNING!!!
        # Надо получить подсвеченные слова из query, поэтому в качестве
        # костыля проверяем что подсветится в query для ad_phrase, а не
        # наоборот. Когда этот костыль будет пофикшен нормально, можно
        # будет убрать вызов self.clean_phrase(ad_phrase).
        if debug:
            print "args: q=%s, a=%s" % (query, ad_phrase)
        if not query or not ad_phrase:
            return None, None
        ad_phrase = self.clean_phrase(
            ad_phrase, remove_stopwords=False, raw=True)
        if debug:
            print "cleaned ad_phrase:", ad_phrase
        hl_marker = 'UnIqMrKr'
        _mrks = _hiliter.THiliteMarks(hl_marker, hl_marker)
        hiliter = _hiliter.THiliter(
            ad_phrase,
            marks=_mrks,
        )
        highlighted_query = hiliter.HiliteString(query)
        if debug:
            print "highlighted_query: ", highlighted_query

        hl = []
        nhl = []
        mr_len = len(hl_marker)
        for w in highlighted_query.split():
            if w.startswith(hl_marker) and w.endswith(hl_marker):
                hl.append(w[mr_len:-mr_len])
            else:
                nhl.append(w)

        return hl, nhl

    @_cached_method
    def is_stop_word(self, word):
        if not isinstance(word, str):
            word = word.encode('utf-8')
        return self.stop_word_checker(word)

    @_cached_method
    def get_lemmas(self, word):
        if isinstance(word, unicode):
            word_str = word.encode("utf-8")
        else:
            word_str = word

        try:
            return tuple(
                l.decode("utf-8") for l in clemmer.analyze2(word_str)[0].lemmas
            )
        except IndexError:
            # Например, леммы не возвращаются для слова "______"
            return (word, )


word_helper = WordHelper()


def criteria_num_words(criteria):
    if not criteria:
        return None
    try:
        return len(word_helper.clean_phrase(criteria, unique=True).split())
    except:
        return None


def _remove_from_query_free_stopwords(query, criteria):
    if not criteria or not query:
        return None
    # Есть требование к стоп-словам в запросе: учитывать их
    # в подсветке только если они зафиксированы в условии показа
    # (aka ключевая фраза).
    try:
        clean_criteria = word_helper.clean_phrase(criteria)
    except:
        # могут встречаться синтаксически неверные конструкции
        # например, одинокий `!`: "[!burgstadt !]"
        clean_criteria = ""
    cc_words = set(clean_criteria.split())
    clean_query_words = []
    for word in word_helper.clean_phrase(
            query,
            remove_stopwords=False,
            unique=True,
            raw=True,
    ).split():
        if word in cc_words or not word_helper.is_stop_word(word):
            clean_query_words.append(word)
    clean_query = " ".join(clean_query_words)
    return clean_query


def _query_num_words(criteria, query):
    clean_query = _remove_from_query_free_stopwords(query, criteria)
    # можно счиать по пробелам потому что выше мы по ним склеили
    return clean_query.count(" ") + 1


def query_num_words(criteria, query):
    try:
        return _query_num_words(criteria, query)
    except:
        return None


def _get_hl_nhl(criteria, query, snippet, debug):
    clean_query = _remove_from_query_free_stopwords(query, criteria)

    hl_snippet_words, nhl_snippet_words = word_helper.get_hl_nhl(
        clean_query,
        snippet,
        debug,
    )
    return dict(
        [
            ["hl", hl_snippet_words],
            ["nhl", nhl_snippet_words],
            ["query", query],
            ["snippet", snippet],
            ["criteria", criteria],
        ]
    )


def get_hl_nhl(criteria, query, snippet, debug=False):
    try:
        return _get_hl_nhl(criteria, query, snippet, debug)
    except:
        return dict(
                [
                    ["hl", None],
                    ["nhl", None],
                    ["query", query],
                    ["snippet", snippet],
                    ["criteria", criteria],
                ]
        )
