class ColorUtils(object):
    """
    Various helpers and utilities for working with colors
    """

    @staticmethod
    def _circular_interp(start, end, amount: float) -> tuple:
        h = lerp_degrees(start[0], end[0], amount)
        s = lerp(start[1], end[1], amount)
        v = lerp(start[2], end[2], amount)
        a1 = a2 = 1.0
        if len(start) > 3:
            a1 = start[3]
        if len(end) > 3:
            a2 = end[3]
        a = lerp(a1, a2, amount)
        return (h, s, v, a)


    @staticmethod
    def hue_gradient(start: float=0.0, length: int=360) -> list:
        """
        Generate a gradient which spans all hues

        :param start: starting hue
        :param length: number of colors which should be produced
        :return: list of colors
        """
        step = 360 / length
        return [Color.NewFromHsv((start + (step * x)) % 360, 1, 1) for x in range(0, length)]


    @staticmethod
    def _hsva(color: Color) -> Color:
        return (*rgb_to_hsluv(color.rgb), color.alpha)


    @staticmethod
    @colorarg
    def hsv_gradient(color1: ColorType, color2: ColorType, steps: int) -> list:
        """
        Generate a gradient between two points in HSV colorspace

        :param color1: Starting color
        :param color2: Ending color
        :param steps: Number of steps in the gradient
        :param loop: If the gradient should "loop" back around to it's starting point

        :return: List of colors in the gradient
        """
        start = ColorUtils._hsva(color1)
        end = ColorUtils._hsva(color2)

        gradient = []
        for x in range(0, steps):
            amount = float(x) / float(steps - 1)
            i = ColorUtils._circular_interp(start, end, amount)
            gradient.append(Color.NewFromRgb(*hsluv_to_rgb([i[0], i[1], i[2]]), i[3]))

        return gradient


    @staticmethod
    def gradient(length: int, *colors, loop=True) -> list:
        """
        Generate a looped gradient from multiple evenly-spaced colors

        Uses the new HSLUV colorspace
        :param length: Total number of entries in the final gradient
        :param colors: Color stops, varargs

        :return: List of colors in the gradient
        """

        luv_colors = [rgb_to_hsluv(to_color(x).rgb) for x in colors]
        if loop:
            luv_colors.append(luv_colors[0])

        steps = max(len(luv_colors), math.floor(length / (len(luv_colors) - 1)))
        gradient = []
        for color_idx in range(0, len(luv_colors) - 1):
            start = luv_colors[color_idx]
            end = luv_colors[(color_idx + 1)]

            for interp in range(0, steps):
                amount = float(interp) / float(steps)
                i = ColorUtils._circular_interp(start, end, amount)
                gradient.append(Color.NewFromRgb(*hsluv_to_rgb([i[0], i[1], i[2]]), i[3]))

        return gradient


    @staticmethod
    def color_generator(gradient: list, randomize: bool=False,
                        alternate: bool=False, rgb: bool=False):
        """
        Create a generator which returns colors from the given gradient,
        optionally randomizing, or moving across the gradient in alternating
        directions.

        :param gradient: a list of colors
        :param randomize: true if colors should be chosen randomly instead of
                          sequentially
        :param alternate: true if two iterators should move sequentially
                          thru the gradient in opposite directions
        :param rgb: true if RGB int tuples should be returned
        :return: generator
        """
        grad = gradient[:]

        if not randomize:
            grad.extend(grad[::-1])

        if rgb:
            grad = [to_rgb(x) for x in grad]

        if randomize:
            while True:
                yield random.choice(grad)
        else:
            cycle = itertools.cycle(grad)
            cycle2 = None
            if alternate:
                mid = int(len(grad) / 2)
                grad2 = grad[mid:]
                grad2.extend(grad[:mid])
                cycle2 = itertools.cycle(grad2)

            while True:
                yield next(cycle)
                if alternate:
                    yield next(cycle2)


    @staticmethod
    @colorarg
    def color_scheme(color: ColorType=None, base_color: ColorType=None, steps: int=11):
        """
        Generator which produces a continuous stream of colors based on a
        color scheme of two overlapping colors.

        :param color: The "top" color
        :param base_color: The base or bg_color color
        :param randomize: True if values should be chosen at random instead of sequential
        :param steps: Number of steps used for the gradient
        :param rgb: True if we should return RGB tuples

        :return: generator
        """
        c0 = c1 = None
        if base_color is not None and color is not None:
            c0, c1 = color.AnalogousScheme(angle=15)

        elif base_color is not None:
            c0, c1 = ColorUtils.increase_contrast(base_color).TriadicScheme(angle=150)

        return ColorUtils.gradient(steps, c0, c1)


    @staticmethod
    @colorarg
    def scheme_generator(color: ColorType=None, base_color: ColorType=None, randomize: bool=False,
                         alternate: bool=False, steps: int=11, rgb: bool=False):
        """
        Generator which produces a continuous stream of colors based on a
        color scheme of two overlapping colors.

        :param color: The "top" color
        :param base_color: The base or bg_color color
        :param randomize: True if values should be chosen at random instead of sequential
        :param steps: Number of steps used for the gradient
        :param rgb: True if we should return RGB tuples

        :return: generator
        """
        gradient = ColorUtils.color_scheme(color, base_color, steps)
        return ColorUtils.color_generator(gradient, randomize, alternate, rgb)


    @staticmethod
    def interference(length, freq1: float=0.3, freq2: float=0.3, freq3: float=0.3,
                     phase1: float=0.0, phase2: float=2.0, phase3: float=4.0,
                     center: float=128.0, width: float=127.0):
        """
        Creates an interference pattern of three sine waves
        """
        phase1 = phase1 * math.pi / 3
        phase2 = phase2 * math.pi / 3
        phase3 = phase3 * math.pi / 3

        center /= 255.0
        width /= 255.0

        gradient = []

        for i in range(0, length):
            r = math.sin(freq1 * i + phase1) * width + center
            g = math.sin(freq2 * i + phase2) * width + center
            b = math.sin(freq3 * i + phase3) * width + center
            gradient.append(Color.NewFromRgb(r, g, b))

        return gradient


    @staticmethod
    def rainbow_generator(randomize: bool=False, alternate: bool=False,
                          steps: int=33, rgb: bool=False):
        """
        Generate a smooth rainbow gradient

        :param steps: Length or smoothness of the gradient
        :param rgb: True if RGB tuples should be generated
        :param alternate: If alternating values (moving in opposite directions
                          should be returned on subsequent calls

        :return: generator
        """
        gradient = ColorUtils.interference(steps)

        return ColorUtils.color_generator(gradient, randomize, alternate, rgb)


    @staticmethod
    def random_generator(rgb: bool=False):
        """
        Generate random colors using the golden ratio conjugate

        :param rgb: True if RGB tuples should be generated

        :return: generator:
        """
        golden_ratio_conjugate = (1 + math.sqrt(5)) / 2
        hue = random.random()
        c0 = Color.NewFromHsv(0, 1.0, 1.0)
        while True:
            hue += golden_ratio_conjugate
            hue %= 1
            value = c0.ColorWithHue(hue * 360)
            if rgb:
                yield to_rgb(value)
            else:
                yield value


    @staticmethod
    @colorarg
    def luminance(color: ColorType) -> float:
        """
        Calculate the relative luminance (as defined by WCAG 2.0) of
        the given color.

        :param color: a color
        :return: the calculated relative luminance between 0.0 and 10
        """
        rgb = color.rgb
        vals = []
        for c in rgb:
            if c <= 0.03928:
                c /= 12.92
            else:
                c = math.pow((c + 0.055) / 1.055, 2.4)
            vals.append(c)
        L = 0.2126 * vals[0] + 0.7152 * vals[1] + 0.0722 * vals[2]
        return L


    @staticmethod
    @colorarg
    def contrast_ratio(color1: ColorType, color2: ColorType) -> float:
        """
        Calculate the contrast ratio (as defined by WCAG 2.0) between
        two colors. If the two colors have the same relative luminance,
        the result is 1.0. For black/white, the result is 21.0.

        :param color1: a color
        :param color2: a color
        :return: the calculated contrast ratio
        """
        ratio = (0.05 + ColorUtils.luminance(color1)) / \
                (0.05 + ColorUtils.luminance(color2))
        if ratio < 1:
            return 1 / ratio
        return ratio


    @staticmethod
    @colorarg
    def inverse(color: ColorType) -> float:
        """
        Get the RGB inverse of this color (1 - component)

        :param color: a color
        :return: Inverse of the given color
        """
        rgb = color.rgb
        return Color.NewFromRgb(1.0 - rgb[0], 1.0 - rgb[1], 1.0 - rgb[2], color.alpha)


    @staticmethod
    @colorarg
    def increase_contrast(color: ColorType) -> Color:
        """
        Performs contrast inversion if a hue rotation would result in
        white-on-white or black-on-black.

        :param color: The color to check

        :return: The new color with adjusted contrast
        """
        hsl = list(color.hsl)
        if hsl[2] < 0.1 or hsl[2] > 0.7:
            hsl[2] = 1.0 - hsl[2]
            color = Color.NewFromHsl(*hsl, color.alpha)
        return color


    @staticmethod
    @colorarg
    def rgba2rgb(arr: np.ndarray, bg_color: ColorType=None) -> np.ndarray:
        """
        Alpha-composites data in the numpy array against the given
        background color and returns a new buffer without the
        alpha component.

        :param arr: The input array of RGBA data
        :param bg_color: The background color
        :param out_buf: Optional buffer to render into

        :return: Array of composited RGB data
        """
        if bg_color is None:
            bg_color = np.array([0.0, 0.0, 0.0, 1.0])
        else:
            bg_color = np.array(tuple(bg_color), dtype=np.float)

        alpha = arr[..., -1]
        channels = arr[..., :-1]

        out_buf = np.empty_like(channels)

        for ichan in range(channels.shape[-1]):
            out_buf[..., ichan] = np.clip(
                (1 - alpha) * bg_color[ichan] + alpha * channels[..., ichan],
                a_min=0, a_max=1)

        return dtype.img_as_ubyte(out_buf) class ColorSchemeTrait(List):
    """
    A list of ColorTraits which comprise a scheme
    """
    info_text = 'a list of colors'

    def __init__(self, trait=ColorTrait(), default_value=(),
                 minlen=0, maxlen=sys.maxsize, **kwargs):
        super(ColorSchemeTrait, self).__init__(trait=trait, default_value=default_value,
                                               minlen=minlen, maxlen=maxlen, **kwargs) class ColorTrait(TraitType):
    """
    A traitlet which encapsulates a grapefruit.Color and performs
    type coercion as needed.
    """
    info_text = "a color"
    allow_none = True
    default_value = 'black'

    def __init__(self, *args, **kwargs):
        super(ColorTrait, self).__init__(*args, **kwargs)

    def validate(self, obj, value):
        try:
            if value is not None:
                value = to_color(value)
        except:
            self.error(obj, value)
        return value class UseEnumCaseless(UseEnum):
    """
    Subclass of UseEnum which allows selection of values using
    case insensitive strings
    """

    def select_by_name(self, value, default=Undefined):
        if value.startswith(self.name_prefix):
            # -- SUPPORT SCOPED-NAMES, like: "Color.red" => "red"
            value = value.replace(self.name_prefix, "", 1)

        keys = [x.lower() for x in self.enum_class.__members__.keys()]
        idx = keys.index(value.lower())
        if idx < 0:
            return Undefined
        return self.enum_class[list(self.enum_class.__members__.keys())[idx]] class BaseFX(HasTraits):

    # meta
    hidden = Bool(default_value=False, read_only=True)
    description = Unicode('_unimplemented_', read_only=True)


    def __init__(self, fxmod, driver, *args, **kwargs):
        super(BaseFX, self).__init__(*args, **kwargs)
        self._fxmod = fxmod
        self._driver = driver

    @abstractmethod
    def apply(self) -> bool:
        return False class FXModule:
    def __init__(self, driver):
        self._driver = driver
        self._available_fx = frozendict(self._load_fx())


    def _load_fx(self) -> dict:
        fx = {}
        for k, v in inspect.getmembers(self.__class__, \
                lambda x: inspect.isclass(x) and issubclass(x, BaseFX)):
            key = camel_to_snake(re.sub(r'FX$', '', k))
            if key in [item.lower() for item in self._driver.hardware.supported_fx]:
                fx[key] = v

        return fx


    @property
    def available_fx(self):
        return self._available_fx


    def create_fx(self, fx_name) -> BaseFX:
        fx_name = fx_name.lower()
        if fx_name not in self._available_fx:
            return None
        return self._available_fx[fx_name](self, self._driver) class Hardware(BaseHardware):
    """
    Static hardware configuration data

    Loaded by Configuration from YAML.
    """

    class Type(Enum):
        HEADSET = 'Headset'
        KEYBOARD = 'Keyboard'
        KEYPAD = 'Keypad'
        LAPTOP = 'Laptop'
        MOUSE = 'Mouse'
        MOUSEPAD = 'Mousepad'

    @property
    def has_matrix(self) -> bool:
        """
        True if the device has an addressable key matrix
        """
        return self.dimensions is not None and self.dimensions.x > 1 and self.dimensions.y > 1


    def has_quirk(self, *quirks) -> bool:
        """
        True if quirk is required for the device

        :param: quirks The quirks to check (varargs)

        :return: True if the quirk is required
        """
        if self.quirks is None:
            return False

        for quirk in quirks:
            if isinstance(self.quirks, (list, tuple)) and quirk in self.quirks:
                return True
            if self.quirks == quirk:
                return True

        return False


    @classmethod
    def get_type(cls, hw_type) -> 'Hardware':
        if hw_type is None:
            return None

        config_path = os.path.join(os.path.dirname(__file__), 'data')
        yaml_path = os.path.join(config_path, '%s.yaml' % hw_type.name.lower())

        config = cls.load_yaml(yaml_path)
        assert config is not None

        return config


    @classmethod
    def _get_device(cls, product_id: int, hw_type) -> 'Hardware':
        if product_id is None:
            return None

        config = cls.get_type(hw_type)

        result = config.search('product_id', product_id)
        if not result:
            return None

        if isinstance(result, list) and len(result) == 1:
            return result[0]

        return result


    @classmethod
    def get_device(cls, product_id: int, hw_type=None) -> 'Hardware':
        if hw_type is not None:
            return cls._get_device(product_id, hw_type)

        for hw in Hardware.Type:
            device = cls._get_device(product_id, hw)
            if device is not None:
                return device

        return None


    def _yaml_header(self) -> str:
        header = '#\n#  uChroma device configuration\n#\n'
        if self.name is not None:
            header += '#  Model: %s (%s)\n' % (self.name, self.type.value)
        elif self.type is not None:
            header += '#  Type: %s\n' % self.type.name.title()
        header += '#  Updated on: %s\n' % datetime.now().isoformat(' ')
        header += '#\n'

        return header class Quirks(IntEnum):
    """
    Various "quirks" that are found across hardware models.
    """

    # Always use transaction code 0x3F
    TRANSACTION_CODE_3F = 1

    # Use "extended" commands
    EXTENDED_FX_CMDS = 2

    # Control device brightness with the scroll wheel LED
    SCROLL_WHEEL_BRIGHTNESS = 3

    # Device has charge and dock controls
    WIRELESS = 4

    # Needs transaction code 0x80 for custom frame data
    CUSTOM_FRAME_80 = 5

    # Control device brightness with the logo LED
    LOGO_LED_BRIGHTNESS = 6

    # Device has individual "profile" LEDs
    PROFILE_LEDS = 7

    # Device only supports spectrum effect on the backlight LED
    BACKLIGHT_LED_FX_ONLY = 8 class BaseCommand(Enum):
    """
    Base class for Command enumerations

    Tuples, in the form of:
        (command_class, command_id, data_length)
    """ class LEDType(Enum):
    """
    Enumeration of LED types

    All types not available on all devices.
    """
    SCROLL_WHEEL = (0x01, {'rgb': True, 'has_modes': True})
    MISC = (0x02, {'rgb': True, 'has_modes': True})
    BATTERY = (0x03, {'rgb': True})
    LOGO = (0x04, {})
    BACKLIGHT = (0x05, {})
    MACRO = (0x07, {'rgb': True, 'has_modes': True})
    GAME = (0x08, {'rgb': True, 'has_modes': True})
    PROFILE_RED = (0x0E, {'rgb': True, 'color': 'red'})
    PROFILE_GREEN = (0x0C, {'rgb': True, 'color': 'green'})
    PROFILE_BLUE = (0x0D, {'rgb': True, 'color': 'blue'})

    def __init__(self, hwid, caps):
        self._hardware_id = hwid
        self._rgb = caps.get('rgb', False)
        self._color = Color.NewFromHtml(caps.get('color', 'green'))
        self._has_modes = caps.get('has_modes', False)

    @property
    def hardware_id(self) -> int:
        return self._hardware_id

    @property
    def rgb(self) -> bool:
        return self._rgb

    @property
    def default_color(self) -> Color:
        return self._color

    @property
    def has_modes(self) -> bool:
        return self._has_modes
