import os
from functools import wraps
from PIL import Image
import numpy as np
from scipy.misc import imsave
from matplotlib.colors import ColorConverter
from matplotlib import pyplot as plt
import qrcode.image.base
import cv2
from qrdecode.utils.patterns import PatternCollection


def ndarray_check(matrix, dimension=3, channels=3):
    """
    Checks if the matrix parameter is a numpy.ndarray and if its shape is correct.

    :param matrix: The parameter that will be checked.
    :type matrix: numpy.ndarray
    :param dimension: The dimension of the ``matrix`` parameter.
    :type dimension: int
    :param channels: The number of channels of the ``matrix`` parameter.
    :type channels: int
    :return: True if the ``matrix`` parameter is of type numpy.ndarray. False otherwise.
    :rtype: bool
    :raises: AssertionError

    .. note::
        ``Matrix`` has to be a "channels" and "dimension" shape array. It will raise an AssertionError \
        if it is not.

    """
    if isinstance(matrix, np.ndarray):
        shape = matrix.shape
        assert shape[dimension-1] == channels, \
            "ndarray is not a {} channel(s) and {} dimension(s) array , with shape: {}.".format(
            channels, dimension, shape
        )
        return True
    else:
        return False


def image_array(image_function):
    """
    This decorator allows abstraction from image origin, that could be a numpy-like object, a PIL-like object,
    a path-like object, etc.

    :param image_function: The function that will be used to process the image.
    :type image_function: function
    :return: The decorated function.
    :rtype: function

    """
    @wraps(image_function)
    def imaging_wrapper(*args, **kwargs):
        """
        The wrapper function.

        :param args: The arguments that will be passed to ``image_function``.
        :type args: tuple
        :param kwargs: The arguments that will be passed to ``image_function``.
        :type kwargs: dict
        :return:
        """

        # Check if args[0] or args[1] is image or readable file (due to 'self' for methods)
        arg0_flag = isinstance(args[0], (Image.Image, np.ndarray, str, bytes, int))
        if len(args) > 1:
            arg1_flag = isinstance(args[1], (Image.Image, np.ndarray, str, bytes, int))
            assert arg0_flag or arg1_flag, "A suitable object must be placed in first or second args element."
            img = args[0] if arg0_flag else args[1]
        else:
            img = args[0]

        if isinstance(img, Image.Image):
            img = img.convert("RGB")
            img = np.array(img)
            img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
        if not ndarray_check(img):
            assert os.path.isfile(img), "Input image is not rather a numpy-like object nor a valid path."
            img = cv2.imread(img)
        if len(args) > 1:
            args = (img, ) + args[1:] if arg0_flag else (args[0], img) + args[2:]
        else:
            args = (img, )
        return image_function(*args, **kwargs)

    return imaging_wrapper


def threshold(threshold_func):
    """
    A python decorator to abstract the implementation of image imports for threshold computing

    :param threshold_func: The function that will compute the threshold.
    :type threshold_func: function
    :return: The decorated function.
    :rtype: function

    """

    @image_array
    @wraps(threshold_func)
    def threshold_func_wrapper(img, *args, **kwargs):
        img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        return threshold_func(img_gray, *args, **kwargs)

    return threshold_func_wrapper


@threshold
def simple_threshold(img, thres=127, max_value=255, threshold_type=cv2.THRESH_BINARY):
    """
    Applies the threshold ``threshold_type``` to the image ``img``.

    :param img: The image to which apply the threshold indicated by ``threshold_type``.
    :type img: numpy.ndarray
    :param thres: The threshold value.
    :type thres: int
    :param max_value: The maximum threshold value.
    :type max_value: int
    :param threshold_type: The type of the threshold to be applied to the image.
    :type threshold_type: int
    :return: The thresholded image.
    :rtype: numpy.ndarray

    .. note::
        The list of the available thresholds types can be found in the OpenCV wiki \
        (https://docs.opencv.org/trunk/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576)

    """
    _, img_th = cv2.threshold(img, thres, max_value, threshold_type)
    return img_th


@threshold
def adaptive_threshold(img,
                       max_value=255,
                       adaptive_method=cv2.ADAPTIVE_THRESH_MEAN_C,
                       threshold_type=cv2.THRESH_BINARY,
                       block_size=45,
                       c=10):
    """
    Applies an adaptive threshold to the image ``img``. :ocv:pyfunc:`cv2.adaptiveThreshold`.

    :param img: The image to which apply the adaptive threshold.
    :type img: numpy.ndarray
    :param max_value: The maximum threshold value.
    :type max_value: int
    :param adaptive_method: The adaptive method.
    :type adaptive_method: int
    :param threshold_type: The threshold type.
    :type threshold_type: int
    :param block_size: The size of a pixel neighborhood that is used to calculate a threshold value for the pixel.
    :type block_size: int
    :param c: The constant substracted from the mean or weighed mean.
    :type c: int
    :return: The thresholded image.
    :rtype: numpy.ndarray

    .. note::
        The list of the available adaptive methods can be found in the OpenCV wiki
        (https://docs.opencv.org/trunk/d7/d1b/group__imgproc__misc.html#gaa42a3e6ef26247da787bf34030ed772c). \n
        The list of the available threshold types can be found in the OpenCV wiki
        (https://docs.opencv.org/trunk/d7/d1b/group__imgproc__misc.html#gaa9e58d2860d4afa658ef70a9b1115576).

    """
    img_th = cv2.adaptiveThreshold(img, max_value, adaptive_method, threshold_type, block_size, c)
    return img_th


