import itertools
import collections
import heapq
import random


def _sort_kern_dict(input_dict):
    '''
    Returns sorted kerning as an OrderedDict
    '''
    sorted_dict = collections.OrderedDict(
        sorted(input_dict.items(), key=lambda t: t[0]))
    return sorted_dict


def _gamut(value_list):
    '''
    Returns the maximum value distance in a list of values
    '''
    difference = 0
    n_list = [i for i in value_list if i]
    if any(n_list):
        difference = (max(n_list) - min(n_list))

    return difference


def _average(value_list, conscious=True):
    '''
    Average distance, not average value
    '''
    if conscious:
        n_list = [i for i in value_list if i]
    else:
        n_list = numeric_value_list(value_list)

    # not sure this is necessary
    abs_list = [abs(v) for v in n_list]
    average_value = 0
    if len(abs_list) > 0:
        average_value = sum(abs_list) / len(abs_list)

    return average_value


def _outlier(value_list, factor=4, test=False):
    '''
    Finds value lists in which one or more values are much larger
    (defined by *factor*) than the average
    '''
    n_list = numeric_value_list(value_list, absolute=True)
    if len(set(n_list)) == 1:
        # if all values are zero, no thanks
        return

    average_value = sum(n_list) / len(n_list)
    exceeding_values = [abs(v) >= average_value * factor for v in n_list]
    if test:
        print('average:', average_value)
    if any(exceeding_values):
        if test:
            print(exceeding_values)
        return True
    return


def numeric_value_list(value_list, absolute=False):
    '''
    Convert a list (which may contain Nones) to integers.
    '''
    if absolute:
        return [0 if i is None else abs(i) for i in value_list]
    else:
        return [0 if i is None else i for i in value_list]


def random_value_list(length=None):
    '''
    Just for testing.
    Can be deleted.
    '''
    if length is not None:
        l_length = length
    else:
        l_length = random.randint(2, 20)
    r_list = [random.randint(-1000, 1000) for i in range(l_length)]
    return r_list


def get_repr_pair(font, def_pair):
    '''
    Returns representative glyphs for a given kerning pair
    (which may involve groups). At this point, the method is
    not smart enough to filter out exceptions.
    '''
    left_item, right_item = def_pair
    font_glyph_order = font.glyphOrder
    font_groups = list(font.groups.keys())
    if set(def_pair) <= set(font_glyph_order + font_groups):
        left_glyphs = font.groups.get(def_pair[0], [left_item])
        right_glyphs = font.groups.get(def_pair[1], [right_item])
        left_glyphs.sort(key=lambda x: font_glyph_order.index(x))
        right_glyphs.sort(key=lambda x: font_glyph_order.index(x))
        return left_glyphs[0], right_glyphs[0]
    return


def get_combined_kern_dict(fonts):
    '''
    Returns a sorted, combined kerning dictionary for a
    number of fonts. If a specific pair is not kerned,
    kerning value is None.
    '''
    combined_pairs = set(itertools.chain.from_iterable(
        font.kerning.keys() for font in fonts))

    c_kerning = {}
    for font in fonts:
        for pair in combined_pairs:
            value = font.kerning.get(pair, None)
            c_kerning.setdefault(pair, []).append(value)

    # make the dict an ordered dict, so we do not have
    # to sort it downstream
    return _sort_kern_dict(c_kerning)


def same_value_dict(cmb_kerning):
    '''
    Pairs in which all items are kerned by the same value
    '''
    output = {}
    for pair, values in cmb_kerning.items():
        if len(set(values)) == 1:
            output[pair] = values
    return _sort_kern_dict(output)


def zero_value_dict(cmb_kerning):
    '''
    Pairs in which all items are unkerned, or kerned by 0
    '''
    output = {}
    for pair, values in cmb_kerning.items():
        if not any([v for v in values]):
            output[pair] = values
    return _sort_kern_dict(output)


def outlier_dict(cmb_kerning, factor=4):
    '''
    Pairs in which one of the values is drastically different from others
    '''
    output = {}
    for pair, values in cmb_kerning.items():
        if _outlier(values, factor):
            output[pair] = values
    return _sort_kern_dict(output)


def high_gamut_dict(cmb_kerning, approx_amount=100):
    '''
    Pairs with the highest kerning gamut
    '''
    output = collections.OrderedDict({})
    gamut_dict = {}
    for pair, values in cmb_kerning.items():
        gamut = _gamut(values)
        gamut_dict.setdefault(gamut, []).append(pair)
    max_gamut_list = sorted(gamut_dict.keys(), reverse=True)
    pair_count = 0
    for gamut_value in max_gamut_list:
        pairs = gamut_dict.get(gamut_value)
        if pair_count < approx_amount:
            for pair in pairs:
                output[pair] = cmb_kerning.get(pair)
        pair_count += len(pairs)
    return output