from enum import Enum
from traitlets import Bool, Int, Unicode
from uchroma.color import ColorUtils
from uchroma.traits import ColorSchemeTrait, ColorTrait, UseEnumCaseless
from .fx import BaseFX, FXModule
from .hardware import Hardware, Quirks
from .types import BaseCommand, LEDType
    class ReactiveFX(BaseFX):
        description = Unicode('Keys light up when pressed')
        color = ColorTrait(default_value='skyblue').tag(config=True)
        speed = Int(1, min=1, max=4).tag(config=True)

        def apply(self) -> bool:
            """
            Lights up keys when they are pressed

            :param color: Color of lighting when keys are pressed
            :param speed: Speed (1-4) at which the keys should fade out

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.REACTIVE, self.speed, self.color)


    class SweepFX(BaseFX):
        description = Unicode('Colors sweep across the device')
        color = ColorTrait(default_value='green').tag(config=True)
        base_color = ColorTrait().tag(config=True)
        speed = Int(default_value=15, min=1, max=30).tag(config=True)
        direction = UseEnumCaseless(enum_class=Direction, \
                default_value=Direction.RIGHT).tag(config=True)

        def apply(self) -> bool:
            """
            Produces colors which sweep across the device

            :param color: The color to sweep with (defaults to light blue)
            :param base_color: The base color for the effect (defaults to black)
            :param direction: The direction for the sweep
            :param speed: Speed of the sweep
            :param preset: Predefined color pair to use. Invalid with color/base_color.

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.SWEEP, self.direction, self.speed,
                                          self.base_color, self.color)


    class MorphFX(BaseFX):
        description = Unicode('Morphing colors when keys are pressed')
        color = ColorTrait(default_value='magenta').tag(config=True)
        base_color = ColorTrait(default_value='darkblue').tag(config=True)
        speed = Int(default_value=2, min=1, max=4).tag(config=True)

        def apply(self) -> bool:
            """
            A "morphing" color effect when keys are pressed

            :param color: The color when keys are pressed (defaults to magenta)
            :param base_color: The base color for the effect (defaults to blue)
            :param speed: Speed of the sweep
            :param preset: Predefined color pair to use. Invalid with color/base_color.

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.MORPH, 0x04, self.speed, self.color, self.base_color)


    class FireFX(BaseFX):
        description = Unicode('Keys on fire')
        color = ColorTrait(default_value='red').tag(config=True)
        speed = Int(default_value=0x40, min=0x10, max=0x80).tag(config=True)

        def apply(self) -> bool:
            """
            Animated fire!

            :param color: Color scheme of the fire
            :param speed: Speed of the fire

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.FIRE, 0x01, self.speed, self.color)


    class RippleFX(BaseFX):
        description = Unicode('Ripple effect when keys are pressed')
        color = ColorTrait(default_value='green').tag(config=True)
        speed = Int(default_value=3, min=1, max=8).tag(config=True)

        def apply(self) -> bool:
            return self._fxmod.set_effect(FX.RIPPLE, 0x01, self.speed * 10, self.color)


    class RippleSolidFX(BaseFX):
        description = Unicode('Ripple effect on a solid background')
        color = ColorTrait(default_value='green').tag(config=True)
        speed = Int(default_value=3, min=1, max=8).tag(config=True)

        def apply(self) -> bool:
            return self._fxmod.set_effect(FX.RIPPLE_SOLID, 0x01, self.speed * 10, self.color)



    class StarlightFX(BaseFX):
        description = Unicode('Keys sparkle with color')
        colors = ColorSchemeTrait(minlen=0, maxlen=2).tag(config=True)
        speed = Int(default_value=1, min=1, max=4).tag(config=True)

        def apply(self) -> bool:
            """
            Activate the "starlight" effect. Colors sparkle across the device.

            This effect allows up to two (optional) colors. If no colors are
            specified, random colors will be used.

            :param color1: First color
            :param color2: Second color
            :param speed: Speed of the effect
            :param preset: Predefined color pair, invalid when color1/color2 is supplied

            :return: True if successful
            """
            return self._fxmod.set_effect(FX.STARLIGHT, Mode(len(self.colors)),
                                          self.speed, *self.colors)



    class BreatheFX(BaseFX):