# SPDX-FileCopyrightText: 2021 Sandy Macdonald
#
# SPDX-License-Identifier: MIT

"""
`Keybow 2040 CircuitPython library`
====================================================

CircuitPython driver for the Pimoroni Keybow 2040.

Drop the keybow2040.py file into your `lib` folder on your `CIRCUITPY` drive.

* Author: Sandy Macdonald

Notes
--------------------

**Hardware:**

* Pimoroni Keybow 2040
  <https://shop.pimoroni.com/products/keybow-2040>_

**Software and Dependencies:**

* Adafruit CircuitPython firmware for Keybow 2040:
  <https://circuitpython.org/board/pimoroni_keybow2040/>_

* Adafruit CircuitPython IS31FL3731 library:
  <https://github.com/adafruit/Adafruit_CircuitPython_IS31FL3731>_
"""

import time
import board
from adafruit_is31fl3731 import keybow2040
from digitalio import DigitalInOut, Direction, Pull

# These are the 16 switches on Keybow, with their board-defined names.
_PINS = [board.SW0,
        board.SW1,
        board.SW2,
        board.SW3,
        board.SW4,
        board.SW5,
        board.SW6,
        board.SW7,
        board.SW8,
        board.SW9,
        board.SW10,
        board.SW11,
        board.SW12,
        board.SW13,
        board.SW14,
        board.SW15]

NUM_KEYS = 16


