import asyncio

from joycontrol import utils
from joycontrol.controller import Controller
from joycontrol.memory import FlashMemory


class ControllerState:
    def __init__(self, protocol, controller: Controller, spi_flash: FlashMemory = None):
        self._protocol = protocol
        self._controller = controller
        self._nfc_content = None

        self._spi_flash = spi_flash

        self.button_state = ButtonState(controller)

        # create left stick state
        self.l_stick_state = self.r_stick_state = None
        if controller in (Controller.PRO_CONTROLLER, Controller.JOYCON_L):
            # load calibration data from memory
            calibration = None
            if spi_flash is not None:
                calibration_data = spi_flash.get_user_l_stick_calibration()
                if calibration_data is None:
                    calibration_data = spi_flash.get_factory_l_stick_calibration()
                calibration = LeftStickCalibration.from_bytes(calibration_data)

            self.l_stick_state = StickState(calibration=calibration)
            if calibration is not None:
                self.l_stick_state.set_center()

        # create right stick state
        if controller in (Controller.PRO_CONTROLLER, Controller.JOYCON_R):
            # load calibration data from memory
            calibration = None
            if spi_flash is not None:
                calibration_data = spi_flash.get_user_r_stick_calibration()
                if calibration_data is None:
                    calibration_data = spi_flash.get_factory_r_stick_calibration()
                calibration = RightStickCalibration.from_bytes(calibration_data)

            self.r_stick_state = StickState(calibration=calibration)
            if calibration is not None:
                self.r_stick_state.set_center()

        self.sig_is_send = asyncio.Event()

    def get_controller(self):
        return self._controller

    def get_flash_memory(self):
        return self._spi_flash

    def set_nfc(self, nfc_content):
        self._nfc_content = nfc_content

    def get_nfc(self):
        return self._nfc_content

    async def send(self):
        """
        Invokes protocol.send_controller_state(). Returns after the controller state was send.
        Raises NotConnected exception if the connection was lost.
        """
        await self._protocol.send_controller_state()

    async def connect(self):
        """
        Waits until the switch is paired with the controller and accepts button commands
        """
        await self._protocol.sig_set_player_lights.wait()


