import itertools
from functools import wraps
from qrcode.util import pattern_position
import cv2

from .geometry import *


def adj_sin(x, l, a, b, p):
    """
    Computes the adjacent sinus.

    :param x: An array of points.
    :type x: numpy.ndarray
    :param l: A value.
    :type l: float
    :param a: A value.
    :type a: float
    :param b: A value.
    :type b: float
    :param p: An array of points.
    :type p: numpy.ndarray
    :return: The adjacent sinus.
    :rtype: numpy.ndarray

    """
    return a * np.sin(p + x*2*np.pi/l) + b


def get_alignment_table_centers(version):
    """
    Gets the centers of the alignment table.

    :param version: The version.
    :type version: int or float
    :return: The centers of the alignment table.
    :rtype: list

    """
    version = int(version)
    pos = pattern_position(version)
    return list(itertools.product(pos, pos))


def contour_iterator(approximation_function):
    """
    Gets the pattern collection of the contour.

    :param approximation_function: The approximation function to be used.
    :type approximation_function: function
    :return: The collection pattern of the contour.
    :rtype: :class:`qrdecode.utils.patterns.PatternCollection`

    """
    @wraps(approximation_function)
    def approximation_wrap(pattern_collection):
        assert isinstance(pattern_collection, PatternCollection)
        contours_output = []

        for cnt in pattern_collection.contours:
            contours_output.append(approximation_function(cnt))

        return PatternCollection(contours_output, hierarchy=pattern_collection.hierarchy)

    return approximation_wrap


def contour_filter(filter_func):
    """
    Filters the contours of the pattern collection.

    :param filter_func: The filter function to be used.
    :type filter_func: function
    :return: The collection pattern of the filtered contour.
    :rtype: :class:`qrdecode.utils.patterns.PatternCollection`

    """

    @wraps(filter_func)
    def filter_wrapper(pattern_collection, **kwargs):
        """
        The filter wrapper function.

        :param pattern_collection: The pattern collection.
        :type pattern_collection: :class:`qrdecode.utils.patterns.PatternCollection.
        :param depth: The window size of the filter.
        :type depth: int
        :param ini: Initial index.
        :type ini: int
        :return:
        """
        depth = kwargs.pop("depth", 3)
        ini = kwargs.pop("ini", 0)

        assert isinstance(pattern_collection, PatternCollection)

        cnt_candidates = []

        for index in range(pattern_collection.count - depth + 1):
            h_vector = []
            a_vector = []

            for ii in range(depth):
                h_vector.append(pattern_collection.hierarchy[0, index + ii + ini, :])
                a_vector.append(pattern_collection.areas[index + ii + ini])

            fargs = [index, h_vector, a_vector]
            # index = actual
            # h_vector = herencies que corresponen a la finestra
            # a_vector = arees que corresponen a la finestra

            if filter_func(*fargs, **kwargs):
                cnt_candidates.append(pattern_collection.contours[index])

        return PatternCollection(cnt_candidates)

    return filter_wrapper


@contour_filter
def position_filter(i, h, a, epsilon=2):
    """
    Checks if the candidate is a position pattern member.

    :param i: The index of the candidate.
    :type i: int
    :param h: The hierarchy array corresponding to the actual window.
    :type h: numpy.ndarray
    :param a: The areas corresponding to the actual window.
    :type a: numpy.array
    :param epsilon: The epsilon of the candidate.
    :type epsilon: float
    :return: True if the candidate is a position pattern member. False otherwise.
    :rtype: bool

    """
    unit_area = a[2] / 9
    if unit_area:
        return not i == 0 \
               and ((h[0][0] == i + 3) or (h[0][0] == -1)) \
               and (h[0][1] <= i - 1) \
               and (h[0][2] == i + 1) \
               and (h[1][2] == i + 2) \
               and (h[1][0] == -1) \
               and (49 / epsilon < a[0] / unit_area < 49 * epsilon) \
               and (25 / epsilon < a[1] / unit_area < 25 * epsilon)
    else:
        return None


