"""
Library functions for graham algorithm implementation (find the convex hull of a given list of points).
"""

from copy import deepcopy

import numpy as np


def clean_ROI(image):
    """
    Remove instruments borders from an observation.
    """
    H, J = [], []

    shape = np.array(image.shape)
    row, col = np.indices(shape)

    for i in range(0, shape[0]):
        r = row[i, :][image[i, :] > 0.0]
        c = col[i, :][image[i, :] > 0.0]
        if len(r) > 1 and len(c) > 1:
            H.append((r[0], c[0]))
            H.append((r[-1], c[-1]))
    H = np.array(H)
    for j in range(0, shape[1]):
        r = row[:, j][image[:, j] > 0.0]
        c = col[:, j][image[:, j] > 0.0]
        if len(r) > 1 and len(c) > 1:
            J.append((r[0], c[0]))
            J.append((r[-1], c[-1]))
    J = np.array(J)
    xmin = np.min([H[:, 1].min(), J[:, 1].min()])
    xmax = np.max([H[:, 1].max(), J[:, 1].max()]) + 1
    ymin = np.min([H[:, 0].min(), J[:, 0].min()])
    ymax = np.max([H[:, 0].max(), J[:, 0].max()]) + 1
    return np.array([xmin, xmax, ymin, ymax])


# Define angle and vectors operations
def vector(A, B):
    """
    Define a vector from the inputed points A, B.
    """
    return (B[0] - A[0], B[1] - A[1])


def determinant(u, v):
    """
    Compute the determinant of 2 vectors.
    --------
    Inputs:
    u, v : vectors (list of length 2)
        Vectors for which the determinant must be computed.
    ----------
    Returns:
    determinant : real
        Determinant of the input vectors.
    """
    return u[0] * v[1] - u[1] * v[0]


def pseudo_angle(A, B, C):
    """
    Define a pseudo-angle from the inputed points A, B, C.
    """
    u = vector(A, B)
    v = vector(A, C)
    return determinant(u, v)


def distance(A, B):
    """
    Compute the euclidian distance betwwen 2 points of the plan.
    ----------
    Inputs:
    A, B : points (list of length 2)
        Points between which the distance must be computed.
    ----------
    Returns:
    distance : real
        Euclidian distance between A, B.
    """
    x, y = vector(A, B)
    return np.sqrt(x**2 + y**2)


# Define lexicographic and composition order
def lexico(A, B):
    """
    Define the lexicographic order between points A, B.
    A <= B if:
        - y_a < y_b
    or  - y_a = y_b and x_a < x_b
    ----------
    Inputs:
    A, B : points (list of length 2)
        Points compared for the lexicographic order
    ----------
    Returns:
    lexico : boolean
        True if A<=B, False otherwise.
    """
    return (A[1] < B[1]) or (A[1] == B[1] and A[0] <= B[0])


def min_lexico(s):
    """
    Return the minimum of a list of points for the lexicographic order.
    ----------
    Inputs:
    s : list of points
        List of points in which we look for the minimum for the lexicographic
        order
    ----------
    Returns:
    m : point (list of length 2)
        Minimum for the lexicographic order in the input list s.
    """
    m = s[0]
    for x in s:
        if lexico(x, m):
            m = x
    return m


def comp(Omega, A, B):
    """
    Test the order relation between points such that A <= B :
    This is True if :
        - (Omega-A,Omega-B) is a right-hand basis of the plan.
    or  - (Omega-A,Omega-B) are linearly dependent.
    ----------
    Inputs:
    Omega, A, B : points (list of length 2)
        Points compared for the composition order
    ----------
    Returns:
    comp : boolean
        True if A <= B, False otherwise.
    """
    t = pseudo_angle(Omega, A, B)
    if t > 0:
        return True
    elif t < 0:
        return False
    else:
        return distance(Omega, A) <= distance(Omega, B)


