"""SC Controller - Steam Controller Wireless Receiver (aka Dongle) Driver.

Called and used when Dongle is detected on USB bus.
Handles one or multiple controllers connected to dongle.
"""

from __future__ import annotations

import logging
import struct
from enum import IntEnum
from math import cos, sin
from math import pi as PI
from typing import TYPE_CHECKING, NamedTuple

from scc.config import Config
from scc.constants import STICK_PAD_MAX, STICK_PAD_MIN, SCButtons
from scc.controller import Controller
from scc.drivers.usb import SCUSBDevice, register_hotplug_device

if TYPE_CHECKING:
	from usb1 import USBDevice, USBDeviceHandle

	from scc.drivers.sc2 import SC2Device
	from scc.drivers.sc_by_bt import SCByBt
	from scc.drivers.sc_by_cable import SCByCable
	from scc.drivers.steamdeck import Deck
	from scc.sccdaemon import SCCDaemon

class ControllerInput(NamedTuple):
	"""Based on INPUT_FORMAT except anything starting with "ukn_"""

	type: int
	status: int
	seq: int
	buttons: int
	ltrig: int
	rtrig: int
	lpad_x: int
	lpad_y: int
	rpad_x: int
	rpad_y: int
	accel_x: int
	accel_y: int
	accel_z: int
	gpitch: int
	groll: int
	gyaw: int
	q1: int
	q2: int
	q3: int
	q4: int

VENDOR_ID = 0x28DE
PRODUCT_ID = 0x1142
FIRST_ENDPOINT = 2
FIRST_CONTROLIDX = 1
INPUT_FORMAT = [
	("b", "type"),
	("x", "ukn_01"),
	("B", "status"),
	("x", "ukn_02"),
	("H", "seq"),
	("x", "ukn_03"),
	("I", "buttons"),
	("B", "ltrig"),
	("B", "rtrig"),
	("x", "ukn_04"),
	("x", "ukn_05"),
	("x", "ukn_06"),
	("h", "lpad_x"),
	("h", "lpad_y"),
	("h", "rpad_x"),
	("h", "rpad_y"),
	("4x", "ukn_06"),
	("h", "accel_x"),
	("h", "accel_y"),
	("h", "accel_z"),
	("h", "gpitch"),
	("h", "groll"),
	("h", "gyaw"),
	("h", "q1"),
	("h", "q2"),
	("h", "q3"),
	("h", "q4"),
	("16x", "ukn_07"),
]
FORMATS, NAMES = zip(*INPUT_FORMAT)
TUP_FORMAT = "<" + "".join(FORMATS)
SCI_NULL = ControllerInput._make(struct.unpack("<" + "".join(FORMATS), b"\x00" * 64))
STICKPRESS = 0b1000000000000000000000000000000


log = logging.getLogger("SCDongle")


class Dongle(SCUSBDevice):
	MAX_ENDPOINTS = 4
	_available_serials = set()  # used only is ignore_serials option is enabled

	def __init__(self, device: USBDevice, handle: USBDeviceHandle, daemon: SCCDaemon) -> None:
		self.daemon: SCCDaemon = daemon
		SCUSBDevice.__init__(self, device, handle)

		self.claim_by(klass=3, subclass=0, protocol=0)
		self._controllers: dict[int, SCController] = {}
		self._no_serial = []
		for i in range(Dongle.MAX_ENDPOINTS):
			# Steam dongle apparently can do only 4 controllers at once
			self.set_input_interrupt(FIRST_ENDPOINT + i, 64, self._on_input)

	def close(self) -> None:
		# Called when dongle is removed
		for c in self._controllers.values():
			self.daemon.remove_controller(c)
		self._controllers = {}
		SCUSBDevice.close(self)

	def _add_controller(self, endpoint: int) -> None:
		"""Called when new controller is detected either by HOTPLUG message or by receiving first input event."""
		ccidx: int = FIRST_CONTROLIDX + endpoint - FIRST_ENDPOINT
		c = SCController(self, ccidx, endpoint)
		c.configure()
		c.read_serial()
		self._controllers[endpoint] = c

	def _on_input(self, endpoint: int, data) -> None:
		tup = ControllerInput._make(struct.unpack(TUP_FORMAT, data))
		if tup.status == SCStatus.HOTPLUG:
			# Most of INPUT_FORMAT doesn't apply here
			# data[4] is the connect flag (2 == connected)
			if data[4] == 2:
				# Controller connected
				if endpoint not in self._controllers:
					self._add_controller(endpoint)
			# Controller disconnected
			elif endpoint in self._controllers:
				self.daemon.remove_controller(self._controllers[endpoint])
				self._controllers[endpoint].disconnected()
				del self._controllers[endpoint]
		elif tup.status == SCStatus.INPUT:
			if endpoint not in self._controllers:
				self._add_controller(endpoint)
			elif len(self._no_serial):
				for x in self._no_serial:
					x.read_serial()
				self._no_serial = []
			else:
				self._controllers[endpoint].input(tup)


