"""
Joystick to drive the car around manually without keyboard.
Inspired by https://github.com/chrippa/ds4drv
"""
import fcntl
from io import FileIO
import os
from struct import Struct
import time
from evdev import InputDevice
from binascii import crc32
from pyudev import Context
from derp.part import Part
import derp.util


class DS4State:
    left_analog_x = 128
    left_analog_y = 128
    right_analog_x = 128
    right_analog_y = 128
    up = 0
    down = 0
    left = 0
    right = 0
    button_square = 0
    button_cross = 0
    button_circle = 0
    button_triangle = 0
    button_l1 = 0
    button_l2 = 0
    button_l3 = 0
    button_r1 = 0
    button_r2 = 0
    button_r3 = 0
    button_share = 0
    button_options = 0
    button_trackpad = 0
    button_ps = 0
    timestamp = 0
    left_trigger = 0
    right_trigger = 0
    accel_y = 0
    accel_x = 0
    accel_z = 0
    orientation_roll = 0
    orientation_yaw = 0
    orientation_pitch = 0
    trackpad_0_id = -1
    trackpad_0_active = False
    trackpad_0_x = 0
    trackpad_0_y = 0
    trackpad_1_id = -1
    trackpad_2_active = False
    trackpad_3_x = 0
    trackpad_4_y = 0
    battery_level = 0
    usb = False
    audio = False
    mic = False

    def __init__(self, recv_buffer=None):
        if recv_buffer:
            self.import_buffer(recv_buffer)

    def import_buffer(self, recv_buffer):
        short = Struct("<h")
        dpad = recv_buffer[7] % 16
        self.left_analog_x = recv_buffer[3]
        self.left_analog_y = recv_buffer[4]
        self.right_analog_x = recv_buffer[5]
        self.right_analog_y = recv_buffer[6]
        self.up = dpad in (0, 1, 7)
        self.down = dpad in (3, 4, 5)
        self.left = dpad in (5, 6, 7)
        self.right = dpad in (1, 2, 3)
        self.button_square = (recv_buffer[7] & 16) != 0
        self.button_cross = (recv_buffer[7] & 32) != 0
        self.button_circle = (recv_buffer[7] & 64) != 0
        self.button_triangle = (recv_buffer[7] & 128) != 0
        self.button_l1 = (recv_buffer[8] & 1) != 0
        self.button_l2 = (recv_buffer[8] & 4) != 0
        self.button_l3 = (recv_buffer[8] & 64) != 0
        self.button_r1 = (recv_buffer[8] & 2) != 0
        self.button_r2 = (recv_buffer[8] & 8) != 0
        self.button_r3 = (recv_buffer[8] & 128) != 0
        self.button_share = (recv_buffer[8] & 16) != 0
        self.button_options = (recv_buffer[8] & 32) != 0
        self.button_trackpad = (recv_buffer[9] & 2) != 0
        self.button_ps = (recv_buffer[9] & 1) != 0
        self.timestamp = recv_buffer[9] >> 2
        self.left_trigger = recv_buffer[10]
        self.right_trigger = recv_buffer[11]
        self.accel_y = short.unpack_from(recv_buffer, 15)[0]
        self.accel_x = short.unpack_from(recv_buffer, 17)[0]
        self.accel_z = short.unpack_from(recv_buffer, 19)[0]
        self.orientation_roll = -(short.unpack_from(recv_buffer, 21)[0])
        self.orientation_yaw = short.unpack_from(recv_buffer, 23)[0]
        self.orientation_pitch = short.unpack_from(recv_buffer, 25)[0]
        self.trackpad_0_id = recv_buffer[37] & 0x7F
        self.trackpad_0_active = (recv_buffer[37] >> 7) == 0
        self.trackpad_0_x = ((recv_buffer[39] & 0x0F) << 8) | recv_buffer[38]
        self.trackpad_0_y = recv_buffer[40] << 4 | ((recv_buffer[39] & 0xF0) >> 4)
        self.trackpad_1_id = recv_buffer[41] & 0x7F
        self.trackpad_2_active = (recv_buffer[41] >> 7) == 0
        self.trackpad_3_x = ((recv_buffer[43] & 0x0F) << 8) | recv_buffer[42]
        self.trackpad_4_y = recv_buffer[44] << 4 | ((recv_buffer[43] & 0xF0) >> 4)
        self.battery_level = recv_buffer[32] % 16
        self.usb = (recv_buffer[32] & 16) != 0
        self.audio = (recv_buffer[32] & 32) != 0
        self.mic = (recv_buffer[32] & 64) != 0


