""" Defines a Clontroller class that manages pressing buttons for your console"""

import dataclasses
import platform
import copy
import time

from melee.console import Console
from melee import enums

def fix_analog_stick(x: float) -> float:
    """Fixes the analog stick values to match Console.step output."""
    # The formula for observed raw values is raw = floor[(x - 0.5) * 254]
    # Here we invert this, assuming the `x` is a raw value scaled to [0, 1]
    # which is what Console.step outputs.

    raw = round((x - 0.5) * 160)  # Desired raw value in [-80, 80]
    fudged = raw + 0.1  # Go slightly above the threshold to avoid rounding issues
    return (fudged / (127 * 2)) + 0.5  # Desired input value in [0, 1]

def fix_analog_trigger(x: float) -> float:
    """Fixes the analog trigger values to match Console.step output."""
    raw = round(x * 140)  # Desired raw value in [0, 140]
    fudged = raw + 0.1  # Go slightly above the threshold to avoid rounding issues
    return fudged / 255  # Desired input value in [0, 1]

_POSSIBLE_BUTTONS = [
    enums.Button.BUTTON_A,
    enums.Button.BUTTON_B,
    enums.Button.BUTTON_X,
    enums.Button.BUTTON_Y,
    enums.Button.BUTTON_Z,
    enums.Button.BUTTON_L,
    enums.Button.BUTTON_R,
    enums.Button.BUTTON_START,
    enums.Button.BUTTON_D_UP,
    enums.Button.BUTTON_D_DOWN,
    enums.Button.BUTTON_D_LEFT,
    enums.Button.BUTTON_D_RIGHT,
]

_new_buttons = lambda: {b: False for b in _POSSIBLE_BUTTONS}
_field = lambda f: dataclasses.field(default_factory=f)

@dataclasses.dataclass(slots=True)
class ControllerState:
    """A snapshot of the state of a virtual controller"""

    #Boolean buttons
    """(dict of enums.Button to bool): For the each Button as key, tells you if the button is pressed."""
    button: dict[enums.Button, bool] = _field(_new_buttons)
    processed_button: dict[enums.Button, bool] = _field(_new_buttons)
    #Analog sticks
    """(pair of floats): The main stick's x,y position. Ranges from 0->1, 0.5 is neutral"""
    main_stick: tuple[float, float] = (0.5, 0.5)
    """(pair of floats): The C stick's x,y position. Ranges from 0->1, 0.5 is neutral"""
    c_stick: tuple[float, float] = (0.5, 0.5)
    """(pair of ints): The raw unprocessed main stick coordinates. Ranges from -128 -> 127. 0 is neutral."""
    raw_main_stick: tuple[int, int] = (0, 0)
    #Analog shoulders
    l_shoulder: float = 0
    """(float): L shoulder analog press. Ranges from 0 (not pressed) to 1 (fully pressed)"""
    r_shoulder: float = 0
    """(float): R shoulder analog press. Ranges from 0 (not pressed) to 1 (fully pressed)"""

    def __str__(self):
        string = ""
        for val in self.button:
            string += str(val) + ": " + str(self.button[val])
            string += "\n"
        string += "MAIN_STICK: " + str(self.main_stick) + "\n"
        string += "C_STICK: " + str(self.c_stick) + "\n"
        string += "L_SHOULDER: " + str(self.l_shoulder) + "\n"
        string += "R_SHOULDER: " + str(self.r_shoulder) + "\n"
        return string

