"""SC Controller - Steam Controller Driver

Driver for Steam Controller over bluetooth (evdev)

Shares a lot of classes with sc_dongle.py
"""
from __future__ import annotations

import ctypes
import logging
import os
import struct
import sys
from math import cos, sin
from typing import TYPE_CHECKING

# TODO(Martin): Remove this mess after https://github.com/vpelletier/python-hidraw/issues/8 is resolved
# Try hidraw_pure(hopefully what hidraw-pure renames itself to)
# If that fails, try hidraw, which is either from python-hidapi (wrong) or from hidraw-pure v1.2
# If all fails just use the vendored version
try:
	from hidraw_pure import HIDRaw
except ImportError:
	try:
		from hidraw import HIDRaw
	except ImportError:
		from scc.lib.hidraw import HIDRaw

from scc.constants import STICK_PAD_MAX, STICK_PAD_MIN, ControllerFlags
from scc.tools import find_library

from .sc_dongle import SCConfigType, SCController, SCPacketLength, SCPacketType

if TYPE_CHECKING:
	from typing import BinaryIO, Never

	from scc.poller import Poller
	from scc.sccdaemon import SCCDaemon

VENDOR_ID = 0x28DE
PRODUCT_ID = 0x1106
PACKET_SIZE = 20

log = logging.getLogger("SCBT")


class SCByBtControllerInput(ctypes.Structure):
	_fields_ = [
		("type", ctypes.c_uint16),
		("buttons", ctypes.c_uint32),
		("ltrig", ctypes.c_uint8),
		("rtrig", ctypes.c_uint8),
		("lstick_x", ctypes.c_int32),
		("lstick_y", ctypes.c_int32),
		("lpad_x", ctypes.c_int32),
		("lpad_y", ctypes.c_int32),
		("rpad_x", ctypes.c_int32),
		("rpad_y", ctypes.c_int32),
		("accel_x", ctypes.c_int32),
		("accel_y", ctypes.c_int32),
		("accel_z", ctypes.c_int32),
		("gpitch", ctypes.c_int32),
		("groll", ctypes.c_int32),
		("gyaw", ctypes.c_int32),
		("q1", ctypes.c_int32),
		("q2", ctypes.c_int32),
		("q3", ctypes.c_int32),
		("q4", ctypes.c_int32),
	]


class SCByBtC(ctypes.Structure):
	_fields_ = [
		("fileno", ctypes.c_int),
		("buffer", ctypes.c_char * 256),
		("long_packet", ctypes.c_uint8),
		("state", SCByBtControllerInput),
		("old_state", SCByBtControllerInput),
	]


SCByBtCPtr = ctypes.POINTER(SCByBtC)


class Driver:
	"""Similar to USB driver, but with hidraw used for backend"""

	# TODO: It should be possible to merge this, usb and hiddrv

	def __init__(self, daemon: SCCDaemon, config: dict) -> None:
		self.config: dict = config
		self.daemon: SCCDaemon = daemon
		self.reconnecting: set[str] = set()
		self._lib: ctypes.CDLL = find_library("libsc_by_bt")
		read_input = self._lib.read_input
		read_input.restype = ctypes.c_int
		read_input.argtypes = [SCByBtCPtr]
		daemon.get_device_monitor().add_callback("bluetooth", VENDOR_ID, PRODUCT_ID, self.new_device_callback, None)

	def retry(self, syspath: str) -> None:
		"""Schedules reconnecting controller after read operation fails."""

		def reconnect(*a) -> None:
			if syspath in self.reconnecting:
				self.reconnecting.remove(syspath)
				log.debug("Reconnecting to controller...")
				self.new_device_callback(syspath)

		self.reconnecting.add(syspath)
		self.daemon.get_device_monitor().add_remove_callback(syspath, self._retry_cancel)
		self.daemon.get_scheduler().schedule(1.0, reconnect)

	def _retry_cancel(self, syspath: str, vendor: int, product: int) -> None:
		"""Cancels reconnection scheduled by 'retry'.

		Called when device monitor reports controller (as in BT device) being disconencted.
		"""
		if syspath in self.reconnecting:
			self.reconnecting.remove(syspath)

	def new_device_callback(self, syspath: str, *whatever) -> SCByBt | None:
		hidrawname = self.daemon.get_device_monitor().get_hidraw(syspath)
		if hidrawname is None:
			return None
		try:
			device_file = open(os.path.join("/dev/", hidrawname), "r+b", buffering=0)
			hidraw = HIDRaw(device_file)
			return SCByBt(self, syspath, hidraw, device_file)
		except Exception as e:
			log.exception(e)
			return None


