"""Imports VDF profile and converts it to Profile object."""

import logging
from typing import Self

from scc.actions import (
	Action,
	AxisAction,
	ButtonAction,
	DPadAction,
	HatDownAction,
	HatLeftAction,
	HatRightAction,
	HatUpAction,
	MouseAction,
	MultiAction,
	NoAction,
	RelAreaAction,
	TriggerAction,
	XYAction,
)
from scc.constants import ROLL, TRIGGER_CLICK, YAW, HapticPos, SCButtons
from scc.lib.vdf import ensure_list, parse_vdf
from scc.menu_data import MenuData, MenuItem
from scc.modifiers import (
	BallModifier,
	DoubleclickModifier,
	FeedbackModifier,
	HoldModifier,
	ModeModifier,
	SensitivityModifier,
)
from scc.parser import ActionParser, ParseError
from scc.profile import Profile
from scc.special_actions import ChangeProfileAction, GridMenuAction, MenuAction, RadialMenuAction
from scc.uinput import Axes, Keys, Rels

log = logging.getLogger("import.vdf")


class VDFProfile(Profile):
	BUTTON_TO_BUTTON = {
		# maps button keys from vdf file to SCButtons constants
		# It looks like uppercase button and lowercase button aliases are needed
		"button_a": SCButtons.A,
		"button_A": SCButtons.A,
		"button_b": SCButtons.B,
		"button_B": SCButtons.B,
		"button_x": SCButtons.X,
		"button_X": SCButtons.X,
		"button_y": SCButtons.Y,
		"button_Y": SCButtons.Y,
		"button_back_left": SCButtons.LGRIP,
		"button_back_right": SCButtons.RGRIP,
		"button_menu": SCButtons.BACK,
		"button_escape": SCButtons.START,  # what what what
		"left_bumper": SCButtons.LB,
		"right_bumper": SCButtons.RB,
		"left_click": SCButtons.LPAD,
		"right_click": SCButtons.RPAD,
	}

	SPECIAL_KEYS = {
		# Maps some key names from vdf file to Keys.* constants.
		# Rest of key names are converted in convert_key_name.
		"FORWARD_SLASH": Keys.KEY_SLASH,
		"VOLUME_DOWN": Keys.KEY_VOLUMEDOWN,
		"VOLUME_UP": Keys.KEY_VOLUMEUP,
		"NEXT_TRACK": Keys.KEY_NEXTSONG,
		"PREV_TRACK": Keys.KEY_PREVIOUSSONG,
		"PAGE_UP": Keys.KEY_PAGEUP,
		"PAGE_DOWN": Keys.KEY_PAGEDOWN,
		"SINGLE_QUOTE": Keys.KEY_APOSTROPHE,
		"DASH": Keys.KEY_MINUS,
		"RETURN": Keys.KEY_ENTER,
		"ESCAPE": Keys.KEY_ESC,
		"PERIOD": Keys.KEY_DOT,
		"LEFT_BRACKET": Keys.KEY_LEFTBRACE,
		"RIGHT_BRACKET": Keys.KEY_RIGHTBRACE,
		"KEYPAD_DASH": Keys.KEY_KPMINUS,
		"KEYPAD_FORWARD_SLASH": Keys.KEY_KPSLASH,
		"LEFT_CONTROL": Keys.KEY_LEFTCTRL,
		"RIGHT_CONTROL": Keys.KEY_RIGHTCTRL,
	}

	SPECIAL_BUTTONS = {
		# As SPECIAL_KEYS, but for buttons.
		"shoulder_left": Keys.BTN_TL,
		"shoulder_right": Keys.BTN_TR,
		"joystick_left": Keys.BTN_THUMBL,
		"joystick_right": Keys.BTN_THUMBR,
	}

	REGION_IMPORT_FACTOR = 0.6  # Bulgarian const.

	def __init__(self, name="Unnamed"):
		Profile.__init__(self, ActionParser())
		self.name = name
		self.next_menu_id = 1
		self.action_set_id = 0
		self.action_sets = {"default": self}
		self.action_set_switches = set()

	def parse_action(self, lst_or_str, button=None):
		"""Parses action from vdf file. a_string can be either string or list of
		strings, in which case MultiAction is returned.

		Returns Action instance or ParseError if action is not recognized.
		"""
		if type(lst_or_str) == list:
			return MultiAction.make(*[self.parse_action(x) for x in lst_or_str])
		# Split string into binding type, name and parameters
		binding, params = lst_or_str.split(" ", 1)
		if "," in params:
			params, name = params.split(",", 1)
		else:
			params, name = params, None
		params = params.split(" ")
		if name:
			name = name.strip()
		# Return apropriate Action for binding type
		if binding in ("key_press", "mouse_button"):
			if binding == "mouse_button":
				b = VDFProfile.convert_button_name(params[0])
			else:
				b = VDFProfile.convert_key_name(params[0])
			return ButtonAction(b).set_name(name)
		if binding == "xinput_button":
			# Special cases, as dpad is apparently button on Windows
			b = params[0].strip().lower()
			if b == "dpad_up":
				return HatUpAction(Axes.ABS_HAT0Y)
			if b == "dpad_down":
				return HatDownAction(Axes.ABS_HAT0Y)
			if b == "dpad_left":
				return HatLeftAction(Axes.ABS_HAT0X)
			if b == "dpad_right":
				return HatRightAction(Axes.ABS_HAT0X)
			if b == "trigger_left":
				return AxisAction(Axes.ABS_Z)
			if b == "trigger_right":
				return AxisAction(Axes.ABS_RZ)
			b = VDFProfile.convert_button_name(b)
			return ButtonAction(b).set_name(name)
		if binding in ("mode_shift"):
			if button is None:
				log.warning("Ignoring modeshift assigned to no button: '%s'" % (lst_or_str,))
				return NoAction()
			if button not in VDFProfile.BUTTON_TO_BUTTON:
				log.warning("Ignoring modeshift assigned to unknown button: '%s'" % (button,))
				return NoAction()
			self.modeshift_buttons[VDFProfile.BUTTON_TO_BUTTON[button]] = (params[1], params[0])
			return NoAction()
		if binding in ("controller_action"):
			if params[0] == "CHANGE_PRESET":
				id = int(params[1]) - 1
				cpa = ChangeProfileAction("action_set:%s" % (id,))
				self.action_set_switches.add(cpa)
				return cpa

			log.warning("Ignoring controller_action '%s' binding" % (params[0],))
			return NoAction()
		if binding == "mouse_wheel":
			if params[0].lower() == "scroll_down":
				return MouseAction(Rels.REL_WHEEL, -1)
			return MouseAction(Rels.REL_WHEEL, 1)
		if binding == "game_action":
			log.warning("Ignoring game_action binding: '%s'" % (lst_or_str,))
			return NoAction()

		raise ParseError("Unknown binding: '%s'" % (binding,))

	@staticmethod
	def parse_modifiers(group, action, side):
		"""If passed group or activator has 'settings' key, converts known
		settings to one or more Modifier.

		Returns resulting Action
		"""
		if "settings" in group:
			settings = group["settings"]
			sens = 1.0, 1.0, 1.0
			if "sensitivity" in settings:
				s = float(settings["sensitivity"]) / 100.0
				sens = s, s, s
			if "haptic_intensity" in settings:
				action = FeedbackModifier(
					HapticPos.LEFT if side == Profile.LEFT else HapticPos.RIGHT,
					512 * int(settings["haptic_intensity"]),
					8,
					action,
				)
			if "invert_x" in settings and int(settings["invert_x"]):
				sens = -1.0 * sens[0], sens[1], sens[2]
			if "invert_y" in settings and int(settings["invert_y"]):
				sens = sens[0], -1.0 * sens[1], sens[2]
			if "invert_z" in settings and int(settings["invert_z"]):
				sens = sens[0], sens[1], -1.0 * sens[2]

			if sens != (1.0, 1.0, 1.0):
				action = SensitivityModifier(sens[0], sens[1], sens[2], action)

		return action

	@staticmethod
	def convert_key_name(name):
		"""Convert key names used in vdf profiles to Keys.KEY_* constants."""
		if name in VDFProfile.SPECIAL_KEYS:
			return VDFProfile.SPECIAL_KEYS[name]
		if name.endswith("_ARROW"):
			key = "KEY_%s" % (name[:-6],)
		elif "KEYPAD_" in name:
			key = "KEY_%s" % (name.replace("KEYPAD_", "KP"),)
		elif "LEFT_" in name:
			key = "KEY_%s" % (name.replace("LEFT_", "LEFT"),)
		elif "RIGHT_" in name:
			key = "KEY_%s" % (name.replace("RIGHT_", "RIGHT"),)
		else:
			key = "KEY_%s" % (name,)
		if hasattr(Keys, key):
			return getattr(Keys, key)
		if hasattr(Keys, key.upper()):
			return getattr(Keys, key.upper())
		raise ParseError("Unknown key: '%s'" % (name,))

	@staticmethod
	def convert_button_name(name):
		"""Convert button names used in vdf profiles to Keys.BTN_* constants."""
		if name.lower() in VDFProfile.SPECIAL_BUTTONS:
			return VDFProfile.SPECIAL_BUTTONS[name.lower()]
		key = "BTN_%s" % (name.upper(),)
		if hasattr(Keys, key):
			return getattr(Keys, key)
		raise ParseError("Unknown button: '%s'" % (name,))

	def parse_button(self, bdef, button=None):
		"""Parses button definition from vdf file.
		Parameter can be either string, as used in v2, or dict used in v3.
		"""
		if type(bdef) == str:
			# V2
			return self.parse_action(bdef, button)
		if type(bdef) == list:
			# V2
			return MultiAction.make(*[self.parse_action(x, button) for x in bdef])
		if "activators" in bdef:
			# V3
			act_actions = []
			for k in ("Full_Press", "Double_Press", "Long_Press"):
				a = NoAction()
				if k in bdef["activators"]:
					# TODO: Handle multiple bindings
					bindings = ensure_list(bdef["activators"][k])[0]
					a = self.parse_action(bindings["bindings"]["binding"], button)
					a = VDFProfile.parse_modifiers(bindings, a, Profile.RIGHT)
					# holly...
				act_actions.append(a)
			normal, double, hold = act_actions
			if not double and not hold:
				return normal
			if hold and not double:
				return HoldModifier(hold, normal)
			action = DoubleclickModifier(double, normal)
			action.holdaction = hold
			return action
		log.warning("Failed to parse button definition: %s" % (bdef,))

	@staticmethod
	def get_inputs(group):
		"""Returns 'inputs' or 'bindings', whichever exists in passed group.
		If neither exists, return None.
		"""
		if "inputs" in group:
			return group["inputs"]
		if "bindings" in group:
			return group["bindings"]
		return {}

	@staticmethod
	def find_group(data, id):
		"""Returns group with specified ID or None"""
		for g in data.get_all_for("group"):
			if "id" in g and g["id"] == id:
				return g
		return None

	def parse_group(self, group, side):
		"""Parses output (group) from vdf profile.
		Returns Action.
		"""
		if "mode" not in group:
			raise ParseError("Group without mode")
		mode = group["mode"]
		inputs = VDFProfile.get_inputs(group)

		settings = group["settings"] if "settings" in group else {}
		for o in ("output_trigger", "output_joystick"):
			if o in settings:
				if int(settings[o]) <= 1:
					side = Profile.LEFT
				else:
					side = Profile.RIGHT

		if mode == "dpad":
			keys = []
			for k in ("dpad_north", "dpad_south", "dpad_east", "dpad_west"):
				if k in inputs:
					keys.append(self.parse_button(inputs[k]))
				else:
					keys.append(NoAction())
			action = DPadAction(*keys)
		elif mode == "four_buttons":
			keys = []
			for k in ("button_y", "button_a", "button_x", "button_b"):
				if k in inputs:
					keys.append(self.parse_button(inputs[k]))
				else:
					keys.append(NoAction())
			action = DPadAction(*keys)
		elif mode == "joystick_move":
			if side == Profile.LEFT:
				# Left
				action = XYAction(AxisAction(Axes.ABS_X), AxisAction(Axes.ABS_Y))
			else:
				# Right
				action = XYAction(AxisAction(Axes.ABS_RX), AxisAction(Axes.ABS_RY))
		elif mode == "joystick_camera":
			output_joystick = 0
			if "output_joystick" in settings:
				output_joystick = int(settings["output_joystick"])
			if output_joystick == 0:
				action = BallModifier(XYAction(AxisAction(Axes.ABS_X), AxisAction(Axes.ABS_Y)))
			elif output_joystick == 1:
				action = BallModifier(XYAction(AxisAction(Axes.ABS_RX), AxisAction(Axes.ABS_RY)))
			else:
				# TODO: Absolute mouse? Doesn't seems to do anything in Steam
				action = BallModifier(SensitivityModifier(0.1, 0.1, MouseAction()))
		elif mode == "mouse_joystick":
			action = BallModifier(XYAction(AxisAction(Axes.ABS_RX), AxisAction(Axes.ABS_RY)))
		elif mode == "scrollwheel":
			action = BallModifier(XYAction(MouseAction(Rels.REL_HWHEEL), MouseAction(Rels.REL_WHEEL)))
		elif mode == "touch_menu":
			# Touch menu is converted to GridMenu
			items = []
			next_item_id = 1
			for k in inputs:
				action = self.parse_button(inputs[k])
				items.append(
					MenuItem(
						"item_%s" % (next_item_id,),
						action.describe(Action.AC_BUTTON),
						action,
					),
				)
				next_item_id += 1
			# Menu is stored in profile, with generated ID
			menu_id = "menu_%s" % (self.next_menu_id,)
			self.next_menu_id += 1
			self.menus[menu_id] = MenuData(*items)

			action = GridMenuAction(
				menu_id,
				"LEFT" if side == Profile.LEFT else "RIGHT",
				SCButtons.LPAD if side == Profile.LEFT else SCButtons.RPAD,
			)
		elif mode == "radial_menu":
			items = []
			next_item_id = 1
			for k in inputs:
				action = self.parse_button(inputs[k])
				items.append(MenuItem("item_%s" % (next_item_id,), action.describe(Action.AC_BUTTON), action))
				next_item_id += 1
			# Menu is stored in profile, with generated ID
			menu_id = "menu_%s" % (self.next_menu_id,)
			self.next_menu_id += 1
			self.menus[menu_id] = MenuData(*items)

			action = RadialMenuAction(
				menu_id,
				"LEFT" if side == Profile.LEFT else "RIGHT",
				SCButtons.LPAD if side == Profile.LEFT else SCButtons.RPAD,
			)
		elif mode == "absolute_mouse":
			if "click" in inputs:
				if side == Profile.LEFT:
					self.add_by_binding(SCButtons.LPAD, self.parse_button(inputs["click"]))
				else:
					self.add_by_binding(SCButtons.RPAD, self.parse_button(inputs["click"]))
			if "gyro_axis" in settings:
				if int(settings["gyro_axis"]) == 1:
					action = MouseAction(ROLL)
				else:
					action = MouseAction(YAW)
			else:
				action = MouseAction()
		elif mode == "mouse_wheel":
			action = BallModifier(XYAction(MouseAction(Rels.REL_HWHEEL), MouseAction(Rels.REL_WHEEL)))
		elif mode == "trigger":
			actions = []
			if "click" in inputs:
				actions.append(TriggerAction(TRIGGER_CLICK, self.parse_button(inputs["click"])))

			if side == Profile.LEFT:
				actions.append(AxisAction(Axes.ABS_Z))
			else:
				actions.append(AxisAction(Axes.ABS_RZ))

			action = MultiAction.make(*actions)
		elif mode == "mouse_region":
			# Read value and assume dafaults
			scale = float(settings["scale"]) if "scale" in settings else 100.0
			x = float(settings["position_x"]) if "position_x" in settings else 50.0
			y = float(settings["position_y"]) if "position_y" in settings else 50.0
			w = float(settings["sensitivity_horiz_scale"]) if "sensitivity_horiz_scale" in settings else 100.0
			h = float(settings["sensitivity_vert_scale"]) if "sensitivity_vert_scale" in settings else 100.0
			# Apply scale
			w = w * scale / 100.0
			h = h * scale / 100.0
			# Convert to (0, 1) range
			x, y = x / 100.0, 1.0 - (y / 100.0)
			w, h = w / 100.0, h / 100.0
			# Convert to rectangle
			x1 = max(0.0, x - (w * VDFProfile.REGION_IMPORT_FACTOR))
			x2 = min(1.0, x + (w * VDFProfile.REGION_IMPORT_FACTOR))
			y1 = max(0.0, y - (h * VDFProfile.REGION_IMPORT_FACTOR))
			y2 = min(1.0, y + (h * VDFProfile.REGION_IMPORT_FACTOR))

			action = RelAreaAction(x1, y1, x2, y2)
		else:
			raise ParseError("Unknown mode: '%s'" % (group["mode"],))

		action = VDFProfile.parse_modifiers(group, action, side)
		return action

	def parse_switches(self, group):
		"""Use for special cases of input groups that contain buttons."""
		inputs = VDFProfile.get_inputs(group)
		for button in inputs:
			if button in ("trigger_left", "left_trigger"):
				self.add_by_binding(
					"left_trigger",
					AxisAction(Axes.ABS_Z),
				)
			elif button in ("trigger_right", "right_trigger"):
				self.add_by_binding(
					"right_trigger",
					AxisAction(Axes.ABS_RZ),
				)
			elif button in VDFProfile.BUTTON_TO_BUTTON:
				self.add_by_binding(VDFProfile.BUTTON_TO_BUTTON[button], self.parse_button(inputs[button], button))
			else:
				raise ParseError("Unknown button: '%s'" % (button,))

	def parse_input_binding(self, data, group_id, binding):
		group = VDFProfile.find_group(data, group_id)
		if group and "mode" in group:
			if binding.startswith("switch") or binding.startswith("button_diamond"):
				self.parse_switches(group)
			else:
				if binding.startswith("right_"):
					action = self.parse_group(group, Profile.RIGHT)
				else:
					action = self.parse_group(group, Profile.LEFT)
				if binding.endswith("modeshift"):
					modeshift_id = (group_id, binding.split(" ")[0])
					self.modeshifts[modeshift_id] = action
				else:
					self.set_by_binding(binding, action)

	def set_by_binding(self, binding, action):
		"""Set action specified by binding, one of group_source_bindings keys used in vdf profile. Also supports SCButtons constants for buttons.

		Throws ParseError if key is not supported.
		"""
		if binding in SCButtons.__members__.values():
			self.buttons[binding] = action
		elif binding.startswith("left_trackpad"):
			self.pads[Profile.LEFT] = action
		elif binding.startswith("right_trackpad"):
			self.pads[Profile.RIGHT] = action
		elif binding.startswith("left_trigger"):
			self.triggers[Profile.LEFT] = action
		elif binding.startswith("right_trigger"):
			self.triggers[Profile.RIGHT] = action
		elif binding.startswith("joystick"):
			self.stick = action
		elif binding.startswith("gyro"):
			self.gyro = action
		else:
			raise ParseError("Unknown group source binding: '%s'" % (binding,))

	def add_by_binding(self, binding, action):
		"""As set_by_binding, but if there is alrady action for specified binding set, creates MultiAction."""
		old = self.get_by_binding(binding)
		new = MultiAction.make(old, action)
		if isinstance(new, MultiAction):
			new = new.deduplicate()
		self.set_by_binding(binding, new)

	def get_by_binding(self, binding):
		"""Return action specified by binding, one of group_source_bindings keys used in vdf profile. Also supports SCButtons constants for buttons.

		Throws ParseError if key is not supported.
		"""
		if binding in SCButtons.__members__.values():
			return self.buttons[binding]
		if binding.startswith("left_trackpad"):
			return self.pads[Profile.LEFT]
		if binding.startswith("right_trackpad"):
			return self.pads[Profile.RIGHT]
		if binding.startswith("left_trigger"):
			return self.triggers[Profile.LEFT]
		if binding.startswith("right_trigger"):
			return self.triggers[Profile.RIGHT]
		if binding.startswith("joystick"):
			return self.stick
		if binding.startswith("gyro"):
			return self.gyro
		raise ParseError("Unknown group source binding: '%s'" % (binding,))

	@staticmethod
	def _load_preset(data, profile, preset):
		profile.modeshifts = {}
		profile.modeshift_buttons = {}
		if "group_source_bindings" not in preset:
			# Empty preset
			return

		gsb = preset["group_source_bindings"]
		for group_id in gsb:
			binding = gsb[group_id]
			if not binding.endswith("inactive"):
				profile.parse_input_binding(data, group_id, binding)

		if "switch_bindings" in preset:
			profile.parse_switches(preset["switch_bindings"])

		for b in profile.modeshift_buttons:
			if profile.modeshift_buttons[b] in profile.modeshifts:
				# Should be always
				modeshift = profile.modeshift_buttons[b]
			else:
				continue
			action = profile.modeshifts[modeshift]
			trash, binding = modeshift
			old = profile.get_by_binding(binding)
			profile.set_by_binding(
				binding,
				ModeModifier(
					b,
					action,
					old,
				),
			)

	@staticmethod
	def _get_preset_name(data, preset):
		"""Return name of specified preset."""
		name = preset["name"].lower()
		if "actions" in data and name in data["actions"]:
			name = data["actions"][name]["title"]
		return name

	def action_set_by_id(self, id):
		"""Return name of action set with specified id."""
		for s in self.action_sets:
			if self.action_sets[s].action_set_id == id:
				return s
		return None

	def load(self, filename):
		"""Load profile from vdf file. Returns self.

		May raise ValueError.
		"""
		with open(filename) as file:
			data = parse_vdf(file)
		self.load_data(data)

	def load_data(self, data) -> Self:
		if "controller_mappings" not in data:
			raise ValueError("Invalid profile file")
		data = data["controller_mappings"]
		if "title" in data:
			name = data["title"].strip()
			if name:
				self.name = name
		presets = ensure_list(data["preset"])
		for p in presets:
			id = int(p["id"])
			if id == 0:
				# Default profile
				VDFProfile._load_preset(data, self, p)
			else:
				aset = VDFProfile(VDFProfile._get_preset_name(data, p))
				aset.action_set_id = id
				aset.action_set_switches = self.action_set_switches
				self.action_sets[aset.name] = aset
				VDFProfile._load_preset(data, aset, p)

		for aset in self.action_sets.values():
			aset.buttons[SCButtons.C] = HoldModifier(
				MenuAction("Default.menu"),
				MenuAction("Default.menu"),
			)

		return self


if __name__ == "__main__":
	import sys

	from scc.tools import init_logging

	init_logging()
	f = VDFProfile().load(sys.argv[1])
	f.save("output.sccprofile")