# Implement quicksort
def partition(s, left, right, order):
    """
    Take a random element of a list 's' between indexes 'left', 'right' and place it
    at its right spot using relation order 'order'. Return the index at which
    it was placed.
    ----------
    Inputs:
    s : list
        List of elements to be ordered.
    left, right : int
        Index of the first and last elements to be considered.
    order : func: A, B -> bool
        Relation order between 2 elements A, B that returns True if A<=B,
        False otherwise.
    ----------
    Returns:
    index : int
        Index at which have been placed the element chosen by the function.
    """
    i = left - 1
    for j in range(left, right):
        if order(s[j], s[right]):
            i = i + 1
            temp = deepcopy(s[i])
            s[i] = deepcopy(s[j])
            s[j] = deepcopy(temp)
    temp = deepcopy(s[i + 1])
    s[i + 1] = deepcopy(s[right])
    s[right] = deepcopy(temp)
    return i + 1


def sort_aux(s, left, right, order):
    """
    Sort a list 's' between indexes 'left', 'right' using relation order 'order' by
    dividing it in 2 sub-lists and sorting these.
    """
    if left <= right:
        # Call partition function that gives an index on which the list will be divided
        q = partition(s, left, right, order)
        sort_aux(s, left, q - 1, order)
        sort_aux(s, q + 1, right, order)


def quicksort(s, order):
    """
    Implement the quicksort algorithm on the list 's' using the relation order
    'order'.
    """
    # Call auxiliary sort on whole list.
    sort_aux(s, 0, len(s) - 1, order)


def sort_angles_distances(Omega, s):
    """
    Sort the list of points 's' for the composition order given reference point
    Omega.
    """

    def order(A, B):
        return comp(Omega, A, B)

    quicksort(s, order)


# Define fuction for stacks (use here python lists with stack operations).
def empty_stack():
    return []


def stack(S, A):
    S.append(A)


def unstack(S):
    S.pop()


def stack_top(S):
    return S[-1]


def stack_sub_top(S):
    return S[-2]


# Alignement handling
def del_aux(s, k, Omega):
    """
    Returne the index of the first element in 's' after the index 'k' that is
    not linearly dependent of (Omega-s[k]), first element not aligned to Omega
    and s[k].
    ----------
    Inputs:
    s : list
        List of points
    k : int
        Index from which to look for aligned points in s.
    Omega : point (list of length 2)
        Reference point in 's' to test alignement.
    ----------
    Returns:
    index : int
        Index of the first element not aligned to Omega and s[k].
    """
    p = s[k]
    j = k + 1
    while (j < len(s)) and (pseudo_angle(Omega, p, s[j]) == 0):
        j = j + 1
    return j


def del_aligned(s, Omega):
    """
    Deprive a list of points 's' from all intermediary points that are aligned
    using Omega as a reference point.
    ----------
    Inputs:
    s : list
        List of points
    Omega : point (list of length 2)
        Point of reference to test alignement.
    -----------
    Returns:
    s1 : list
        List of points where no points are aligned using reference point Omega.
    """
    s1 = [s[0]]
    n = len(s)
    k = 1
    while k < n:
        # Get the index of the last point aligned with (Omega-s[k])
        k = del_aux(s, k, Omega)
        s1.append(s[k - 1])
    return s1


# Implement Graham algorithm
def convex_hull(H):
    """
    Implement Graham algorithm to find the convex hull of a given list of
    points, handling aligned points.
    ----------
    Inputs:
    A : list
        List of points for which the the convex hull must be returned.
    ----------
    S : list
        List of points defining the convex hull of the input list A.
    """
    S = empty_stack()
    Omega = min_lexico(H)
    sort_angles_distances(Omega, H)
    A = del_aligned(H, Omega)
    if len(A) < 3:
        return H
    else:
        stack(S, A[0])
        stack(S, A[1])
        stack(S, A[2])
        for i in range(3, len(A)):
            while pseudo_angle(stack_sub_top(S), stack_top(S), A[i]) <= 0:
                unstack(S)
            stack(S, A[i])
        return S


