"""Python utilities for colors"""

__version__ = "0.4.0"

from colorsys import hls_to_rgb, hsv_to_rgb, rgb_to_hls, rgb_to_hsv
from math import sqrt
from random import randint

from averager import average

from .exceptions import InvalidRGBValue, WrongLengthError
from .vars import CSS_COLORS


def normalize_hex(hex_code: str) -> str:
    """Normalizes a hex color code

    Removes the leading ``#`` if there is one, expands 3-character hex codes,
    and lowercases the hex code

    Args:
        hex_code (:class:`str`): A hex code to normalize

    Returns:
        :class:`str`: The normalized hex code

    Raises:
        :exception:`~pigment.exceptions.WrongLengthError`: The provided hex
            code had the wrong length
    """

    hex_code = hex_code.lstrip("#").lower()

    if len(hex_code) == 3:
        hex_code = "".join(c * 2 for c in hex_code)

    if len(hex_code) != 6:
        raise WrongLengthError("hex_code")

    return hex_code


class Color:
    """Represents a color

    Args:
        red (:class:`int`): The color's red RGB component (0-255)
        green (:class:`int`): The color's green RGB component (0-255)
        blue (:class:`int`): The color's blue RGB component (0-255)

    Note:
        The :attr:`~cmyk`, :attr:`~hex_code`, :attr:`~hls`, :attr:`~hsv`, and
        :attr:`~rgb` properties can be set to update the color object.
    """

    def __init__(self, red: int, green: int, blue: int):
        self.rgb = (red, green, blue)

    def __repr__(self):
        return "<pigment.Color r=%r g=%r b=%r>" % self.rgb

    def __eq__(self, other):
        if isinstance(other, Color):
            return self.rgb == other.rgb

        return self.rgb == other

    @property
    def rgb(self):
        """The color as an RGB tuple

        * Red (0-255)
        * Green (0-255)
        * Blue (0-255)
        """

        return self._rgb

    @rgb.setter
    def rgb(self, value):
        if len(value) != 3:
            raise WrongLengthError()

        for c in value:
            if c not in range(256):
                raise InvalidRGBValue()

        self._rgb = value

    @property
    def hex_code(self):
        """The color's hex code"""

        return "%02x%02x%02x" % (self.rgb[0], self.rgb[1], self.rgb[2])

    @hex_code.setter
    def hex_code(self, value):
        self.rgb = tuple(
            int(normalize_hex(value)[i : i + 2], 16) for i in (0, 2, 4)
        )

    @property
    def hsv(self):
        """The color as an HSV tuple

        * Hue: color (0-360)
        * Saturation: amount of gray vs. color (0-100)
        * Value: amount of black vs. color (0-100)
        """

        res = rgb_to_hsv(
            self.rgb[0] / 255, self.rgb[1] / 255, self.rgb[2] / 255
        )

        return (
            round(res[0] * 360),
            round(res[1] * 100),
            round(res[2] * 100),
        )

    @hsv.setter
    def hsv(self, value):
        self.rgb = tuple(
            round(x * 255)
            for x in hsv_to_rgb(value[0] / 360, value[1] / 100, value[2] / 100)
        )

    @property
    def hue(self):
        """The color's hue (0-360)"""

        return self.hsv[0]

    @hue.setter
    def hue(self, value):
        self.hsv = (value, *self.hsv[1:])

    @property
    def hls(self):
        """The color as an HLS tuple

        * Hue: color (0-360)
        * Lightness: amount of white vs. color (0-100)
        * Saturation: amount of gray vs. color (0-100)
        """

        res = rgb_to_hls(
            self.rgb[0] / 255, self.rgb[1] / 255, self.rgb[2] / 255
        )

        return (
            round(res[0] * 360),
            round(res[1] * 100),
            round(res[2] * 100),
        )

    @hls.setter
    def hls(self, value):
        self.rgb = tuple(
            round(x * 255)
            for x in hls_to_rgb(value[0] / 360, value[1] / 100, value[2] / 100)
        )

    @property
    def cmyk(self):
        """The color as a CMYK tuple

        * Cyan (0-100)
        * Magenta (0-100)
        * Yellow (0-100)
        * Key/Black (0-100)
        """

        if self.rgb == (0, 0, 0):
            return (0, 0, 0, 100)

        c = 1 - self.rgb[0] / 255
        m = 1 - self.rgb[1] / 255
        y = 1 - self.rgb[2] / 255

        min_cmy = min(c, m, y)
        c = (c - min_cmy) / (1 - min_cmy)
        m = (m - min_cmy) / (1 - min_cmy)
        y = (y - min_cmy) / (1 - min_cmy)
        k = min_cmy

        return (round(c * 100), round(m * 100), round(y * 100), round(k * 100))

    @cmyk.setter
    def cmyk(self, value):
        cyan, magenta, yellow, key = value

        self.rgb = (
            round(255 * (1 - cyan / 100) * (1 - key / 100)),
            round(255 * (1 - magenta / 100) * (1 - key / 100)),
            round(255 * (1 - yellow / 100) * (1 - key / 100)),
        )

    @classmethod
    def from_css_name(cls, css_color: str):
        """Gets a color from a CSS color name

        Args:
            css_color (:class:`str`): The name of the CSS color

        Returns:
            :class:`~pigment.Color`
        """

        return Color(*CSS_COLORS[css_color])

    @classmethod
    def random(
        cls,
        red: tuple = (0, 255),
        green: tuple = (0, 255),
        blue: tuple = (0, 255),
    ):
        """Generates a random color

        This works by generating random red, green, and blue values using
        :func:`random.randint` from the standard library using the min/max
        values specified if any

        Args:
            red (:class:`tuple`): The two arguments to pass for the red value
            green (:class:`tuple`): The two arguments to pass for the green
                value
            blue (:class:`tuple`): The two arguments to pass for the blue value

        Returns:
            :class:`~pigment.Color`
        """

        return Color(randint(*red), randint(*green), randint(*blue))


def blend(color1: Color, color2: Color) -> Color:
    """Blends two colors together

    Args:
        color1 (:class:`~pigment.Color`): The first color
        color2 (:class:`~pigment.Color`): The second color

    Returns:
        :class:`~pigment.Color`
    """

    colors = (color1.rgb, color2.rgb)

    return Color(
        *(round(sqrt(average([c[i] ** 2 for c in colors]))) for i in range(3))
    )
