# coding=utf-8
#
# Copyright (C) 2006 Jos Hirth, kaioa.com
# Copyright (C) 2007 Aaron C. Spike
# Copyright (C) 2009 Monash University
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
"""
Basic color controls
"""

from .tween import interpcoord

# All the names that get added to the inkex API itself.
__all__ = ('Color', 'ColorError', 'ColorIdError')

SVG_COLOR = {
    'aliceblue': '#f0f8ff',
    'antiquewhite': '#faebd7',
    'aqua': '#00ffff',
    'aquamarine': '#7fffd4',
    'azure': '#f0ffff',
    'beige': '#f5f5dc',
    'bisque': '#ffe4c4',
    'black': '#000000',
    'blanchedalmond': '#ffebcd',
    'blue': '#0000ff',
    'blueviolet': '#8a2be2',
    'brown': '#a52a2a',
    'burlywood': '#deb887',
    'cadetblue': '#5f9ea0',
    'chartreuse': '#7fff00',
    'chocolate': '#d2691e',
    'coral': '#ff7f50',
    'cornflowerblue': '#6495ed',
    'cornsilk': '#fff8dc',
    'crimson': '#dc143c',
    'cyan': '#00ffff',
    'darkblue': '#00008b',
    'darkcyan': '#008b8b',
    'darkgoldenrod': '#b8860b',
    'darkgray': '#a9a9a9',
    'darkgreen': '#006400',
    'darkgrey': '#a9a9a9',
    'darkkhaki': '#bdb76b',
    'darkmagenta': '#8b008b',
    'darkolivegreen': '#556b2f',
    'darkorange': '#ff8c00',
    'darkorchid': '#9932cc',
    'darkred': '#8b0000',
    'darksalmon': '#e9967a',
    'darkseagreen': '#8fbc8f',
    'darkslateblue': '#483d8b',
    'darkslategray': '#2f4f4f',
    'darkslategrey': '#2f4f4f',
    'darkturquoise': '#00ced1',
    'darkviolet': '#9400d3',
    'deeppink': '#ff1493',
    'deepskyblue': '#00bfff',
    'dimgray': '#696969',
    'dimgrey': '#696969',
    'dodgerblue': '#1e90ff',
    'firebrick': '#b22222',
    'floralwhite': '#fffaf0',
    'forestgreen': '#228b22',
    'fuchsia': '#ff00ff',
    'gainsboro': '#dcdcdc',
    'ghostwhite': '#f8f8ff',
    'gold': '#ffd700',
    'goldenrod': '#daa520',
    'gray': '#808080',
    'grey': '#808080',
    'green': '#008000',
    'greenyellow': '#adff2f',
    'honeydew': '#f0fff0',
    'hotpink': '#ff69b4',
    'indianred': '#cd5c5c',
    'indigo': '#4b0082',
    'ivory': '#fffff0',
    'khaki': '#f0e68c',
    'lavender': '#e6e6fa',
    'lavenderblush': '#fff0f5',
    'lawngreen': '#7cfc00',
    'lemonchiffon': '#fffacd',
    'lightblue': '#add8e6',
    'lightcoral': '#f08080',
    'lightcyan': '#e0ffff',
    'lightgoldenrodyellow': '#fafad2',
    'lightgray': '#d3d3d3',
    'lightgreen': '#90ee90',
    'lightgrey': '#d3d3d3',
    'lightpink': '#ffb6c1',
    'lightsalmon': '#ffa07a',
    'lightseagreen': '#20b2aa',
    'lightskyblue': '#87cefa',
    'lightslategray': '#778899',
    'lightslategrey': '#778899',
    'lightsteelblue': '#b0c4de',
    'lightyellow': '#ffffe0',
    'lime': '#00ff00',
    'limegreen': '#32cd32',
    'linen': '#faf0e6',
    'magenta': '#ff00ff',
    'maroon': '#800000',
    'mediumaquamarine': '#66cdaa',
    'mediumblue': '#0000cd',
    'mediumorchid': '#ba55d3',
    'mediumpurple': '#9370db',
    'mediumseagreen': '#3cb371',
    'mediumslateblue': '#7b68ee',
    'mediumspringgreen': '#00fa9a',
    'mediumturquoise': '#48d1cc',
    'mediumvioletred': '#c71585',
    'midnightblue': '#191970',
    'mintcream': '#f5fffa',
    'mistyrose': '#ffe4e1',
    'moccasin': '#ffe4b5',
    'navajowhite': '#ffdead',
    'navy': '#000080',
    'oldlace': '#fdf5e6',
    'olive': '#808000',
    'olivedrab': '#6b8e23',
    'orange': '#ffa500',
    'orangered': '#ff4500',
    'orchid': '#da70d6',
    'palegoldenrod': '#eee8aa',
    'palegreen': '#98fb98',
    'paleturquoise': '#afeeee',
    'palevioletred': '#db7093',
    'papayawhip': '#ffefd5',
    'peachpuff': '#ffdab9',
    'peru': '#cd853f',
    'pink': '#ffc0cb',
    'plum': '#dda0dd',
    'powderblue': '#b0e0e6',
    'purple': '#800080',
    'rebeccapurple': '#663399',
    'red': '#ff0000',
    'rosybrown': '#bc8f8f',
    'royalblue': '#4169e1',
    'saddlebrown': '#8b4513',
    'salmon': '#fa8072',
    'sandybrown': '#f4a460',
    'seagreen': '#2e8b57',
    'seashell': '#fff5ee',
    'sienna': '#a0522d',
    'silver': '#c0c0c0',
    'skyblue': '#87ceeb',
    'slateblue': '#6a5acd',
    'slategray': '#708090',
    'slategrey': '#708090',
    'snow': '#fffafa',
    'springgreen': '#00ff7f',
    'steelblue': '#4682b4',
    'tan': '#d2b48c',
    'teal': '#008080',
    'thistle': '#d8bfd8',
    'tomato': '#ff6347',
    'turquoise': '#40e0d0',
    'violet': '#ee82ee',
    'wheat': '#f5deb3',
    'white': '#ffffff',
    'whitesmoke': '#f5f5f5',
    'yellow': '#ffff00',
    'yellowgreen': '#9acd32',
    'none': None,
}
COLOR_SVG = dict([(value, name) for name, value in SVG_COLOR.items()])

