import pwmio
from math import e, exp, pi, sin

from kmk.extensions import Extension, InvalidExtensionEnvironment
from kmk.keys import make_argumented_key, make_key
from kmk.utils import clamp


class LEDKeyMeta:
    def __init__(self, *leds):
        self.leds = leds
        self.brightness = None


class AnimationModes:
    OFF = 0
    STATIC = 1
    STATIC_STANDBY = 2
    BREATHING = 3
    USER = 4


class LED(Extension):
    def __init__(
        self,
        led_pin,
        brightness=50,
        brightness_step=5,
        brightness_limit=100,
        breathe_center=1.5,
        animation_mode=AnimationModes.STATIC,
        animation_speed=1,
        user_animation=None,
        val=100,
    ):
        try:
            pins_iter = iter(led_pin)
        except TypeError:
            pins_iter = [led_pin]

        try:
            self._leds = [pwmio.PWMOut(pin) for pin in pins_iter]
        except Exception as e:
            print(e)
            raise InvalidExtensionEnvironment(
                'Unable to create pwmio.PWMOut() instance with provided led_pin'
            )

        self._brightness = brightness
        self._pos = 0
        self._effect_init = False
        self._enabled = True

        self.brightness_step = brightness_step
        self.brightness_limit = brightness_limit
        self.animation_mode = animation_mode
        self.animation_speed = animation_speed
        self.breathe_center = breathe_center
        self.val = val

        if user_animation is not None:
            self.user_animation = user_animation

        make_argumented_key(
            names=('LED_TOG',),
            validator=self._led_key_validator,
            on_press=self._key_led_tog,
        )
        make_argumented_key(
            names=('LED_INC',),
            validator=self._led_key_validator,
            on_press=self._key_led_inc,
        )
        make_argumented_key(
            names=('LED_DEC',),
            validator=self._led_key_validator,
            on_press=self._key_led_dec,
        )
        make_argumented_key(
            names=('LED_SET',),
            validator=self._led_set_key_validator,
            on_press=self._key_led_set,
        )
        make_key(names=('LED_ANI',), on_press=self.METHOD_NAME)
        make_key(names=('LED_AND',), on_press=self._key_led_and)
        make_key(
            names=('LED_MODE_PLAIN', 'LED_M_P'), on_press=self._key_led_mode_static
        )
        make_key(
            names=('LED_MODE_BREATHE', 'LED_M_B'), on_press=self._key_led_mode_breathe
        )

    def __repr__(self):
        return f'LED({self._to_dict()})'

    def _to_dict(self):
        return {
            '_brightness': self._brightness,
            '_pos': self._pos,
            'brightness_step': self.brightness_step,
            'brightness_limit': self.brightness_limit,
            'animation_mode': self.animation_mode,
            'animation_speed': self.animation_speed,
            'breathe_center': self.breathe_center,
            'val': self.val,
        }

    def on_runtime_enable(self, sandbox):
        return

    def on_runtime_disable(self, sandbox):
        return

    def during_bootup(self, sandbox):
        return

    def before_matrix_scan(self, sandbox):
        return

    def after_matrix_scan(self, sandbox):
        return

    def before_hid_send(self, sandbox):
        return

    def after_hid_send(self, sandbox):
        self.animate()

    def on_powersave_enable(self, sandbox):
        return

    def on_powersave_disable(self, sandbox):
        return

    def _init_effect(self):
        self._pos = 0
        self._effect_init = False
        return self

    def set_brightness(self, percent, leds=None):
        leds = leds or range(0, len(self._leds))
        for i in leds:
            self._leds[i].duty_cycle = int(percent / 100 * 65535)

    def step_brightness(self, step, leds=None):
        leds = leds or range(0, len(self._leds))
        for i in leds:
            brightness = int(self._leds[i].duty_cycle / 65535 * 100) + step
            self.set_brightness(clamp(brightness), [i])

    def increase_brightness(self, step=None, leds=None):
        if step is None:
            step = self.brightness_step
        self.step_brightness(step, leds)

    def decrease_brightness(self, step=None, leds=None):
        if step is None:
            step = self.brightness_step
        self.step_brightness(-step, leds)

    def off(self):
        self.set_brightness(0)

    def increase_ani(self):
        '''
        Increases animation speed by 1 amount stopping at 10
        :param step:
        '''
        if (self.animation_speed + 1) >= 10:
            self.animation_speed = 10
        else:
            self.val += 1

    def decrease_ani(self):
        '''
        Decreases animation speed by 1 amount stopping at 0
        :param step:
        '''
        if (self.val - 1) <= 0:
            self.val = 0
        else:
            self.val -= 1

    def effect_breathing(self):
        # http://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/
        # https://github.com/qmk/qmk_firmware/blob/9f1d781fcb7129a07e671a46461e501e3f1ae59d/quantum/rgblight.c#L806
        sined = sin((self._pos / 255.0) * pi)
        multip_1 = exp(sined) - self.breathe_center / e
        multip_2 = self.brightness_limit / (e - 1 / e)

        self._brightness = int(multip_1 * multip_2)
        self._pos = (self._pos + self.animation_speed) % 256
        self.set_brightness(self._brightness)

    def effect_static(self):
        self.set_brightness(self._brightness)
        # Set animation mode to standby to prevent cycles from being wasted
        self.animation_mode = AnimationModes.STATIC_STANDBY

    def animate(self):
        '''
        Activates a "step" in the animation based on the active mode
        :return: Returns the new state in animation
        '''
        if self._effect_init:
            self._init_effect()
        if self._enabled:
            if self.animation_mode == AnimationModes.BREATHING:
                return self.effect_breathing()
            elif self.animation_mode == AnimationModes.STATIC:
                return self.effect_static()
            elif self.animation_mode == AnimationModes.STATIC_STANDBY:
                pass
            elif self.animation_mode == AnimationModes.USER:
                return self.user_animation(self)
            else:
                self.off()

    def _led_key_validator(self, *leds):
        if leds:
            for led in leds:
                assert self._leds[led]
        return LEDKeyMeta(*leds)

    def _led_set_key_validator(self, brightness, *leds):
        meta = self._led_key_validator(*leds)
        meta.brightness = brightness
        return meta

    def _key_led_tog(self, *args, **kwargs):
        if self.animation_mode == AnimationModes.STATIC_STANDBY:
            self.animation_mode = AnimationModes.STATIC

        if self._enabled:
            self.off()
        self._enabled = not self._enabled

    def _key_led_inc(self, key, *args, **kwargs):
        self.increase_brightness(leds=key.meta.leds)

    def _key_led_dec(self, key, *args, **kwargs):
        self.decrease_brightness(leds=key.meta.leds)

    def _key_led_set(self, key, *args, **kwargs):
        self.set_brightness(percent=key.meta.brightness, leds=key.meta.leds)

    def METHOD_NAME(self, *args, **kwargs):
        self.increase_ani()

    def _key_led_and(self, *args, **kwargs):
        self.decrease_ani()

    def _key_led_mode_static(self, *args, **kwargs):
        self._effect_init = True
        self.animation_mode = AnimationModes.STATIC

    def _key_led_mode_breathe(self, *args, **kwargs):
        self._effect_init = True
        self.animation_mode = AnimationModes.BREATHING