"""
Motivation

- Convenience thread for listening to and caching key presses / releases
- Accepts a generic KeyMapping class to quickly add meaning to key combos

Usage:

- Subclass KeyMapping to define what to do on_press and on_release for any key
- Instantiate a KeyboardManager with your KeyMapping child
- Call get_cache to get the current state of key presses

Limitations:

- Since this is polling based, key strokes can be missed
    - If that's unacceptable, `pynput.keyboard.Listener` can work event based 
"""

import abc
import copy
import threading
from typing import Generic, TypeVar, Union

from pynput import keyboard

_T = TypeVar("_T")


KeyType = Union[keyboard.Key, keyboard.KeyCode, None]
Key = keyboard.Key
KeyCode = keyboard.KeyCode

import forcen_public_utils.checked as ch
from forcen_public_utils.loggers.console_logger import ConsoleLogger


class KeyMapping(Generic[_T], abc.ABC):
    """
    Usage:
    - subclass this to implement your own key mapping
    """

    @abc.abstractmethod
    def on_press(self, cache: _T, key: KeyType) -> _T: ...

    @abc.abstractmethod
    def on_release(self, cache: _T, key: KeyType) -> _T: ...


class KeyboardManager(Generic[_T]):
    def __init__(self, keyboard_mapping: KeyMapping[_T], init_cache: _T):
        ConsoleLogger.get().info("Instantiating KeyboardManager")
        self._listener = None
        self._listener_lock = threading.Lock()

        self._cache_lock = threading.Lock()
        self._cache = init_cache

        self._keyboard_mapping = keyboard_mapping

    def start(self) -> ch.Bool:
        with self._listener_lock:
            ConsoleLogger.get().info("Starting KeyboardManager")
            if self._listener is not None:
                return ch.ok(message="Keyboard - already started")

            self._listener = keyboard.Listener(on_press=self._on_press, on_release=self._on_release)
            self._listener.start()

            return ch.ok(message="Keyboard - started")

    def stop(self) -> ch.Bool:
        with self._listener_lock:
            ConsoleLogger.get().info("Stopping KeyboardManager")
            if self._listener is None:
                return ch.ok(message="Keyboard - already stopped")

            self._listener.stop()
            self._listener = None

            return ch.ok(message="Keyboard - stopped")

    def get_cache(self) -> _T:
        with self._cache_lock:
            return copy.deepcopy(self._cache)

    def _on_press(self, key: KeyType) -> None:
        try:
            with self._cache_lock:
                self._cache = self._keyboard_mapping.on_press(self._cache, key)
        except Exception as e:  # pylint: disable=broad-exception-caught
            ConsoleLogger.get().debug(f"on press got exception {type(e)}: {e}")

        return

    def _on_release(self, key: KeyType) -> None:
        try:
            with self._cache_lock:
                self._cache = self._keyboard_mapping.on_release(self._cache, key)
        except Exception as e:  # pylint: disable=broad-exception-caught
            ConsoleLogger.get().warning(f"On release got exception {type(e)}: {e}")

        return
