# colormodel.py
# Walker M. White (wmw2)
# September 2, 2012
"""Classes for three different color models.

The classes are RGB, CMYK, HSV.  The constants in this module are all
defined in the RGB color space."""
import colorsys

# To handle round off error
_epsilon = 1e-13

class RGB(object):
    """An instance is a RGB color value."""
    
    @property
    def red(self):
        """The red channel.
        
        **Invariant**: Value must be an int between 0 and 255, inclusive."""
        return self._red
       
    @red.setter
    def red(self, value):
        assert (type(value) == int), "value %s is not an int" % `value`
        assert (value >= 0 and value <= 255), "value %s is outside of range [0,255]" % `value`
        self._red = value
       
    @red.deleter
    def red(self):
        del self._red 
    
    @property
    def green(self):
        """The green channel.
        
        **Invariant**: Value must be an int between 0 and 255, inclusive."""
        return self._green
    
    @green.setter
    def green(self, value):
        assert (type(value) == int), "value %s is not an int" % `value`
        assert (value >= 0 and value <= 255), "value %s is outside of range [0,255]" % `value`
        self._green = value
        
    @green.deleter
    def green(self):
        del self._green     
    
    @property
    def blue(self):
        """The blue channel.
        
        **Invariant**: Value must be an int between 0 and 255, inclusive."""
        return self._blue
    
    @blue.setter
    def blue(self, value):
        assert (type(value) == int), "value %s is not an int" % `value`
        assert (value >= 0 and value <= 255), "value %s is outside of range [0,255]" % `value`
        self._blue = value
        
    @blue.deleter
    def blue(self):
        del self._blue     
 
    @property
    def alpha(self):
        """The alpha channel.
        
        Used for transparency effects (but not in this course).
        
        **Invariant**: Value must be an int between 0 and 255, inclusive."""
        return self._alpha
        
    @alpha.setter
    def alpha(self, value):
        assert (type(value) == int), "value %s is not an int" % `value`
        assert (value >= 0 and value <= 255), "value %s is outside of range [0,255]" % `value`
        self._alpha = value
            
    @alpha.deleter
    def alpha(self):
        del self._alpha     

    # METHODS
    
    def __init__(self, r, g, b, a=255):
        """**Constructor**: creates a new RGB value (r,g,b,a).
        
            :param r: initial red value
            **Precondition**: int between 0 and 255, inclusive.
        
            :param g: initial green value
            **Precondition**: int between 0 and 255, inclusive.
        
            :param b: initial blue value
            **Precondition**: int between 0 and 255, inclusive.
        
            :param a: initial alpha value (default 255)
            **Precondition**: int between 0 and 255, inclusive.
        
        The alpha channel is 255 by default, unless otherwise specified."""        
        self.red = r
        self.green = g
        self.blue = b
        self.alpha = a
            
    def __eq__(self, other):
        """Returns: True if self and other are equivalent RGB colors. """
        return (type(other) == RGB and self.red == other.red and 
                self.green == other.green and self.blue == other.blue and
                self.alpha == other.alpha)

    def __ne__(self, other):
        """Returns: True if self and other are not equivalent RGB colors. """
        return (type(other) != RGB or self.red != other.red or 
                self.green != other.green or self.blue != other.blue or
                self.alpha != other.alpha)

    def __str__(self):
        """Returns: Readable string representation of this color. """
        return "("+str(self.red)+","+str(self.green)+","+str(self.blue)+","+str(self.alpha)+")"

    def __repr__(self):
        """Returns: Unambiguous String representation of this color. """
        return "(red="+str(self.red)+",green="+str(self.green)+",blue="+str(self.blue)+",alpha="+str(self.alpha)+")"

    def glColor(self):
        """**Returns**: 4 element list of the attributes in the range 0 to 1
        
        This is a conversion of this object into a format that can be used in
        openGL graphics"""
        return [self.red/255.0, self.green/255.0, self.blue/255.0, self.alpha/255.0]

    def turtleColor(self):
        """**Returns**: 3 element tuple of the attributes in the range 0 to 1
        
        This is a conversion of this object into a format that can be used by
        the drawing turtle."""
        return (self.red/255.0, self.green/255.0, self.blue/255.0)