class SCByBt(SCController):
	flags: int = ControllerFlags.LPAD_RPAD_IS_CIRCLE | ControllerFlags.SEPARATE_LSTICK

	def __init__(self, driver: Driver, syspath: str, hidrawdev: HIDRaw, device_file: BinaryIO) -> None:
		self._serial: bytes
		self._cmsg = []  # controll messages
		self._transfer_list = []
		self.driver: Driver = driver
		self.daemon: SCCDaemon = driver.daemon
		self.syspath: str = syspath
		SCController.__init__(self, self, -1, -1)
		self._led_level = 30
		self._device_name: str = hidrawdev.getName()
		self._hidrawdev: HIDRaw = hidrawdev
		self._device_file: BinaryIO = device_file
		self._fileno: int = self._device_file.fileno()
		self._c_data: SCByBtC = SCByBtC(fileno=self._fileno, long_packet=0)
		self._c_data_ptr = ctypes.byref(self._c_data)
		self._old_state = self._c_data.old_state
		self._state = self._c_data.state
		self._poller: Poller = self.daemon.get_poller()
		if self._poller:
			self._poller.register(self._fileno, self._poller.POLLIN, self._input)
		self.daemon.get_device_monitor().add_remove_callback(syspath, self.close)
		self.read_serial()
		self.configure()
		self.flush()
		self.daemon.add_controller(self)

	def is_bluetooth(self) -> bool:
		return True

	def get_device_name(self) -> str:
		# Method needed by evdev driver
		# return self._device_name
		return "Steam Controller over Bluetooth"

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

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

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

		See SCController.configure method in sc_dongle.py;

		This method is almost the same, with different set of hardcoded constants.
		"""
		# ------
		"""
		packet format:
		 - uint8_t type - SCPacketType.CONFIGURE
		 - uint8_t size - SCPacketLength.CONFIGURE_BT or SCPacketLength.LED
		 - uint8_t config_type - SCConfigType.CONFIGURE_BT or SCConfigType.LED
		 - (variable) data

		Format for data when configuring controller:
		 - 12B		unknown1 - (hex 0000310200080700070700300)
		 - uint8	enable gyro sensor - 0x14 enables, 0x00 disables
		 - 2b		unknown2 - (0x00, 0x2e)

		Format for data when configuring led:
		 - uint8	led
		 - 60b		unused
		"""
		# idle_timeout is ignored
		if enable_gyros is not None:
			self._enable_gyros = enable_gyros
		if led_level is not None:
			self._led_level = int(led_level)

		unknown1 = b"\x00\x00\x31\x02\x00\x08\x07\x00\x07\x07\x00\x30"
		unknown2 = b"\x00\x2e"

		# Timeout & Gyros
		self.overwrite_control(
			self._ccidx,
			struct.pack(
				">BBB12sB2s",
				SCPacketType.CONFIGURE,
				SCPacketLength.CONFIGURE_BT,
				SCConfigType.CONFIGURE_BT,
				unknown1,
				# 0x10 (Gyro) | 0x08 (Accel) | 0x04 (Quat)
				0x1C if self._enable_gyros else 0,
				unknown2,
			),
		)

		# LED
		self.overwrite_control(
			self._ccidx,
			struct.pack(">BBBB", SCPacketType.CONFIGURE, SCPacketLength.LED, SCConfigType.LED, self._led_level),
		)

	def read_serial(self) -> None:
		self._serial = self._hidrawdev.getPhysicalAddress().replace(b":", b"")

	def send_control(self, index, data) -> None:
		"""Schedules writing control to device"""
		# For BT controller, index is ignored
		zeros = b"\x00" * (PACKET_SIZE - len(data) - 1)
		self._cmsg.insert(0, b"\xc0" + data + zeros)

	def overwrite_control(self, index, data) -> None:
		"""Similar to send_control, but this one checks and overwrites already scheduled controll for same device/index."""
		# For BT controller, index is ignored
		for x in self._cmsg:
			# First byte is reserved, following 3 are for PacketType, size and ConfigType
			if x[0:4] == data[0:4]:
				self._cmsg.remove(x)
				break
		self.send_control(index, data)

	def make_request(self, index, callback, data, size=PACKET_SIZE):
		"""There are no requests one can send to BT controller, so this just causes exception."""
		raise RuntimeError("make_request over BT not implemented")

	def flush(self) -> None:
		"""Flushes all prepared control messages to the device"""
		while len(self._cmsg):
			msg = self._cmsg.pop()
			# Feature report data must be sent with report ID 3
			# or Input/output error will occur with later BlueZ versions (5.64)
			# Does not affect older BlueZ versions
			self._hidrawdev.sendFeatureReport(msg, 3)

	def input(self, idata):
		raise RuntimeError("This shouldn't be called, ever")

	def turnoff(self) -> None:
		super().turnoff()
		# Need to call flush to make sure packet is sent to controller
		self.flush()

	def close(self, *a) -> None:
		if self._poller:
			self._poller.unregister(self._fileno)
		self.daemon.remove_controller(self)
		self._device_file.close()

	def disconnected(self) -> None:
		pass

	def _input(self, *a) -> None:
		r = self.driver._lib.read_input(self._c_data_ptr)

		if r == 1:
			if self.mapper is not None:
				if self._input_rotation_l and (self._state.type & 0x0100) != 0:
					lx, ly = self._state.lpad_x, self._state.lpad_y
					s, c = sin(self._input_rotation_l), cos(self._input_rotation_l)

					# Adjust LX for rotation and clamp
					value = int(lx * c - ly * s)
					self._state.lpad_x = max(STICK_PAD_MIN, min(STICK_PAD_MAX, value))

					# Adjust LY for rotation and clamp
					value = int(lx * s + ly * c)
					self._state.lpad_y = max(STICK_PAD_MIN, min(STICK_PAD_MAX, value))
				if self._input_rotation_r and (self._state.type & 0x0200) != 0:
					rx, ry = self._state.rpad_x, self._state.rpad_y
					s, c = sin(self._input_rotation_r), cos(self._input_rotation_r)

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

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

				self.mapper.input(self, self._old_state, self._state)
			self.flush()
		elif r > 1:
			log.error("Read Failed")
			self.close()
			self.driver.retry(self.syspath)


def hidraw_test(filename: str) -> Never:
	class FakeDaemon:
		def add_error(self, id, error) -> None:
			log.error(error)

		def remove_error(*a) -> None:
			pass

		def add_mainloop(*a) -> None:
			pass

		def get_active_ids(*a):
			return []

		def get_poller(self) -> None:
			return None

	class TestSC(SCByBt):
		def input(self, tup) -> None:
			print(tup)

	device_file = open(filename, "w+b")
	hidraw = HIDRaw(device_file)
	driver = Driver(FakeDaemon(), {})
	c = TestSC(driver, None, hidraw, device_file)
	c.configure()
	c.flush()
	while True:
		c._input()
		print({x[0]: getattr(c._state, x[0]) for x in c._state._fields_})


def init(daemon: SCCDaemon, config: dict) -> bool:
	"""Registers hotplug callback for controller dongle"""
	# if not (HAVE_EVDEV and config["drivers"].get("evdevdrv")):
	# 	log.warning("Evdev driver is not enabled, Steam Controller over Bluetooth support cannot be enabled.")
	# 	return False
	_drv = Driver(daemon, config)
	return True


_drv: Driver | None = None

if __name__ == "__main__":
	""" Called when executed as script """
	from scc.tools import init_logging, set_logging_level

	init_logging()
	set_logging_level(True, True)
	sys.exit(hidraw_test(sys.argv[1]))
