from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

try:
    basestring
except NameError:
    basestring = str

import colorsys
import re

from compas.colors.html_colors import HTML_TO_RGB255
from compas.data import Data

BASE16 = '0123456789abcdef'

try:
    HEX_DEC = {v: int(v, base=16) for v in [x + y for x in BASE16 for y in BASE16]}
except Exception:
    HEX_DEC = {v: int(v, 16) for v in [x + y for x in BASE16 for y in BASE16]}


class Color(Data):
    """Class for working with colors.

    Parameters
    ----------
    red : float
        The red component in the range of 0-1.
    green : float
        The green component in the range of 0-1.
    blue : float
        The blue component in the range of 0-1.
    alpha : float, optional
        Transparency setting.
        If ``alpha = 0.0``, the color is fully transparent.
        If ``alpha = 1.0``, the color is fully opaque.

    Other Parameters
    ----------------
    **kwargs : dict, optional
        See :class:`Data` for more information.

    Attributes
    ----------
    r : float
        Red component of the color in RGB1 color space.
    g : float
        Green component of the color in RGB1 color space.
    b : float
        Blue component of the color in RGB1 color space.
    rgb : tuple[float, float, float]
        RGB1 color tuple, with components in the range 0-1.
    rgb255 : tuple[int, int, int]
        RGB255 color tuple, with components in the range 0-255.
    hex : str
        Hexadecimal color string.
    hls : tuple[float, float, float]
        Hue, Lightness, Saturation.
    hsv : tuple[float, float, float]
        Hue, Saturation, Value / Brightness.
    lightness : float
        How much white the color appears to contain.
        This is the "Lightness" in HLS.
        Making a color "lighter" is like adding more white.
    brightness : float
        How well-lit the color appears to be.
        This is the "Value" in HSV.
        Making a color "brighter" is like shining a stronger light on it, or illuminating it better.
    is_light : bool
        If True, the color is considered light.

    Examples
    --------
    >>> Color(1, 0, 0)
    Color(1.0, 0.0, 0.0, 1.0)
    >>> Color.red()
    Color(1.0, 0.0, 0.0, 1.0)
    >>> Color(1, 0, 0) == Color.red()
    True

    >>> Color.magenta()
    Color(1.0, 0.0, 1.0, 1.0)
    >>> Color.lime()
    Color(0.5, 1.0, 0.0, 1.0)
    >>> Color.navy()
    Color(0.0, 0.0, 0.5, 1.0)
    >>> Color.olive()
    Color(0.5, 0.5, 0.0, 1.0)

    >>> Color.lime().is_light
    True
    >>> Color.navy().is_light
    False

    """

    def __init__(self, red, green, blue, alpha=1.0, **kwargs):
        super(Color, self).__init__(**kwargs)
        self._r = 1.0
        self._g = 1.0
        self._b = 1.0
        self._a = 1.0
        self.r = red
        self.g = green
        self.b = blue
        self.a = alpha

    # --------------------------------------------------------------------------
    # data
    # --------------------------------------------------------------------------

    @property
    def data(self):
        return {'red': self.r, 'green': self.g, 'blue': self.b, 'alpha': self.a}

    @data.setter
    def data(self, data):
        self.r = data['red']
        self.g = data['green']
        self.b = data['blue']
        self.a = data['alpha']

    @classmethod
    def from_data(cls, data):
        return cls(data['red'], data['green'], data['blue'], data['alpha'])

    # --------------------------------------------------------------------------
    # properties
    # --------------------------------------------------------------------------

    @property
    def r(self):
        return self._r

    @r.setter
    def r(self, red):
        if red > 1.0 or red < 0.0:
            raise ValueError("Components of an RGBA color should be in the range 0-1.")
        self._r = float(red)

    @property
    def g(self):
        return self._g

    @g.setter
    def g(self, green):
        if green > 1.0 or green < 0.0:
            raise ValueError("Components of an RGBA color should be in the range 0-1.")
        self._g = float(green)

    @property
    def b(self):
        return self._b

    @b.setter
    def b(self, blue):
        if blue > 1.0 or blue < 0.0:
            raise ValueError("Components of an RGBA color should be in the range 0-1.")
        self._b = float(blue)

    @property
    def a(self):
        return self._a

    @a.setter
    def a(self, alpha):
        if alpha > 1.0 or alpha < 0.0:
            raise ValueError("Components of an RGBA color should be in the range 0-1.")
        self._a = float(alpha)

    @property
    def rgb(self):
        r = self.r
        g = self.g
        b = self.b
        return r, g, b

    @property
    def rgb255(self):
        r = int(self.r * 255)
        g = int(self.g * 255)
        b = int(self.b * 255)
        return r, g, b

    @property
    def rgba(self):
        r, g, b = self.rgb
        a = self.a
        return r, g, b, a

    @property
    def rgba255(self):
        r, g, b = self.rgb255
        a = int(self.a * 255)
        return r, g, b, a

    @property
    def hex(self):
        return '#{0:02x}{1:02x}{2:02x}'.format(* self.rgb255)

    @property
    def hls(self):
        return colorsys.rgb_to_hls(* self.rgb)

    @property
    def hsv(self):
        return colorsys.rgb_to_hsv(* self.rgb)

    @property
    def lightness(self):
        return self.hls[1]

    @property
    def brightness(self):
        return self.hsv[2]

    @property
    def is_light(self):
        return self.luminance > 0.179

    @property
    def yuv(self):
        """tuple[float, float, float] :
        Luma and chroma components, with chroma defined by the blue and red projections.
        """
        y = self.luma
        u, v = self.chroma
        return y, u, v

    @property
    def luma(self):
        """float :
        The brightness of a yuv signal.
        """
        return 0.299 * self.r + 0.587 * self.g + 0.114 * self.b

    @property
    def chroma(self):
        """tuple[float, float] :
        The color of a yuv signal.
        "How different from a grey of the same lightness the color appears to be."
        """
        y = self.luma
        u = 0.492 * (self.b - y)
        v = 0.877 * (self.r - y)
        return u, v

    @property
    def luminance(self):
        """float :
        The amount of light that passes through, is emitted from, or is reflected from a particular area.
        Here, it expresses the preceived brightness of the color.
        Note that this is not the same as the "Lightness" of HLS or the "Value/Brightness" of HSV.
        """
        return 0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b

    @property
    def saturation(self):
        """float : The perceived freedom of whiteness."""
        maxval = max(self.r, self.g, self.b)
        minval = min(self.r, self.g, self.b)
        return (maxval - minval) / maxval

    # --------------------------------------------------------------------------
    # customization
    # --------------------------------------------------------------------------

    def __repr__(self):
        return 'Color({}, {}, {}, {})'.format(self.r, self.g, self.b, self.a)

    def __getitem__(self, key):
        if key == 0:
            return self.r
        if key == 1:
            return self.g
        if key == 2:
            return self.b
        raise KeyError

    def __len__(self):
        return 3

    def __iter__(self):
        return iter(self.rgb)

    def __eq__(self, other):
        return all(a == b for a, b in zip(self, other))

    # --------------------------------------------------------------------------
    # constructors
    # --------------------------------------------------------------------------

    @classmethod
    def from_rgb255(cls, r, g, b):
        """Construct a color from RGB255 components.

        Parameters
        ----------
        r : int & valuerange[0, 255]
            Red component.
        g : int & valuerange[0, 255]
            Green component.
        b : int & valuerange[0, 255]
            Blue component.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(r/255, g/255, b/255)

    @classmethod
    def from_hls(cls, h, l, s):  # noqa: E741
        """Construct a color from Hue, Luminance, and Saturation.

        Parameters
        ----------
        h : float
            Hue.
        l : float
            Luminance.
        s : float
            Saturation.

        Returns
        -------
        :class:`compas.colors.Color`

        See Also
        --------
        https://en.wikipedia.org/wiki/HSL_and_HSV

        """
        r, g, b = colorsys.hls_to_rgb(h, l, s)
        return cls(r, g, b)

    @classmethod
    def from_hsv(cls, h, s, v):
        """Construct a color from Hue, Saturation, and Value.

        Parameters
        ----------
        h : float
            Hue.
        s : float
            Saturation.
        v : float
            Value.

        Returns
        -------
        :class:`compas.colors.Color`

        See Also
        --------
        https://en.wikipedia.org/wiki/HSL_and_HSV

        """
        r, g, b = colorsys.hsv_to_rgb(h, s, v)
        return cls(r, g, b)

    @classmethod
    def from_yiq(cls, y, i, q):
        """Construct a color from components in the YIQ color space.

        Parameters
        ----------
        y : float
            Luma.
        i : float
            Orange-blue chroma.
        q : float
            Purple-green chroma.

        Returns
        -------
        :class:`compas.colors.Color`

        See Also
        --------
        https://en.wikipedia.org/wiki/YIQ

        """
        r, g, b = colorsys.yiq_to_rgb(y, i, q)
        return cls(r, g, b)

    @classmethod
    def from_yuv(cls, y, u, v):
        """Construct a color from components in the YUV color space.

        Parameters
        ----------
        y : float
            Luma.
        u : float
            Blue projection chroma.
        v : float
            Red projection chroma.

        Returns
        -------
        :class:`compas.colors.Color`

        See Also
        --------
        https://en.wikipedia.org/wiki/YUV

        """
        r = y + 1.140 * v
        g = y - 0.395 * u - 0.581 * v
        b = y + 2.032 * u
        return cls(r, g, b)

    @classmethod
    def from_i(cls, i):
        """Construct a color from a single number in the range 0-1.

        Parameters
        ----------
        i : float
            Number in the range 0-1, representing the color.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        if i == 0.0:
            r, g, b = 0, 0, 255
        elif 0.0 < i < 0.25:
            r, g, b = 0, int(255 * (4 * i)), 255
        elif i == 0.25:
            r, g, b = 0, 255, 255
        elif 0.25 < i < 0.5:
            r, g, b = 0, 255, int(255 - 255 * 4 * (i - 0.25))
        elif i == 0.5:
            r, g, b = 0, 255, 0
        elif 0.5 < i < 0.75:
            r, g, b = int(0 + 255 * 4 * (i - 0.5)), 255, 0
        elif i == 0.75:
            r, g, b = 255, 255, 0
        elif 0.75 < i < 1.0:
            r, g, b,  = 255, int(255 - 255 * 4 * (i - 0.75)), 0
        elif i == 1.0:
            r, g, b = 255, 0, 0
        else:
            r, g, b = 0, 0, 0
        return cls(r / 255.0, g / 255.0, b / 255.0)

    @classmethod
    def from_hex(cls, value):
        """Construct a color from a hexadecimal color value.

        Parameters
        ----------
        value : str
            The hexadecimal color.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        value = value.lstrip('#').lower()
        r = HEX_DEC[value[0:2]]
        g = HEX_DEC[value[2:4]]
        b = HEX_DEC[value[4:6]]
        return cls(r / 255.0, g / 255.0, b / 255.0)

    @classmethod
    def from_name(cls, name):
        """Construct a color from a name in the extended color table of HTML/CSS/SVG.

        Parameters
        ----------
        name : str
            The color name. The name is case-insensitive.

        Returns
        -------
        :class:`compas.colors.Color`

        See Also
        --------
        https://www.w3.org/TR/css-color-3/#svg-color
        """
        rgb255 = HTML_TO_RGB255.get(name.lower())
        if rgb255 is None:
            raise ValueError("Color name not found.")
        return cls.from_rgb255(*rgb255)

    # --------------------------------------------------------------------------
    # presets
    # --------------------------------------------------------------------------

    @classmethod
    def white(cls):
        """Construct the color white.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(1.0, 1.0, 1.0)

    @classmethod
    def black(cls):
        """Construct the color black.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 0.0, 0.0)

    @classmethod
    def grey(cls):
        """Construct the color grey.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 0.5, 0.5)

    @classmethod
    def red(cls):
        """Construct the color red.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(1.0, 0.0, 0.0)

    @classmethod
    def orange(cls):
        """Construct the color orange.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(1.0, 0.5, 0.0)

    @classmethod
    def yellow(cls):
        """Construct the color yellow.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(1.0, 1.0, 0.0)

    @classmethod
    def lime(cls):
        """Construct the color lime (or chartreuse green).

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 1.0, 0.0)

    @classmethod
    def green(cls):
        """Construct the color green.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 1.0, 0.0)

    @classmethod
    def mint(cls):
        """Construct the color mint (or spring green).

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 1.0, 0.5)

    @classmethod
    def cyan(cls):
        """Construct the color cyan.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 1.0, 1.0)

    @classmethod
    def azure(cls):
        """Construct the color azure.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 0.5, 1.0)

    @classmethod
    def blue(cls):
        """Construct the color blue.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 0.0, 1.0)

    @classmethod
    def violet(cls):
        """Construct the color violet.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 0.0, 1.0)

    @classmethod
    def magenta(cls):
        """Construct the color magenta.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(1.0, 0.0, 1.0)

    @classmethod
    def pink(cls):
        """Construct the color pink.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(1.0, 0.0, 0.5)

    # --------------------------------------------------------------------------
    # other presets
    # --------------------------------------------------------------------------

    @classmethod
    def maroon(cls):
        """Construct the color maroon.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 0.0, 0.0)

    @classmethod
    def brown(cls):
        """Construct the color brown.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 0.25, 0.0)

    @classmethod
    def olive(cls):
        """Construct the color olive.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 0.5, 0.0)

    @classmethod
    def teal(cls):
        """Construct the color teal.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 0.5, 0.5)

    @classmethod
    def navy(cls):
        """Construct the color navy.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.0, 0.0, 0.5)

    @classmethod
    def purple(cls):
        """Construct the color purple.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.5, 0.0, 0.5)

    @classmethod
    def silver(cls):
        """Construct the color silver.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        return cls(0.75, 0.75, 0.75)

    # ochre
    # beige
    # bordeaux
    # hotpink
    # steel
    # midnight
    # salmon

    # --------------------------------------------------------------------------
    # methods
    # --------------------------------------------------------------------------

    def is_rgb1(self):
        """Verify that the color is in the RGB 1 color space.

        Returns
        -------
        bool

        """
        return all(isinstance(c, float) and (c >= 0 and c <= 1) for c in self)

    def is_rgb255(self):
        """Verify that the color is in the RGB 255 color space.

        Returns
        -------
        bool

        """
        return all(isinstance(c, int) and (c >= 0 and c <= 255) for c in self)

    def is_hex(self):
        """Verify that the color is in hexadecimal format.

        Returns
        -------
        bool

        """
        if isinstance(self, basestring):
            match = re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', self)
            if match:
                return True
            return False
        return False

    def lighten(self, factor=10):
        """Lighten the color.

        Parameters
        ----------
        factor : float, optional
            Percentage of lightness increase.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the percentage of lightness increase is not in the range 0-100.

        """
        if factor > 100 or factor < 0:
            raise ValueError('Percentage of increased lightness should be in the range 0-100.')

        factor = 1.0 + factor / 100

        h, l, s = self.hls
        r, g, b = colorsys.hls_to_rgb(h, min(1.0, l * factor), s)
        self.r = r
        self.g = g
        self.b = b

    def lightened(self, factor=10):
        """Return a lightened copy of the color.

        Parameters
        ----------
        factor : float, optional
            Percentage of lightness increase.

        Returns
        -------
        :class:`compas.colors.Color`

        Raises
        ------
        ValueError
            If the percentage of lightness increase is not in the range 0-100.

        """
        color = self.copy()
        color.lighten(factor=factor)
        return color

    def darken(self, factor=10):
        """Darken the color.

        Parameters
        ----------
        factor : float, optional
            Percentage of lightness reduction.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the percentage of lightness reduction is not in the range 0-100.

        """
        if factor > 100 or factor < 0:
            raise ValueError('Percentage of reduced lightness should be in the range 0-100.')

        factor = 1.0 - factor / 100

        h, l, s = self.hls
        r, g, b = colorsys.hls_to_rgb(h, max(0.0, l * factor), s)
        self.r = r
        self.g = g
        self.b = b

    def darkened(self, factor=10):
        """Return a darkened copy of the color.

        Parameters
        ----------
        factor : float, optional
            Percentage of lightness reduction.

        Returns
        -------
        :class:`compas.colors.Color`

        Raises
        ------
        ValueError
            If the percentage of lightness reduction is not in the range 0-100.

        """
        color = self.copy()
        color.darken(factor=factor)
        return color

    def invert(self):
        """Invert the current color wrt to the RGB color circle.

        Returns
        -------
        None

        """
        self.r = 1.0 - self.r
        self.g = 1.0 - self.g
        self.b = 1.0 - self.b

    def inverted(self):
        """Return an inverted copy of the color.

        Returns
        -------
        :class:`compas.colors.Color`

        """
        color = self.copy()
        color.invert()
        return color

    def saturate(self, factor=10):
        """Saturate the color by a given percentage.

        Parameters
        ----------
        factor : float, optional
            Percentage of saturation increase.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the percentage of saturation is not in the range 0-100.

        """
        if factor > 100 or factor < 0:
            raise ValueError('Percentage of saturation should be in the range 0-100.')

        factor = 1.0 + factor / 100

        h, l, s = self.hls
        r, g, b = colorsys.hls_to_rgb(h, l, min(1.0, s * factor))
        self.r = r
        self.g = g
        self.b = b

    def saturated(self, factor=10):
        """Return a saturated copy of the color.

        Parameters
        ----------
        factor : float, optional
            Percentage of saturation increase.

        Returns
        -------
        :class:`compas.colors.Color`

        Raises
        ------
        ValueError
            If the percentage of desaturation is not in the range 0-100.

        """
        color = self.copy()
        color.saturate(factor=factor)
        return color

    def desaturate(self, factor=10):
        """Desaturate the color by a given percentage.

        Parameters
        ----------
        factor : float, optional
            Percentage of saturation reduction.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the percentage of desaturation is not in the range 0-100.

        """
        if factor > 100 or factor < 0:
            raise ValueError('Percentage of desaturation should be in the range 0-100.')

        factor = 1.0 - factor / 100

        h, l, s = self.hls
        r, g, b = colorsys.hls_to_rgb(h, l, max(0.0, s * factor))
        self.r = r
        self.g = g
        self.b = b

    def desaturated(self, factor=10):
        """Return a desaturated copy of the color.

        Parameters
        ----------
        factor : float, optional
            Percentage of saturation reduction.

        Returns
        -------
        :class:`compas.colors.Color`

        Raises
        ------
        ValueError
            If the percentage of desaturation is not in the range 0-100.

        """
        color = self.copy()
        color.desaturate(factor=factor)
        return color
