import array
import struct
import time
from fcntl import ioctl
from typing import IO

from platypush.backend import Backend
from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent, \
    JoystickButtonPressedEvent, JoystickButtonReleasedEvent, JoystickAxisEvent


class JoystickLinuxBackend(Backend):
    """
    This backend intercepts events from joystick devices through the native Linux API implementation.

    It is loosely based on https://gist.github.com/rdb/8864666, which itself uses the
    `Linux kernel joystick API <https://www.kernel.org/doc/Documentation/input/joystick-api.txt>`_ to interact with
    the devices.

    Triggers:

        * :class:`platypush.message.event.joystick.JoystickConnectedEvent` when the joystick is connected.
        * :class:`platypush.message.event.joystick.JoystickDisconnectedEvent` when the joystick is disconnected.
        * :class:`platypush.message.event.joystick.JoystickButtonPressedEvent` when a joystick button is pressed.
        * :class:`platypush.message.event.joystick.JoystickButtonReleasedEvent` when a joystick button is released.
        * :class:`platypush.message.event.joystick.JoystickAxisEvent` when an axis value of the joystick changes.

    """

    # These constants were borrowed from linux/input.h
    axis_names = {
        0x00: 'x',
        0x01: 'y',
        0x02: 'z',
        0x03: 'rx',
        0x04: 'ry',
        0x05: 'rz',
        0x06: 'throttle',
        0x07: 'rudder',
        0x08: 'wheel',
        0x09: 'gas',
        0x0a: 'brake',
        0x10: 'hat0x',
        0x11: 'hat0y',
        0x12: 'hat1x',
        0x13: 'hat1y',
        0x14: 'hat2x',
        0x15: 'hat2y',
        0x16: 'hat3x',
        0x17: 'hat3y',
        0x18: 'pressure',
        0x19: 'distance',
        0x1a: 'tilt_x',
        0x1b: 'tilt_y',
        0x1c: 'tool_width',
        0x20: 'volume',
        0x28: 'misc',
    }

    button_names = {
        0x120: 'trigger',
        0x121: 'thumb',
        0x122: 'thumb2',
        0x123: 'top',
        0x124: 'top2',
        0x125: 'pinkie',
        0x126: 'base',
        0x127: 'base2',
        0x128: 'base3',
        0x129: 'base4',
        0x12a: 'base5',
        0x12b: 'base6',
        0x12f: 'dead',
        0x130: 'a',
        0x131: 'b',
        0x132: 'c',
        0x133: 'x',
        0x134: 'y',
        0x135: 'z',
        0x136: 'tl',
        0x137: 'tr',
        0x138: 'tl2',
        0x139: 'tr2',
        0x13a: 'select',
        0x13b: 'start',
        0x13c: 'mode',
        0x13d: 'thumbl',
        0x13e: 'thumbr',
        0x220: 'dpad_up',
        0x221: 'dpad_down',
        0x222: 'dpad_left',
        0x223: 'dpad_right',
        # XBox 360 controller uses these codes.
        0x2c0: 'dpad_left',
        0x2c1: 'dpad_right',
        0x2c2: 'dpad_up',
        0x2c3: 'dpad_down',
    }

    def __init__(self, device: str = '/dev/input/js0', *args, **kwargs):
        """
        :param device: Joystick device to monitor (default: ``/dev/input/js0``).
        """
        super().__init__(*args, **kwargs)
        self.device = device
        self._axis_states = {}
        self._button_states = {}
        self._axis_map = []
        self._button_map = []

    def _init_joystick(self, dev: IO):
        # Get the device name.
        buf = array.array('B', [0] * 64)
        ioctl(dev, 0x80006a13 + (0x10000 * len(buf)), buf)  # JSIOCGNAME(len)
        js_name = buf.tobytes().rstrip(b'\x00').decode('utf-8')

        # Get number of axes and buttons.
        buf = array.array('B', [0])
        ioctl(dev, 0x80016a11, buf)  # JSIOCGAXES
        num_axes = buf[0]

        buf = array.array('B', [0])
        ioctl(dev, 0x80016a12, buf)  # JSIOCGBUTTONS
        num_buttons = buf[0]

        # Get the axis map.
        buf = array.array('B', [0] * 0x40)
        ioctl(dev, 0x80406a32, buf)  # JSIOCGAXMAP

        for axis in buf[:num_axes]:
            axis_name = self.axis_names.get(axis, 'unknown(0x%02x)' % axis)
            self._axis_map.append(axis_name)
            self._axis_states[axis_name] = 0.0

        # Get the button map.
        buf = array.array('H', [0] * 200)
        ioctl(dev, 0x80406a34, buf)  # JSIOCGBTNMAP

        for btn in buf[:num_buttons]:
            btn_name = self.button_names.get(btn, 'unknown(0x%03x)' % btn)
            self._button_map.append(btn_name)
            self._button_states[btn_name] = 0

        self.bus.post(JoystickConnectedEvent(device=self.device, name=js_name, axes=self._axis_map,
                                             buttons=self._button_map))

    def run(self):
        super().run()
        self.logger.info(f'Opening {self.device}...')

        while not self.should_stop():
            # Open the joystick device.
            try:
                jsdev = open(self.device, 'rb')
                self._init_joystick(jsdev)
            except Exception as e:
                self.logger.debug(f'Joystick device on {self.device} not available: {e}')
                time.sleep(5)
                continue

            # Joystick event loop
            while not self.should_stop():
                try:
                    evbuf = jsdev.read(8)
                    if evbuf:
                        _, value, evt_type, number = struct.unpack('IhBB', evbuf)

                        if evt_type & 0x80:  # Initial state notification
                            continue

                        if evt_type & 0x01:
                            button = self._button_map[number]
                            if button:
                                self._button_states[button] = value
                                evt_class = JoystickButtonPressedEvent if value else JoystickButtonReleasedEvent
                                # noinspection PyTypeChecker
                                self.bus.post(evt_class(device=self.device, button=button))

                        if evt_type & 0x02:
                            axis = self._axis_map[number]
                            if axis:
                                fvalue = value / 32767.0
                                self._axis_states[axis] = fvalue
                                # noinspection PyTypeChecker
                                self.bus.post(JoystickAxisEvent(device=self.device, axis=axis, value=fvalue))
                except OSError as e:
                    self.logger.warning(f'Connection to {self.device} lost: {e}')
                    self.bus.post(JoystickDisconnectedEvent(device=self.device))
                    break