@contour_filter
def alignment_filter(i, h, a):
    """
    Checks if the candidate is a position pattern member.

    :param i: The index of the candidate.
    :type i: int
    :param h: The hierarchy array corresponding to the actual window.
    :type h: numpy.ndarray
    :param a: The areas corresponding to the actual window.
    :type a: numpy.array
    :return: True if the candidate is an alignment pattern member. False otherwise.
    :rtype: bool

    """
    if a[1]:
        return not i == 0 \
               and h[1][3] == i - 1 \
               and h[1][0] == -1 and h[1][1] == -1 and h[1][2] == -1 \
               and (9 / 2 < a[0] / a[1] < 9 * 1.2)
    else:
        return None


@contour_filter
def dot_filter(i, h, a, bit_size=1):
    """
    Returns true if the candidate is a dot.

    :param i: The index of the candidate.
    :type i: int
    :param h: The hierarchy array corresponding to the actual window.
    :type h: numpy.ndarray
    :param a: The areas corresponding to the actual window.
    :type a: numpy.array
    :param bit_size: The size of the bit.
    :type bit_size: int
    :return: Returns True in case the candidate is a dot. False otherwise.
    :rtype: bool

    """
    s2 = bit_size ** 2
    if a[1]:
        return s2 * 0.5 < a[1] < s2 * 1.5


@contour_filter
def alignment_canny_filter(i, h, a, epsilon=2):
    """
    Returns true if the candidate is an alignment pattern member using a canny filter.

    :param i: The index of the candidate.
    :type i: int
    :param h: The hierarchy array corresponding to the actual window.
    :type h: numpy.ndarray
    :param a: The areas corresponding to the actual window.
    :type a: numpy.array
    :param epsilon: The epsilon of the candidate.
    :type epsilon: float
    :return: True if the candidate is a position pattern. False otherwise.
    :rtype: bool

    """

    hierarchy_cond = not i == 0 \
                     and ((h[0][0] == i + 3) or (h[0][0] == -1)) \
                     and (h[0][1] <= i - 1) \
                     and (h[0][2] == i + 1) \
                     and (h[1][2] == i + 2) \
                     and (h[1][0] == -1)

    if hierarchy_cond:
        area_inner_dot = a[2]  # Normalize to this factor, must be 1².
        if area_inner_dot:
            area_outer_dot = a[1] / area_inner_dot
            area_inner_align = a[0] / area_inner_dot
            return (9 / epsilon < area_inner_align < 9 * epsilon) and area_outer_dot < 4


@contour_iterator
def farthest_approximation(cnt):
    """
    Finds the furthest point of the quadrant of the auxiliary rectangle.

    :param cnt: The contours array.
    :type cnt: numpy.array
    :return: The furthest point.
    :rtype: numpy.array

    """
    # Farthest points:
    # We find the furthest point for every quadrant of the auxiliary rectangle.

    # We call a OpenCV function that creates a bounding rectangle over the contour.
    x, y, w, h = cv2.boundingRect(cnt)

    # We define a set of auxiliary points to trim the contour into 4 pieces.
    center = np.array((x + w / 2, y + h / 2))
    distances = [0, 0, 0, 0]
    point = [0, 0, 0, 0]

    for index, cnt_point in enumerate(cnt):
        cnt_point = cnt_point.tolist()[0]
        # print(point)
        d = distance(center, cnt_point)
        if cnt_point[0] < center[0] and cnt_point[1] < center[1]:
            if d > distances[0]:
                distances[0] = d
                point[0] = np.array([cnt_point])
        elif cnt_point[0] > center[0] and cnt_point[1] < center[1]:
            if d > distances[1]:
                distances[1] = d
                point[1] = np.array([cnt_point])
        elif cnt_point[0] > center[0] and cnt_point[1] > center[1]:
            if d > distances[2]:
                distances[2] = d
                point[2] = np.array([cnt_point])
        elif cnt_point[0] < center[0] and cnt_point[1] > center[1]:
            if d > distances[3]:
                distances[3] = d
                point[3] = np.array([cnt_point])

    return np.array(point)


