"""SCC - Steam Deck Driver

Based on sc_by_cable and steamdeck.c

Deck uses slightly different packed format and so common handle_inpu is not used.

On top of that, deck will automatically enable lizard mode unless requested
to not do so periodically.
"""
from __future__ import annotations

import ctypes
import logging
import struct
from enum import IntEnum
from typing import TYPE_CHECKING

from scc.constants import STICK_PAD_MAX, STICK_PAD_MIN, ControllerFlags, SCButtons
from scc.drivers.sc_dongle import SCController, SCPacketType
from scc.drivers.usb import SCUSBDevice, register_hotplug_device

if TYPE_CHECKING:
	from usb1 import USBDevice, USBDeviceHandle

	from scc.sccdaemon import SCCDaemon


VENDOR_ID = 0x28DE
PRODUCT_ID = 0x1205
ENDPOINT = 3
CONTROLIDX = 2
PACKET_SIZE = 128
UNLIZARD_INTERVAL = 100
# Basically, sticks on deck tend to return to non-zero position
STICK_DEADZONE = 3000

log = logging.getLogger("deck")


class DeckInput(ctypes.Structure):
	_fields_ = [
		("type", ctypes.c_uint8),
		("_a1", ctypes.c_uint8 * 3),
		("seq", ctypes.c_uint32),
		("buttons", ctypes.c_uint64),
		("lpad_x", ctypes.c_int16),
		("lpad_y", ctypes.c_int16),
		("rpad_x", ctypes.c_int16),
		("rpad_y", ctypes.c_int16),
		("accel_x", ctypes.c_int16),
		("accel_y", ctypes.c_int16),
		("accel_z", ctypes.c_int16),
		("gpitch", ctypes.c_int16),
		("groll", ctypes.c_int16),
		("gyaw", ctypes.c_int16),
		("q1", ctypes.c_uint16),
		("q2", ctypes.c_uint16),
		("q3", ctypes.c_uint16),
		("q4", ctypes.c_uint16),
		("ltrig", ctypes.c_uint16),
		("rtrig", ctypes.c_uint16),
		("stick_x", ctypes.c_int16),
		("stick_y", ctypes.c_int16),
		("rstick_x", ctypes.c_int16),
		("rstick_y", ctypes.c_int16),
		# Values above are readed directly from deck
		# Values bellow are converted so mapper can understand them
		("dpad_x", ctypes.c_int16),
		("dpad_y", ctypes.c_int16),
	]


class DeckButton(IntEnum):
	DOTS        = 0b100000000000000000000000000000000000000000000000000
	RSTICKTOUCH = 0b000100000000000000000000000000000000000000000000000
	LSTICKTOUCH = 0b000010000000000000000000000000000000000000000000000
	RGRIP2      = 0b000000001000000000000000000000000000000000000000000
	LGRIP2      = 0b000000000100000000000000000000000000000000000000000
	RSTICKPRESS = 0b000000000000000000000000100000000000000000000000000
	LSTICKPRESS = 0b000000000000000000000000000010000000000000000000000
	# bit 21 unused?
	RPADTOUCH   = 0b000000000000000000000000000000100000000000000000000
	LPADTOUCH   = 0b000000000000000000000000000000010000000000000000000
	RPADPRESS   = 0b000000000000000000000000000000001000000000000000000
	LPADPRESS   = 0b000000000000000000000000000000000100000000000000000
	RGRIP       = 0b000000000000000000000000000000000010000000000000000
	LGRIP       = 0b000000000000000000000000000000000001000000000000000
	START       = 0b000000000000000000000000000000000000100000000000000
	C           = 0b000000000000000000000000000000000000010000000000000
	BACK        = 0b000000000000000000000000000000000000001000000000000
	DPAD_DOWN   = 0b000000000000000000000000000000000000000100000000000
	DPAD_LEFT   = 0b000000000000000000000000000000000000000010000000000
	DPAD_RIGHT  = 0b000000000000000000000000000000000000000001000000000
	DPAD_UP     = 0b000000000000000000000000000000000000000000100000000
	A           = 0b000000000000000000000000000000000000000000010000000
	X           = 0b000000000000000000000000000000000000000000001000000
	B           = 0b000000000000000000000000000000000000000000000100000
	Y           = 0b000000000000000000000000000000000000000000000010000
	LB          = 0b000000000000000000000000000000000000000000000001000
	RB          = 0b000000000000000000000000000000000000000000000000100
	LT          = 0b000000000000000000000000000000000000000000000000010
	RT          = 0b000000000000000000000000000000000000000000000000001


DIRECTLY_TRANSLATABLE_BUTTONS = (
	0
	| DeckButton.A
	| DeckButton.B
	| DeckButton.X
	| DeckButton.Y
	| DeckButton.LB
	| DeckButton.RB
	| DeckButton.LT
	| DeckButton.RT
	| DeckButton.START
	| DeckButton.C
	| DeckButton.BACK
	| DeckButton.RGRIP
	| DeckButton.LGRIP
	| DeckButton.RPADTOUCH
	| DeckButton.LPADTOUCH
	| DeckButton.RPADPRESS
	| DeckButton.LPADPRESS
)