class Joystick(Part):
    """Joystick to drive the car around manually without keyboard."""

    def __init__(self, config):
        """Joystick to drive the car around manually without keyboard."""
        super(Joystick, self).__init__(config, "joystick", [])

        # State/Controls
        self.speed = 0
        self.steer = 0
        self.speed_offset = 0
        self.steer_offset = 0
        self.is_calibrated = True
        self.is_autonomous = False
        self.state = DS4State()
        self.last_state = DS4State()
        self.__fd = None
        self.__input_device = None
        self.__report_fd = None
        self.__report_id = 0x11
        self.__keep_running = True
        self.__connect()

    def __del__(self):
        self.publish("action", isManual=True, speed=0, steer=0)
        self.publish("controller", isAutonomous=False, speedOffset=0, steerOffset=0, exit=True)
        super(Joystick, self).__del__()
        try:
            self.send(red=1, rumble_high=1)
            time.sleep(0.5)
            self.send(blue=0.1, green=0.1, red=0.5)
        except:
            pass
        if self.__fd is not None:
            self.__fd.close()
        if self.__input_device is not None:
            self.__input_device.ungrab()

    def __find_device(self):
        context = Context()
        for hidraw_device in context.list_devices(subsystem="hidraw"):
            hid_device = hidraw_device.parent
            if hid_device.subsystem != "hid" or hid_device.get("HID_NAME") != "Wireless Controller":
                continue
            for child in hid_device.parent.children:
                event_device = child.get("DEVNAME", "")
                if event_device.startswith("/dev/input/event"):
                    break
            else:
                continue

            device_addr = hid_device.get("HID_UNIQ", "").upper()
            return device_addr, hidraw_device.device_node, event_device
        return None, None, None

    def __connect(self):
        device_addr, hidraw_device, event_device = self.__find_device()
        if device_addr is None:
            return False
        self.__report_fd = os.open(hidraw_device, os.O_RDWR | os.O_NONBLOCK)
        self.__fd = FileIO(self.__report_fd, "rb+", closefd=False)
        self.__input_device = InputDevice(event_device)
        self.__input_device.grab()
        buf = bytearray(38)
        buf[0] = 0x02
        try:
            return bool(fcntl.ioctl(self.__fd, 3223734279, bytes(buf)))
        except:
            pass
        if self.recv():
            self.update_controller()

    def __in_deadzone(self, value):
        """ Deadzone checker for analog sticks """
        return 128 - self._config["deadzone"] < value <= 128 + self._config["deadzone"]

    def __normalize_stick(self, value, deadzone):
        """
        Normalize stick value from [0, 255] to [0, 1]
        Ignore a 128-centered deadzone
        """
        value -= 128
        value = value - deadzone if value > 0 else value + deadzone
        value /= 127 - deadzone
        return value

    def recv(self, limit=1000, duration=0.001, report_size=78):
        """
        Attempt to get a message from the device. 
        Args:
            limit (int): number of device polls to do
            duration (int): how long to wait between polls
        Returns:
            Whether we have successfully updated the status of the program
        """
        for i in range(limit):
            time.sleep(duration)
            recv_buffer = bytearray(report_size)
            try:
                ret = self.__fd.readinto(recv_buffer)
            except IOError:
                # print("joystick: IO Error")
                continue
            except AttributeError:
                # print("joystick: Attribute Error")
                continue
            if ret is None:
                # print("joystick: ret is none")
                continue
            if ret < report_size:
                # print("joystick: ret too small (%i) expected (%i)" % (ret, report_size))
                continue
            if recv_buffer[0] != self.__report_id:
                # print("joystick: Wrong report id (%i) expected (%i):"
                #      % (recv_buffer[0], self.__report_id))
                continue
            self._timestamp = derp.util.get_timestamp()
            self.last_state = self.state
            self.state = DS4State(recv_buffer)
            self.process_state()
            return True
        return False

    def update_controller(self):
        """Send the state of the system to the controller"""
        green = 1.0 if self.is_autonomous else 0
        red = 1.0 if self.is_calibrated else 0
        blue = 1.0
        light_on = 1.0
        light_off = 0.0
        self.send(red=red, green=green, blue=blue, light_on=light_on, light_off=light_off)
        return True

    def send(self, rumble_high=0, rumble_low=0, red=0, green=0, blue=0, light_on=0, light_off=0):
        """Actuate the controller by setting its rumble or light color/blink"""
        packet = bytearray(79)
        packet[:5] = [0xA2, 0x11, 0x80, 0x00, 0xFF]
        packet[7] = int(rumble_high * 255 + 0.5)
        packet[8] = int(rumble_low * 255 + 0.5)
        packet[9] = int(red * 255 + 0.5)
        packet[10] = int(green * 255 + 0.5)
        packet[11] = int(blue * 255 + 0.5)
        packet[12] = int(light_on * 255 + 0.5)
        packet[13] = int(light_off * 255 + 0.5)
        crc = crc32(packet[:-4])
        packet[-4] = crc & 0x000000FF
        packet[-3] = (crc & 0x0000FF00) >> 8
        packet[-2] = (crc & 0x00FF0000) >> 16
        packet[-1] = (crc & 0xFF000000) >> 24
        hid = bytearray((self.__report_id,))
        if self.__fd is not None:
            self.__fd.write(hid + packet[2:])
            return True
        return False

    def process_state(self):
        """
        For the given  input, figure out how we should affect the state
        and put that into out.
        """
        self.controller_changed = False
        self.action_changed = False
        self.__keep_running = not self.state.button_trackpad
        if not self.__in_deadzone(self.state.left_analog_x):
            steer = self.__normalize_stick(self.state.left_analog_x, self._config["deadzone"]) * self._config['steer_normalizer']
            if steer != self.steer:
                self.steer = steer
                self.action_changed = True
        elif not self.__in_deadzone(self.last_state.left_analog_x):
            self.steer = 0
            self.action_changed = True
        if self.state.left_trigger:
            speed = -self.state.left_trigger / 255 * self._config['speed_normalizer']
            if speed != self.speed:
                self.speed = speed
                self.action_changed = True
        elif self.last_state.left_trigger:
            self.speed = 0
            self.action_changed = True
        if self.state.right_trigger:
            speed = self.state.right_trigger / 255 * self._config['speed_normalizer']
            if speed != self.speed:
                self.speed = speed
                self.action_changed = True
        elif self.last_state.right_trigger:
            self.speed = 0
            self.action_changed = True
        if self.state.left and not self.last_state.left:
            self.steer_offset -= 5 / 255
            self.controller_changed = True
        if self.state.right and not self.last_state.right:
            self.steer_offset += 5 / 255
            self.controller_changed = True
        if self.state.up and not self.last_state.up:
            self.speed_offset += 5 / 255
            self.controller_changed = True
        if self.state.down and not self.last_state.down:
            self.speed_offset -= 5 / 255
            self.controller_changed = True
        if self.state.button_square and not self.last_state.button_square:
            pass
        if self.state.button_cross and not self.last_state.button_cross:
            self.speed = 0
            self.steer = 0
            self.speed_offset = 0
            self.is_autonomous = False
            self.action_changed = True
            self.controller_changed = True
        if self.state.button_triangle and not self.last_state.button_triangle:
            self.is_autonomous = True
            self.controller_changed = True
        if self.state.button_circle and not self.last_state.button_circle:
            self.controller_changed = True

    def run(self):
        """Query one set of inputs from the joystick and send it out."""
        start_time = derp.util.get_timestamp()
        if not self.recv():
            print("joystick: timed out", start_time)
            self.__connect()
            return True
        if self.controller_changed:
            self.update_controller()
            self.publish(
                "controller",
                isAutonomous=self.is_autonomous,
                speedOffset=self.speed_offset,
                steerOffset=self.steer_offset,
            )
        if self.action_changed:
            self.publish("action", isManual=True, speed=self.speed, steer=self.steer)
        return self.__keep_running