"""
qrdecode module
===============
"""

import itertools

import cv2
import numpy as np
from scipy.optimize import curve_fit
from sklearn.cluster import KMeans

import reedsolo
from qrdecode.decoder import QRMatrix

from qrdecode.utils.patterns import PatternCollection, adj_sin, get_alignment_table_centers
from qrdecode.utils.image import ImageProcessor
from qrdecode.utils import geometry
from qrdecode.utils.geometry import rotate_point, create_line_iterator
from qrdecode.utils.constants import CV_QR_EAST, CV_QR_NORTH, CV_QR_SOUTH, CV_QR_WEST
from qrdecode.utils.geometry import collinear, line_equation, line_slope, get_intersection_point, create_line_iterator
from qrdecode.utils.image import ImageProcessor
from qrdecode.utils.patterns import PatternCollection, adj_sin, get_alignment_table_centers
import matplotlib.pyplot as plt


class QRProcessor(object):
    """
    :class:`QRProcessor` is implemented to sampling and extracting QR modules, the "information pixels" located
    inside the code. It assumes a QR has been cropped by a :class:`Scanner` object or captured with its correct
    orientation and need input information about the location of the QR inside an image.
    """

    def __init__(self, file=None, **kwargs):
        """
        Initializer function of :class:`QRProcessor` class.

        :param file: The input image which contains a orientation corrected QR code. Input image will be converted to
                an :class:`ImageProcessor` object and saved as :attr:`QRProcessor.image_processor` attribute.
        :type file: ImageProcessor or any type accepted by it
        :param code_border: The code border.
        :type code_border: int
        :param code_size: The code size.
        :type code_size: int
        :param code_offset: The code offset.
        :type code_offset: (int, int)
        :param auto_update_th: argument passed to :class:`ImageProcessor` if has to be called.
        :type auto_update_th: bool
        :param kwargs: The unused kwargs will get passed to :func:`get_contours` method.
        :type kwargs: dict

        """

        auto_update_th = kwargs.pop("auto_update_th", True)
        if isinstance(file, ImageProcessor):
            self.image_processor = file
        else:
            self.image_processor = ImageProcessor(file, auto_update_th=auto_update_th)

        self._threshold = "gaussian"
        self.image_patterns = None

        self._bit_size = 1

        self.position_patterns = None
        self.position_boxes = None
        self.alignment_patterns = None
        self.dot_patterns = None
        self._alignment_table_centers = None
        self._alignment_table_centers_sampled = None

        self.get_contours(**kwargs)

        self._code_border = None
        self._code_size = None
        self._code_offset = None

        self.code_border = kwargs.pop("code_border", 10)
        self.code_size = kwargs.pop("code_size", 235)
        self.code_offset = kwargs.pop("code_offset", (0, 0))
        self.dst = None

        self.total_mean_size = self.code_size.mean()

        self._src_corners = None
        self._dst_corners = None
        self._warp_matrix = None

        self._src_sample = None
        self._dst_sample = None
        self._warp_matrix_sample = None

        self._warp_matrix_alignments = None

        self._color_matrix = None
        self._binary_matrix = None

        self.decoder = None

        self.geometric_bit_size()
        self.timing_bit_size()

    @property
    def image(self):
        """
        A sugar-syntax access for :attr:`QRProcessor.image_processor.image`.

        :getter: Returns the image being processed.
        :setter: Sets the image to be processed.
        :type: array

        """
        return self.image_processor.image

    @image.setter
    def image(self, img):
        self.image_processor.image = img

    @property
    def threshold(self):
        """
        A threshold to be used among all the configured threshold of the :class:`ImageProcessor` object in
        :attr:`QRProcessor.image_processor`. If a custom threshold is given, it must have been declared using
        :func:`add_threshold` method and an associate function set.

        :getter: Returns the threshold being used.
        :setter: Sets the threshold to be used.
        :type: string

        """
        return self._threshold

    @threshold.setter
    def threshold(self, threshold):
        if threshold in self.image_processor.THRESHOLDS:
            self._threshold = threshold
        else:
            print("Threshold name isn't present, still using '{}' threshold. Plese use, add_threshold()"
                  " method to define the threshold.".format(self.threshold))

    @classmethod
    def add_threshold(cls, name, threshold_func, *args, **kwargs):
        """
        A sugar-syntax access for :func:`QRProcessor.image_processor.add_threshold`.

        :param name: The name of the threshold.
        :type name: string
        :param threshold_func: The function associated to the threshold.
        :type threshold_func: function
        :param args: Arguments of the function associated to the threshold that will be passed to \
            :func:`add_threshold`.
        :type args: tuple
        :param kwargs: K-arguments of the function associated to the threshold that will be passed to \
            :func:`add_threshold`.
        :type kwargs: dict
        :return: Void function.
        :rtype: None

        """
        ImageProcessor.add_threshold(name, threshold_func, *args, **kwargs)

    def get_contours(self, **kwargs):
        """
        Extracts image contours using the given :attr:`QRProcessor.threshold` and :attr:`QRProcessor.image_processor`
        capabilities.

        :param threshold: If given, updates the :attr:`QRProcessor.threshold` attribute.
        :type threshold: string
        :return: A :class:`PatternCollection` instance which contains a list of image contours and contour heritages.
        :rtype: PatternCollection

        """
        self.threshold = kwargs.pop('threshold', self.threshold)
        self.image_processor.threshold(self.threshold)
        self.image_patterns = self.image_processor.pattern(self.threshold,
                                                           mode=cv2.RETR_TREE,
                                                           method=cv2.CHAIN_APPROX_SIMPLE)
        # self.image_patterns = self.image_processor.patterns.get(self.threshold, PatternCollection())

        # First find the pattern contours of position patterns
        self.position_patterns = self.image_patterns.find("position")
        self.position_patterns = self.position_patterns.orientate()
        position_boxes = self.position_patterns.approximate(method="poly")
        assert isinstance(position_boxes, PatternCollection)
        self.position_boxes = position_boxes.contours

        return self.image_processor.pattern(self.threshold)

    @property
    def code_border(self):
        """
        The expected size in pixels of the border from the QR code edged to image edged in the input image.

        :getter: Returns the code border being used.
        :setter: Sets the code border being used.
        :param border: The code border.
        :type border: int or (numpy.ndarray, list, tuple)
        :rtype: numpy.ndarray

        """
        return self._code_border

    @code_border.setter
    def code_border(self, border):
        if isinstance(border, int):
            self._code_border = np.array([border, border], dtype=int)
        elif isinstance(border, (np.ndarray, list, tuple)):
            border = np.array(border)
            assert border.shape == (2,)
            self._code_border = border
        else:
            print("Value error. Using the saved one: {}".format(self.code_border))

    @property
    def code_size(self):
        """
        The expected size in pixels of the QR code in the input image. 

        :setter: Sets the code size.
        :getter: Returns the code size.
        :param size: The code size
        :type size: int or (numpy.ndarray, list, tuple)
        :rtype: numpy.ndarray

        """
        return self._code_size

    @code_size.setter
    def code_size(self, size):
        if isinstance(size, int):
            self._code_size = np.array([size, size], dtype=int)
        elif isinstance(size, (np.ndarray, list, tuple)):
            size = np.array(size)
            assert size.shape == (2,)
            self._code_size = size
        else:
            print("Value error. Using the saved one: {}".format(self.code_size))

    @property
    def code_offset(self):
        """
        The expected size in pixels of the QR offset from its suposed location knowing its :attr:`QRProcessor.code_size`
        and :attr:`QRProcessor.code_border`.

        :setter: Sets the code offset.
        :getter: Returns the code offset.
        :param offset: The code offset.
        :type: int or (numpy.ndarray, list, tuple)
        :rtype: numpy.array

        """
        return self._code_offset

    @code_offset.setter
    def code_offset(self, offset):
        if isinstance(offset, int):
            self._code_offset = np.array([offset, offset], dtype=int)
        elif isinstance(offset, (np.ndarray, list, tuple)):
            offset = np.array(offset)
            assert offset.shape == (2,)
            self._code_offset = offset
        else:
            print("Value error. Using the saved one: {}".format(self.code_border))

    @property
    def size_tuple(self):
        """
        Returns the :attr:`QRProcessor.image` size tuple.

        :return:
        :rtype: tuple

        """
        n, m, c = self.image.shape
        return n, m

    @property
    def dst_corners(self):
        """
        The destination corners.

        :setter: Sets the destination corners.
        :getter: Returns the destination corners.
        :param corners: The destination corners.
        :type corners: array
        :rtype: numpy.array
        :raises: AssertionError

        .. note::
            The shape of the numpy.array that is created using the tuple passed as an argument
            must be (N,2), where N>3.

        """
        return self._dst_corners

    @dst_corners.setter
    def dst_corners(self, corners):
        corners = np.array(corners, dtype=np.float32)
        assert len(corners.shape) == 2 and corners.shape[1] == 2 and corners.shape[0] > 3
        self._dst_corners = corners

    @property
    def src_corners(self):
        """
        The source corners.

        :setter: Sets the source corners.
        :getter: Returns the source corners.
        :type: array
        :rtype: numpy.array
        :raises: AssertionError

        .. note::
            The shape of the numpy.array that is created using the tuple passed as an argument
            must be (N,2), where N>3.

        """
        return self._src_corners

    @src_corners.setter
    def src_corners(self, corners):
        corners = np.array(corners, dtype=np.float32)
        assert len(corners.shape) == 2 and corners.shape[1] == 2 and corners.shape[0] > 3
        self._src_corners = corners

    @property
    def dst_sample(self):
        """
        The destination corners.

        :setter: Sets the destination corners.
        :getter: Returns the destination corners.
        :param corners: The destination corners.
        :type corners: array
        :rtype: numpy.array
        :raises: AssertionError

        .. note::
            The shape of the numpy.array that is created using the tuple passed as an argument
            must be (N,2), where N>3.

        """
        return self._dst_sample

    @dst_sample.setter
    def dst_sample(self, corners):
        corners = np.array(corners, dtype=np.float32)
        assert len(corners.shape) == 2 and corners.shape[1] == 2 and corners.shape[0] > 3
        self._dst_sample = corners

    @property
    def src_sample(self):
        """
        The source corners.

        :setter: Sets the source corners.
        :getter: Returns the source corners.
        :type: array
        :rtype: numpy.array
        :raises: AssertionError

        .. note::
            The shape of the numpy.array that is created using the tuple passed as an argument
            must be (N,2), where N>3.

        """
        return self._src_sample

    @src_sample.setter
    def src_sample(self, corners):
        corners = np.array(corners, dtype=np.float32)
        assert len(corners.shape) == 2 and corners.shape[1] == 2 and corners.shape[0] > 3
        self._src_sample = corners

    @property
    def warp_matrix(self):
        """
        Computes the warp matrix.

        :return: The result of the computation of the perspective transform using the src corners \
        and dst corners.
        :rtype: numpy.array

        """
        # OpenCV auxiliary function to compute the warp matrix.
        self._warp_matrix, mask = cv2.findHomography(self.src_corners, self.dst_corners)
        return self._warp_matrix

    @property
    def warp_matrix_sample(self):
        """
        Computes the sample warp matrix.

        :return: The result of the computation of the perspective transform using the src corners \
        and dst corners.
        :rtype: numpy.array

        """
        self._warp_matrix_sample, mask = cv2.findHomography(self.src_sample, self.dst_sample)
        return self._warp_matrix_sample

    @property
    def warp_matrix_alignments(self):
        """
        Computes the alignment warp matrix.

        :return: The result of the computation of the perspective transform using the src corners \
        and dst corners.
        :rtype: numpy.array

        """
        self._warp_matrix_alignments, mask = cv2.findHomography(self.src_corners, self.dst_corners)
        return self._warp_matrix_alignments

    @property
    def code_modules_len(self):
        """
        Returns the code of the modules length.

        :return: The code of the module length.
        :rtype: int

        """
        return 21 + (self.version - 1) * 4

    @property
    def alignment_table_centers_sampled(self):
        """
        Returns the sampled alignment table centers.

        :return: The alignment table centers.
        :rtype: numpy.array

        """
        if self.version:
            self._alignment_table_centers_sampled = np.array(get_alignment_table_centers(self.version),
                                                             dtype=np.float64)
            return self._alignment_table_centers_sampled

    @property
    def alignment_table_centers(self):
        """
        Returns the alignment table centers.

        :return: The alignment table centers.
        :rtype: numpy.array

        """
        self._alignment_table_centers = self.alignment_table_centers_sampled.copy()
        self._alignment_table_centers += 0.5
        self._alignment_table_centers *= self.bit_size
        self._alignment_table_centers += self.code_border + self.code_offset

        return self._alignment_table_centers

    @property
    def version(self):
        """
        Returns the version of the QR.

        :return: The version of the QR.
        :rtype: int

        """
        return 1 + int(round(((self.total_mean_size / self.bit_size) - 21) / 4))

    @property
    def bit_size(self):
        """
        Returns the bit size.

        :setter: Sets the bit size.
        :getter: Returns the bit size.
        :type: int or float
        :return: The bit size.
        :rtype: float

        """
        origin = self.position_boxes[0][0][0]

        up_right_pos = self.position_boxes[0][1][0] - origin
        down_left_pos = self.position_boxes[0][3][0] - origin

        return (up_right_pos[0] + down_left_pos[1]) / 14

    @bit_size.setter
    def bit_size(self, bit_size):
        assert isinstance(bit_size, (int, float))
        self._bit_size = bit_size

    def geometric_bit_size(self):
        """
        Returns the geometric bit size.

        :return: The tuple representing the geometric bit size.
        :rtype: float, int, int

        """

        origin = self.position_boxes[0][0][0]
        up_right_qr = self.position_boxes[1][1][0] - origin
        down_left_qr = self.position_boxes[2][3][0] - origin

        self.total_mean_size = (up_right_qr[0] + down_left_qr[1]) / 2

        # First guess:
        print("------ Version1 -------")
        print("The guess version is: {}".format(self.version))
        print("total_mean_size/bit_size = {}".format(self.total_mean_size / self.bit_size))
        print("tabulated size: {}".format(21 + (self.version - 1) * 4))
        print("-----------------------")

        return self.total_mean_size, self.bit_size, self.version

    def timing_bit_size(self):
        """from scipy.signal import gaussian
        Returns the timing bit size.

        :return: The tuple representing the timing bit size.
        :rtype: float, int, int

        """
        # A second version is guessed from timing patterns:

        p1 = self.position_boxes[0][2][0] - self.bit_size / 2
        p1 = p1.astype(np.int64)
        p2 = self.position_boxes[1][3][0] + self.bit_size * np.array([+1, -1]) / 2
        p2 = p2.astype(np.int64)
        points, line = geometry.create_line_iterator(p1, p2, self.image)

        fitted_bit_size = []
        for c, channel in enumerate(line.T):
            # TODO filter channel with simple BP around 1/bit_size
            maxi = max(channel)
            x = np.arange(len(channel))
            p0 = [self.bit_size * 2, maxi / 2, maxi / 2, -np.pi / 2]
            popt0, pcov0 = curve_fit(adj_sin, x, channel, p0=p0)
            fitted_bit_size.append(popt0[0] / 2)

        # Second guess:
        self.bit_size = np.mean(np.array(fitted_bit_size))
        self.total_mean_size -= self.bit_size

        print("------ Version2 -------")
        print("The timming version is: {}".format(self.version))
        print("total_mean_size/bit_size = {}".format(self.total_mean_size / self.bit_size))
        print("tabulated size: {}".format(21 + (self.version - 1) * 4))
        print("-----------------------")

        return self.total_mean_size, self.bit_size, self.version

    def recursive_line_attachment(self, image, point, degrees, range, length):
        print("recursive line attachment with parameters " + str(point) + " " + str(degrees) + " " + str(range))
        if range < 12:
            # the range is less than 12º, so the area will be scanned 0.5 by 0.5 degrees
            target_point = rotate_point(point, degrees, length)
            points, line = create_line_iterator(point, target_point, image)
            percentage = self.black_pixels_line(line)/len(line)
            min = [points[-1], percentage]
            i = 0
            while i < 12:
                target_point = rotate_point(point, degrees + i, length)
                points, line = create_line_iterator(point, target_point, image)
                percentage = self.black_pixels_line(line)/len(line)
                if percentage < min[1]:  # the most fitted point will be the one whose line has more white pixels
                    min = [points[-1][:], percentage]
                i += 0.5
            return min[0]
        else:
            # the range is greater than 12º, so the area will be divided in three sections
            new_range = range/3  # the new range of each section
            first_probe_degree = new_range*2/10
            second_probe_degree = new_range*5/10
            third_probe_degree = new_range*8/10

            # first section probing
            first_target_point = rotate_point(point, degrees + first_probe_degree, length)
            second_target_point = rotate_point(point, degrees + second_probe_degree, length)
            third_target_point = rotate_point(point, degrees + third_probe_degree, length)
            print("points for first section: " + str(first_target_point) + " " + str(second_target_point) + " " +
            str(third_target_point))
            _, line = create_line_iterator(point, first_target_point, image)
            _, line2 = create_line_iterator(point, second_target_point, image)
            _, line3 = create_line_iterator(point, third_target_point, image)
            first_section_percentage = (self.black_pixels_line(line) + self.black_pixels_line(line2) +
                                        self.black_pixels_line(line3)) / (len(line) + len(line2) + len(line3))

            # second section probing
            first_target_point = rotate_point(point, degrees + new_range + first_probe_degree, length)
            second_target_point = rotate_point(point, degrees + new_range + second_probe_degree, length)
            third_target_point = rotate_point(point, degrees + new_range + third_probe_degree, length)
            print("points for second section: " + str(first_target_point) + " " + str(second_target_point) + " " +
                  str(third_target_point))
            _, line = create_line_iterator(point, first_target_point, image)
            _, line2 = create_line_iterator(point, second_target_point, image)
            _, line3 = create_line_iterator(point, third_target_point, image)
            second_section_percentage = (self.black_pixels_line(line) + self.black_pixels_line(line2) +
                                         self.black_pixels_line(line3)) / (len(line) + len(line2) + len(line3))

            # third section probing
            first_target_point = rotate_point(point, degrees + 2*new_range + first_probe_degree, length)
            second_target_point = rotate_point(point, degrees + 2*new_range + second_probe_degree, length)
            third_target_point = rotate_point(point, degrees + 2*new_range + third_probe_degree, length)
            print("points for third section: " + str(first_target_point) + " " + str(second_target_point) + " " +
                  str(third_target_point))
            _, line = create_line_iterator(point, first_target_point, image)
            _, line2 = create_line_iterator(point, second_target_point, image)
            _, line3 = create_line_iterator(point, third_target_point, image)
            third_section_percentage = (self.black_pixels_line(line) + self.black_pixels_line(line2) +
                                        self.black_pixels_line(line3)) / (len(line) + len(line2) + len(line3))

            if first_section_percentage < second_section_percentage and first_section_percentage <\
                    third_section_percentage:
                # the first section is the most fitted one
                return self.recursive_line_attachment(image, point, degrees, new_range, length)
            elif second_section_percentage < third_section_percentage:
                # the second section is the most fitted one
                return self.recursive_line_attachment(image, point, degrees+new_range, new_range, length)
            else:
                # the third section is the most fitted one
                return self.recursive_line_attachment(image, point, degrees+2*new_range, new_range, length)

    def correct_fourth_corner(self, **kwargs):
        """
        Finds the four corners of a QR. The QR must be oriented in a North orientation.

        :return: The coordinates of the four corners with the following order: North-West, North-East, South-West,
        South-East.
        :rtype: list

        """
        # 0.Grayscaling with histogram

        pixels, colors = np.histogram(self.image, bins=255)
        colors = np.delete(colors, 127)  # the colors array has 1 more column that the pixels one
        prod = np.multiply(pixels, colors)  # product element-wise
        thresholded = self.image_processor.threshold(self.threshold).copy()
        # TODO fix outer noise from thresholded image.
        # thresholded_fix = thresholded.copy()
        # thresholded_fix[310:349, 0:40] = 255

        # 1.Calculate the position of the gravity center

        image_h = self.image.shape[0]
        image_w = self.image.shape[1]
        image_center = np.array([image_h/2, image_w/2], int)

        # 2.Scan a line from outer to inner in 3 direction to search the 3 corners:
        found_points = [geometry.corner_scan(thresholded, corner=c) for c in (0, 1, 3)]

        # import matplotlib.pyplot as plt
        # plt.figure()
        # plt.imshow(thresholded, interpolation="none")

        # 3.Correction of the three corners associated to the Position Patterns
        for i, last_point in enumerate(found_points):
            new_point = geometry.adjust_corners(thresholded, last_point, image_center, 2)

            ii = 50
            while not(last_point[0] == new_point[0] and last_point[1] == new_point[1]):
                last_point = new_point[:]  # deep-copy to ensure that we are not comparing the same mem references
                new_point = geometry.adjust_corners(thresholded, last_point, image_center, 2)
                ii = ii + 5

            found_points[i] = new_point

        # 4. Line attachment method to find the fourth point

        image_canny = self.image_processor.thresholds.get("canny").copy()

        # First retrieve the "y" attached line:
        line_y_ini = found_points[1]
        line_y_fin, (density_y, angles_y) = geometry.line_attachment(image_canny, line_y_ini)

        # Then, retrive the "x" attached line:
        line_x_ini = found_points[2]
        line_x_fin, (density_x, angles_x) = geometry.line_attachment(image_canny, line_x_ini, axis="x")

        found_points.append(geometry.get_intersection_point(line_x_ini, line_x_fin, line_y_ini, line_y_fin))

        self.src_corners = found_points.copy()
        self.dst_corners = [self.code_border,
                            [self.code_size[0], self.code_border[1]],
                            [self.code_border[0], self.code_size[1]],
                            self.code_size]

        self.image = cv2.warpPerspective(self.image.copy(),
                                         self.warp_matrix,
                                         self.size_tuple,
                                         borderMode=cv2.BORDER_CONSTANT,
                                         borderValue=(255, 255, 255)
                                         )

        m = self.code_modules_len
        self.dst_sample = np.array([[-0.5, -0.5],
                                    [m-1+0.5, -0.5],
                                    [-0.5, m-1+0.5],
                                    [m-1+0.5, m-1+0.5]], dtype=np.float32)

        self.src_sample = self.dst_corners.copy()

        return self.image

    def correct_alignment(self, **kwargs):
        """
        Corrects the image using the alingment patterns.

        :return: The image that has been corrected.
        :rtype: numpy.ndarray

        """

        # When the version is guessed we can extract information related to alignment patterns

        self.alignment_classifier = KMeans(n_clusters=len(self.alignment_table_centers), random_state=0)
        self.alignment_classifier = self.alignment_classifier.fit(self.alignment_table_centers)

        xx, yy = np.meshgrid(np.arange(self.image.shape[0]), np.arange(self.image.shape[0]))
        Z = self.alignment_classifier.predict(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)

        order_map = self.alignment_classifier.predict(self.alignment_table_centers)

        # self.mask_list = [Z == ii for ii in range(len(self.alignment_table_centers))]
        # self.mask_list = np.array(self.mask_list)[order_map]

        # self.mask_images = []
        # plt.figure()
        # sub_plot_size = int(np.sqrt(len(self.alignment_table_centers)))
        # for ii, mask in enumerate(self.mask_list):
        #     plt.subplot(sub_plot_size, sub_plot_size, ii+1)
        #     img = self.image.copy()
        #     img[~mask] = 0
        #     plt.imshow(img, interpolation="none")
        #     ImageProcessor(img)
        # plt.show()

        canny_patterns = self.image_processor.patterns.get("canny")
        assert isinstance(canny_patterns, PatternCollection)

        gaussian_patterns = self.image_processor.patterns.get("gaussian")
        assert isinstance(gaussian_patterns, PatternCollection)

        alignment_candidates = canny_patterns.find("alignment_canny")
        alignment_candidates += gaussian_patterns.find("alignment")
        alignment_candidates += gaussian_patterns.find("dot")

        alignment_candidates_centers = np.array(alignment_candidates.centers)

        candidates_prediction = self.alignment_classifier.predict(alignment_candidates_centers)

        candidates_cluster_map = [np.where(k == candidates_prediction)[0] for k in
                                  range(len(self.alignment_classifier.cluster_centers_))]

        candidates_indices = np.array(candidates_cluster_map)[order_map]

        candidates_classified_centers = [alignment_candidates_centers[indices] for indices in candidates_indices]

        dst_points = []
        src_points = []

        len_tab = len(self.alignment_table_centers)
        len_sqrt = int(np.sqrt(len(self.alignment_table_centers)))

        print("Alignment classification")
        print("Tabulated points : found candidates")
        for ii, (destiny, sources) in enumerate(zip(self.alignment_table_centers, candidates_classified_centers)):
            print(destiny, ": ", sources.tolist())
            if len(sources):
                if ii != 0 and ii != len_sqrt - 1 and ii != (len_tab - len_sqrt):
                    dst_points.append(destiny)
                    differences = sources - destiny
                    distances = np.dot(differences ** 2, [1, 1])
                    src_points.append(sources[np.argmin(distances)])

        if len(dst_points) == 4:
            # Check collinearity of each pack of 3 points.
            point_comb = itertools.combinations(dst_points, 3)

            collinear_flags = [geometry.collinear(*points) for points in point_comb]
        else:
            collinear_flags = []

        if True in collinear_flags or len(dst_points) < 4:
            dst_points += [self.position_boxes[0][0][0],
                           self.position_boxes[1][1][0],
                           self.position_boxes[2][3][0]
                           ]

            src_points += [self.position_boxes[0][0][0],
                           self.position_boxes[1][1][0],
                           self.position_boxes[2][3][0]
                           ]

        print("Final pairing:")
        for d, s in zip(dst_points, src_points):
            print(d, ":", s)

        self.src_corners = src_points.copy()
        self.dst_corners = dst_points.copy()

        # First warp to correct the input image with findHomography approximation:
        self.image = cv2.warpPerspective(self.image.copy(),
                                         self.warp_matrix,
                                         self.size_tuple,
                                         borderMode=cv2.BORDER_CONSTANT,
                                         borderValue=(255, 255, 255)
                                         )

        self.src_sample = self.alignment_table_centers
        self.dst_sample = self.alignment_table_centers_sampled

        return self.image

    def sample(self,
               correct_alignment=False,
               correct_fourth_corner=False,
               threshold=None):

        """
        Samples the image loaded in the QRProcessor.

        :param threshold:
        :param correct_fourth_corner:
        :param correct_alignment: Indicates if the image has to be corrected before it is sampled.
        :type correct_alignment: bool
        :param kwargs: Extra arguments to be used in the image correction function.
        :type kwargs: dict
        :return: The sampled image.
        :rtype: numpy.ndarray

        """
        # First correct, if necessary, the fourth corner:

        if correct_fourth_corner:
            self.correct_fourth_corner()

        # Second correct, if necessary using aligment patterns:
        if correct_alignment:
            if self.version > 1:
                print("Using alignment based correction method.")
                self.correct_alignment()
            else:
                print("Avoid alignment correction, due to 1 version QR Code.")

        if self.src_sample is not None and self.dst_sample is not None:

            threshold = self.threshold if threshold is None else threshold
            gray_matrix = cv2.warpPerspective(self.image_processor.thresholds.get(threshold),
                                              self.warp_matrix_sample,
                                              (self.code_modules_len, self.code_modules_len),
                                              borderMode=cv2.BORDER_CONSTANT,
                                              borderValue=(255, 255, 255)
                                              )

            self._binary_matrix = gray_matrix

            self._color_matrix = cv2.warpPerspective(self.image,
                                                     self.warp_matrix_sample,
                                                     (self.code_modules_len, self.code_modules_len),
                                                     borderMode=cv2.BORDER_CONSTANT,
                                                     borderValue=(255, 255, 255)
                                                     )
            return self._color_matrix

    @property
    def colors(self):
        """
        Returns the image with the perspective corrected and indicated in the image.

        :return: The image with the perspective indicated.
        :rtype: numpy.ndarray

        """
        if self._color_matrix is None:
            self.sample()
        return self._color_matrix

    def colors_extraction(self, qr):
        """
        Extracts the colors found in the currently processing Qr.
        :param qr: The known in image QR containig the real colors and data
        :return: The found colors in the processor's qr sorted
        """
        color__r_g_b_list = []
        original_saved = []  # TODO: Improve this with a bool list
        color_coi = []
        assert len(self.colors) == len(qr.colored_modules)  # both qr's have to have same dim
        for row in range(qr.modules_count):
            for col in range(qr.modules_count):
                if np.any(qr.colored_modules[row][col] != np.array([0, 0, 0])) and \
                        np.any(qr.colored_modules[row][col] != np.array([255, 255, 255])):  # not black nor white

                    if not list(qr.colored_modules[row][col]) in original_saved:  # not saved before
                            color__r_g_b_list.append(list(self.colors[row][col]))
                            original_saved.append(list(qr.colored_modules[row][col]))

        # adding white and black
        color__r_g_b_list.append(qr.colored_modules[0][0])
        color__r_g_b_list.append(qr.colored_modules[0][7])
        original_saved.append(([0, 0, 0]))
        original_saved.append([255, 255, 255])
        src_rgb = np.array(color__r_g_b_list)
        dst_rgb = np.array(original_saved)

        return src_rgb, dst_rgb

    @property
    def matrix(self):
        """
        Returns the binary matrix.

        :return: The binary matrix.
        :rtype: numpy.ndarray

        """
        if self._binary_matrix is None:
            self._binary_matrix = self.sample()
        return self._binary_matrix

    def decode(self, **kwargs):
        """
        Decodes the QR code contained in the image.

        :params kwargs: The arguments contained on kwargs are passed to the QRMatrix class initializator \
            :func:`qrdecode.main.QRMatrix.__init__`
        :type kwargs: dict
        :return: The decoded data of the QR.
        :rtype: string

        """
        try:
            try:
                self.decoder = QRMatrix(self.matrix, **kwargs)
                data = self.decoder.decode()
            except reedsolo.ReedSolomonError:
                self.decoder = QRMatrix(np.swapaxes(self.matrix, 0, 1), **kwargs)
                data = self.decoder.decode()

                self._color_matrix = np.swapaxes(self._color_matrix, 0, 1)

            return data

        except Exception as e:
            print(e)