@contour_iterator
def poly_approximation(cnt):
    """
    The polygon approximation of the quadrant of the auxiliary rectangle.

    :param cnt: The contours array.
    :type cnt: numpy.array
    :return: The polygon approximation of the quadrant.
    :rtype: numpy.ndarray

    """

    # We call a OpenCV function that creates a bounding rectangle over the contour.
    x, y, w, h = cv2.boundingRect(cnt)

    center = (x + w / 2, y + h / 2)  # center of the rectangle

    # By doing an polygon approximation.
    arc_length = cv2.arcLength(cnt, True)
    for ratio in np.arange(0, 1, 0.05):
        # From the OpenCV computed length of the contour we define a "density" for the approximation.
        epsilon = ratio * arc_length
        # We call the OpenCV solution for the approximation.
        approx = cv2.approxPolyDP(cnt, epsilon, True)
        if len(approx) == 4:
            break

    # The approx solution corners are ordered clockwise. However, their position in the list is random.
    # We can use again the bounding box to order them.
    point = approx[0, :, :][0]
    if point[0] < center[0] and point[1] < center[1]:
        k = 0
    elif point[0] > center[0] and point[1] < center[1]:
        k = 3
    elif point[0] > center[0] and point[1] > center[1]:
        k = 2
    elif point[0] < center[0] and point[1] > center[1]:
        k = 1

    point = np.concatenate((approx[k:], approx[0:k]))

    return point