class Keybow2040(object):
    """
    Represents a Keybow 2040 and hence a set of Key instances with
    associated LEDs and key behaviours.

    :param i2c: the I2C bus for Keybow 2040
    """
    def __init__(self, i2c):
        self.pins = _PINS
        self.display = keybow2040.Keybow2040(i2c)
        self.keys = []
        self.time_of_last_press = time.monotonic()
        self.time_since_last_press = None
        self.led_sleep_enabled = False
        self.led_sleep_time = 60
        self.sleeping = False
        self.was_asleep = False
        self.last_led_states = None
        # self.rotation = 0

        for i in range(len(self.pins)):
            _key = Key(i, self.pins[i], self.display)
            self.keys.append(_key)

    def update(self):
        # Call this in each iteration of your while loop to update
        # to update everything's state, e.g. `keybow.update()`

        for _key in self.keys:
            _key.update()

        # Used to work out the sleep behaviour, by keeping track
        # of the time of the last key press.
        if self.any_pressed():
            self.time_of_last_press = time.monotonic()
            self.sleeping = False

        self.time_since_last_press = time.monotonic() - self.time_of_last_press

        # If LED sleep is enabled, but not engaged, check if enough time
        # has elapsed to engage sleep. If engaged, record the state of the
        # LEDs, so it can be restored on wake.
        if self.led_sleep_enabled and not self.sleeping:
            if time.monotonic() - self.time_of_last_press > self.led_sleep_time:
                self.sleeping = True
                self.last_led_states = [k.rgb if k.lit else [0, 0, 0] for k in self.keys]
                self.set_all(0, 0, 0)
                self.was_asleep = True

        # If it was sleeping, but is no longer, then restore LED states.
        if not self.sleeping and self.was_asleep:
            for k in range(len(self.keys)):
                self.keys[k].set_led(*self.last_led_states[k])
            self.was_asleep = False

    def set_led(self, number, r, g, b):
        # Set an individual key's LED to an RGB value by its number.

        self.keys[number].set_led(r, g, b)

    def set_all(self, r, g, b):
        # Set all of Keybow's LEDs to an RGB value.

        if not self.sleeping:
            for _key in self.keys:
                _key.set_led(r, g, b)
        else:
            for _key in self.keys:
                _key.led_off()

    def get_states(self):
        # Returns a Boolean list of Keybow's key states
        # (0=not pressed, 1=pressed).

        _states = [_key.state for _key in self.keys]
        return _states

    def get_pressed(self):
        # Returns a list of key numbers currently pressed.

        _pressed = [_key.number for _key in self.keys if _key.state == True]
        return _pressed

    def any_pressed(self):
        # Returns True if any key is pressed, False if none are pressed.

        if any(self.get_states()):
            return True
        else:
            return False

    def none_pressed(self):
        # Returns True if none of the keys are pressed, False is any key
        # is pressed.

        if not any(self.get_states()):
            return True
        else:
            return False

    def on_press(self, _key, handler=None):
        # Attaches a press function to a key, via a decorator. This is stored as
        # `key.press_function` in the key's attributes, and run if necessary
        # as part of the key's update function (and hence Keybow's update 
        # function). It can be attached as follows:

        # @keybow.on_press(key)
        # def press_handler(key, pressed):
        #     if pressed:
        #         do something
        #     else:
        #         do something else

        if _key is None:
            return

        def attach_handler(handler):
            _key.press_function = handler

        if handler is not None:
            attach_handler(handler)
        else:
            return attach_handler

    def on_release(self, _key, handler=None):
        # Attaches a release function to a key, via a decorator. This is stored
        # as `key.release_function` in the key's attributes, and run if
        # necessary as part of the key's update function (and hence Keybow's
        # update function). It can be attached as follows:

        # @keybow.on_release(key)
        # def release_handler(key):
        #     do something

        if _key is None:
            return

        def attach_handler(handler):
            _key.release_function = handler

        if handler is not None:
            attach_handler(handler)
        else:
            return attach_handler

    def on_hold(self, _key, handler=None):
        # Attaches a hold unction to a key, via a decorator. This is stored as 
        # `key.hold_function` in the key's attributes, and run if necessary
        # as part of the key's update function (and hence Keybow's update 
        # function). It can be attached as follows:

        # @keybow.on_hold(key)
        # def hold_handler(key):
        #     do something

        if _key is None:
            return

        def attach_handler(handler):
            _key.hold_function = handler

        if handler is not None:
            attach_handler(handler)
        else:
            return attach_handler

    # def rotate(self, degrees):
    #     # Rotates all of Keybow's keys by a number of degrees, clamped to
    #     # the closest multiple of 90 degrees. Because it shuffles the order
    #     # of the Key instances, all of the associated attributes of the key
    #     # are retained. The x/y coordinate of the keys are rotated also. It
    #     # also handles negative degrees, e.g. -90 to rotate 90 degrees anti-
    #     # clockwise.

    #     # Rotate as follows: `keybow.rotate(270)`

    #     self.rotation = degrees
    #     num_rotations = degrees // 90

    #     if num_rotations == 0:
    #         return

    #     if num_rotations < 1:
    #         num_rotations = 4 + num_rotations

    #     matrix = [[(x * 4) + y for y in range(4)] for x in range(4)]

    #     for r in range(num_rotations):
    #         matrix = zip(*matrix[::-1])
    #         matrix = [list(x) for x in list(matrix)]

    #     flat_matrix = [x for y in matrix for x in y]

    #     for i in range(len(self.keys)):
    #         self.keys[i].number = flat_matrix[i]

    #     self.keys = sorted(self.keys, key=lambda x:x.number)


