#
# uchroma - Copyright (C) 2017 Steve Kondik
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, version 3.
#
# 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 Lesser General Public
# License for more details.
#
from collections import OrderedDict
from enum import Enum

from traitlets import Bool, Float, HasTraits, observe
from grapefruit import Color

from uchroma.color import to_color
from uchroma.traits import ColorTrait, UseEnumCaseless, WriteOnceUseEnumCaseless
from uchroma.util import scale_brightness, Signal

from .types import BaseCommand, LEDType


NOSTORE = 0
VARSTORE = 1


class LEDMode(Enum):
    """
    Enumeration of LED effect modes
    """
    STATIC = 0x00
    BLINK = 0x01
    PULSE = 0x02
    SPECTRUM = 0x04


class LED(HasTraits):
    led_type = WriteOnceUseEnumCaseless(enum_class=LEDType)
    state = Bool(default_value=False, allow_none=False)

    """
    Control individual LEDs which may be present on a device

    This class should not need to be instantiated directly, the
    correct singleton instance is obtained by calling the
    get_led() method of UChromaDevice.

    The API does not yet support listing the LED types actually
    available on a device.
    """

    class Command(BaseCommand):
        """
        Commands used by this class
        """
        SET_LED_STATE = (0x03, 0x00, 0x03)
        SET_LED_COLOR = (0x03, 0x01, 0x05)
        SET_LED_MODE = (0x03, 0x02, 0x03)
        SET_LED_BRIGHTNESS = (0x03, 0x03, 0x03)

        GET_LED_STATE = (0x03, 0x80, 0x03)
        GET_LED_COLOR = (0x03, 0x81, 0x05)
        GET_LED_MODE = (0x03, 0x82, 0x03)
        GET_LED_BRIGHTNESS = (0x03, 0x83, 0x03)


    class ExtendedCommand(BaseCommand):
        SET_LED_BRIGHTNESS = (0x0F, 0x04, 0x03)
        GET_LED_BRIGHTNESS = (0x0F, 0x84, 0x03)


    def __init__(self, driver, led_type: LEDType, *args, **kwargs):
        super(LED, self).__init__(*args, **kwargs)
        self._driver = driver
        self._led_type = led_type
        self._logger = driver.logger
        self.led_type = led_type
        self._restoring = True
        self._refreshing = False
        self._dirty = True

        # dynamic traits, since they are normally class-level
        brightness = Float(min=0.0, max=100.0, default_value=80.0,
                           allow_none=False).tag(config=True)
        color = ColorTrait(default_value=led_type.default_color,
                           allow_none=False).tag(config=led_type.rgb)
        mode = UseEnumCaseless(enum_class=LEDMode, default_value=LEDMode.STATIC,
                               allow_none=False).tag(config=led_type.has_modes)

        self.add_traits(color=color, mode=mode, brightness=brightness)

        self._restoring = False


    def __getattribute__(self, name):
        if name in ('brightness', 'color', 'mode', 'state') and self._dirty:
            self._refresh()
            self._dirty = False

        return super().__getattribute__(name)


    def _get(self, cmd):
        return self._driver.run_with_result(cmd, VARSTORE, self._led_type.hardware_id)


    def _set(self, cmd, *args):
        return self._driver.run_command(cmd, *(VARSTORE, self._led_type.hardware_id) + args, delay=0.035)


    def _get_brightness(self):
        if self._driver.has_quirk(Quirks.EXTENDED_FX_CMDS):
            return self._get(LED.ExtendedCommand.GET_LED_BRIGHTNESS)
        return self._get(LED.Command.GET_LED_BRIGHTNESS)


    def _set_brightness(self, value):
        if self._driver.has_quirk(Quirks.EXTENDED_FX_CMDS):
            self._set(LED.ExtendedCommand.SET_LED_BRIGHTNESS, value)
        else:
            self._set(LED.Command.SET_LED_BRIGHTNESS, value)


    def _refresh(self):
        try:
            self._refreshing = True

            # state
            value = self._get(LED.Command.GET_LED_STATE)
            if value is not None:
                self.state = bool(value[2])

            # color
            value = self._get(LED.Command.GET_LED_COLOR)
            if value is not None:
                self.color = Color.NewFromRgb(value[2] / 255.0,
                                              value[3] / 255.0,
                                              value[4] / 255.0)

            # mode
            value = self._get(LED.Command.GET_LED_MODE)
            if value is not None:
                self.mode = LEDMode(value[2])

            # brightness
            value = self._get_brightness()
            if value is not None:
                self.brightness = scale_brightness(int(value[2]), True)

        finally:
            self._refreshing = False


    @observe('color', 'mode', 'state', 'brightness')
    def _observer(self, change):
        if self._refreshing or change.old == change.new:
            return

        if change.name == 'color':
            self._set(LED.Command.SET_LED_COLOR, to_color(change.new))
        elif change.name == 'mode':
            self._set(LED.Command.SET_LED_MODE, change.new)
        elif change.name == 'brightness':
            self._set_brightness(scale_brightness(change.new))
            if change.old == 0 and change.new > 0:
                self._set(LED.Command.SET_LED_STATE, 1)
            elif change.old > 0 and change.new == 0:
                self._set(LED.Command.SET_LED_STATE, 0)
        else:
            raise ValueError("Unknown LED property: %s" % change.new)

        if not self._restoring:
            self._dirty = True

            if self.led_type != LEDType.BACKLIGHT:
                self._update_prefs()


    def __str__(self):
        values = ', '.join('%s=%s' % (k, getattr(self, k)) \
                for k in ('led_type', 'state', 'brightness', 'color', 'mode'))
        return 'LED(%s)' % values


    def _update_prefs(self):
        prefs = OrderedDict()
        if self._driver.preferences.leds is not None:
            prefs.update(self._driver.preferences.leds)

        prefs[self.led_type.name.lower()] = self.get_values()
        self._driver.preferences.leds = prefs


    def get_values(self) -> dict:
        tdict = OrderedDict()
        for attr in sorted([k for k, v in self.traits().items() \
                            if v.metadata.get('config', False)]):
            tdict[attr] = getattr(self, attr)
        return tdict


    def set_values(self, values: dict):
        self._restoring = True
        for key, value in values.items():
            setattr(self, key, value)
        self._restoring = False


    __repr__ = __str__


class LEDManager:
    def __init__(self, driver):
        self._driver = driver
        self._leds = {}

        self.led_changed = Signal()

        driver.restore_prefs.connect(self._restore_prefs)


    @property
    def supported_leds(self):
        return self._driver.hardware.supported_leds


    def get(self, led_type: LEDType) -> LED:
        """
        Fetches the requested LED interface on this device

        :param led_type: The LED type to fetch

        :return: The LED interface, if available
        """
        if led_type not in self._driver.supported_leds:
            return None

        if led_type not in self._leds:
            self._leds[led_type] = LED(self._driver, led_type)
            self._leds[led_type].observe(self._led_changed)

        return self._leds[led_type]


    def _led_changed(self, change):
        self.led_changed.fire(change.owner)


    def _restore_prefs(self, prefs):
        led_prefs = prefs.leds

        for led_type in self.supported_leds:
            if led_type == LEDType.BACKLIGHT:
                # handled elsewhere
                continue

            key = led_type.name.lower()
            led = self.get(led_type)

            if led_prefs is not None and key in led_prefs:
                led.set_values(led_prefs[key])