class ButtonState:
    """
    Utility class to set buttons in the input report
    https://github.com/dekuNukem/Nintendo_Switch_Reverse_Engineering/blob/master/bluetooth_hid_notes.md
    Byte 	0 	    1 	    2 	    3 	    4 	    5 	    6 	    7
    1   	Y 	    X 	    B 	    A 	    SR 	    SL 	    R 	    ZR
    2       Minus 	Plus 	R Stick L Stick Home 	Capture
    3       Down 	Up 	    Right 	Left 	SR 	    SL 	    L 	    ZL

    Example for generated methods: home button (byte_2, 4)

    def home(self, pushed=True):
        if pushed != utils.get_bit(self.byte_2, 4):
            self.byte_2 = utils.flip_bit(self.byte_2, 4)

    def home_is_set(self):
        return get_bit(self.byte_2, 4)
    """
    def __init__(self, controller: Controller):
        self.controller = controller

        # 3 bytes
        self._byte_1 = 0
        self._byte_2 = 0
        self._byte_3 = 0

        # generating methods for each button
        def button_method_factory(byte, bit):
            def setter(pushed=True):
                _byte = getattr(self, byte)

                if pushed != utils.get_bit(_byte, bit):
                    setattr(self, byte, utils.flip_bit(_byte, bit))

            def getter():
                return utils.get_bit(getattr(self, byte), bit)
            return setter, getter

        if self.controller == Controller.PRO_CONTROLLER:
            self._available_buttons = {'y', 'x', 'b', 'a', 'r', 'zr',
                                       'minus', 'plus', 'r_stick', 'l_stick', 'home', 'capture',
                                       'down', 'up', 'right', 'left', 'l', 'zl'}
        elif self.controller == Controller.JOYCON_R:
            self._available_buttons = {'y', 'x', 'b', 'a', 'sr', 'sl', 'r', 'zr',
                                       'plus', 'r_stick', 'home'}
        elif self.controller == Controller.JOYCON_L:
            self._available_buttons = {'minus', 'l_stick', 'capture',
                                       'down', 'up', 'right', 'left', 'sr', 'sl', 'l', 'zl'}

        # byte 1
        if self.controller == Controller.PRO_CONTROLLER or self.controller == Controller.JOYCON_R:
            self.y, self.y_is_set = button_method_factory('_byte_1', 0)
            self.x, self.x_is_set = button_method_factory('_byte_1', 1)
            self.b, self.b_is_set = button_method_factory('_byte_1', 2)
            self.a, self.a_is_set = button_method_factory('_byte_1', 3)

            if self.controller == Controller.JOYCON_R:
                self.sr, self.sr_is_set = button_method_factory('_byte_1', 4)
                self.sl, self.sl_is_set = button_method_factory('_byte_1', 5)

            self.r, self.r_is_set = button_method_factory('_byte_1', 6)
            self.zr, self.zr_is_set = button_method_factory('_byte_1', 7)

        # byte 2
        self.minus, self.minus_is_set = button_method_factory('_byte_2', 0)
        self.plus, self.plus_is_set = button_method_factory('_byte_2', 1)
        self.r_stick, self.r_stick_is_set = button_method_factory('_byte_2', 2)
        self.l_stick, self.l_stick_is_set = button_method_factory('_byte_2', 3)
        if self.controller == Controller.JOYCON_R or self.controller == Controller.PRO_CONTROLLER:
            self.home, self.home_is_set = button_method_factory('_byte_2', 4)
        if self.controller == Controller.JOYCON_L or self.controller == Controller.PRO_CONTROLLER:
            self.capture, self.capture_is_set = button_method_factory('_byte_2', 5)

        # byte 3
        if self.controller == Controller.PRO_CONTROLLER or self.controller == Controller.JOYCON_L:
            self.down, self.down_is_set = button_method_factory('_byte_3', 0)
            self.up, self.up_is_set = button_method_factory('_byte_3', 1)
            self.right, self.right_is_set = button_method_factory('_byte_3', 2)
            self.left, self.left_is_set = button_method_factory('_byte_3', 3)

            if self.controller == Controller.JOYCON_L:
                self.sr, self.sr_is_set = button_method_factory('_byte_3', 4)
                self.sl, self.sl_is_set = button_method_factory('_byte_3', 5)

            self.l, self.l_is_set = button_method_factory('_byte_3', 6)
            self.zl, self.zl_is_set = button_method_factory('_byte_3', 7)

    def set_button(self, button, pushed=True):
        if button not in self._available_buttons:
            raise ValueError(f'Given button "{button}" is not available to {self.controller.device_name()}.')
        getattr(self, button)(pushed=pushed)

    def get_button(self, button):
        if button not in self._available_buttons:
            raise ValueError(f'Given button "{button}" is not available to {self.controller.device_name()}.')
        return getattr(self, f'{button}_is_set')()

    def get_available_buttons(self):
        """
        :returns: set of valid buttons
        """
        return set(self._available_buttons)

    def __iter__(self):
        """
        :returns: iterator over the button bytes
        """
        yield self._byte_1
        yield self._byte_2
        yield self._byte_3

    def clear(self):
        self._byte_1 = self._byte_2 = self._byte_3 = 0


async def button_press(controller_state, *buttons):
    """
    Set given buttons in the controller state to the pressed down state and wait till send.
    :param controller_state:
    :param buttons: Buttons to press down (see ButtonState.get_available_buttons)
    """
    if not buttons:
        raise ValueError('No Buttons were given.')

    button_state = controller_state.button_state

    for button in buttons:
        # push button
        button_state.set_button(button, pushed=True)

    # wait until report is send
    await controller_state.send()


async def button_release(controller_state, *buttons):
    """
    Set given buttons in the controller state to the unpressed state and wait till send.
    :param controller_state:
    :param buttons: Buttons to set to unpressed (see ButtonState.get_available_buttons)
    """
    if not buttons:
        raise ValueError('No Buttons were given.')

    button_state = controller_state.button_state

    for button in buttons:
        # release button
        button_state.set_button(button, pushed=False)

    # wait until report is send
    await controller_state.send()


async def button_push(controller_state, *buttons, sec=0.1):
    """
    Shortly push the given buttons. Wait until the controller state is send.
    :param controller_state:
    :param buttons: Buttons to push (see ButtonState.get_available_buttons)
    :param sec: Seconds to wait before releasing the button, default: 0.1
    """
    await button_press(controller_state, *buttons)
    await asyncio.sleep(sec)
    await button_release(controller_state, *buttons)


class _StickCalibration:
    def __init__(self, h_center, v_center, h_max_above_center, v_max_above_center, h_max_below_center, v_max_below_center):
        self.h_center = h_center
        self.v_center = v_center

        self.h_max_above_center = h_max_above_center
        self.v_max_above_center = v_max_above_center
        self.h_max_below_center = h_max_below_center
        self.v_max_below_center = v_max_below_center

    def __str__(self):
        return f'h_center:{self.h_center} v_center:{self.v_center} h_max_above_center:{self.h_max_above_center} ' \
               f'v_max_above_center:{self.v_max_above_center} h_max_below_center:{self.h_max_below_center} ' \
               f'v_max_below_center:{self.v_max_below_center}'


