#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: psakic

This sub-module of geodezyx.utils contains functions for operations
related to Python's list manipulations.

it can be imported directly with:
from geodezyx import utils

The GeodeZYX Toolbox is a software for simple but useful
functions for Geodesy and Geophysics under the GNU LGPL v3 License

Copyright (C) 2019 Pierre Sakic et al. (IPGP, sakic@ipgp.fr)
GitHub repository :
https://github.com/GeodeZYX/geodezyx-toolbox
"""


########## BEGIN IMPORT ##########
#### Import the logger
import logging
import re
import os

import numpy as np

#### geodeZYX modules
log = logging.getLogger("geodezyx")


##########  END IMPORT  ##########


def is_listoflist(inp):
    """
    check if inp is a list of list [[...] , [...] , ... ,[...]]
    """
    # On fait l'hypothèse que c'est un iterable qui va dedans de toute façon
    return any(isinstance(el, (list, np.ndarray)) for el in inp)


def shrink_listoflist(lin):
    """
    if lin (list in) is a list of list and contains only one element
    then returns the sublist, e.g. : [[a,b,c]] => [a,b,c]
    """
    if is_listoflist(lin) and len(lin) == 1:
        return lin[0]
    else:
        return lin


def uniqify_list(seq, idfun=None):
    """
    order preserving uniq function
    based on
    https://www.peterbe.com/plog/uniqifiers-benchmark
    """
    if idfun is None:

        def idfun(x):
            return x

    seen = {}
    result = []
    for item in seq:
        marker = idfun(item)
        # in old Python versions:
        # if seen.has_key(marker)
        # but in new ones:
        if marker in seen:
            continue
        seen[marker] = 1
        result.append(item)
    return result


def uniqify_list_of_lists(L):
    """
    source
    http://stackoverflow.com/questions/3724551/python-uniqueness-for-list-of-lists
    """
    return [list(x) for x in set(tuple(x) for x in L)]


def find_common_elts(*lists):
    """
    Find common elements in different lists
    """
    sett = set(lists[0])

    for l in lists[1:]:
        sett = sett.intersection(l)

    return np.array(sorted(list(sett)))


def uniq_and_sort(L, natural_sort=True):
    """
    In a list, remove duplicates and sort the list
    """
    import natsort
    if natural_sort:
        return natsort.natsorted(list(set(L)))
    else:
        return sorted(list(set(L)))


def df_sel_val_in_col(DF, col_name, col_val):
    """
    Return a selected value of a column in a DataFrame
    i.e.
    return DF[DF[col_name] == col_val]
    """
    return DF[DF[col_name] == col_val]


def uniquetol(A, tol):
    """
    tol : absolute tolerance
    source
    http://stackoverflow.com/questions/37847053/uniquify-an-array-list-with-a-tolerance-in-python-uniquetol-equivalent
    """
    A = np.array(A)
    return A[~(np.triu(np.abs(A[:, None] - A) <= tol, 1)).any(0)]


def uniquetol2(A, tol=10**-6):
    """
    This one is faster
    https://stackoverflow.com/questions/5426908/find-unique-elements-of-floating-point-array-in-numpy-with-comparison-using-a-d
    """
    return np.unique(A.round(decimals=4))


def groups_near_central_values(A, tol, B=None):
    """
    beta ... bug if tol is bad
    tol : absolute tolerance
    170514 : B is an annex list
    """
    A = np.array(A)

    Auniq = uniquetol(A, tol)
    Grouplis = []

    if B is None:
        Bbool = False
    else:
        Bbool = True

    if Bbool:
        GrouplisB = []

    for iauniq, auniq in enumerate(Auniq):
        Group = []
        Grouplis.append(Group)

        if Bbool:
            GroupB = []
            GrouplisB.append(GroupB)

        for ia, a in enumerate(A):
            count = 0
            if np.abs(a - auniq) <= tol:
                Group.append(a)
                count += 1

                if Bbool:
                    GroupB.append(B[ia])

        if count > 1:
            log.warning("%s in %s groups", a, count)

    if Bbool:
        return Grouplis, GrouplisB
    else:
        return Grouplis


def uniq_set_list(setlis, frozen=True):
    """uniqify a list of sets"""
    if not frozen:
        return [set(e) for e in list(set([frozenset(e) for e in setlis]))]
    else:
        return [frozenset(e) for e in list(set([frozenset(e) for e in setlis]))]


def sort_binom_list(X, Y, array_out=False):
    """
    sort Y according to X
    and sort X
    """
    if len(X) != len(Y):
        log.warning("len(X) != len(Y) !!!")

    Ynew = [y for (x, y) in sorted(zip(X, Y))]
    Xnew = sorted(X)
    if not array_out:
        return Xnew, Ynew
    else:
        return np.array(Xnew), np.array(Ynew)


def sort_multinom_list(X, *Y):
    """
    sort Y according to X
    and sort X
    """
    Ynew_stk = []
    for YY in Y:
        Ynew = [y for (x, y) in sorted(zip(X, YY))]
        Ynew_stk.append(Ynew)
    Xnew = sorted(X)
    fintup = (np.array(Xnew),) + tuple(Ynew_stk)
    return fintup


def sort_basename(file_paths):
    """
    Sort a list of file paths by their basenames.

    Parameters
    ----------
    file_paths : list
        List of file paths to be sorted.

    Returns
    -------
    list
        Sorted list of file paths by their basenames.
    """
    return list(sorted(file_paths, key=os.path.basename))


def most_common(lst):
    """
    http://stackoverflow.com/questions/1518522/python-most-common-element-in-a-list
    """
    if type(lst) is not list:
        lst = list(lst)
    return max(set(lst), key=lst.count)


def median_improved(L):
    """
    manage the case where len(L) is even
    in this case, doesn't return the mean of the 2 medians, but the nearest value
    """
    L = np.array(L)
    if len(L) % 2 == 0:
        return find_nearest(L, np.median(L))[0]
    else:
        return np.median(L)


def trio_lists_2_tab(Xlis, Ylis, Vlis):
    """
    From a trio of lists
    Xlis => parameter you want in columns
    Ylis => parameter you want in rows
    Vlis => data(X,Y)

    make a tab compatible with mabular module
    tabulate(Finalis, headers="firstrow")

    NB : very dirty research of the elements ...
    """

    Xlis_uniq = sorted(np.unique(Xlis))
    Ylis_uniq = sorted(np.unique(Ylis))

    Finalis = []
    Finalis.append(Xlis_uniq)

    for y in Ylis_uniq:
        curlis = [y]
        Finalis.append(curlis)
        for x in Xlis_uniq:
            for xx, yy, vv in zip(Xlis, Ylis, Vlis):
                if xx == x and yy == y:
                    curlis.append(vv)
    return Finalis


def minmax(L):
    return np.min(L), np.max(L)


def middle(Lin):
    Lout = []
    for i in range(len(Lin) - 1):
        Lout.append((Lin[i] + Lin[i + 1]) / 2)
    return Lout


def second_smallest(numbers):
    m1, m2 = float("inf"), float("inf")
    for x in numbers:
        if x <= m1:
            m1, m2 = x, m1
        elif x < m2:
            m2 = x
    return m2


def find_index_multi_occurences(L, elt):
    return [i for i, x in enumerate(L) if x == elt]


def find_surrounding(L, v):
    """
    find the surrounding values

    Parameters
    ----------
    L : Iterable
        Input list/array.

    Returns
    -------
    surrounding_values : tuple
        surounding values.
    surrounding_index : tuple
        surounding indexes.


    """
    a = np.array(L)
    b = v

    surrounding_values = np.sort(
        [b + i for i in sorted(np.subtract(a, b), key=lambda x: abs(x))[:2]]
    )
    surrounding_index = (
        np.where(surrounding_values[0] == a)[0][0],
        np.where(surrounding_values[1] == a)[0][0],
    )

    return tuple(surrounding_values), surrounding_index


##################
### LIST IT FCTs
##################


def chunkIt(seq, num):
    """make num sublists of a list"""
    # http://stackoverflow.com/questions/2130016/splitting-a-list-of-arbitrary-size-into-only-roughly-n-equal-parts
    avg = len(seq) / float(num)
    out = []
    last = 0.0
    while last < len(seq):
        out.append(seq[int(last) : int(last + avg)])
        last += avg
    return out


def sliceIt(seq, num):
    """make sublist of num elts of a list"""
    # http://stackoverflow.com/questions/4501636/creating-sublists
    return [seq[i : i + num] for i in range(0, len(seq), num)]


def sublistsIt(seq, lenofsublis_lis, output_array=False):
    """
    make sublists of seq , accoding to len of the sublist in lenofsublis_lis
    ex lenofsublis_lis = [2,3,4,2]

    if output_array : output list of array , else list of list

    (fct perso)
    """

    if np.sum(lenofsublis_lis) != len(seq):
        raise Exception("sublistsIt : sum(lenofsublis_lis) != len(seq) ")
    seq = list(seq)
    sublis_lis = []
    start = 0
    for l in lenofsublis_lis:
        end = start + l
        sublis = seq[start:end]
        sublis_lis.append(sublis)
        start = end
    if output_array:
        return [np.array(e) for e in sublis_lis]
    else:
        return sublis_lis


def identical_consecutive_eltsIt(Lin):
    Lout_big = []
    Linter = [Lin[0]]
    Lout_big.append(Linter)

    for e in Lin[1:]:
        if e == Linter[-1]:
            Linter.append(e)
        else:
            Linter = [e]
            Lout_big.append(Linter)

    return Lout_big


def find_nearest(listin, value):
    """
    Returns:
        value of the nearest , index of the nearest
    """
    array = np.array(listin)
    idx = (np.abs(array - value)).argmin()

    return listin[idx], idx


def find_interval_bound(listin, val, outindexes=True):
    """
    trouve les bornes d'un intervalle
    (listin est supposé triée)
    """
    import bisect
    i = bisect.bisect(listin, val)
    if outindexes:
        return i - 1, i
    else:
        return listin[i - 1], listin[i]


def occurence(L, tolerence=None, pretty_output=False):
    """
    Input :
        tolerence : tolerence to find close elements of L
                    if no tolerence is given then a set() is used
    Returns :
        if pretty_output = False :
            return a list of 2-tuples : (element of the list, number of occurence of this element in the list)
        if pretty_output 0 True :

    Nota : pretty_output is implemented because the first mode is not really useful (180612)
           the equal test is also replaced by is close
    """
    if tolerence:
        Lset = uniquetol2(L, tol=tolerence)
    else:
        Lset = set(L)

    L = np.array(L)
    outlisoccur = []
    for l in Lset:
        outlisoccur.append((l, np.sum(L == l)))

    if pretty_output:
        Mtmp = np.vstack(outlisoccur)
        Vals = Mtmp[:, 0]
        Occurs = Mtmp[:, 1]
        Occurs, Vals = sort_binom_list(Occurs, Vals)

        output = (Occurs, Vals)
    else:
        output = outlisoccur

    return output


def decimateIt(listin, n):
    """
    n so as np.mod(i,n) == 0
    """
    outlist = []
    for i in range(len(listin)):
        if np.mod(i, n) == 0:
            outlist.append(listin[i])
    return outlist


def consecutive_groupIt(data, only_start_end=False):
    """
    Identify groups of continuous numbers in a list

    Useful for time period with a prealable conversion to MJD

    Source :
    https://stackoverflow.com/questions/2154249/identify-groups-of-continuous-numbers-in-a-list
    """
    from operator import itemgetter
    import itertools

    groups_stk = []
    for k, g in itertools.groupby(enumerate(data), lambda A: A[0] - A[1]):
        groups_stk.append(list(map(itemgetter(1), g)))

    if only_start_end:
        return [(g[0], g[-1]) for g in groups_stk]
    else:
        return groups_stk


def identical_groupIt(data):
    """
    Source :
        https://stackoverflow.com/questions/30293071/python-find-same-values-in-a-list-and-group-together-a-new-list
    """
    import itertools
    return [list(j) for i, j in itertools.groupby(data)]


def get_interval(start, end, delta):
    # after http://stackoverflow.com/questions/10688006/generate-a-list-of-datetimes-between-an-interval-in-python
    outlist = []
    curr = start
    while curr < end:
        outlist.append(curr)
        curr += delta
    return outlist


def duplicates_finder(seq):
    """
    from
    http://stackoverflow.com/questions/9835762/find-and-list-duplicates-in-python-list
    moooeeeep solution
    """
    seen = set()
    seen_add = seen.add
    # adds all elements it doesn't know yet to seen and all other to seen_twice
    seen_twice = set(x for x in seq if x in seen or seen_add(x))
    # turn the set into a list (as requested)
    return list(seen_twice)


def sort_table(table, col):
    """sort a table by multiple columns
    table: a list of lists (or tuple of tuples) where each inner list
           represents a row
    cols: specifying the column numbers to sort by
    """
    refcol = list(table[col])
    outtable = []
    for col in table:
        outtable.append([x for (y, x) in sorted(zip(refcol, col))])
    return outtable


def dicofdic(mat, names):

    # a partir d'un matrice de N x N
    # et d'une liste de N noms
    # Fabrique un dictionnaire 2D dic[nom1][nom2] = M[n1,n2]
    # http://stackoverflow.com/questions/13326042/2d-dictionary-with-multiple-keys-per-value
    import collections
    d2_dict = collections.defaultdict(dict)

    for i in range(mat.shape[0]):
        for j in range(mat.shape[1]):
            d2_dict[names[i]][names[j]] = mat[i, j]

    return d2_dict


def find_regex_in_list(regex, L, only_first_occurence=False, line_number=False):
    Lout = []
    for i, e in enumerate(L):
        if re.search(regex, e):
            if not line_number:
                found = e
            else:
                found = (i, e)

            if only_first_occurence:
                return found
            else:
                Lout.append(found)
    return Lout
