import threading
import time
from time import sleep
import numpy as np
import pygame
from dataclasses import dataclass
from pygame.locals import (
    JOYAXISMOTION,
    JOYBUTTONDOWN,
    JOYBUTTONUP,
    K_ESCAPE,
    KEYDOWN,
    NOEVENT,
    QUIT,
    K_q,
)

@dataclass
class xKeySwitch:
    R1: int
    L1: int
    start: int
    select: int
    R2: int
    L2: int
    F1: int
    F2: int
    A: int
    B: int
    X: bool
    Y: int
    up: int
    right: int
    down: int
    left: int


@dataclass
class xRockerBtn:
    head: list
    btn: xKeySwitch
    lx: float
    rx: float
    ry: float
    L2: float
    ly: float

class PyGameJoyManager:
    def __init__(self, user_callback=None):
        self.dt = 0.01  # Sampling frequence of the joystick
        self.running = False
        self.joy_thread_handle = threading.Thread(target=self.joy_thread)
        # an optional callback that can be used to send the reading values to a user provided function
        self.user_callback = user_callback
        pygame.init()
        self.offset = None

    def joy_thread(self):
        while self.running:
            for event in [
                pygame.event.wait(200),
            ] + pygame.event.get():
                # QUIT             none
                # ACTIVEEVENT      gain, state
                # KEYDOWN          unicode, key, mod
                # KEYUP            key, mod
                # MOUSEMOTION      pos, rel, buttons
                # MOUSEBUTTONUP    pos, button
                # MOUSEBUTTONDOWN  pos, button
                # JOYAXISMOTION    joy, axis, value
                # JOYBALLMOTION    joy, ball, rel
                # JOYHATMOTION     joy, hat, value
                # JOYBUTTONUP      joy, button
                # JOYBUTTONDOWN    joy, button
                # VIDEORESIZE      size, w, h
                # VIDEOEXPOSE      none
                # USEREVENT        code
                if event.type == QUIT:
                    self.stop_daq()
                elif event.type == KEYDOWN and event.key in [K_ESCAPE, K_q]:
                    self.stop_daq()
                elif event.type == JOYAXISMOTION:
                    self.analog_cmd[event.axis] = event.value
                elif event.type == JOYBUTTONUP:
                    self.digital_cmd[event.button] = 0
                elif event.type == JOYBUTTONDOWN:
                    self.digital_cmd[event.button] = 1
                if self.user_callback is not None and event.type != NOEVENT:
                    if self.offset is None:
                        self.user_callback(self.analog_cmd, self.digital_cmd)
                    else:
                        self.user_callback(
                            np.array(self.analog_cmd) - self.offset, self.digital_cmd
                        )

    def start_daq(self, joy_idx):
        # Get the joy object
        assert (
            pygame.joystick.get_count() != 0
        ), "No joysticks detected, you can not start the class"
        assert (
            pygame.joystick.get_count() >= joy_idx
        ), "The requested joystick ID exceeds the number of available devices"
        self.joy = pygame.joystick.Joystick(joy_idx)

        self.analog_cmd = [self.joy.get_axis(i) for i in range(self.joy.get_numaxes())]
        self.digital_cmd = [
            self.joy.get_button(i) for i in range(self.joy.get_numbuttons())
        ]
        self.running = True
        self.joy_thread_handle.start()

    def stop_daq(self):
        self.running = False
        self.joy_thread_handle.join()

    def read_raw(self):
        return self.analog_cmd, self.digital_cmd

    def offset_calibration(self):
        analog, _ = self.read_raw()
        offset = np.array(analog)
        print("Put your stick at zero location and do not touch it")
        sleep(1)
        for i in range(2000):
            sleep(0.001)
            analog, _ = self.read_raw()
            offset += np.array(analog)
        self.offset = offset / 2000

    def read_values(self):
        analog, digital = self.read_raw()
        if self.offset is None:
            return analog, digital
        else:
            return analog - self.offset, digital


class Logitech3DPro:
    """
    Handles operations specific to the Logitech 3D Pro joystick.

    This class serves as a wrapper around PyGameJoyManager, offering methods
    for decoding and interpreting joystick values specific to Logitech 3D Pro.

    Attributes:
        joymanager (PyGameJoyManager): Instance of PyGameJoyManager to handle
                                       the joystick events.
        joy_id (int): The identifier for the joystick.
        analog_raw (list of float): The raw analog values from the joystick axes.
        digital_raw (list of int): The raw digital values from the joystick buttons.
    """

    def __init__(self, joy_id=0):
        """
        Initializes a Logitech3DPro object.

        Starts data acquisition and performs initial calibration.

        Parameters:
            joy_id (int): Identifier for the joystick. Default is 0.
        """
        self.joymanager = PyGameJoyManager()
        self.joymanager.start_daq(joy_id)
        print("Calibrating joystick, please do not touch the stick and wait...")
        time.sleep(2)
        self.joymanager.offset_calibration()
        print("Calibration finished.")
        self.joy_id = joy_id

    def decode(self):
        """
        Decode Raw Values from Joystick.

        Decodes the raw analog and digital values from the joystick. The
        decoded values are stored in the object's attributes.
        """
        self.analog_raw, self.digital_raw = self.joymanager.read_values()

    def readAnalog(self):
        """
        Reads Analog Values.

        Retrieves the analog values from the joystick and presents them in a
        dictionary format. Performs decoding before fetching the values.

        Returns:
            dict: A dictionary containing the analog axis values with keys as 'x', 'y', 'z', and 'aux'.
        """
        self.decode()
        data = {
            "x": self.analog_raw[0],
            "y": self.analog_raw[1],
            "z": self.analog_raw[2],
            "aux": self.analog_raw[3],
        }
        return data

    def readDigital(self):
        """
        Reads Digital Values.

        Retrieves the digital button values from the joystick and presents
        them in a dictionary format. Performs decoding before fetching the values.

        Returns:
            dict: A dictionary containing the digital button states with keys as 'shoot', '2', '3',
                  up to '12'.
        """
        self.decode()
        data = {
            "shoot": self.digital_raw[0],
            "2": self.digital_raw[1],
            "3": self.digital_raw[2],
            "4": self.digital_raw[3],
            "5": self.digital_raw[4],
            "6": self.digital_raw[5],
            "7": self.digital_raw[6],
            "8": self.digital_raw[7],
            "9": self.digital_raw[8],
            "10": self.digital_raw[9],
            "11": self.digital_raw[10],
            "12": self.digital_raw[11],
        }
        return data