@threshold
def canny_filter(img, th1=50, th2=100, aperture_size=3, accurate_gradient=False):
    """
    Applies a canny filter to the image ``img``.

    :param img: The image to which apply the canny filter.
    :type img: numpy.ndarray
    :param th1: The first threshold for the hysteresis procedure.
    :type th1: int
    :param th2: THe second threshold for the hsyteresis procedure.
    :type th2: int
    :param aperture_size: The aperture size for the Sobel operator.
    :type aperture_size: int
    :param accurate_gradient: The flag that indicates whether a more accurate L2 should be used to calculate the image \
    gradient magntude (``accurate_gradient`` =True) or whether the default is enough (``accurate_gradient`` =False)
    :type accurate_gradient: bool
    :return: The image with the canny filter applied.
    :rtype: numpy.ndarray

    """
    img_th = cv2.Canny(img, th1, th2, apertureSize=aperture_size, L2gradient=accurate_gradient)
    return img_th

def default_thresholds(cls):
    cls.add_threshold("simple", simple_threshold)
    cls.add_threshold("mean", adaptive_threshold)
    cls.add_threshold("gaussian", adaptive_threshold, adaptive_method=cv2.ADAPTIVE_THRESH_GAUSSIAN_C)
    cls.add_threshold("canny", canny_filter)
    return cls
    