class SCStatus(IntEnum):
	IDLE = 0x04
	INPUT = 0x01
	HOTPLUG = 0x03


class SCPacketType(IntEnum):
	OFF = 0x9F
	AUDIO = 0xB6
	CLEAR_MAPPINGS = 0x81
	CONFIGURE = 0x87
	LED = 0x87
	CALIBRATE_JOYSTICK = 0xBF
	CALIBRATE_TRACKPAD = 0xA7
	SET_AUDIO_INDICES = 0xC1
	LIZARD_MODE = 0x8E
	FEEDBACK = 0x8F
	RESET = 0x95
	GET_SERIAL = 0xAE


class SCPacketLength(IntEnum):
	LED = 0x03
	OFF = 0x04
	FEEDBACK = 0x07
	CONFIGURE = 0x15
	CONFIGURE_BT = 0x0F
	GET_SERIAL = 0x15


class SCConfigType(IntEnum):
	LED = 0x2D
	CONFIGURE = 0x32
	CONFIGURE_BT = 0x18


class SCController(Controller):
	def __init__(self, driver: Deck | Dongle | SCByBt | SCByCable | SC2Device, ccidx: int, endpoint: int) -> None:
		Controller.__init__(self)
		self._driver: Deck | Dongle | SCByBt | SCByCable | SC2Device = driver
		self._endpoint: int = endpoint
		self._idle_timeout: int = 600
		self._enable_gyros: bool = False
		self._input_rotation_l = 0
		self._input_rotation_r = 0
		self._led_level: int = 10
		# TODO: Is serial really used anywhere?
		self._serial: str = "0000000000"
		self._id: str = self._generate_id() if driver else "-"
		self._old_state: ControllerInput = SCI_NULL
		self._ccidx: int = ccidx

	def get_type(self) -> str:
		return "sc"

	def __repr__(self) -> str:
		return f"<SCWireless {self.get_id()}>"

	def input(self, idata: ControllerInput) -> None:
		old_state, self._old_state = self._old_state, idata
		if self.mapper:
			# if idata.buttons & SCButtons.LPAD:
			# # STICKPRESS button may signalize pressing stick instead
			# if (idata.buttons & STICKPRESS) and not (idata.buttons & STICKTILT):
			# idata = ControllerInput.replace(buttons=idata.buttons & ~SCButtons.LPAD)

			if self._input_rotation_l or self._input_rotation_r:
				lx, ly = idata.lpad_x, idata.lpad_y
				rx, ry = idata.rpad_x, idata.rpad_y

				if self._input_rotation_l and idata.buttons & SCButtons.LPADTOUCH:
					s, c = sin(self._input_rotation_l), cos(self._input_rotation_l)
					# Adjust LX for rotation and clamp
					value = int(idata.lpad_x * c - idata.lpad_y * s)
					lx = max(STICK_PAD_MIN, min(STICK_PAD_MAX, value))

					# Adjust LY for rotation and clamp
					value = int(idata.lpad_x * s + idata.lpad_y * c)
					ly = max(STICK_PAD_MIN, min(STICK_PAD_MAX, value))

				if self._input_rotation_r and idata.buttons & SCButtons.RPADTOUCH:
					s, c = sin(self._input_rotation_r), cos(self._input_rotation_r)

					# Adjust RX for rotation and clamp
					value = int(idata.rpad_x * c - idata.rpad_y * s)
					rx = max(STICK_PAD_MIN, min(STICK_PAD_MAX, value))

					# Adjust RY for rotation and clamp
					value = int(idata.rpad_x * s + idata.rpad_y * c)
					ry = max(STICK_PAD_MIN, min(STICK_PAD_MAX, value))

				# TODO: This is awful :(
				idata = ControllerInput(
					idata.type,
					idata.status,
					idata.seq,
					idata.buttons,
					idata.ltrig,
					idata.rtrig,
					lx,
					ly,
					rx,
					ry,
					idata.accel_x,
					idata.accel_y,
					idata.accel_z,
					idata.gpitch,
					idata.groll,
					idata.gyaw,
					idata.q1,
					idata.q2,
					idata.q3,
					idata.q4,
				)

			self.mapper.input(self, old_state, idata)

	def _generate_id(self) -> str:
		"""ID is generated as 'scX' where where 'X' starts as 0 and increases as more controllers are connected.

		This is used only when reading serial numbers from device is disabled.
		sc_by_cable generates ids in scBUS:PORT format.
		"""
		magic_number = 1
		tp = self.get_type()
		controller_id = None
		while controller_id is None or controller_id in self._driver.daemon.get_active_ids():
			controller_id = f"{tp}{magic_number}"
			magic_number += 1
		return controller_id

	def read_serial(self) -> None:
		"""Requests and reads serial number from controller"""
		if Config()["ignore_serials"]:
			# Special exception for cases when controller drops instead of
			# sending serial number. See issue #103
			self.generate_serial()
			self.on_serial_got()
			return

		def cb(rawserial) -> None:
			size, serial = struct.unpack(">xBx12s49x", rawserial)
			if size > 1:
				serial = serial.strip(b" \x00").decode("ASCII")
				self._serial = serial
				self.on_serial_got()
			else:
				self._driver._no_serial.append(self)

		self._driver.make_request(
			self._ccidx, cb, struct.pack(">BBB61x", SCPacketType.GET_SERIAL, SCPacketLength.GET_SERIAL, 0x01),
		)

	def generate_serial(self) -> None:
		"""Called only if ignore_serials is enabled"""
		if len(self._driver._available_serials) > 0:
			self._serial = self._driver._available_serials.pop()
		else:
			self._serial = self.get_id()
		log.debug("Not requesting serial number for SC %s", self._serial)

	def on_serial_got(self) -> None:
		try:
			log.debug("Got wireless SC with serial %s", self._serial)
		except UnicodeDecodeError:
			log.debug("Failed to decode wireless SC serial")
			self._serial = self._driver._available_serials.pop()
		self._id = str(self._serial)
		self._driver.daemon.add_controller(self)

	def apply_config(self, config: dict) -> None:
		self.configure(idle_timeout=int(config["idle_timeout"]), led_level=float(config["led_level"]))
		self._input_rotation_l = float(config["input_rotation_l"]) * PI / -180.0
		self._input_rotation_r = float(config["input_rotation_r"]) * PI / -180.0

	def disconnected(self) -> None:
		# If ignore_serials config option is enabled, fake serial used by this
		# controller is stored away and reused when next controller is connected
		if Config()["ignore_serials"]:
			self._driver._available_serials.add(self._serial)

	FORMAT1 = b">BBBBB13sB2s43x"
	# Has to be overriden in sc_by_cable
	FORMAT2 = b">BBBB59x"

	def configure(
		self, idle_timeout: int | None = None, enable_gyros: bool | None = None, led_level: int | None = None,
	) -> None:
		"""Sets and, if possible, sends configuration to controller.

		Only value that is provided is changed.
		'idle_timeout' is in seconds.
		'led_level' is precent (0-100)
		"""
		# ------
		"""
		packet format:
		 - uint8_t type - SCPacketType.CONFIGURE
		 - uint8_t size - SCPacketLength.CONFIGURE or SCPacketLength.LED
		 - uint8_t config_type - SCConfigType.CONFIGURE or SCConfigType.LED
		 - 61B data

		Format for data when configuring controller:
		 - uint16	timeout
		 - 13B		unknown1 - (0x18, 0x00, 0x00, 0x31, 0x02, 0x00, 0x08, 0x07, 0x00, 0x07, 0x07, 0x00, 0x30)
		 - uint8	enable gyro sensor - 0x14 enables, 0x00 disables
		 - 2B		unknown2 - (0x00, 0x2e)
		 - 43B		unused

		Format for data when configuring led:
		 - uint8	led
		 - 60B		unused
		"""

		if idle_timeout is not None:
			self._idle_timeout = idle_timeout
		if enable_gyros is not None:
			self._enable_gyros = enable_gyros
		if led_level is not None:
			self._led_level = led_level

		unknown1 = b"\x18\x00\x00\x31\x02\x00\x08\x07\x00\x07\x07\x00\x30"
		unknown2 = b"\x00\x2e"
		timeout1 = self._idle_timeout & 0x00FF
		timeout2 = (self._idle_timeout & 0xFF00) >> 8

		# Timeout & Gyros
		self._driver.overwrite_control(
			self._ccidx,
			struct.pack(
				self.FORMAT1,
				SCPacketType.CONFIGURE,
				SCPacketLength.CONFIGURE,
				SCConfigType.CONFIGURE,
				timeout1,
				timeout2,
				unknown1,
				# 0x10 (Gyro) | 0x08 (Accel) | 0x04 (Quat)
				0x1C if self._enable_gyros else 0,
				unknown2,
			),
		)

		# LED
		self._driver.overwrite_control(
			self._ccidx,
			struct.pack(
				self.FORMAT2, SCPacketType.CONFIGURE, SCPacketLength.LED, SCConfigType.LED, int(self._led_level),
			),
		)

	def set_led_level(self, level: int) -> None:
		level = min(100, int(level)) & 0xFF
		if self._led_level != level:
			self._led_level = level
			self._driver.overwrite_control(
				self._ccidx, struct.pack(">BBBB59x", SCPacketType.CONFIGURE, 0x03, SCConfigType.LED, self._led_level),
			)

	def set_gyro_enabled(self, enabled: bool) -> None:
		self.configure(enable_gyros=enabled)

	def turnoff(self) -> None:
		log.debug("Turning off the controller...")

		# Mercilessly stolen from scraw library
		self._driver.send_control(self._ccidx, struct.pack("<BBBBBB", SCPacketType.OFF, 0x04, 0x6F, 0x66, 0x66, 0x21))

	def get_gyro_enabled(self) -> bool:
		"""Returns True if gyroscope input is currently enabled"""
		return self._enable_gyros

	def feedback(self, data) -> None:
		self._feedback(*data.data)

	def _feedback(self, position: int, amplitude: int = 128, period: int = 0, count: int = 1) -> None:
		"""Add haptic feedback to be send on next usb tick.

		@param int position		haptic to use 1 for left 0 for right
		@param int amplitude	signal amplitude from 0 to 65535
		@param int period		signal period from 0 to 65535
		@param int count		number of period to play
		"""
		if amplitude >= 0:
			self._driver.send_control(
				self._ccidx, struct.pack("<BBBHHH", SCPacketType.FEEDBACK, 0x07, position, amplitude, period, count),
			)


def init(daemon: SCCDaemon, config: dict) -> Dongle | bool:
	"""Registers hotplug callback for controller dongle"""

	def cb(device: USBDevice, handle: USBDeviceHandle) -> Dongle:
		return Dongle(device, handle, daemon)

	register_hotplug_device(cb, VENDOR_ID, PRODUCT_ID)
	return True
