import warnings
import enum
import math
from scipy import interpolate
import numpy as np
from hapycolor import config, exceptions, helpers
from . import base


class Filter(enum.Enum):
    BRIGHT = 1
    DARK = 2
    SATURATION = 3


def hyperplan_file(filter_type):
    configuration = config.ConfigurationManager.load("hyperplan")
    path = config.ROOT_DIR
    if filter_type == Filter.DARK:
        path /= configuration["dark"]
    elif filter_type == Filter.BRIGHT:
        path /= configuration["bright"]
    elif filter_type == Filter.SATURATION:
        path /= configuration["saturation"]
    else:
        msg = "Unknown filter type"
        raise exceptions.UnknownLuminosityFilterTypeError(msg)
    return path.as_posix()


class LumFilter(base.Filter):
    # Set up grid
    dark_interp = None
    dark_max = None

    bright_interp = None
    bright_min = None

    def interpolate_hyperplans(json_file, filter_type):
        """
        Generates a data structures that represent the interpolated
        luminosity hyperplan (dark or bright).

        .. todo::
            This should be replaced by a function that only load a json
            file where that contains the interpolated points instead of
            regenating them each time
        """
        # Load the provided points
        data = helpers.load_json(json_file)
        hsl_points = []
        for s in data:
            for c in data[s][filter_type]:
                hsl_points.append(helpers.rgb_to_hsl(tuple(c)))

        if filter_type == "bright":
            LumFilter.bright_min = min(hsl_points, key=lambda c: c[2])[2]
        elif filter_type == "dark":
            LumFilter.dark_max = max(hsl_points, key=lambda c: c[2])[2]

        hues_step = 22.5
        hues_divisions = 16
        # Adding outer and inner circles for better results
        for h in [hues_step * i for i in range(hues_divisions)]:
            # When the json_file was encoded, some precision has been lost for
            # the hues, this is why we have to ceil or floor the hues
            outer_lum = max(filter(lambda c: c[0] == math.ceil(h)
                                   or c[0] == math.floor(h), hsl_points),
                            key=lambda c: c[1])[2]
            inner_lum = min(filter(lambda c: c[0] == math.ceil(h)
                                   or c[0] == math.floor(h), hsl_points),
                            key=lambda c: c[1])[2]

            # Adding outer circles
            for s in [1.2, 1.5, 1.7, 2]:
                hsl_points.append((h, s, outer_lum))
            # Addding "inner" circle
            hsl_points.append((h, 0, inner_lum))

        # Convert points to catesian
        points = np.asarray([LumFilter.polar_to_cartesian(e)
                            for e in hsl_points])
        x = [e[0] for e in points]
        y = [e[1] for e in points]
        z = [e[2] for e in points]

        # scipy raises:
        # /usr/local/lib/python3.6/site-packages/scipy/interpolate/_fitpack_impl.py:975:
        # RuntimeWarning: No more knots can be added because the number of
        # B-spline coefficients already exceeds the number of data points m.
        # Probable causes: either s or m too small. (fp>s)
        # kx,ky=1,1 nx,ny=16,14 m=160 fp=0.004157 s=0.000000
        # warnings.warn(RuntimeWarning(_iermess2[ierm][0] + _mess))
        #
        # I don't really know how to avoid that, so I'll just silence it,
        # seems wise enough to me
        with warnings.catch_warnings(record=True) as w:
            return interpolate.interp2d(x, y, z, kind="linear")

    def gen_sat_interpolation(json_file):
        # Load the provided points
        data = helpers.load_json(json_file)

        hue_table = {}
        for e in data:
            if e[0] in hue_table:
                hue_table[e[0]].append(e)
            else:
                hue_table[e[0]] = [e]

        hue_interp = {}
        for h in hue_table:
            R = [e[1] for e in hue_table[h]]
            Z = [e[2] for e in hue_table[h]]
            hue_interp[h] = interpolate.interp1d(Z, R, kind="linear")
        return hue_interp

    @staticmethod
    def polar_to_cartesian(polar_point):
        """
        .. warning::
            The variable's ordering follows the 'hsl' format and not
            the classic (radius, angle, z).
        """
        theta, r, z = polar_point
        # if r < 0 or r > 1 or theta < 0 or theta >= 360 or z < 0 or z > 1:
        #     raise exceptions.NotPolarException("The input tuple does not"
        #                                        + "match polar coordinates")
        return r * np.cos(np.radians(theta)), r * np.sin(np.radians(theta)), z

    def initialization():
        """
        Initializes the interpolation functions and the max/min values of
        the dark/bright surfaces
        """
        LumFilter.dark_interp = LumFilter.interpolate_hyperplans(
                hyperplan_file(Filter.DARK), "dark")

        LumFilter.bright_interp = LumFilter.interpolate_hyperplans(
                hyperplan_file(Filter.BRIGHT), "bright")

        LumFilter.saturation_interp = LumFilter.gen_sat_interpolation(
                hyperplan_file(Filter.SATURATION))

    @staticmethod
    def apply(palette):
        LumFilter.initialization()

        # Set background
        hsl_bg = helpers.rgb_to_hsl(palette.background)
        hsl_bg = (hsl_bg[0], hsl_bg[1], hsl_bg[2]*2)
        if not LumFilter.analyze(helpers.hsl_to_rgb(hsl_bg), "darkness"):
            palette.background = (0, 0, 0)
        # if (not LumFilter.is_too_bright(palette.foreground)):
            # palette.background = (255, 255, 255)

        # Set colors
        palette.colors = list(filter(lambda c:
                              not LumFilter.analyze(c, "brightness")
                              and not LumFilter.analyze(c, "darkness")
                              and not LumFilter.analyze(c, "saturation"),
                              palette.colors))
        return palette

    def analyze(rgb_color, kind):
        """
        Analyzes a provided color according to a specific type of analysis. It
        can be: brightness, darkness or saturation.

        :arg rgb_color: The input color to be analyzed.
        :arg kind: Type of analysis available: 'brightness', 'darkness' or
            'saturation'.
        :return: This function will return 'True' if the color is too bright,
            too dark, or not enough satured, else, 'False'.
        """
        if not helpers.can_be_rgb(rgb_color):
            raise exceptions.ColorFormatError("Color must be defined in the"
                                              + " rgb base")

        hsl_color = helpers.rgb_to_hsl(rgb_color)
        x, y, z = LumFilter.polar_to_cartesian(hsl_color)

        if kind == "brightness":
            return z > LumFilter.bright_min \
                   or z > LumFilter.bright_interp(x, y)
        elif kind == "darkness":
            return z < LumFilter.dark_max or z < LumFilter.dark_interp(x, y)
        elif kind == "saturation":
            return not LumFilter.is_enough_saturated(rgb_color)
        else:
            msg = str(kind) + "is not a supported analysis type"
            raise exceptions.UnknownAnalysisTypeException(msg)

    def half_circle_interp(half_circle):
        cartesian_p = [LumFilter.polar_to_cartesian(e) for e in half_circle]
        cartesian_p.sort(key=lambda e: e[0])
        X = [p[0] for p in cartesian_p]
        Y = [p[1] for p in cartesian_p]
        return interpolate.interp1d(X, Y, kind="linear")

    def is_enough_saturated(rgb_color):
        """
        To interpolate the saturation that separates the saturated enough
        colors with those that aren't, for a specific hue and luminosity,
        some 2D points (hue, saturation), are interpolated for the same
        luminosity (the one of the provided argument), and should form a
        circle. Then, this circle is divided into an upper circle and a
        bottom circle, from which an interpolation function can be computed
        with `scipy`. With these two functions, it is possible to assert if a
        color is saturated enough.
        """
        hsl_color = helpers.rgb_to_hsl(rgb_color)
        x, y, z = LumFilter.polar_to_cartesian(hsl_color)

        circle = []
        for h in LumFilter.saturation_interp:
            circle.append((h, LumFilter.saturation_interp[h](z), 0))

        interp_up = LumFilter.half_circle_interp(circle[:len(circle)//2])
        interp_down = LumFilter.half_circle_interp(circle[len(circle)//2:])

        # If the value cannot be interpolated, it means that the point is
        # saturated enough
        try:
            above_upper_half = interp_up(x) > y
            above_bottom_half = interp_down(x) > y
        except ValueError:
            return True
        else:
            return not (above_bottom_half ^ above_upper_half)

import warnings
import enum
import math
from scipy import interpolate
import numpy as np
from hapycolor import config, exceptions, helpers
from . import base


class Filter(enum.Enum):
    BRIGHT = 1
    DARK = 2
    SATURATION = 3


def hyperplan_file(filter_type):
    configuration = config.ConfigurationManager.load("hyperplan")
    path = config.ROOT_DIR
    if filter_type == Filter.DARK:
        path /= configuration["dark"]
    elif filter_type == Filter.BRIGHT:
        path /= configuration["bright"]
    elif filter_type == Filter.SATURATION:
        path /= configuration["saturation"]
    else:
        msg = "Unknown filter type"
        raise exceptions.UnknownLuminosityFilterTypeError(msg)
    return path.as_posix()


class LumFilter(base.Filter):
    # Set up grid
    dark_interp = None
    dark_max = None

    bright_interp = None
    bright_min = None

    def interpolate_hyperplans(json_file, filter_type):
        """
        Generates a data structures that represent the interpolated
        luminosity hyperplan (dark or bright).

        .. todo::
            This should be replaced by a function that only load a json
            file where that contains the interpolated points instead of
            regenating them each time
        """
        # Load the provided points
        data = helpers.load_json(json_file)
        hsl_points = []
        for s in data:
            for c in data[s][filter_type]:
                hsl_points.append(helpers.rgb_to_hsl(tuple(c)))

        if filter_type == "bright":
            LumFilter.bright_min = min(hsl_points, key=lambda c: c[2])[2]
        elif filter_type == "dark":
            LumFilter.dark_max = max(hsl_points, key=lambda c: c[2])[2]

        hues_step = 22.5
        hues_divisions = 16
        # Adding outer and inner circles for better results
        for h in [hues_step * i for i in range(hues_divisions)]:
            # When the json_file was encoded, some precision has been lost for
            # the hues, this is why we have to ceil or floor the hues
            outer_lum = max(filter(lambda c: c[0] == math.ceil(h)
                                   or c[0] == math.floor(h), hsl_points),
                            key=lambda c: c[1])[2]
            inner_lum = min(filter(lambda c: c[0] == math.ceil(h)
                                   or c[0] == math.floor(h), hsl_points),
                            key=lambda c: c[1])[2]

            # Adding outer circles
            for s in [1.2, 1.5, 1.7, 2]:
                hsl_points.append((h, s, outer_lum))
            # Addding "inner" circle
            hsl_points.append((h, 0, inner_lum))

        # Convert points to catesian
        points = np.asarray([LumFilter.polar_to_cartesian(e)
                            for e in hsl_points])
        x = [e[0] for e in points]
        y = [e[1] for e in points]
        z = [e[2] for e in points]

        # scipy raises:
        # /usr/local/lib/python3.6/site-packages/scipy/interpolate/_fitpack_impl.py:975:
        # RuntimeWarning: No more knots can be added because the number of
        # B-spline coefficients already exceeds the number of data points m.
        # Probable causes: either s or m too small. (fp>s)
        # kx,ky=1,1 nx,ny=16,14 m=160 fp=0.004157 s=0.000000
        # warnings.warn(RuntimeWarning(_iermess2[ierm][0] + _mess))
        #
        # I don't really know how to avoid that, so I'll just silence it,
        # seems wise enough to me
        with warnings.catch_warnings(record=True) as w:
            return interpolate.interp2d(x, y, z, kind="linear")

    def gen_sat_interpolation(json_file):
        # Load the provided points
        data = helpers.load_json(json_file)

        hue_table = {}
        for e in data:
            if e[0] in hue_table:
                hue_table[e[0]].append(e)
            else:
                hue_table[e[0]] = [e]

        hue_interp = {}
        for h in hue_table:
            R = [e[1] for e in hue_table[h]]
            Z = [e[2] for e in hue_table[h]]
            hue_interp[h] = interpolate.interp1d(Z, R, kind="linear")
        return hue_interp

    @staticmethod
    def polar_to_cartesian(polar_point):
        """
        .. warning::
            The variable's ordering follows the 'hsl' format and not
            the classic (radius, angle, z).
        """
        theta, r, z = polar_point
        # if r < 0 or r > 1 or theta < 0 or theta >= 360 or z < 0 or z > 1:
        #     raise exceptions.NotPolarException("The input tuple does not"
        #                                        + "match polar coordinates")
        return r * np.cos(np.radians(theta)), r * np.sin(np.radians(theta)), z

    def initialization():
        """
        Initializes the interpolation functions and the max/min values of
        the dark/bright surfaces
        """
        LumFilter.dark_interp = LumFilter.interpolate_hyperplans(
                hyperplan_file(Filter.DARK), "dark")

        LumFilter.bright_interp = LumFilter.interpolate_hyperplans(
                hyperplan_file(Filter.BRIGHT), "bright")

        LumFilter.saturation_interp = LumFilter.gen_sat_interpolation(
                hyperplan_file(Filter.SATURATION))

    @staticmethod
    def apply(palette):
        LumFilter.initialization()

        # Set background
        hsl_bg = helpers.rgb_to_hsl(palette.background)
        hsl_bg = (hsl_bg[0], hsl_bg[1], hsl_bg[2]*2)
        if not LumFilter.analyze(helpers.hsl_to_rgb(hsl_bg), "darkness"):
            palette.background = (0, 0, 0)
        # if (not LumFilter.is_too_bright(palette.foreground)):
            # palette.background = (255, 255, 255)

        # Set colors
        palette.colors = list(filter(lambda c:
                              not LumFilter.analyze(c, "brightness")
                              and not LumFilter.analyze(c, "darkness")
                              and not LumFilter.analyze(c, "saturation"),
                              palette.colors))
        return palette

    def analyze(rgb_color, kind):
        """
        Analyzes a provided color according to a specific type of analysis. It
        can be: brightness, darkness or saturation.

        :arg rgb_color: The input color to be analyzed.
        :arg kind: Type of analysis available: 'brightness', 'darkness' or
            'saturation'.
        :return: This function will return 'True' if the color is too bright,
            too dark, or not enough satured, else, 'False'.
        """
        if not helpers.can_be_rgb(rgb_color):
            raise exceptions.ColorFormatError("Color must be defined in the"
                                              + " rgb base")

        hsl_color = helpers.rgb_to_hsl(rgb_color)
        x, y, z = LumFilter.polar_to_cartesian(hsl_color)

        if kind == "brightness":
            return z > LumFilter.bright_min \
                   or z > LumFilter.bright_interp(x, y)
        elif kind == "darkness":
            return z < LumFilter.dark_max or z < LumFilter.dark_interp(x, y)
        elif kind == "saturation":
            return not LumFilter.is_enough_saturated(rgb_color)
        else:
            msg = str(kind) + "is not a supported analysis type"
            raise exceptions.UnknownAnalysisTypeException(msg)

    def half_circle_interp(half_circle):
        cartesian_p = [LumFilter.polar_to_cartesian(e) for e in half_circle]
        cartesian_p.sort(key=lambda e: e[0])
        X = [p[0] for p in cartesian_p]
        Y = [p[1] for p in cartesian_p]
        return interpolate.interp1d(X, Y, kind="linear")

    def is_enough_saturated(rgb_color):
        """
        To interpolate the saturation that separates the saturated enough
        colors with those that aren't, for a specific hue and luminosity,
        some 2D points (hue, saturation), are interpolated for the same
        luminosity (the one of the provided argument), and should form a
        circle. Then, this circle is divided into an upper circle and a
        bottom circle, from which an interpolation function can be computed
        with `scipy`. With these two functions, it is possible to assert if a
        color is saturated enough.
        """
        hsl_color = helpers.rgb_to_hsl(rgb_color)
        x, y, z = LumFilter.polar_to_cartesian(hsl_color)

        circle = []
        for h in LumFilter.saturation_interp:
            circle.append((h, LumFilter.saturation_interp[h](z), 0))

        interp_up = LumFilter.half_circle_interp(circle[:len(circle)//2])
        interp_down = LumFilter.half_circle_interp(circle[len(circle)//2:])

        # If the value cannot be interpolated, it means that the point is
        # saturated enough
        try:
            above_upper_half = interp_up(x) > y
            above_bottom_half = interp_down(x) > y
        except ValueError:
            return True
        else:
            return not (above_bottom_half ^ above_upper_half)

# Set up grid Generates a data structures that represent the interpolated         luminosity hyperplan (dark or bright).          .. todo::             This should be replaced by a function that only load a json             file where that contains the interpolated points instead of             regenating them each time # Load the provided points # Adding outer and inner circles for better results # When the json_file was encoded, some precision has been lost for # the hues, this is why we have to ceil or floor the hues # Adding outer circles # Addding "inner" circle # Convert points to catesian # scipy raises: # /usr/local/lib/python3.6/site-packages/scipy/interpolate/_fitpack_impl.py:975: # RuntimeWarning: No more knots can be added because the number of # B-spline coefficients already exceeds the number of data points m. # Probable causes: either s or m too small. (fp>s) # kx,ky=1,1 nx,ny=16,14 m=160 fp=0.004157 s=0.000000 # warnings.warn(RuntimeWarning(_iermess2[ierm][0] + _mess)) # # I don't really know how to avoid that, so I'll just silence it, # seems wise enough to me # Load the provided points .. warning::             The variable's ordering follows the 'hsl' format and not             the classic (radius, angle, z). # if r < 0 or r > 1 or theta < 0 or theta >= 360 or z < 0 or z > 1: #     raise exceptions.NotPolarException("The input tuple does not" #                                        + "match polar coordinates") Initializes the interpolation functions and the max/min values of         the dark/bright surfaces # Set background # if (not LumFilter.is_too_bright(palette.foreground)): # palette.background = (255, 255, 255) # Set colors Analyzes a provided color according to a specific type of analysis. It         can be: brightness, darkness or saturation.          :arg rgb_color: The input color to be analyzed.         :arg kind: Type of analysis available: 'brightness', 'darkness' or             'saturation'.         :return: This function will return 'True' if the color is too bright,             too dark, or not enough satured, else, 'False'. To interpolate the saturation that separates the saturated enough         colors with those that aren't, for a specific hue and luminosity,         some 2D points (hue, saturation), are interpolated for the same         luminosity (the one of the provided argument), and should form a         circle. Then, this circle is divided into an upper circle and a         bottom circle, from which an interpolation function can be computed         with `scipy`. With these two functions, it is possible to assert if a         color is saturated enough. # If the value cannot be interpolated, it means that the point is # saturated enough