def image_hull(image, step=5, null_val=0.0, inside=True):
    """
    Compute the convex hull of a 2D image and return the 4 relevant coordinates
    of the maximum included rectangle (ie. crop image to maximum rectangle).
    ----------
    Inputs:
    image : numpy.ndarray
        2D array of floats corresponding to an image in intensity that need to
        be cropped down.
    step : int, optional
        For images with straight edges, not all lines and columns need to be
        browsed in order to have a good convex hull. Step value determine
        how many row/columns can be jumped. With step=2 every other line will
        be browsed.
        Defaults to 5.
    null_val : float, optional
        Pixel value determining the threshold for what is considered 'outside'
        the image. All border pixels below this value will be taken out.
        Defaults to 0.
    inside : boolean, optional
        If True, the cropped image will be the maximum rectangle included
        inside the image. If False, the cropped image will be the minimum
        rectangle in which the whole image is included.
        Defaults to True.
    ----------
    Returns:
    vertex : numpy.ndarray
        Array containing the vertex of the maximum included rectangle.
    """
    H = []
    shape = np.array(image.shape)
    row, col = np.indices(shape)
    for i in range(0, int(min(shape) / 2), step):
        r1, r2 = row[i, :][image[i, :] > null_val], row[-i, :][image[-i, :] > null_val]
        c1, c2 = col[i, :][image[i, :] > null_val], col[-i, :][image[-i, :] > null_val]
        if r1.shape[0] > 1:
            H.append((r1[0], c1[0]))
            H.append((r1[-1], c1[-1]))
        if r2.shape[0] > 1:
            H.append((r2[0], c2[0]))
            H.append((r2[-1], c2[-1]))
    S = np.array(convex_hull(H))

    x_min, y_min = S[:, 0] < S[:, 0].mean(), S[:, 1] < S[:, 1].mean()
    x_max, y_max = S[:, 0] > S[:, 0].mean(), S[:, 1] > S[:, 1].mean()
    # Get the 4 extrema
    # S0 = S[x_min*y_min][np.argmin(S[x_min*y_min][:, 0])]
    # S1 = S[x_min*y_max][np.argmax(S[x_min*y_max][:, 1])]
    # S2 = S[x_max*y_min][np.argmin(S[x_max*y_min][:, 1])]
    # S3 = S[x_max*y_max][np.argmax(S[x_max*y_max][:, 0])]
    S0 = S[x_min * y_min][np.abs(0 - S[x_min * y_min].sum(axis=1)).min() == np.abs(0 - S[x_min * y_min].sum(axis=1))][0]
    S1 = S[x_min * y_max][np.abs(shape[1] - S[x_min * y_max].sum(axis=1)).min() == np.abs(shape[1] - S[x_min * y_max].sum(axis=1))][0]
    S2 = S[x_max * y_min][np.abs(shape[0] - S[x_max * y_min].sum(axis=1)).min() == np.abs(shape[0] - S[x_max * y_min].sum(axis=1))][0]
    S3 = S[x_max * y_max][np.abs(shape.sum() - S[x_max * y_max].sum(axis=1)).min() == np.abs(shape.sum() - S[x_max * y_max].sum(axis=1))][0]
    # Get the vertex of the biggest included rectangle
    if inside:
        f0 = np.max([S0[0], S1[0]])
        f1 = np.min([S2[0], S3[0]])
        f2 = np.max([S0[1], S2[1]])
        f3 = np.min([S1[1], S3[1]])
    else:
        f0 = np.min([S0[0], S1[0]])
        f1 = np.max([S2[0], S3[0]])
        f2 = np.min([S0[1], S2[1]])
        f3 = np.max([S1[1], S3[1]])

    return np.array([f0, f1, f2, f3]).astype(int)