class PatternCollection:

    FILTERS = {}
    APPROXIMATORS = {}

    # TODO use all in numpy arrays, without lists.

    def __init__(self, contours=None, **kwargs):
        """
        Initializer function of the :class:`qrdecode.utils.patterns.PatternCollection` class.

        :param hierarchy: The hierarchy to be used.
        :type hierarchy: array
        :returns: Void function.
        :rtype: None

        """

        self._contours = None
        self._hierarchy = None
        self._areas = None
        self._centers = None
        self._moments = None

        self.contours = contours
        hierarchy = kwargs.pop("hierarchy", None)
        self.hierarchy = hierarchy

        # Adding different filters:
        self.add_filter("alignment", alignment_filter, depth=2, ini=-1)
        self.add_filter("alignment_canny", alignment_canny_filter, depth=4)
        self.add_filter("dot", dot_filter, depth=2, ini=-1)
        self.add_filter("position", position_filter)

        self.add_approximator("farthest", farthest_approximation)
        self.add_approximator("furthest", farthest_approximation)
        self.add_approximator("poly", poly_approximation)

    def add_filter(self, name, filter_func, **kwargs):
        """
        Adds a filter to the list of available ones.

        :param name: The name of the filter.
        :type name: str
        :param filter_func: The function associated to the filter.
        :type filter_func: function
        :param kwargs: The not used kwargs will get passed to ´´filter_func´´
        :type kwargs: dict
        :return: Void function.
        :rtype: None

        """
        self.FILTERS[name] = (filter_func, kwargs)

    def find(self, pattern_name, **kwargs):
        """
        Finds the patterns associated to the pattern_name.

        :param pattern_name: The name of the pattern.
        :type pattern_name: str
        :param kwargs: The not used kwargs will get passed to ``filter_func``
        :type kwargs: dict
        :return: The pattern points associated to that pattern.
        :rtype: :class:`qrdecode.utils.patterns.PatternCollection`

        """
        if self.hierarchy is not None:
            if pattern_name in self.FILTERS:
                filter_func, kwargs_saved = self.FILTERS[pattern_name]
                kwargs_saved.update(kwargs)
                return filter_func(self, **kwargs_saved)
            else:
                print("Pattern name not recognised")
                return PatternCollection()
        else:
            print("No hierarchy provided.")
            return PatternCollection()

    @property
    def contours(self):
        """
        The contours of a pattern.

        :setter: Sets the contours.
        :getter: Gets the contours.
        :deleter: Deletes the contours.
        :type: numpy.ndarray

        """
        return self._contours

    @contours.setter
    def contours(self, contours):
        if contours is not None:
            assert isinstance(contours, (list, np.ndarray))

        # TODO improve asserts
        self._contours = contours
        del self.areas
        del self.moments
        del self.centers
        del self.hierarchy

    @contours.deleter
    def contours(self):
        self.contours = None

    @property
    def hierarchy(self):
        """
        Sets the hierarchy of a pattern.

        :setter: Sets the hierarchy.
        :getter: Gets the hierarchy.
        :deleter: Deletes the hierarchy.
        :type: numpy.ndarray

        """
        return self._hierarchy

    @hierarchy.setter
    def hierarchy(self, hierarchy):
        if hierarchy is not None:
            assert isinstance(hierarchy, np.ndarray), "Hierarchy must be np-array like"
            assert self.count == hierarchy.shape[1], "Hierarchy must have the same dimension than contours"
        self._hierarchy = hierarchy

    @hierarchy.deleter
    def hierarchy(self):
        self._hierarchy = None

    @property
    def count(self):
        """
        The size of the contours array.

        :return: The size of the contours.
        :rtype: int

        """
        return len(self.contours)

    @property
    def areas(self):
        """
        The areas of the contours.

        :setter: Sets the areas of the contours.
        :getter: Gets the areas of the contours.
        :deleter: Deletes the areas of the contours.
        :type: list

        """
        if self._areas is None:
            self._areas = [cv2.contourArea(cnt) for cnt in self.contours]
        return self._areas

    @areas.setter
    def areas(self, areas):
        if areas is not None:
            assert isinstance(areas, list)
            assert self.count == len(areas)
        self._areas = areas

    @areas.deleter
    def areas(self):
        self._areas = None

    @property
    def moments(self):
        """
        The moments of the contours.

        :setter: Sets the moments of the contours.
        :getter: Gets the moments of the contours.
        :deleter: Deletes the moments of the contours.
        :type: list

        .. note::
            The length of the moments list has to be equal to the length of the count attribute.

        """
        if self._moments is None:
            self._moments = [cv2.moments(cnt) for cnt in self.contours]
        return self._moments

    @moments.deleter
    def moments(self):
        self._moments = None

    @moments.setter
    def moments(self, moments):
        if moments is not None:
            assert isinstance(moments, list)
            assert self.count == len(moments)
        self._moments = moments

    @property
    def centers(self):
        """
        The centers of the masses computed using the moments.

        :setter: Sets the centers of the masses.
        :getter: Gets the centers of the masses.
        :deleter: Deletes the centers of the masses.
        :type: list

        """
        centers = []
        for ii, m in enumerate(self.moments):
            # It is possible to have a division by 0 doing the calculus of the center of mass, so we can handle it.
            if m['m00']:
                centroid_x = int(m['m10'] / m['m00'])
                centroid_y = int(m['m01'] / m['m00'])
                centers.append((centroid_x, centroid_y))
            else:
                centers.append((None, None))
        return centers

    @centers.setter
    def centers(self, centers):
        if centers is not None:
            assert isinstance(centers, list)
            assert self.count == len(centers)
        self._centers = centers

    @centers.deleter
    def centers(self):
        self._centers = None

    def order(self):
        pass

    def orientate(self):
        """
        Get an ordered version of a PatternCollection of 3 patterns north orientated assuming the 3 points form an
        obtuse angle.

        [NW, NE, SW] (being NW: the NorthWest corner, NE: the NorthEast corner and SW: the SouthWest corner,
        viewing the QR code from a "North" orientation).

        :return: The position patterns ordered according their distance.
        :rtype: :class:`qrdecode.utils.patterns.PatternCollection`
        """

        pre, pre_mc = self.contours.copy(), self.centers.copy()

        # Evaluate if at least 3 patterns are found to order them:
        assert len(pre_mc) == 3, "Unable to apply orientation algorithm, PatternCollection length must be 3."
        # We can group in pairs the candidates in order to compute their relative distance.
        pre_mc_pairs = list(itertools.combinations(pre_mc, 2))
        pre_mc_dista = [distance(x[0], x[1]) for x in pre_mc_pairs]

        # Then we create all the possible permutations of these distances and check one that majorizes.
        major_dist_map = list(map(lambda x: (x[0] > x[1]) and (x[0] > x[2]), itertools.permutations(pre_mc_dista)))
        index = major_dist_map.index(True)

        # We have to recreate a list in which we order the precandidates as follows:
        # The first element should be the center that not takes place in the first distance of the major_dist_map
        # To do so, we can reverse the original list, and with the help of itertools, we make it.
        pre.reverse()
        position_patterns = list(list(itertools.permutations(pre))[index])

        # TODO fix check hierarchy is correct?
        return PatternCollection(position_patterns, hierarchy=self.hierarchy)


    def add_approximator(self, name, approx_func, **kwargs):
        """
        Adds a new aproximator associating the function ``approx_function`` to it.

        :param name: The name of the new approximator.
        :type name: str
        :param approx_func: The function associated to the approximator.
        :type approx_func: function
        :param kwargs: The arguments that will be used in ``approx_func``.
        :type kwargs: dict
        :return: Void function.
        :rtype: None

        """
        self.APPROXIMATORS[name] = (approx_func, kwargs)

    def approximate(self, method="farthest", **kwargs):
        """
        Computes the approximation using a specific approximation method.

        :param method: The name of the method to be used to compute the approximation.
        :type method: str
        :param kwargs: The arguments that will be used in the function associated to the approximator.
        :type kwargs: dict
        :return: The result of the approximation.
        :rtype: :class:`qrdecode.utils.patterns.PatternCollection`
        :raise: AssertionError

        .. note::
            An AssertionError will be raised if the return of the approximation function is not an instance of the
            PatternCollection class.

        """
        if method in self.APPROXIMATORS:
            approx_func, kwargs_saved = self.APPROXIMATORS[method]
            kwargs_saved.update(kwargs)
            output_collection = approx_func(self, **kwargs_saved)
            assert isinstance(output_collection, PatternCollection)
            return output_collection
        else:
            print("Approximation name not recognised")
            return PatternCollection()

    def __add__(self, other):
        """
        Adds a PatternCollection to another one (the implicit parameter).

        :param other: A PatternCollection.
        :type other: :class:`qrdecode.utils.patterns.PatternCollection`
        :return: The Pattern Collection resulting in the sum of both the implicit parameter (``self``) and the argument.
        :rtype: :class:`qrdecode.utils.patterns.PatternCollection`
        :raises: AssertionError

        .. note::
            An AssertionError will be raised if the ``other`` parameter is not an instance of the class \
            PatternCollection.

        """

        assert isinstance(other, PatternCollection), "Only able to add PatternCollection to another one."

        # First mix the contours lists

        contours = self.contours + other.contours

        if (self.hierarchy is not None) and (other.hierarchy is not None):
            other_hierarchy = other.hierarchy.copy()
            other_hierarchy[other_hierarchy != -1] += self.count

            hierarchy = np.hstack((self.hierarchy, other_hierarchy))

        else:
            hierarchy = None

        new_pattern_collection = PatternCollection(contours, hierarchy=hierarchy)

        new_filters = {}

        for name, filter_func in self.FILTERS.items():
            if name in new_pattern_collection.FILTERS:
                func_old = self.FILTERS.get(name)
                func_saved = new_pattern_collection.FILTERS.get(name)
                name = "{}_0".format(name) if not func_old == func_saved else name
                new_filters[name] = func_old

        for name, filter_func in other.FILTERS.items():
            if name in new_pattern_collection.FILTERS:
                func_old = other.FILTERS.get(name)
                func_saved = new_pattern_collection.FILTERS.get(name)
                name = "{}_1".format(name) if not func_old == func_saved else name
                new_filters[name] = func_old

        new_pattern_collection.FILTERS.update(new_filters)

        return new_pattern_collection















