# -*- coding: utf-8 -*-

import string
import statistics # for mean
import colorsys

class Color(object):

    """
    Magic color class implementing and supplying on-the-fly manipulation of
    RGB and HSV (and alpha) attributes.
    """

    def __init__(self, red=None, green=None, blue=None, alpha=None, hue=None, saturation=None, value=None):

        rgb_passed = bool(red)|bool(green)|bool(blue)
        hsv_passed = bool(hue)|bool(saturation)|bool(value)

        if not alpha:
            alpha = 1.0

        if rgb_passed and hsv_passed:
            raise ValueError("Color can't be initialized with RGB and HSV at the same time.")

        elif hsv_passed:

            if not hue:
                hue = 0.0
            if not saturation:
                saturation = 0.0
            if not value:
                value = 0.0

            super().__setattr__('hue', hue)
            super().__setattr__('saturation', saturation)
            super().__setattr__('value', value)
            self._update_rgb()

        else:

            if not red:
                red = 0
            if not green:
                green = 0
            if not blue:
                blue = 0

            super().__setattr__('red', red)
            super().__setattr__('green', green)
            super().__setattr__('blue', blue)
            self._update_hsv()

        super().__setattr__('alpha', alpha)
        self._update_lightness()

    def __setattr__(self, key, value):

        if key in ('red', 'green', 'blue'):
            if value > 1.0:
                value = value % 1.0
            super().__setattr__(key, value)
            self._update_hsv()

        elif key in ('hue', 'saturation', 'value'):
            if key == 'hue' and (value >= 360.0 or value < 0):
                value = value % 360.0
            elif key != 'hue' and value > 1.0:
                value = 1.0
            super().__setattr__(key, value) 
            self._update_rgb()

        elif key == 'alpha':
            if value > 1.0: # TODO: Might this be more fitting in another place?
                value = 1.0
            super().__setattr__(key, value)

        if key == 'lightness': # can't be elif because otherwise _update_lightness won't be triggered correctly

            amount = value - self.lightness

            red = self.red + amount
            blue = self.blue + amount
            green = self.green + amount

            if red > 1:
                red = 1
            if green > 1:
                green = 1
            if blue > 1:
                blue = 1

            super().__setattr__('red', red)
            super().__setattr__('green', green)
            super().__setattr__('blue', blue)
            super().__setattr__(key, value)
            self._update_hsv()

        else:
            self._update_lightness()

    def __repr__(self):

        return '<%s: red %f, green %f, blue %f, hue %f, saturation %f, value %f, alpha %f>' % (
                self.__class__.__name__,
                self.red,
                self.green,
                self.blue,
                self.hue,
                self.saturation,
                self.value,
                self.alpha
            )

    def __str__(self):
        return f"rgba({int(self.red * 255)}, {int(self.green * 255)}, {int(self.blue * 255)}, {self.alpha})"

    def _update_hsv(self):

        hue, saturation, value = colorsys.rgb_to_hsv(self.red, self.green, self.blue)
        super().__setattr__('hue', hue * 360.0)
        super().__setattr__('saturation', saturation)
        super().__setattr__('value', value)

    def _update_rgb(self):

        red, green, blue = colorsys.hsv_to_rgb(self.hue / 360.0, self.saturation, self.value)
        super().__setattr__('red', red)
        super().__setattr__('green', green)
        super().__setattr__('blue', blue)

    def _update_lightness(self):
        super().__setattr__('lightness', statistics.mean([self.red, self.green, self.blue]) * self.alpha)


    @classmethod
    def from_tuplergb(cls, value):
        return cls(red=value[0], green=value[1], blue=value[2])

    @classmethod
    def from_tuplergba(cls, value):
        return cls(red=value[0], green=value[1], blue=value[2], alpha=value[3])

    @classmethod
    def from_hexrgb(cls, value):

        if not value.startswith('#'):
            raise ValueError(f"Not a valid hex-coded RGB color: '{value}'. Must start with a '#'.")
        if len(value) != 7 and len(value) != 4:
            raise ValueError(f"Not a valid hex-coded RGB color: '{value}'. Must have either 3 or 6 hex digits.")

        channels = []
        if len(value) == 4:
            for char in value[1:]:
                if char not in string.hexdigits:
                    raise ValueError(f"Not a valid hex-coded RGB color: '{value}'. '{char}' is not a valid hex digit.")

                channels.append(int(f"{char}{char}", 16))

        elif len(value) == 7:
            for i in range(0,6,2):
                hexchars = value[1:][i:i+2]
                for char in hexchars:
                    if char not in string.hexdigits:
                        raise ValueError(f"Not a valid hex-coded RGB color: '{value}'. '{char}' is not a valid hex digit.")

                channels.append(int(hexchars, 16))

        return cls(red=channels[0] / 255, green = channels[1] / 255, blue = channels[2] / 255)

    def tuple_rgb(self):
        """ return color (without alpha) as tuple, channels being float 0.0-1.0 """
        return (self.red, self.green, self.blue)
    
    def tuple_rgba(self):
        """ return color (*with* alpha) as tuple, channels being float 0.0-1.0 """
        return (self.red, self.green, self.blue, self.alpha)

    def hexrgb(self):

        red = int(self.red * 255)
        green = int(self.green * 255)
        blue = int(self.blue * 255)
        return f"#{red:02X}{green:02X}{blue:02X}"

    def hexrgba(self):
        return f"#{hex(int(self.red * 255))[2:]}{hex(int(self.red * 255))[2:]}{hex(int(self.blue * 255))[2:]}{hex(int(self.alpha * 255))[2:]}"

    def clone(self):
        return Color(red=self.red, green=self.green, blue=self.blue, alpha=self.alpha)

    def blend(self, other, mode='normal'):

        if mode == 'normal':
            if self.alpha >= other.alpha:
                other_influence = other.alpha / self.alpha
                own_influence = 1.0 - other_influence
            else:
                own_influence = self.alpha / other.alpha
                other_influence = 1.0 - own_influence

            self.red = (self.red * own_influence) + (other.red * other_influence)
            self.green = (self.green * own_influence) + (other.green * other_influence)
            self.blue = (self.blue * own_influence) + (other.blue * other_influence)
            self.alpha = statistics.mean([self.alpha, other.alpha])

        elif mode == 'add':
            self.red += other.red
            self.green += other.green
            self.blue += other.blue
            self.alpha += other.alpha

        elif mode == 'subtract':
            self.red -= other.red
            self.green -= other.green
            self.blue -= other.blue
            self.alpha -= other.alpha

        elif mode == 'multiply':
            self.red *= other.red
            self.green *= other.green
            self.blue *= other.blue
            self.alpha *= other.alpha

        elif mode == 'divide':
            self.red /= other.red
            self.green /= other.green
            self.blue /= other.blue
            self.alpha /= other.alpha

        elif mode == 'hue':
            self.hue = other.hue

        elif mode == 'saturation':
            self.saturation = other.saturation

        elif mode == 'value':
            self.value = other.value
