#! /usr/bin/env python
# -*- coding: utf-8 -*-

import logging
import pkgutil
import re
from functools import wraps
from timeit import default_timer

# requirements
import numpy as np
from pandas import NA
from unidecode import unidecode

logger = logging.getLogger(__name__)


def time_it(func):
    """
    decorator for printing the execution time of a function call
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = default_timer()
        result = func(*args, **kwargs)
        end = default_timer()
        name = func.__name__
        logger.info("%s took %s seconds" % (name, round(end - start, 2)))
        return result
    return wrapper


###########
# ANCHORS #
###########
def chunk_anchors(lst, n, exclude={0, 1}):
    """yields n-sized chunks from lst without excluded values"""
    lst_rm = [i for i in lst if i not in exclude]
    for i in range(0, len(lst_rm), n):
        yield lst_rm[i:i + n]


def apply_correction(row, correction):
    """Applies correction to one row in column.

    :param list row: row of dump with [value, context, contextend]
    :param int correction: offset to add/subtract from position
    """

    value, lower_bound, upper_bound = row

    if value == -1:
        # missing value
        return value

    value += correction
    if value < lower_bound:
        value = lower_bound
    if value > upper_bound:
        value = upper_bound
    return value


def correct_anchors(df, corrections):
    """Corrects df columns via dictionary of corrections

    :param DataFrame df: dump
    :param dict corrections: {anchor_name (0-9, column in df): offset (int)}
    """
    for correction in corrections:
        if correction in df.columns:
            logger.info('correcting anchor %d by offset %d' % (
                correction, corrections[correction]
            ))
            df[correction] = df[
                [correction, 'context', 'contextend']
            ].apply(lambda x: apply_correction(x, corrections[correction]), axis=1)
        else:
            logger.warning('anchor "%s" not in dataframe' % str(correction))
    return df


###########
# QUERIES #
###########
def cqp_escape(token):
    """ escape CQP meta-characters

    :param str token: string to escape
    :return: escaped string
    :rtype: str

    """
    return token.translate(str.maketrans({
        ".": r"\.",
        "?": r"\?",
        "*": r"\*",
        "+": r"\+",
        "|": r"\|",
        "(": r"\(",
        ")": r"\)",
        "[": r"\[",
        "]": r"\]",
        "{": r"\{",
        "}": r"\}",
        "^": r"\^",
        "$": r"\$",
        '"': r"\"",
        "'": r"\'"
    }))


def format_cqp_query(items, p_query='word', s_query=None,
                     flags="", escape=True):
    """ wrapper for easy queries

    :return: valid cqp query
    :rtype: str
    """

    # gather all MWUs
    mwu_queries = list()
    single_items = list()
    for item in items:

        # escape item (optional)
        if escape:
            item = cqp_escape(item)

        # split items on white-space
        tokens = item.split(" ")
        if len(tokens) > 1:
            mwu_query = ""

            # convert to CQP syntax considering p-att and flags
            for token in tokens:
                mwu_query += '[{p_query}="{token}"{flags}]'.format(
                    p_query=p_query, token=token, flags=flags
                )

            # add MWU item to list
            mwu_queries.append("(" + mwu_query + ")")
        else:
            single_items.append(tokens[0])

    singles_query = "([" + p_query + "=" + '"' + "|".join(single_items) + '"' + flags + "])"
    queries = mwu_queries + [singles_query]
    # disjunctive join
    query = ' | '.join(queries)

    # add s_query (optional)
    if s_query is not None:
        query = '({query}) within {s_query};'.format(
            query=query, s_query=s_query
        )

    # return
    return query


def find_all(pattern, string):
    p = re.compile(pattern)
    matches = dict()
    for m in p.finditer(string):
        matches[m.group()] = [m.start(), m.end()]
    return matches


def preprocess_query(query):
    """
    - get s_query and strip 'within' statement
    - get anchors
    - get macros
    - get wordlists

    :return: raw query, s_query, anchors, macros, wordlists
    :rtype: dict
    """

    # get s_query and strip 'within' statement
    s_query = None
    query = query.rstrip(";")
    tokens = query.split(" ")
    if len(tokens) > 2:
        if tokens[-2] == "within":
            query = " ".join(tokens[:-2])
            s_query = tokens[-1]

    # find anchors, macros, wordlists
    anchors = [int(a[1]) for a in find_all(r"@\d", query).keys()]
    macros = list(find_all(r"/[A-Za-z_]+\[\d*\]", query).keys())
    wordlists = list(find_all(r"\$[A-Za-z_]+", query).keys())

    return {
        'query': query,
        's_query': s_query,
        'anchors': anchors,
        'macros': macros,
        'wordlists': wordlists
    }


#################################
# working on nodes and contexts #
#################################
def _node2cotext(match, matchend, context, contextend):
    """converts the four cpos-values of nodes into a dict of lists of
    cpos, match, and offset

    """
    # get lists
    cpos_list = list(range(context, contextend + 1))
    match_list = [match] * len(cpos_list)
    offset_list = [
        (cpos - match) if cpos < match else
        (cpos - matchend) if cpos > matchend else
        0 for cpos in cpos_list
    ]

    # return object
    result = {
        'match_list': match_list,
        'cpos_list': cpos_list,
        'offset_list': offset_list
    }

    return result


node2cotext = np.vectorize(_node2cotext)


def merge_intervals(inter, start_index=0):
    """ for merging contexts """
    for i in range(start_index, len(inter)-1):
        if inter[i][1] >= inter[i+1][0]:
            new_start = inter[i][0]
            new_end = inter[i+1][1]
            inter[i] = [new_start, new_end]
            del inter[i+1]
            return merge_intervals(inter.copy(), start_index=i)
    return inter


def calculate_offset(row):
    """ calculates offset of y to x """

    # necessary values
    match_x = row['match_x']
    match_y = row['match_y']

    # if match_y is missing, offset is missing
    if match_y is NA or match_y == -1:
        return NA

    # init matchends if necessary
    if 'matchend_x' in row.keys():
        matchend_x = row['matchend_x']
    else:
        matchend_x = match_x
    if 'matchend_y' in row.keys():
        matchend_y = row['matchend_y']
    else:
        matchend_y = match_y

    # y ... x
    if match_x > matchend_y:
        offset = matchend_y - match_x
    # x ... y
    elif matchend_x < match_y:
        offset = match_y - matchend_x
    # overlap
    else:
        offset = 0

    return offset


##################
# s-att handling #
##################
def merge_s_atts(s_query, s_break, s_meta):
    """ DEPRECATED consistencize s-atts """
    # s_query < s_break < s_meta

    # case 1.1: only s_query
    if s_break is None and s_meta is None:
        s_break = s_meta = s_query

    # case 1.2: only s_break
    elif s_query is None and s_meta is None:
        s_query = s_meta = s_break

    # case 1.3: only s_meta
    elif s_query is None and s_break is None:
        s_query = s_break = s_query

    # case 2.1: s_query and s_break
    elif s_meta is None:
        s_meta = s_break

    # case 2.2: s_query and s_meta
    elif s_break is None:
        s_break = s_meta

    # useless cases
    # case 2.3: s_break and s_meta
    # case 3: all given

    if s_meta is not None and not s_meta.endswith("_id"):
        s_meta = s_meta + "_id"

    logger.info("s_query: %s - s_break: %s - s_meta: %s" % (
        str(s_query), str(s_break), str(s_meta)
    ))
    return s_query, s_break, s_meta


##################
# p-att handling #
##################
def fold_item(item, flags="%cd"):

    if flags is None:
        return item

    # is the item a string or a tuple?
    isstr = isinstance(item, str)
    item = (item, ) if isstr else item

    if "c" in flags:
        # lower-case
        item = [i.lower() for i in item]

    if "d" in flags:
        # TODO align with CWB
        # remove diacritica
        item = [unidecode(i) for i in item]

    item = item[0] if isstr else tuple(item)

    return item


def fold_df(df, flags="%cd"):

    if flags is None:
        return df

    df.index = df.index.map(lambda x: fold_item(x, flags))
    grouped = df.groupby(df.index)
    df = grouped.aggregate(np.sum)
    return df


def filter_df(df, path):
    data = pkgutil.get_data(__name__, path)
    items = set(data.decode().split("\n"))
    return df.loc[~df.index.isin(items)]