def is_color(color):
    """Determine if it is a color that we can use. If not, leave it unchanged."""
    try:
        return bool(Color(color))
    except ColorError:
        return False

def constrain(minim, value, maxim, channel):
    """Returns the value so long as it is between min and max values"""
    if channel == 'h': # Hue
        return value % maxim # Wrap around hue value
    return min([maxim, max([minim, value])])

class ColorError(KeyError):
    """Specific color parsing error"""

class ColorIdError(ColorError):
    """Special color error for gradient and color stop ids"""

class Color(list):
    """An RGB array for the color"""
    red = property(lambda self: self.to_rgb()[0])
    red = red.setter(lambda self, value: self._set(0, value))
    green = property(lambda self: self.to_rgb()[1])
    green = green.setter(lambda self, value: self._set(1, value))
    blue = property(lambda self: self.to_rgb()[2])
    blue = blue.setter(lambda self, value: self._set(2, value))
    alpha = property(lambda self: self.to_rgba()[3])
    alpha = alpha.setter(lambda self, value: self._set(3, value, ('rgba',)))
    hue = property(lambda self: self.to_hsl()[0])
    hue = hue.setter(lambda self, value: self._set(0, value, ('hsl',)))
    saturation = property(lambda self: self.to_hsl()[1])
    saturation = saturation.setter(lambda self, value: self._set(1, value, ('hsl',)))
    lightness = property(lambda self: self.to_hsl()[2])
    lightness = lightness.setter(lambda self, value: self._set(2, value, ('hsl',)))

    def __init__(self, color=None, space='rgb'):
        super().__init__()
        if isinstance(color, Color):
            space, color = color.space, list(color)

        if isinstance(color, str):
            # String from xml or css attributes
            space, color = self.parse_str(color.strip())

        if isinstance(color, int):
            # Number from arg parser colour value
            space, color = self.parse_int(color)

        # Empty list means 'none', or no color
        if color is None:
            color = []

        if not isinstance(color, (list, tuple)):
            raise ColorError("Not a known a color value")

        self.space = space
        try:
            for val in color:
                self.append(val)
        except ValueError:
            raise ColorError("Bad color list")

    def __hash__(self):
        """Allow colors to be hashable"""
        return tuple(self.to_rgba()).__hash__()

    def _set(self, index, value, spaces=('rgb', 'rgba')):
        """Set the color value in place, limits setter to specific color space"""
        # Named colors are just rgb, so dump name memory
        if self.space == 'named':
            self.space = 'rgb'
        if not self.space in spaces:
            if index == 3 and self.space == 'rgb':
                # Special, add alpha, don't convert back to rgb
                self.space = 'rgba'
                self.append(constrain(0.0, float(value), 1.0, 'a'))
                return
            # Set in other colour space and convert back and forth
            target = self.to(spaces[0])
            target[index] = constrain(0, int(value), 255, spaces[0][index])
            self[:] = target.to(self.space)
            return
        self[index] = constrain(0, int(value), 255, spaces[0][index])

    def append(self, val):
        """Append a value to the local list"""
        if len(self) == len(self.space):
            raise ValueError("Can't add any more values to color.")

        if isinstance(val, str):
            val = val.strip()
            if val.endswith('%'):
                val = float(val.strip('%')) / 100
            else:
                val = float(val)

        end_type = int
        if len(self) == 3: # Alpha value
            val = min([1.0, val])
            end_type = float
        elif isinstance(val, float) and val <= 1.0:
            val *= 255

        if isinstance(val, (int, float)):
            super().append(max(end_type(val), 0))

    @staticmethod
    def parse_str(color):
        """Creates a rgb int array"""
        # Handle pre-defined svg color values
        if color and color.lower() in SVG_COLOR:
            return 'named', Color.parse_str(SVG_COLOR[color.lower()])[1]

        if color is None:
            return 'rgb', None

        if color.startswith('url('):
            raise ColorIdError("Color references other element id, e.g. a gradient")

        # Next handle short colors (css: #abc -> #aabbcc)
        if color.startswith('#'):
            # Remove any icc or ilab directives
            # FUTURE: We could use icc or ilab information
            col = color.split(' ')[0]
            if len(col) == 4:
                col = '#{1}{1}{2}{2}{3}{3}'.format(*col)

            # Convert hex to integers
            try:
                return 'rgb', (int(col[1:3], 16), int(col[3:5], 16), int(col[5:], 16))
            except ValueError:
                raise ColorError(f"Bad RGB hex color value {col}")

        # Handle other css color values
        elif '(' in color and ')' in color:
            space, values = color.lower().strip().strip(')').split('(')
            return space, values.split(',')

        try:
            return Color.parse_int(int(color))
        except ValueError:
            pass

        raise ColorError(f"Unknown color format: {color}")

    @staticmethod
    def parse_int(color):
        """Creates an rgb or rgba from a long int"""
        space = 'rgb'
        color = [
            ((color >> 24) & 255), # red
            ((color >> 16) & 255), # green
            ((color >> 8) & 255), # blue
            ((color & 255) / 255.), # opacity
        ]
        if color[-1] == 1.0:
            color.pop()
        else:
            space = 'rgba'
        return space, color

    def __str__(self):
        """int array to #rrggbb"""
        if not self:
            return 'none'
        if self.space == 'named':
            rgbhex = '#{0:02x}{1:02x}{2:02x}'.format(*self)
            if rgbhex in COLOR_SVG:
                return COLOR_SVG[rgbhex]
            self.space = 'rgb'
        if self.space == 'rgb':
            return '#{0:02x}{1:02x}{2:02x}'.format(*self)
        if self.space == 'rgba':
            if self[3] == 1.0:
                return 'rgb({:g}, {:g}, {:g})'.format(*self[:3])
            return 'rgba({:g}, {:g}, {:g}, {:g})'.format(*self)
        elif self.space == 'hsl':
            return 'hsl({0:g}, {1:g}, {2:g})'.format(*self)
        raise ColorError(f"Can't print colour space '{self.space}'")

    def __int__(self):
        """int array to large integer"""
        if not self:
            return -1
        color = self.to_rgba()
        return (color[0] << 24) + (color[1] << 16) + (color[2] << 8) + (int(color[3] * 255))

    def to(self, space):
        """Dynamic caller for to_hsl, to_rgb, etc"""
        return getattr(self, 'to_' + space)()

    def to_hsl(self):
        """Turn this color into a Hue/Saturation/Lightness colour space"""
        if not self and self.space in ('rgb', 'named'):
            return self.to_rgb().to_hsl()
        if self.space == 'hsl':
            return self
        elif self.space == 'rgb':
            return Color(rgb_to_hsl(*self.to_floats()), space='hsl')
        raise ColorError(f"Unknown color conversion {self.space}->hsl")

    def to_rgb(self):
        """Turn this color into a Red/Green/Blue colour space"""
        if not self and self.space in ('rgb', 'named'):
            return Color([0, 0, 0])
        if self.space == 'rgb':
            return self
        if self.space in ('rgba', 'named'):
            return Color(self[:3], space='rgb')
        elif self.space == 'hsl':
            return Color(hsl_to_rgb(*self.to_floats()), space='rgb')
        raise ColorError(f"Unknown color conversion {self.space}->rgb")

    def to_rgba(self, alpha=1.0):
        """Turn this color isn't an RGB with Alpha colour space"""
        if self.space == 'rgba':
            return self
        return Color(self.to_rgb() + [alpha], 'rgba')

    def to_floats(self):
        """Returns the colour values as percentage floats (0.0 - 1.0)"""
        return [val / 255.0 for val in self]

    def to_named(self):
        """Convert this color to a named color if possible"""
        if not self:
            return Color()
        return Color(COLOR_SVG.get(str(self), str(self)))

    def interpolate(self, other, fraction):
        """Iterpolate two colours by the given fraction"""
        return Color(
            [interpcoord(c1, c2, fraction)
             for (c1, c2) in zip(self.to_floats(), other.to_floats())]
            )