class CMYK(object):
    """An instance is a CMYK color value."""
    
    @property
    def cyan(self):
        """The cyan channel.
        
        **Invariant**: Value must be a float between 0.0 and 100.0, inclusive."""
        return self._cyan
    
    @cyan.setter
    def cyan(self, value):
        assert (type(value) == int or type(value) == float), "value %s is not a number" % `value`
        if (value > 100.0):
            value = min(value,100.0) if value < 100.0+_epsilon else value
        if (value < 0.0):
            value = max(value,0.0) if value > -_epsilon else value
        assert (value >= 0.0 and value <= 100.0), "value %s is outside of range [0.0,100.0]" % `value`
        self._cyan = float(value)
    
    @cyan.deleter
    def cyan(self):
        del self._cyan

    @property
    def magenta(self):
        """The magenta channel.
        
        **Invariant**: Value must be a float between 0.0 and 100.0, inclusive."""
        return self._magenta
    
    @magenta.setter
    def magenta(self, value):
        assert (type(value) == int or type(value) == float), "value %s is not a number" % `value`
        if (value > 100.0):
            value = min(value,100.0) if value < 100.0+_epsilon else value
        if (value < 0.0):
            value = max(value,0.0) if value > -_epsilon else value
        assert (value >= 0.0 and value <= 100.0), "value %s is outside of range [0.0,100.0]" % `value`
        self._magenta = float(value)
        
    @magenta.deleter
    def magenta(self):
        del self._magenta     
    
    @property
    def yellow(self):
        """The yellow channel.
        
        **Invariant**: Value must be a float between 0.0 and 100.0, inclusive."""
        return self._yellow
    
    @yellow.setter
    def yellow(self, value):
        assert (type(value) == int or type(value) == float), "value %s is not a number" % `value`
        if (value > 100.0):
            value = min(value,100.0) if value < 100.0+_epsilon else value
        if (value < 0.0):
            value = max(value,0.0) if value > -_epsilon else value
        assert (value >= 0.0 and value <= 100.0), "value %s is outside of range [0.0,100.0]" % `value`
        self._yellow = float(value)
        
    @yellow.deleter
    def yellow(self):
        del self._yellow     
 
    @property
    def black(self):
        """The black channel.
        
        **Invariant**: Value must be a float between 0.0 and 100.0, inclusive."""
        return self._black
        
    @black.setter
    def black(self, value):
        assert (type(value) == int or type(value) == float), "value %s is not a number" % `value`
        if (value > 100.0):
            value = min(value,100.0) if value < 100.0+_epsilon else value
        if (value < 0.0):
            value = max(value,0.0) if value > -_epsilon else value
        assert (value >= 0 and value <= 255), "value %s is outside of range [0,255]" % `value`
        self._black = float(value)
            
    @black.deleter
    def black(self):
        del self._black     

    def __init__(self, c, m, y, k):
        """**Constructor**: creates a new CMYK color (c,m,y,k).
        
            :param c: initial cyan value
            **Precondition**: float between 0.0 and 100.0, inclusive.
        
            :param m: initial magenta value
            **Precondition**: float between 0.0 and 100.0, inclusive.
        
            :param y: initial yellow value
            **Precondition**: float between 0.0 and 100.0, inclusive.
        
            :param k: initial black value
            **Precondition**: float between 0.0 and 100.0, inclusive.
        
        No arguments are optional."""

        self.cyan = c
        self.magenta = m
        self.yellow = y
        self.black = k
    
    def __eq__(self, other):
        """Returns: True if self and other are equivalent CMYK colors. """
        return (type(other) == CMYK and self.cyan == other.cyan and 
                self.magenta == other.magenta and self.yellow == other.yellow and
                self.black == other.black)   

    def __ne__(self, other):
        """Returns: True if self and other are not equivalent CMYK colors. """
        return (type(other) != CMYK or self.cyan != other.cyan or 
                self.magenta != other.magenta or self.yellow != other.yellow or
                self.black != other.black)

    def __str__(self):
        """Returns: Readable String representation of this color. """
        return "("+str(self.cyan)+","+str(self.magenta)+","+str(self.yellow)+","+str(self.black)+")"

    def __repr__(self):
        """Returns: Unambiguous String representation of this color. """
        return "(cyan="+str(self.cyan)+",magenta="+str(self.magenta)+",yellow="+str(self.yellow)+",black="+str(self.black)+")"