@default_thresholds
class ImageProcessor(object):
    THRESHOLDS = {}

    def __init__(self, img, auto_update_th=True):
        """
        Initialization function of the :class:`qrdecode.utils.image.ImageProcessor` class.

        :param image: The image to be used by the :class:`qrdecode.utils.image.ImageProcessor` class.
        :type np_array: numpy.ndarray
        :param auto_update_th: The auto update threshold flag.
        :type auto_update_th: bool
        :return: Void function.
        :rtype: None

        """
        self._image = None
        self.image = img
        self._thresholds = {}
        self._pattern_images = {}
        self._patterns = {}
        self.__auto_update_th = auto_update_th

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

        :param name: The name of the new 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 \
            ``threshold_func``.
        :type args: tuple
        :param kwargs: Arguments of the function associated to the threshold that will be passed to \
            ``threshold_func``.
        :type kwargs: dict
        :return: Void function.
        :rtype: None

        """
        cls.THRESHOLDS[name] = (threshold_func, args, kwargs)

    @property
    def image(self):
        """
        The image being used.

        :getter: Returns the image being used.
        :setter: Sets the image being used.
        :param file: The image.
        :type file: numpy.ndarray
        :rtype: numpy.ndarray

        """
        return self._image

    @image.setter
    @image_array
    def image(self, file):
        self._image = file

    def threshold(self, name, saved_args=False, *args, **kwargs):
        """
        Returns the function associated to the threshold ``name``.

        :param name: The name of the threshold.
        :type name: string
        :param saved_args: Indicates if the threshold has arguments previously saved.
        :type saved_args: bool
        :param args: Arguments of the function associated to the threshold that will be passed to \
            ``th_function``.
        :type args: tuple
        :param kwargs: Arguments of the function associated to the threshold that will be passed to \
            ``th_function``.
        :type kwargs: dict
        :return: The function of the threshold (if it exists) or None (if it does not).
        :rtype: function or None

        """
        if name in self.THRESHOLDS:
            (th_function, th_args, th_kwargs) = self.THRESHOLDS[name]
            if saved_args:
                img_threshold = th_function(self.image, *th_args, **th_kwargs)
            else:
                img_threshold = th_function(self.image, *args, **kwargs)
            self._thresholds[name] = img_threshold
            return img_threshold
        else:
            return None

    @property
    def thresholds(self):
        """
        The dictionary of available thresholds.

        :getter: Returns the available thresholds.
        :deleter: Deletes all the thresholds available.
        :rtype: dict

        """
        for name, (th_function, th_args, th_kwargs) in self.THRESHOLDS.items():
            # if (name not in self._thresholds) or not self.__auto_update_th:
                self.threshold(name, *th_args, **th_kwargs)
        return self._thresholds

    @thresholds.deleter
    def thresholds(self):
        self._thresholds = {}

    def pattern(self, name, mode=cv2.RETR_TREE, method=cv2.CHAIN_APPROX_SIMPLE):
        """
        Finds the contours of the image with the threshold indicated by ``name`` and returns its results.

        :param name: The name of the threshold to be used.
        :type name: string
        :param mode: The contour retrieval mode.
        :type mode: int
        :param method: The contour approximation method.
        :type method: int
        :return: The patterns found in the image.
        :rtype: :class:`qrdecode.utils.patterns.PatternCollection`

        .. note:: The list of available contour retrieval modes is available in the OpenCV wiki \
            (https://docs.opencv.org/3.1.0/d3/dc0/group__imgproc__shape.html#ga819779b9857cc2f8601e6526a3a5bc71). \n
            The list of the available contour approximation methods is available in the OpenCV wiki \
            (https://docs.opencv.org/3.1.0/d3/dc0/group__imgproc__shape.html#ga4303f45752694956374734a03c54d5ff).

        """
        img_contours, contours, hierarchy = cv2.findContours(self._thresholds.get(name).copy(), mode, method)
        self._pattern_images[name] = img_contours
        self._patterns[name] = PatternCollection(contours, hierarchy=hierarchy)

        return self._patterns[name]

    @property
    def pattern_images(self):
        """
        Returns the patterns of the images.

        :return: The patterns of the images.
        :rtype: dict

        """
        if not self._pattern_images:
            self.patterns
        return self._pattern_images

    @property
    def patterns(self):
        """
        The patterns of the image processed by the different thresholds available.

        :getter: Returns the patterns of the image.
        :deleter: Erases the patterns previously computed of the image.
        :rtype: dict

        """
        thresholds = self.thresholds
        for name, img_th in thresholds.items():
            self.pattern(name)

        return self._patterns

    @patterns.deleter
    def patterns(self):
        self._pattern_images = {}
        self._patterns = {}


class numpyImage(qrcode.image.base.BaseImage):
    """
    numpy-scipy image builder.
    """

    color_converter = ColorConverter()
    kind = "PNG"

    def __init__(self, *args, **kwargs):
        """
        Initialization function of the :class:`qrdecode.utils.image.numpyImage` class.

        :param args: Arguments to be used by the :class:`qrcode.image.base.BaseImage` class.
        :type args: tuple
        :param kwargs: Arguments to be used by the :class:`qrcode.image.base.BaseImage` class.
        :return: Void function.
        :rtype: None

        """
        self.fill_color = (0, 0, 0)
        self.back_color = (1, 1, 1)
        self.alpha = False
        super(numpyImage, self).__init__(*args, **kwargs)

    def new_image(self, **kwargs):
        """
        Creates a new image with the specifications indicated by the parameters.

        :param back_color: The back color of the image.
        :type back_color: string
        :param fill_color: The fill color of the image.
        :type fill_color: string
        :return: The generated image.
        :rtype: numpy.array

        """
        back_color = kwargs.get("back_color", "white")
        fill_color = kwargs.get("fill_color", "black")

        img = np.zeros((self.pixel_size, self.pixel_size, 4), dtype=np.float32)
        # Colors must become tuples for numpy solution:
        self.fill_color = self.color_converter.to_rgb(fill_color)
        self.alpha = back_color.lower() == "transparent"
        if not self.alpha:
            self.back_color = self.color_converter.to_rgb(back_color)
            img[:, :, 3] = 1

        img[:, :, :3] = np.array(self.back_color, dtype=np.float32)

        return img

    def drawrect(self, row, col):
        """
        Draws a rectangle in the image using the ``row`` and ``col`` parameters.

        :param row: The row coordinates.
        :type row: int
        :param col: The col coordinates.
        :type col: int
        :return: Void function.
        :rtype: None

        """
        (x, y), (w, h) = self.pixel_box(row, col)
        # self._idr.rectangle(box, fill=self.fill_color)
        (r, g, b) = self.fill_color
        a = 1
        self._img[y:h+1, x:w+1] = np.array([r, g, b, a])

    def save(self, stream, format=None, **kwargs):
        """
        Saves an array as an image.

        :param stream: Output file object or file name.
        :type stream: string or file object
        :param format: Image format in which the image will be saved.
        :type format: string
        :param kwargs: Arguments to be used by the :func:`imsave` function.
        :return: Void function.
        :rtype: None

        """
        imsave(stream, self._img, format=format)

    def show(self):
        """
        Shows the image in a new window.

        :return: The shown image.
        :rtype: Figure

        """
        fig = plt.figure()
        plt.imshow(self._img, interpolation="none")
        plt.show()
        return fig

    def __getattr__(self, name):
        return getattr(self._img, name)