[((113, 14, 113, 40), 'array.array', 'array.array', ({(113, 26, 113, 29): '"""B"""', (113, 31, 113, 39): '[0] * 64'}, {}), "('B', [0] * 64)", False, 'import array\n'), ((118, 14, 118, 35), 'array.array', 'array.array', ({(118, 26, 118, 29): '"""B"""', (118, 31, 118, 34): '[0]'}, {}), "('B', [0])", False, 'import array\n'), ((119, 8, 119, 35), 'fcntl.ioctl', 'ioctl', ({(119, 14, 119, 17): 'dev', (119, 19, 119, 29): '(2147576337)', (119, 31, 119, 34): 'buf'}, {}), '(dev, 2147576337, buf)', False, 'from fcntl import ioctl\n'), ((122, 14, 122, 35), 'array.array', 'array.array', ({(122, 26, 122, 29): '"""B"""', (122, 31, 122, 34): '[0]'}, {}), "('B', [0])", False, 'import array\n'), ((123, 8, 123, 35), 'fcntl.ioctl', 'ioctl', ({(123, 14, 123, 17): 'dev', (123, 19, 123, 29): '(2147576338)', (123, 31, 123, 34): 'buf'}, {}), '(dev, 2147576338, buf)', False, 'from fcntl import ioctl\n'), ((127, 14, 127, 42), 'array.array', 'array.array', ({(127, 26, 127, 29): '"""B"""', (127, 31, 127, 41): '[0] * 64'}, {}), "('B', [0] * 64)", False, 'import array\n'), ((128, 8, 128, 35), 'fcntl.ioctl', 'ioctl', ({(128, 14, 128, 17): 'dev', (128, 19, 128, 29): '(2151705138)', (128, 31, 128, 34): 'buf'}, {}), '(dev, 2151705138, buf)', False, 'from fcntl import ioctl\n'), ((136, 14, 136, 41), 'array.array', 'array.array', ({(136, 26, 136, 29): '"""H"""', (136, 31, 136, 40): '[0] * 200'}, {}), "('H', [0] * 200)", False, 'import array\n'), ((137, 8, 137, 35), 'fcntl.ioctl', 'ioctl', ({(137, 14, 137, 17): 'dev', (137, 19, 137, 29): '(2151705140)', (137, 31, 137, 34): 'buf'}, {}), '(dev, 2151705140, buf)', False, 'from fcntl import ioctl\n'), ((144, 22, 145, 70), 'platypush.message.event.joystick.JoystickConnectedEvent', 'JoystickConnectedEvent', (), '', False, 'from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent, JoystickButtonPressedEvent, JoystickButtonReleasedEvent, JoystickAxisEvent\n'), ((158, 16, 158, 29), 'time.sleep', 'time.sleep', ({(158, 27, 158, 28): '(5)'}, {}), '(5)', False, 'import time\n'), ((166, 53, 166, 81), 'struct.unpack', 'struct.unpack', ({(166, 67, 166, 73): '"""IhBB"""', (166, 75, 166, 80): 'evbuf'}, {}), "('IhBB', evbuf)", False, 'import struct\n'), ((188, 34, 188, 79), 'platypush.message.event.joystick.JoystickDisconnectedEvent', 'JoystickDisconnectedEvent', (), '', False, 'from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent, JoystickButtonPressedEvent, JoystickButtonReleasedEvent, JoystickAxisEvent\n'), ((185, 46, 185, 108), 'platypush.message.event.joystick.JoystickAxisEvent', 'JoystickAxisEvent', (), '', False, 'from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent, JoystickButtonPressedEvent, JoystickButtonReleasedEvent, JoystickAxisEvent\n')]