class LeftStickCalibration(_StickCalibration):
    @staticmethod
    def from_bytes(_9bytes):
        h_max_above_center = (_9bytes[1] << 8) & 0xF00 | _9bytes[0]
        v_max_above_center = (_9bytes[2] << 4) | (_9bytes[1] >> 4)
        h_center =           (_9bytes[4] << 8) & 0xF00 | _9bytes[3]
        v_center =           (_9bytes[5] << 4) | (_9bytes[4] >> 4)
        h_max_below_center = (_9bytes[7] << 8) & 0xF00 | _9bytes[6]
        v_max_below_center = (_9bytes[8] << 4) | (_9bytes[7] >> 4)

        return _StickCalibration(h_center, v_center, h_max_above_center, v_max_above_center,
                                 h_max_below_center, v_max_below_center)


class RightStickCalibration(_StickCalibration):
    @staticmethod
    def from_bytes(_9bytes):
        h_center =           (_9bytes[1] << 8) & 0xF00 | _9bytes[0]
        v_center =           (_9bytes[2] << 4) | (_9bytes[1] >> 4)
        h_max_below_center = (_9bytes[4] << 8) & 0xF00 | _9bytes[3]
        v_max_below_center = (_9bytes[5] << 4) | (_9bytes[4] >> 4)
        h_max_above_center = (_9bytes[7] << 8) & 0xF00 | _9bytes[6]
        v_max_above_center = (_9bytes[8] << 4) | (_9bytes[7] >> 4)

        return _StickCalibration(h_center, v_center, h_max_above_center, v_max_above_center,
                                 h_max_below_center, v_max_below_center)


class StickState:
    def __init__(self, h=0, v=0, calibration: _StickCalibration = None):
        for val in (h, v):
            if not 0 <= val < 0x1000:
                raise ValueError(f'Stick values must be in [0,{0x1000})')

        self._h_stick = h
        self._v_stick = v

        self._calibration = calibration

    def set_h(self, value):
        if not 0 <= value < 0x1000:
            raise ValueError(f'Stick values must be in [0,{0x1000})')
        self._h_stick = value

    def get_h(self):
        return self._h_stick

    def set_v(self, value):
        if not 0 <= value < 0x1000:
            raise ValueError(f'Stick values must be in [0,{0x1000})')
        self._v_stick = value

    def get_v(self):
        return self._v_stick

    def set_center(self):
        """
        Sets stick to center position using the calibration data.
        """
        if self._calibration is None:
            raise ValueError('No calibration data available.')
        self._h_stick = self._calibration.h_center
        self._v_stick = self._calibration.v_center

    def is_center(self, radius=0):
        return self._calibration.h_center - radius <= self._h_stick <= self._calibration.h_center + radius and \
               self._calibration.v_center - radius <= self._v_stick <= self._calibration.v_center + radius

    def set_up(self):
        """
        Sets stick to up position using the calibration data.
        """
        if self._calibration is None:
            raise ValueError('No calibration data available.')
        self._h_stick = self._calibration.h_center
        self._v_stick = self._calibration.v_center + self._calibration.v_max_above_center

    def set_down(self):
        """
        Sets stick to down position using the calibration data.
        """
        if self._calibration is None:
            raise ValueError('No calibration data available.')
        self._h_stick = self._calibration.h_center
        self._v_stick = self._calibration.v_center - self._calibration.v_max_below_center

    def set_left(self):
        """
        Sets stick to left position using the calibration data.
        """
        if self._calibration is None:
            raise ValueError('No calibration data available.')
        self._h_stick = self._calibration.h_center - self._calibration.h_max_below_center
        self._v_stick = self._calibration.v_center

    def set_right(self):
        """
        Sets stick to right position using the calibration data.
        """
        if self._calibration is None:
            raise ValueError('No calibration data available.')
        self._h_stick = self._calibration.h_center + self._calibration.h_max_above_center
        self._v_stick = self._calibration.v_center

    def set_calibration(self, calibration):
        self._calibration = calibration

    def get_calibration(self):
        if self._calibration is None:
            raise ValueError('No calibration data available.')
        return self._calibration

    @staticmethod
    def from_bytes(_3bytes):
        stick_h = _3bytes[0] | ((_3bytes[1] & 0xF) << 8)
        stick_v = (_3bytes[1] >> 4) | (_3bytes[2] << 4)

        return StickState(h=stick_h, v=stick_v)

    def __bytes__(self):
        byte_1 = 0xFF & self._h_stick
        byte_2 = (self._h_stick >> 8) | ((0xF & self._v_stick) << 4)
        byte_3 = self._v_stick >> 4
        assert all(0 <= byte <= 0xFF for byte in (byte_1, byte_2, byte_3))
        return bytes((byte_1, byte_2, byte_3))