class Key:
    """
    Represents a key on Keybow 2040, with associated switch and
    LED behaviours.

    :param number: the key number (0-15) to associate with the key
    :param pin: the pin object for the key, e.g. board.SW0
    :param display: the IS31FL3731 matrix instance for the LEDs
    """
    def __init__(self, number, pin, display):
        self.pin = pin
        self.number = number
        self.switch = DigitalInOut(self.pin)
        self.switch.direction = Direction.INPUT
        self.switch.pull = Pull.UP
        self.state = 0
        self.pressed = 0
        self.last_state = None
        self.time_of_last_press = time.monotonic()
        self.time_since_last_press = None
        self.time_held_for = 0
        self.held = False
        self.hold_time = 0.75
        self.modifier = False
        self.rgb = [0, 0, 0]
        self.lit = False
        self.xy = self.get_xy()
        self.x, self.y = self.xy
        self.display = display
        self.led_off()
        self.press_function = None
        self.release_function = None
        self.hold_function = None
        self.press_func_fired = False
        self.hold_func_fired = False
        self.debounce = 0.125
        self.key_locked = False

    def get_state(self):
        # Returns the state of the key (0=not pressed, 1=pressed).

        return int(not self.switch.value)

    def update(self):
        # Updates the state of the key and updates all of its
        # attributes.

        self.time_since_last_press = time.monotonic() - self.time_of_last_press

        # Keys get locked during the debounce time.
        if self.time_since_last_press < self.debounce:
            self.key_locked = True
        else:
            self.key_locked = False

        self.state = self.get_state()
        self.pressed = self.state
        update_time = time.monotonic()

        # If there's a `press_function` attached, then call it,
        # returning the key object and the pressed state.
        if self.press_function is not None and self.pressed and not self.press_func_fired and not self.key_locked:
            self.press_function(self)
            self.press_func_fired = True
            # time.sleep(0.05)  # A little debounce

        # If the key has been pressed and releases, then call
        # the `release_function`, if one is attached.
        if not self.pressed and self.last_state == True:
            if self.release_function is not None:
                self.release_function(self)
            self.last_state = False
            self.press_func_fired = False

        if not self.pressed:
            self.time_held_for = 0
            self.last_state = False

        # If the key has just been pressed, then record the
        # `time_of_last_press`, and update last_state.
        elif self.pressed and self.last_state == False:
            self.time_of_last_press = update_time
            self.last_state = True

        # If the key is pressed and held, then update the
        # `time_held_for` variable.
        elif self.pressed and self.last_state == True:
            self.time_held_for = update_time - self.time_of_last_press
            self.last_state = True

        # If the `hold_time` theshold is crossed, then call the
        # `hold_function` if one is attached. The `hold_func_fired`
        # ensures that the function is only called once.
        if self.time_held_for > self.hold_time:
            self.held = True
            if self.hold_function is not None and not self.hold_func_fired:
                self.hold_function(self)
                self.hold_func_fired = True
        else:
            self.held = False
            self.hold_func_fired = False

    def get_xy(self):
        # Returns the x/y coordinate of a key from 0,0 to 3,3.

        return number_to_xy(self.number)

    def get_number(self):
        # Returns the key number, from 0 to 15.

        return xy_to_number(self.x, self.y)

    def is_modifier(self):
        # Designates a modifier key, so you can hold the modifier
        # and tap another key to trigger additional behaviours.

        if self.modifier:
            return True
        else:
            return False

    def set_led(self, r, g, b):
        # Set this key's LED to an RGB value.

        if [r, g, b] == [0, 0, 0]:
            self.lit = False
        else:
            self.lit = True
            self.rgb = [r, g, b]

        self.display.pixelrgb(self.x, self.y, r, g, b)

    def led_on(self):
        # Turn the LED on, using its current RGB value.

        r, g, b = self.rgb
        self.set_led(r, g, b)

    def led_off(self):
        # Turn the LED off.

        self.set_led(0, 0, 0)

    def led_state(self, state):
        # Set the LED's state (0=off, 1=on)

        state = int(state)

        if state == 0:
            self.led_off()
        elif state == 1:
            self.led_on()
        else:
            return

    def toggle_led(self, rgb=None):
        # Toggle the LED's state, retaining its RGB value for when it's toggled
        # back on. Can also be passed an RGB tuple to set the colour as part of
        # the toggle.

        if rgb is not None:
            self.rgb = rgb
        if self.lit:
            self.led_off()
        else:
            self.led_on()

    def __str__(self):
        # When printed, show the key's state (0 or 1).
        return self.state

def xy_to_number(x, y):
    # Convert an x/y coordinate to key number.
    return x + (y * 4)

def number_to_xy(number):
    # Convert a number to an x/y coordinate.
    x = number % 4
    y = number // 4

    return (x, y)

def hsv_to_rgb(h, s, v):
    # Convert an HSV (0.0-1.0) colour to RGB (0-255)
    if s == 0.0:
        rgb = [v, v, v]
    
    i = int(h * 6.0)

    f = (h*6.)-i; p,q,t = v*(1.-s), v*(1.-s*f), v*(1.-s*(1.-f)); i%=6
    
    if i == 0:
        rgb = [v, t, p]
    if i == 1:
        rgb = [q, v, p]
    if i == 2:
        rgb = [p, v, t]
    if i == 3:
        rgb = [p, q, v]
    if i == 4:
        rgb = [t, p, v]
    if i == 5:
        rgb = [v, p, q]

    rgb = tuple(int(c * 255) for c in rgb)

    return rgb