def largest_value_dict(cmb_kerning, amount=200):
    '''
    Pairs kerned by the largest distance
    '''

    # dictionaries that contain the largest and smallest value per pair
    max_value_dict = {
        pair: max(numeric_value_list(v_list)) for
        (pair, v_list) in cmb_kerning.items()
    }
    min_value_dict = {
        pair: min(numeric_value_list(v_list)) for
        (pair, v_list) in cmb_kerning.items()
    }

    # sorting by value
    max_value_list = sorted(
        # Lambda function to sort the resulting pairs
        # based on the sum of all kerning values
        heapq.nlargest(amount // 2, max_value_dict, key=max_value_dict.get),
        key=lambda pair: sum(numeric_value_list(cmb_kerning.get(pair)))
    )
    min_value_list = sorted(
        # Lambda function to sort the resulting pairs
        # based on the sum of all kerning values
        heapq.nsmallest(amount // 2, min_value_dict, key=min_value_dict.get),
        key=lambda pair: sum(numeric_value_list(cmb_kerning.get(pair)))
    )

    # re-establishing the combined-value dict based on lists created above
    max_value_cmb_dict = {
        pair: cmb_kerning.get(pair) for pair in max_value_list}
    min_value_cmb_dict = {
        pair: cmb_kerning.get(pair) for pair in min_value_list}

    # sorting those dictionaries by the greatest value in the valuelist
    max_sorted_dict = collections.OrderedDict(
        sorted(
            max_value_cmb_dict.items(),
            key=lambda t: max_value_dict.get(t[0]),
            reverse=True))

    min_sorted_dict = collections.OrderedDict(
        sorted(
            min_value_cmb_dict.items(),
            key=lambda t: min_value_dict.get(t[0]),
            reverse=True))

    output = {}
    output.update(max_sorted_dict)
    output.update(min_sorted_dict)
    return output


def _make_grouped_dicts(groups):
    '''
    Creates two dictionaries to identify which group(s)
    a specific glyph belongs to.
    '''
    grouped_dict_left = {}
    grouped_dict_right = {}

    for groupName, glyphList in groups.items():
        if groupName.startswith('public.kern1.'):
            for gName in glyphList:
                grouped_dict_left.setdefault(gName, None)
                grouped_dict_left[gName] = '{}'.format(groupName)

        if groupName.startswith('public.kern2.'):
            for gName in glyphList:
                grouped_dict_right.setdefault(gName, None)
                grouped_dict_right[gName] = '{}'.format(groupName)

    return grouped_dict_left, grouped_dict_right


def single_exception_list(font):
    '''
    Creates a list of exceptions for a single font
    '''
    g_flag = 'public.kern'
    grouped_dict_l, grouped_dict_r = _make_grouped_dicts(font.groups)

    group_group_pairs = [
        pair for pair in font.kerning.keys() if
        all([g_flag in item for item in pair])]
    glyph_group_pairs = [
        pair for pair in font.kerning.keys() if
        g_flag not in pair[0] and g_flag in pair[1]]
    group_glyph_pairs = [
        pair for pair in font.kerning.keys() if
        g_flag in pair[0] and g_flag not in pair[1]]
    glyph_glyph_pairs = [
        pair for pair in font.kerning.keys() if not
        any([g_flag in item for item in pair])]

    pair_list = []
    covered_pairs = []
    for (left, right) in group_glyph_pairs:
        group_pair = (
            left, grouped_dict_r.get(right, right))
        if group_pair in group_group_pairs:
            pair_list.append((left, right))
            pair_list.append(group_pair)
            covered_pairs.append((left, right))

    for (left, right) in glyph_group_pairs:
        group_pair = (
            grouped_dict_l.get(left, left), right)
        if group_pair in group_group_pairs:
            pair_list.append((left, right))
            pair_list.append(group_pair)
            covered_pairs.append((left, right))

    for (left, right) in glyph_glyph_pairs:
        group_l_pair = (
            grouped_dict_l.get(left, left), right)
        group_r_pair = (
            left, grouped_dict_r.get(right, right))
        group_both_pair = (
            grouped_dict_l.get(left, left), grouped_dict_r.get(right, right))

        if (
            group_l_pair in group_glyph_pairs and
            group_l_pair not in covered_pairs
        ):
            pair_list.append((left, right))
            pair_list.append(group_l_pair)
            covered_pairs.append(group_l_pair)
        if (
            group_r_pair in glyph_group_pairs and
            group_r_pair not in covered_pairs
        ):
            pair_list.append((left, right))
            pair_list.append(group_r_pair)
            covered_pairs.append(group_r_pair)
        if (
            group_both_pair in group_group_pairs and
            group_both_pair not in covered_pairs
        ):
            pair_list.append((left, right))
            pair_list.append(group_both_pair)
            covered_pairs.append(group_both_pair)

    zip_list = sorted(zip(pair_list[::2], pair_list[1::2]))
    sorted_pair_list = []
    for exception, base_pair in zip_list:
        sorted_pair_list.append(exception)
        # sorted_pair_list.append(base_pair)
    return sorted_pair_list


def exception_dict(fonts, cmb_kerning):
    all_exceptions = []
    output = collections.OrderedDict({})
    for font in fonts:
        font_exceptions = single_exception_list(font)
        all_exceptions.extend(font_exceptions)
    for pair in all_exceptions:
        if pair not in output.keys():
            output[pair] = cmb_kerning.get(pair)
    return output


def small_average_dict(cmb_kerning, small_av_value=5):
    output = collections.OrderedDict({})
    for pair, values in cmb_kerning.items():
        if _average(values) in range(-small_av_value, small_av_value):
            output[pair] = cmb_kerning.get(pair)
    return output


def single_pair_dict(cmb_kerning):
    output = collections.OrderedDict({})
    for pair, values in cmb_kerning.items():
        if not any([side.startswith('public') for side in pair]):
            output[pair] = cmb_kerning.get(pair)
    return output