class HSV(object):
    """An instance is a HSV color value."""

    @property
    def hue(self):
        """The hue channel.
        
        **Invariant**: Value must be a float between 0.0 and 360.0, not including 360.0."""
        return self._hue
       
    @hue.setter
    def hue(self, value):
        assert (type(value) == int or type(value) == float), "value %s is not a number" % `value`
        if (value < 0.0):
            value = max(value,0.0) if value > -_epsilon else value
        assert (value >= 0.0 and value < 360.0), "value %s is outside of range [0.0,360.0)" % `value`
        self._hue = float(value)
       
    @hue.deleter
    def hue(self):
        del self._hue 
    
    @property
    def saturation(self):
        """The staturation channel.
        
        **Invariant**: Value must be a float between 0.0 and 1.0, inclusive."""
        return self._saturation
    
    @saturation.setter
    def saturation(self, value):
        assert (type(value) == int or type(value) == float), "value %s is not a number" % `value`
        if (value > 1.0):
            value = min(value,1.0) if value < 100.0+_epsilon else value
        if (value < 0.0):
            value = max(value,0.0) if value > -_epsilon else value
        assert (value >= 0.0 and value <= 1.0), "value %s is outside of range [0.0,1.0]" % `value`
        self._saturation = float(value)
        
    @saturation.deleter
    def saturation(self):
        del self._saturation     
    
    @property
    def value(self):
        """The value channel.
        
        **Invariant**: Value must be a float between 0.0 and 1.0, inclusive."""
        return self._value
    
    @value.setter
    def value(self, val):
        assert (type(val) == int or type(val) == float), "value %s is not a number" % `val`
        if (val > 1.0):
            val = min(val,1.0) if value < 100.0+_epsilon else val
        if (val < 0.0):
            val = max(val,0.0) if value > -_epsilon else val
        assert (val >= 0.0 and val <= 1.0), "value %s is outside of range [0.0,1.0]" % `val`
        self._value = float(val)
        
    @value.deleter
    def value(self):
        del self._value     
        
    def __init__(self, h, s, v):
        """**Constructor**: creates a new HSV color (h,s,v).
            
            :param h: the initial hue
            **Precondition**: float between 0.0 and 360.0, not including including 360.0.
        
            :param s: the initial saturation 
            **Precondition**: float between 0.0 and 1.0, inclusive.
        
            :param v: the initial value
            **Precondition**: float between 0.0 and 1.0, inclusive.
        
        No arguments are optional."""
        self.hue = h
        self.saturation = s
        self.value = v
    
    def __eq__(self, other):
        """Returns: True if self and other are equivalent HSV colors. """
        return (type(other) == HSV and self.hue == other.hue and 
                self.saturation == other.saturation and self.value == other.value)
    
    def __ne__(self, other):
        """Returns: True if self and other are equivalent HSV colors. """
        return (type(other) != HSV or self.hue != other.hue or 
                self.saturation != other.saturation or self.value != other.value)

    def __str__(self):
        """Returns: Readable String representation of this color. """
        return "("+str(self.hue)+","+str(self.saturation)+","+str(self.value)+")"

    def __repr__(self):
        """Returns: Unambiguous String representation of this color. """
        return "(hue="+str(self.hue)+",saturation="+str(self.saturation)+",value="+str(self.value)+")"

    def glColor(self):
        """**Returns**: 4 element list of the equivalent rgba color.
        
        This method converts this object to an RGB object and then extracts
        a 4 element list with color values between 0 and 1. This is a conversion
        of this object into a format that can be used in openGL graphics"""
        rgb = colorsys.hsv_to_rgb(self.hue/360.0,self.saturation,self.value)
        return [rgb[0], rgb[1], rgb[2], 1.0]

    def turtleColor(self):
        """**Returns**: 3 element tuple of the equivalent rgb color.
        
        This method converts this object to an RGB object and then extracts
        a 3 element tuple with color values between 0 and 1. This is a conversion
        of this object into a format that can be used by the drawing turtle"""
        return colorsys.hsv_to_rgb(self.hue/360.0,self.saturation,self.value)


# Color Constants

#: The color white in the default sRGB space.
WHITE = RGB(255, 255, 255)

#: The color light gray in the default sRGB space.
LIGHT_GRAY = RGB(192, 192, 192)

#: The color gray in the default sRGB space.
GRAY = RGB(128, 128, 128)

#: The color dark gray in the default sRGB space.
DARK_GRAY = RGB(64, 64, 64)

#: The color black in the default sRGB space.
BLACK = RGB(0, 0, 0)

#: The color red, in the default sRGB space.
RED = RGB(255, 0, 0)

#: The color pink in the default sRGB space.
PINK = RGB(255, 175, 175)

#: The color orange in the default sRGB space.
ORANGE = RGB(255, 200, 0)

#: The color yellow in the default sRGB space.
YELLOW = RGB(255, 255, 0)

#: The color green in the default sRGB space.
GREEN = RGB(0, 255, 0);

#: The color magenta in the default sRGB space.
MAGENTA = RGB(255, 0, 255)

#: The color cyan in the default sRGB space.
CYAN = RGB(0, 255, 255)

#: The color blue in the default sRGB space.
BLUE = RGB(0, 0, 255)