class Controller:
    """Manages virtual controller state and button presses

    The Controller object is your primary input mechanism for a bot. It's used for pressing
    buttons programatically, but also automatically configuring the controller with dolphin
    """

    def __init__(
            self,
            console: Console,
            port: int,
            type: enums.ControllerType = enums.ControllerType.STANDARD,
            fix_analog_inputs: bool = True,
            ):
        """Create a new virtual controller

        Args:
            console (console.Console): A console object to attach the controller to
            port (int): Which controller port to plug into. Must be 1-4.
            type (enums.ControllerType): The type of controller this is
            fix_analog_sticks (bool): If True, analog stick values will be remapped
              to the [-80, 80] range that melee uses internally. This will ensure
              that the stick values from Console.step are consistent with the values
              you send as inputs, modulo the deadzone or sticks with magnitude > 80.
              Also adjusts the analog triggers in an analogous way.
        """
        if not console.is_dolphin:
            raise ValueError("Controllers can only be used with Dolphin console")

        self.pipe_path = console.get_dolphin_pipes_path(port)
        self.pipe = None

        self.port = port
        self.prev = ControllerState()
        self.current = ControllerState()
        self.logger = console.logger
        self._console = console
        self._type = type
        self._fix_analog_inputs = fix_analog_inputs

        # Configure our controller with the console
        self._console.setup_dolphin_controller(port, type)

    def __del__(self):
        """Clean up any resources held by the controller object"""
        self.disconnect()

    def connect(self):
        """Connect the controller to the console

            Note:
                Blocks until the other side is ready
        """
        if self._type is not enums.ControllerType.STANDARD:
            return True  # nothing to do here

        # Add ourselves to the console's controller list
        self._console.controllers.append(self)

        if platform.system() == "Windows":
            import win32file, pywintypes
            # Windows can take a little while to actually make the pipes
            #   So keep trying a few times to connect to it
            for _ in range(5):
                try:
                    # "Create File" in windows is what you use to open a file. Not
                    #   create one. Because the windows API is stupid.
                    self.pipe = win32file.CreateFile(
                        self.pipe_path,
                        win32file.GENERIC_WRITE,
                        0,
                        None,
                        win32file.OPEN_EXISTING,
                        0,
                        None
                    )
                    return True
                except pywintypes.error:
                    time.sleep(1)
            return False

        self.pipe = open(self.pipe_path, "w")
        return True

    def disconnect(self):
        """Disconnects the controller from the console"""
        if self.pipe:
            try:
                self.pipe.close()
            except BrokenPipeError:
                pass
            self.pipe = None

    def _check_pipe(self):
        if not self.pipe:
            raise RuntimeError("Controller is not connected to console")

    def simple_press(self, x, y, button):
        """Here is a simpler representation of a button press, in case
            you don't want to bother with the tedium of manually doing everything.
            It isn't capable of doing everything the normal controller press functions
            can, but probably covers most scenarios.
            Notably, a difference here is that doing a button press releases all
            other buttons pressed previously.

            Note:
                Don't call this function twice in the same frame
                    x = 0 (left) to 1 (right) on the main stick
                    y = 0 (down) to 1 (up) on the main stick
                    button = the button to press. Enter None for no button"""
        self._check_pipe()

        #Tilt the main stick
        self.tilt_analog(enums.Button.BUTTON_MAIN, x, y)
        #Release the shoulders
        self.press_shoulder(enums.Button.BUTTON_L, 0)
        self.press_shoulder(enums.Button.BUTTON_R, 0)
        #Press the right button
        for item in enums.Button:
            #Don't do anything for the main or c-stick
            if item == enums.Button.BUTTON_MAIN:
                continue
            if item == enums.Button.BUTTON_C:
                continue
            #Press our button, release all others
            if item == button:
                self.press_button(item)
            else:
                self.release_button(item)

    def press_button(self, button: enums.Button):
        """Press a single button

        If already pressed, this has no effect

        Args:
            button (enums.Button): Button to press
        """
        self._check_pipe()

        self.current.button[button] = True

        command = "PRESS " + str(button.value) + "\n"
        if self.logger:
            self.logger.log("Buttons Pressed", command, concat=True)
        self._write(command)

    def release_button(self, button):
        """Release a single button

        If already released, this has no effect

        Args:
            button (enums.Button): Button to release
        """
        self.current.button[button] = False
        self._check_pipe()

        command = "RELEASE " + str(button.value) + "\n"
        if self.logger:
            self.logger.log("Buttons Pressed", command, concat=True)
        self._write(command)

    def press_shoulder(self, button: enums.Button, amount: float):
        """Press the analog shoulder buttons to a given amount

        Args:
            button (enums.Button): Has to be L or R
            amount (float): Ranges from 0 (not pressed at all) and 1 (Fully pressed in)

        Note:
            The 'digital' button press of L or R are handled separately
            as normal button presses. Pressing the shoulder all the way in
            will not cause the digital button to press
        """
        if button == enums.Button.BUTTON_L:
            self.current.l_shoulder = amount
        elif button == enums.Button.BUTTON_R:
            self.current.r_shoulder = amount

        self._check_pipe()
        if self._fix_analog_inputs:
            amount = fix_analog_trigger(amount)
        command = "SET " + str(button.value) + " " + str(amount) + "\n"
        if self.logger:
            self.logger.log("Buttons Pressed", command, concat=True)
        self._write(command)

    def tilt_analog(self, button: enums.Button, x: float, y: float):
        """ Tilt one of the analog sticks to a given (x,y) value

        Args:
            button (enums.Button): Must be main stick or C stick
            x (float): Ranges between 0 (left) and 1 (right)
            y (float): Ranges between 0 (down) and 1 (up)
        """
        if self._fix_analog_inputs:
            x = fix_analog_stick(x)
            y = fix_analog_stick(y)

        if button == enums.Button.BUTTON_MAIN:
            self.current.main_stick = (x, y)
        elif button == enums.Button.BUTTON_C:
            self.current.c_stick = (x, y)
        else:
            raise ValueError(f"Invalid button type {button} for tilt_analog.")

        self._check_pipe()
        command = "SET " + str(button.value) + " " + str(x) + " " + str(y) + "\n"
        if self.logger:
            self.logger.log("Buttons Pressed", command, concat=True)
        self._write(command)

    def tilt_analog_unit(self, button, x, y):
        """Tilt one of the analog sticks to a given (x,y) value.

        Values range from -1 -> 1 (with 0 center) rather than 0 -> 1 (with 0.5 center)
        This doesn't press the stick any further than the tilt_analog(),
        it's just a compat helper.

        Args:
            button (enums.Button): Must be main stick or C stick
            x (float): Ranges between -1 (left) and 1 (right)
            y (float): Ranges between -1 (down) and 1 (up)
        """
        self.tilt_analog(button, (x + 1) / 2, (y + 1) / 2)

    # Left around for compat reasons. Might disappear at any time
    #   left undocumented. Just use release_all()
    def empty_input(self):
        self.release_all()

    def release_all(self):
        """Resets the controller to a resting state

        All buttons are released, all sticks set to 0.5, all shoulders set to 0
        """
        #Set the internal state back to neutral
        self.current.button[enums.Button.BUTTON_A] = False
        self.current.button[enums.Button.BUTTON_B] = False
        self.current.button[enums.Button.BUTTON_X] = False
        self.current.button[enums.Button.BUTTON_Y] = False
        self.current.button[enums.Button.BUTTON_Z] = False
        self.current.button[enums.Button.BUTTON_L] = False
        self.current.button[enums.Button.BUTTON_R] = False
        self.current.button[enums.Button.BUTTON_START] = False
        self.current.button[enums.Button.BUTTON_D_UP] = False
        self.current.button[enums.Button.BUTTON_D_DOWN] = False
        self.current.button[enums.Button.BUTTON_D_LEFT] = False
        self.current.button[enums.Button.BUTTON_D_RIGHT] = False
        self.current.main_stick = (.5, .5)
        self.current.c_stick = (.5, .5)
        self.current.l_shoulder = 0
        self.current.r_shoulder = 0

        self._check_pipe()

        command = "RELEASE A" + "\n"
        command += "RELEASE B" + "\n"
        command += "RELEASE X" + "\n"
        command += "RELEASE Y" + "\n"
        command += "RELEASE Z" + "\n"
        command += "RELEASE L" + "\n"
        command += "RELEASE R" + "\n"
        command += "RELEASE START" + "\n"
        command += "RELEASE D_UP" + "\n"
        command += "RELEASE D_DOWN" + "\n"
        command += "RELEASE D_LEFT" + "\n"
        command += "RELEASE D_RIGHT" + "\n"
        command += "SET MAIN .5 .5" + "\n"
        command += "SET C .5 .5" + "\n"
        command += "SET L 0" + "\n"
        command += "SET R 0" + "\n"
        #Send the presses to dolphin
        self._write(command)
        if self.logger:
            self.logger.log("Buttons Pressed", "Empty Input", concat=True)

    def _write(self, command):
        """ Platform independent button write function.
        """
        if platform.system() == "Windows":
            import win32file, pywintypes
            try:
                win32file.WriteFile(self.pipe, command.encode())
            except pywintypes.error:
                pass
        else:
            self.pipe.write(command)

    def flush(self):
        """Actually send the button presses to the console

        Up until this point, any buttons you 'press' are just queued in a pipe.
        It doesn't get sent to the console until you flush
        """
        # Move the current controller state into the previous one
        self.prev = copy.copy(self.current)

        self._write("FLUSH\n")
        if platform.system() != "Windows":
            self.pipe.flush()