def map_button(i, from_, to):
	return to if (i.buttons & from_) else 0


def map_dpad(i, low, hi) -> int:
	if (i.buttons & low) != 0:
		return STICK_PAD_MIN
	if (i.buttons & hi) != 0:
		return STICK_PAD_MAX
	return 0


def apply_deadzone(value: int, deadzone: int) -> int:
	if value > -deadzone and value < deadzone:
		return 0
	return value


class Deck(SCUSBDevice, SCController):
	flags = (
		0
		| ControllerFlags.SEPARATE_STICK
		| ControllerFlags.HAS_DPAD
		| ControllerFlags.IS_DECK
		| ControllerFlags.HAS_RSTICK
	)

	def __init__(self, device: USBDevice, handle: USBDeviceHandle, daemon: SCCDaemon) -> None:
		self.daemon: SCCDaemon = daemon
		SCUSBDevice.__init__(self, device, handle)
		SCController.__init__(self, self, CONTROLIDX, ENDPOINT)
		self._old_state: DeckInput = DeckInput()
		self._input: DeckInput = DeckInput()
		self._ready: bool = False

		self.claim_by(klass=3, subclass=0, protocol=0)
		self.read_serial()

	def generate_serial(self) -> None:
		self._serial = f"{self.device.getBusNumber()}:{self.device.getPortNumber()}"

	def disconnected(self) -> None:
		# Overrided to skip returning serial# to pool.
		pass

	def set_gyro_enabled(self, enabled: bool) -> None:
		# Always on on deck
		pass

	def get_gyro_enabled(self) -> bool:
		# Always on on deck
		return True

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

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

	def get_gui_config_file(self) -> str:
		return "deck.config.json"

	def configure(self, idle_timeout=None, enable_gyros=None, led_level=None) -> None:
		FORMAT = b">BBBB60x"
		# Timeout & Gyros
		self._driver.overwrite_control(self._ccidx, struct.pack(FORMAT, SCPacketType.CONFIGURE, 0x03, 0x08, 0x07))

	def clear_mappings(self) -> None:
		FORMAT = b">BB62x"
		# Timeout & Gyros
		self._driver.overwrite_control(self._ccidx, struct.pack(FORMAT, SCPacketType.CLEAR_MAPPINGS, 0x01))

	def on_serial_got(self) -> None:
		log.debug("Got SteamDeck with serial %s", self._serial)
		self._id = f"deck{self._serial}"
		self.set_input_interrupt(ENDPOINT, 64, self._on_input)

	def _on_input(self, endpoint: int, data: bytearray) -> None:
		if not self._ready:
			self.daemon.add_controller(self)
			self.configure()
			self._ready = True

		self._old_state, self._input = self._input, self._old_state
		ctypes.memmove(ctypes.addressof(self._input), bytes(data), len(data))
		if self._input.seq % UNLIZARD_INTERVAL == 0:
			# Keeps lizard mode from happening
			self.clear_mappings()

		# Handle dpad
		self._input.dpad_x = map_dpad(self._input, DeckButton.DPAD_LEFT, DeckButton.DPAD_RIGHT)
		self._input.dpad_y = map_dpad(self._input, DeckButton.DPAD_DOWN, DeckButton.DPAD_UP)
		# Convert buttons
		self._input.buttons = (
			0
			| ((self._input.buttons & DIRECTLY_TRANSLATABLE_BUTTONS) << 8)
			| map_button(self._input, DeckButton.DOTS, SCButtons.DOTS)
			# | map_button(self._input, DeckButton.RSTICKTOUCH, ....)	// not mapped
			# | map_button(self._input, DeckButton.LSTICKTOUCH, ....) // not mapped
			| map_button(self._input, DeckButton.LSTICKPRESS, SCButtons.STICKPRESS)
			| map_button(self._input, DeckButton.RSTICKPRESS, SCButtons.RSTICKPRESS)
			| map_button(self._input, DeckButton.LGRIP2, SCButtons.LGRIP2)
			| map_button(self._input, DeckButton.RGRIP2, SCButtons.RGRIP2)
		)
		# Convert triggers
		self._input.ltrig >>= 7
		self._input.rtrig >>= 7
		# Apply deadzones
		self._input.stick_x = apply_deadzone(self._input.stick_x, STICK_DEADZONE)
		self._input.stick_y = apply_deadzone(self._input.stick_y, STICK_DEADZONE)
		self._input.rstick_x = apply_deadzone(self._input.rstick_x, STICK_DEADZONE)
		self._input.rstick_y = apply_deadzone(self._input.rstick_y, STICK_DEADZONE)

		# Invert Gyro Roll to match Steam Controller coordinate system
		self._input.groll = -self._input.groll

		m = self.get_mapper()
		if m:
			self.mapper.input(self, self._old_state, self._input)

	def close(self) -> None:
		if self._ready:
			self.daemon.remove_controller(self)
			self._ready = False
		SCUSBDevice.close(self)

	def turnoff(self) -> None:
		log.warning("Ignoring request to turn off steamdeck.")


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

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

	register_hotplug_device(cb, VENDOR_ID, PRODUCT_ID)
	return True