def rgb_to_hsl(red, green, blue):
    """RGB to HSL colour conversion"""
    rgb_max = max(red, green, blue)
    rgb_min = min(red, green, blue)
    delta = rgb_max - rgb_min
    hsl = [0.0, 0.0, (rgb_max + rgb_min) / 2.0]
    if delta != 0:
        if hsl[2] <= 0.5:
            hsl[1] = delta / (rgb_max + rgb_min)
        else:
            hsl[1] = delta / (2 - rgb_max - rgb_min)

        if red == rgb_max:
            hsl[0] = (green - blue) / delta
        elif green == rgb_max:
            hsl[0] = 2.0 + (blue - red) / delta
        elif blue == rgb_max:
            hsl[0] = 4.0 + (red - green) / delta

        hsl[0] /= 6.0
        if hsl[0] < 0:
            hsl[0] += 1
        if hsl[0] > 1:
            hsl[0] -= 1
    return hsl


def hsl_to_rgb(hue, sat, light):
    """HSL to RGB Color Conversion"""
    if sat == 0:
        return [light, light, light]  # Gray

    if light < 0.5:
        val2 = light * (1 + sat)
    else:
        val2 = light + sat - light * sat
    val1 = 2 * light - val2
    return [_hue_to_rgb(val1, val2, hue * 6 + 2.0),
            _hue_to_rgb(val1, val2, hue * 6),
            _hue_to_rgb(val1, val2, hue * 6 - 2.0)]


def _hue_to_rgb(val1, val2, hue):
    if hue < 0:
        hue += 6.0
    if hue > 6:
        hue -= 6.0
    if hue < 1:
        return val1 + (val2 - val1) * hue
    if hue < 3:
        return val2
    if hue < 4:
        return val1 + (val2 - val1) * (4 - hue)
    return val1