class Scanner(object):

    def __init__(self, file=None, **kwargs):
        """
        Initializer function of the :class:`qrdecode.main.Scanner` class.

        :param file: The file to be used by the image processor.
        :type file: ImageProcessor. or string.
        :param auto_update_th: The auto update threshold.
        :type auto_update_th: bool
        :param output_border: The output border.
        :type output_border: int
        :param output_size: The output size.
        :type output_size: int
        :param output_offset: The output offset.
        :type output_offset: (int, int)
        :param kwargs: The not used kwargs will get passed to :func:`get_contours`
        :type kwargs: dict
        :returns: Void function.
        :rtype: None

        """

        auto_update_th = kwargs.pop("auto_update_th", True)
        if isinstance(file, ImageProcessor):
            self.image_processor = file
        else:
            self.image_processor = ImageProcessor(file, auto_update_th=auto_update_th)

        self._output_border = None
        self._output_size = None
        self._output_offset = None

        self.output_border = kwargs.pop("output_border", 100)
        self.output_size = kwargs.pop("output_size", 235)
        self.output_offset = kwargs.pop("output_offset", (0, 0))

        self._threshold = "simple"
        self.image_patterns = None
        self.position_patterns = None
        self.get_contours(**kwargs)
        self.position_boxes = None

        self._src_corners = None
        self._dst_corners = None
        self._warp_matrix = None

        self.codes = None
        self._processors = None

        self.codes_position = None

        self.dst = None
        self.max_height = None
        self.max_width = None
        self.sliced = None

    @property
    def threshold(self):
        """
        The threshold used by the scanner.

        :setter: Sets the threshold to be used by the scanner.
        :getter: Returns the threshold being used by the scanner.
        :type: string
        :return: The threshold being used by the scanner.
        :rtype: string

        .. note::
            The threshold to be used must have been added by the add_threshold function first.

        """
        return self._threshold

    @threshold.setter
    def threshold(self, threshold):
        if threshold in self.image_processor.THRESHOLDS:
            self._threshold = threshold
        else:
            print("Threshold name isn't present, still using '{}' threshold. Plese use, add_threshold()"
                  " method to define the threshold.".format(self.threshold))

    def add_threshold(self, name, threshold_func, *args, **kwargs):
        """
        Adds a threshold to the list of available ones.

        :param name: The name of the threshold.
        :type name: string
        :param threshold_func: The function associated to the threshold.
        :type threshold_func: string
        :param args: Extra arguments for the threshold function.
        :type args: tuple
        :param kwargs: Extra arguments for the threshold function.
        :type kwargs: dict
        :return: Void function.
        :rtype: None

        .. note::
            The function associated to the threshold (threshold_func parameter) must be written without the "
            symbol, which will cause that Python passes the function in the paramter.

            :Example:

            >>> add_threshold("simple", simple_threshold)

        """
        self.image_processor.add_threshold(name, threshold_func, *args, **kwargs)

    @property
    def image(self):
        """
        A sugar-syntax access for :attr:`Scanner.image_processor.image`.

        :getter: Returns the image being scanned.
        :setter: Sets the image to be scanned.
        :type: array

        """
        return self.image_processor.image

    def add_image(self, file):
        """
        Adds an image to the scanner.

        :param file: The image to be used.
        :type file: string
        :return: Void function.
        :rtype: None

        """
        self.image_processor.image = file

    def get_contours(self, **kwargs):
        """
        Returns the contours of the QR code contained in the image.

        :param threshold: The threshold to be used.
        :type threshold: string
        :param kwargs: The not used kwargs will be passed to :func:`threshold`.
        :type kwargs: dict
        :return: The pattern of the contour.
        :rtype: :class:`qrdecode.utils.patterns.PatternCollection`

        """
        self.threshold = kwargs.pop('threshold', self.threshold)
        self.image_processor.threshold(self.threshold, **kwargs)
        self.image_patterns = self.image_processor.patterns.get(self.threshold, PatternCollection())
        self.position_patterns = self.image_patterns.find("position")
        return self.image_processor.pattern(self.threshold)

    @property
    def output_border(self):
        """
        The output border.

        :setter: Sets the output border.
        :getter: Returns the output border.
        :type: int or (numpy.ndarray, list, tuple).
        :return: The output border.
        :rtype: numpy.ndarray

        """
        return self._output_border

    @output_border.setter
    def output_border(self, border):
        if isinstance(border, int):
            self._output_border = np.array([border, border], dtype=int)
        elif isinstance(border, (np.ndarray, list, tuple)):
            border = np.array(border)
            assert border.shape == (2,)
            self._output_border = border
        else:
            print("Value error. Using the saved one: {}".format(self.output_border))

    @property
    def output_size(self):
        """
        The output size.

        :setter: Sets the output size.
        :getter: Returns the output size.
        :type: int or (numpy.ndarray, list, tuple).
        :return: The output size.
        :rtype: numpy.ndarray

        """
        return self._output_size

    @output_size.setter
    def output_size(self, size):
        if isinstance(size, int):
            self._output_size = np.array([size, size], dtype=int)
        elif isinstance(size, (np.ndarray, list, tuple)):
            size = np.array(size)
            assert size.shape == (2,)
            self._output_size = size
        else:
            print("Value error. Using the saved one: {}".format(self.output_border))

    @property
    def output_offset(self):
        """
        The output offset.

        :setter: Sets the output offset.
        :getter: Returns the output offset.
        :type: int or (numpy.ndarray, list, tuple).
        :return: The output size.
        :rtype: numpy.ndarray

        """
        return self._output_offset

    @output_offset.setter
    def output_offset(self, offset):
        if isinstance(offset, int):
            self._output_offset = np.array([offset, offset], dtype=int)
        elif isinstance(offset, (np.ndarray, list, tuple)):
            offset = np.array(offset)
            assert offset.shape == (2,)
            self._output_offset = offset
        else:
            print("Value error. Using the saved one: {}".format(self.output_border))

    @property
    def size_tuple(self):
        """
        Returns the size tuple.

        :return: The size tuple
        :rtype: int, int

        """
        size_arr = self.output_border + self.output_size
        return size_arr[0], size_arr[1]

    @property
    def dst_corners(self):
        """
        The destination corners.

        :setter: Sets the destination corners.
        :getter: Returns the destination corners.
        :type: tuple
        :rtype: numpy.array
        :raises: AssertionError

        .. note::
            The shape of the numpy.array that is created using the tuple passed as an argument
            must be (4,2).

        """
        # Destiny array
        self._dst_corners = np.array([
            self.output_border,
            [self.output_size[0], self.output_border[1]],
            [self.output_border[0], self.output_size[1]],
            self.output_size
        ], dtype=np.float32)
        self._dst_corners += self.output_offset
        return self._dst_corners

    @dst_corners.setter
    def dst_corners(self, corners):
        corners = np.array(corners, dtype=np.float32)
        assert corners.shape == (4, 2)
        self._dst_corners = corners

    @property
    def src_corners(self):
        """
        The source corners.

        :setter: Sets the source corners.
        :getter: Returns the source corners.
        :type: tuple
        :rtype: numpy.array
        :raises: AssertionError

        .. note::
            The shape of the numpy.array that is created using the tuple passed as an argument
            must be (4,2).

        """
        return self._src_corners

    @src_corners.setter
    def src_corners(self, corners):
        corners = np.array(corners, dtype=np.float32)
        assert corners.shape == (4, 2)
        self._src_corners = corners

    @property
    def warp_matrix(self):
        """
        Computes the warp matrix.
        :return: The result of the computation of the perspective transform using the source corners \
        and destination corners.
        :rtype: numpy.ndarray

        """
        # OpenCV auxiliary function to compute the warp matrix.
        self._warp_matrix = cv2.getPerspectiveTransform(self.src_corners, self.dst_corners)
        return self._warp_matrix

    def scan(self):
        """
        Scans the image to search 2D barcodes, and returns a list of codes found.

        :return: A list containing the scanned QR codes trimmed as a :class:`ImageProcessor` object.
        :rtype: list

        """
        # Reinitialising for rescanning
        self.codes = []
        self.codes_position = []

        qr_num = 1  # Base case is only one Qr found
        if self.position_patterns.count > 3:
            if self.position_patterns.count % 3 != 0:
                print("One QR has not been well detected.")  # TODO: Exceptions
            print("More than 1 QR!")
            print(self.position_patterns.count)
            # Qr numbers found, each Qr has 3 position patterns
            qr_num = int(self.position_patterns.count / 3)

            # Let's cluster the positions patterns with k-means
            centers = self.position_patterns.centers.copy()

            # convert to np.float32
            centers = np.float32(centers)

            # k-means parameters
            criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)

            # Clustering with k-means
            ret, label, center = cv2.kmeans(centers, qr_num, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)

            all_candidates = self.position_patterns.contours.copy()  # All contours
            all_candidates = np.asarray(all_candidates)  # Contours as a numpy array
        print("qrnum" + str(qr_num))

        # Clustering QrProcessor for each clustered Qr
        for i in range(qr_num):

            if qr_num > 1:  # Should have been clustered
                # The contours of this Qr
                orig = all_candidates[label.ravel() == i].tolist()

                self.position_patterns.contours = [np.asarray(orig[0]), np.asarray(orig[1]), np.asarray(orig[2])]
            else:
                cv2.imshow("1qr?", self.image)

            self.position_patterns = self.position_patterns.orientate()  # Position the bottom left corner the first

            # Only the 3 centers and contours
            candidates_mc = self.position_patterns.centers.copy()
            candidates = self.position_patterns.contours.copy()
            candidates_mc.reverse()

            # We know each centers corresponds to each contours, due to the centers have been calc by contours
            # moments.

            # Now we have to ensure where are located the second (candidates[1]) and the third (candidates[2])
            # Two auxiliary functions are called.
            dist = line_equation(*candidates_mc)
            slope, align = line_slope(*candidates_mc[0:2])

            candidates_mc.reverse()

            # 'align' parameter is returned just in case the slope is strictly 0. Not handled for the moment.

            if align:
                # With the sign of 'slope' and 'dist' parameters we are able to know the orientation of the
                # candidates. We assume that candidates[1] is the M point and candidates[2] is the O.
                if (slope < 0) and (dist < 0):  # Orientation - North
                    orientation = CV_QR_NORTH
                    cond = (candidates_mc[1][0] > candidates_mc[2][0]) and (
                            candidates_mc[1][1] < candidates_mc[2][1])
                elif (slope > 0) and (dist < 0):  # Orientation - East
                    cond = (candidates_mc[1][0] > candidates_mc[2][0]) and (
                            candidates_mc[1][1] > candidates_mc[2][1])
                    orientation = CV_QR_EAST
                elif (slope < 0) and (dist > 0):  # Orientation - South
                    cond = (candidates_mc[1][0] < candidates_mc[2][0]) and (
                            candidates_mc[1][1] > candidates_mc[2][1])
                    orientation = CV_QR_SOUTH
                elif (slope > 0) and (dist > 0):  # Orientation - West
                    orientation = CV_QR_WEST
                    cond = (candidates_mc[1][0] < candidates_mc[2][0]) and (
                            candidates_mc[1][1] < candidates_mc[2][1])
            else:
                print('Align is 0')
                cond = False
                orientation = CV_QR_NORTH

            # Then if the original assumption is not valid, here comes the correction:
            if not cond:
                candidates[1], candidates[2] = candidates[2], candidates[1]
                candidates_mc[1], candidates_mc[2] = candidates_mc[2], candidates_mc[1]

            candidates_pc = PatternCollection(candidates)
            qr_patterns = candidates_pc.approximate().contours
            self.position_boxes = candidates_pc.approximate().contours

            # Once we have defined the corners of every position pattern, we must assign the corners of the QR.
            # There exist a dependence with orientation. As it is represented by a number from 0 to 3,
            # we can make a trick.
            qr_corners = np.concatenate((qr_patterns[0][orientation % 4],
                                         qr_patterns[1][(orientation + 1) % 4],
                                         qr_patterns[2][(orientation + 3) % 4]
                                         ))

            # Now we have ordered 3 of the 4 corners of the QR code. The 4th must be deduced by crossing two
            # straight lines
            lines_points = np.concatenate((qr_patterns[1][(orientation + 1) % 4],
                                           qr_patterns[1][(((orientation + 1) % 4) + 1) % 4],
                                           qr_patterns[2][(((orientation + 3) % 4) - 1) % 4],
                                           qr_patterns[2][(orientation + 3) % 4]
                                           ))

            last_point = get_intersection_point(*lines_points)

            # Finally we get the 4 set of origin points for making the warping
            self.src_corners = np.concatenate((qr_corners, [last_point]))
            self.codes_position.append(self.src_corners)

            # OpenCV solution for performing the transform.
            image_trimmed = cv2.warpPerspective(
                self.image_processor.image,
                self.warp_matrix,
                self.size_tuple,
                borderMode=cv2.BORDER_CONSTANT,
                borderValue=(255, 255, 255)
            )
            qr = ImageProcessor(image_trimmed)

            self.codes.append(qr)

        return self.codes

    @property
    def processors(self):
        """
        Property containing a list of processors objects for each found code by :func:`Scanner.scan` method.

        :return: A list of code processors.
        :rtype: list
        """
        if not self.codes:
            self.scan()
        self._processors = [
            QRProcessor(code,
                        code_border=self.output_border,
                        code_offset=self.output_offset,
                        code_size=self.output_size)
            for code in self.codes
        ]

        return self._processors

    def decode(self, **kwargs):
        """
        Decodes the QR codes contained in the image. Scans the image in search of QR codes in case it has not been \
        done previously.
        If there are not any QR codes in the image it will print the message "Can't find any code...".

        :return: The array containing the contents of each QR found in the image. The results of each \
        decoding are represented in a string.
        :rtype: array

        """
        if not self.codes:
            self.scan()
        if self.codes:
            return [code.decode(**kwargs) for code in self.codes]
        else:
            print("Can't find any code...")
