"""SC Controller - Action Editor - Axis Component

Assigns emulated axis to trigger
"""

import logging
import os
from ctypes import POINTER, cast

from gi.repository import Gdk, GdkX11, GLib

from scc.actions import (
	Action,
	AreaAction,
	AxisAction,
	ButtonAction,
	MouseAction,
	MultiAction,
	NoAction,
	RelAreaAction,
	RelWinAreaAction,
	RelXYAction,
	WinAreaAction,
	XYAction,
)
from scc.constants import SCButtons
from scc.gui.ae import AEComponent
from scc.gui.controller_widget import STICKS
from scc.gui.parser import GuiActionParser, InvalidAction
from scc.gui.simple_chooser import SimpleChooser
from scc.lib import xwrappers as X
from scc.modifiers import BallModifier, CircularModifier
from scc.osd.area import Area
from scc.osd.timermanager import TimerManager
from scc.tools import _
from scc.uinput import Axes, Keys, Rels

log = logging.getLogger("AE.AxisAction")

__all__ = ["AxisActionComponent"]


class AxisActionComponent(AEComponent, TimerManager):
	GLADE = "ae/axis_action.glade"
	NAME = "axis_action"
	CTXS = Action.AC_STICK | Action.AC_PAD
	PRIORITY = 3

	def __init__(self, app, editor):
		AEComponent.__init__(self, app, editor)
		TimerManager.__init__(self)
		self._recursing = False
		self.relative_area = False
		self.osd_area_instance = None
		self.on_wayland = False
		self.circular_axis = MouseAction(Rels.REL_WHEEL)
		self.circular_buttons = [None, None]
		self.button = None
		self.parser = GuiActionParser()

	def load(self):
		if self.loaded:
			return
		AEComponent.load(self)
		cbAreaType = self.builder.get_object("cbAreaType")
		cbAreaType.set_row_separator_func(lambda model, iter: model.get_value(iter, 0) == "-")
		self.on_wayland = "WAYLAND_DISPLAY" in os.environ or not isinstance(
			Gdk.Display.get_default(), GdkX11.X11Display,
		)
		if self.on_wayland:
			self.builder.get_object("lblArea").set_text(
				_("Note: Mouse Region option is not available with Wayland-based display server"),
			)
			self.builder.get_object("grArea").set_sensitive(False)

		# Remove options that are not applicable to currently editted input
		if self.editor.get_id() in STICKS:
			# Remove "Mouse Region", "Mouse" and "Mouse (Emulate Stick)" options
			# when editing stick bindings
			cb = self.builder.get_object("cbAxisOutput")
			for row in cb.get_model():
				if row[2] in ("wheel_pad", "area", "mouse", "mouse_pad"):
					cb.get_model().remove(row.iter)
		else:
			# Remove "Mouse" option when editing pads
			# (it's effectivelly same as Trackpad)
			cb = self.builder.get_object("cbAxisOutput")
			for row in cb.get_model():
				if row[2] in ("wheel_stick", "mouse_stick"):
					cb.get_model().remove(row.iter)

	def hidden(self):
		self.update_osd_area(None)

	def set_action(self, mode, action):
		if self.handles(mode, action):
			cb = self.builder.get_object("cbAxisOutput")
			if isinstance(action, AreaAction):
				self.load_area_action(action)
				self.set_cb(cb, "area", 2)
				self.update_osd_area(action)
				return
			self.update_osd_area(None)
			if isinstance(action, MouseAction):
				self.load_mouse_action(action)
			elif isinstance(action, CircularModifier):
				self.load_circular_action(action)
			elif isinstance(action, ButtonAction):
				self.load_button_action(action)
			elif isinstance(action, XYAction):
				if self.editor.friction > 0:
					self.load_mouse_action(action)
				else:
					p = [None, None]
					for x in (0, 1):
						if len(action.actions[0].strip().parameters) >= x:
							if len(action.actions[x].strip().parameters) > 0:
								p[x] = action.actions[x].strip().parameters[0]
					if p[0] == Axes.ABS_X and p[1] == Axes.ABS_Y:
						self.set_cb(cb, "lstick", 2)
					elif p[0] == Axes.ABS_RX and p[1] == Axes.ABS_RY:
						if isinstance(action, RelXYAction):
							self.set_cb(cb, "rstick_rel", 2)
						else:
							self.set_cb(cb, "rstick", 2)
					elif p[0] == Rels.REL_HWHEEL and p[1] == Rels.REL_WHEEL:
						self.set_cb(cb, "wheel_pad", 2)
						self.set_cb(cb, "wheel_stick", 2)
			else:
				self.set_cb(cb, "none", 2)

	def update_osd_area(self, action):
		"""Updates preview area displayed on screen"""
		if action:
			if self.osd_area_instance is None:
				if self.on_wayland:
					# Cannot display preview with non-X11 backends
					return
				self.osd_area_instance = Area()
				self.osd_area_instance.show()
			action.update_osd_area(self.osd_area_instance, FakeMapper(self.editor))
			self.timer("area", 0.5, self.update_osd_area, action)
		elif self.osd_area_instance:
			self.osd_area_instance.quit()
			self.osd_area_instance = None
			self.cancel_timer("area")

	def load_circular_action(self, action):
		cbAxisOutput = self.builder.get_object("cbAxisOutput")
		btCircularAxis = self.builder.get_object("btCircularAxis")
		btCircularButton0 = self.builder.get_object("btCircularButton0")
		btCircularButton1 = self.builder.get_object("btCircularButton1")

		# Turn action into list of subactions (even if it's just single action)
		if isinstance(action.action, MultiAction):
			actions = action.action.actions
		else:
			actions = [action.action]

		# Parse that list
		self.circular_axis, self.circular_buttons = NoAction(), [None, None]
		for action in actions:
			if isinstance(action, ButtonAction):
				self.circular_buttons = [action.button, action.button2]
			else:
				self.circular_axis = action

		# Set labels
		b0, b1 = self.circular_buttons
		btCircularButton0.set_label(ButtonAction.describe_button(b0))
		btCircularButton1.set_label(ButtonAction.describe_button(b1))
		btCircularAxis.set_label(self.circular_axis.describe(Action.AC_PAD))

		self.set_cb(cbAxisOutput, "circular", 2)

	def load_button_action(self, action):
		self.button = action
		cbAxisOutput = self.builder.get_object("cbAxisOutput")
		btSingleButton = self.builder.get_object("btSingleButton")
		btSingleButton.set_label(self.button.describe(Action.AC_PAD))
		self.set_cb(cbAxisOutput, "button", 2)

	def load_mouse_action(self, action):
		cbMouseOutput = self.builder.get_object("cbMouseOutput")
		cbAxisOutput = self.builder.get_object("cbAxisOutput")
		self._recursing = True
		if isinstance(action, MouseAction):
			if self.editor.get_id() in STICKS:
				self.set_cb(cbMouseOutput, "mouse_stick", 1)
				self.set_cb(cbAxisOutput, "mouse_stick", 2)
			else:
				self.set_cb(cbMouseOutput, "mouse", 1)
				self.set_cb(cbAxisOutput, "mouse", 2)
		elif isinstance(action, XYAction):
			if isinstance(action.x, AxisAction):
				if action.x.parameters[0] == Axes.ABS_X:
					self.set_cb(cbMouseOutput, "left", 1)
				else:
					self.set_cb(cbMouseOutput, "right", 1)
				self.set_cb(cbAxisOutput, "mouse", 2)
			elif isinstance(action.x, MouseAction):
				if self.editor.get_id() in STICKS:
					self.set_cb(cbAxisOutput, "wheel_stick", 2)
				else:
					self.set_cb(cbAxisOutput, "wheel_pad", 2)
		self._recursing = False

	def load_area_action(self, action):
		"""Load AreaAction values into UI.
		"""
		cbAreaType = self.builder.get_object("cbAreaType")

		x1, y1, x2, y2 = action.coords
		self.relative_area = False
		if isinstance(action, RelAreaAction):
			key = "screensize"
			self.relative_area = True
			x1, y1, x2, y2 = x1 * 100.0, y1 * 100.0, x2 * 100.0, y2 * 100.0
		elif isinstance(action, RelWinAreaAction):
			key = "windowsize"
			self.relative_area = True
			x1, y1, x2, y2 = x1 * 100.0, y1 * 100.0, x2 * 100.0, y2 * 100.0
		else:
			t1 = "1" if x1 < 0 and x2 < 0 else "0"
			t2 = "1" if y1 < 0 and y2 < 0 else "0"
			x1, y1, x2, y2 = abs(x1), abs(y1), abs(x2), abs(y2)
			if x2 < x1:
				x1, x2 = x2, x1
			if y2 < y1:
				y1, y2 = y2, y1
			if isinstance(action, WinAreaAction):
				key = "window-%s%s" % (t1, t2)
			else:
				key = "screen-%s%s" % (t1, t2)

		self._recursing = True
		self.builder.get_object("sbAreaX1").set_value(x1)
		self.builder.get_object("sbAreaY1").set_value(y1)
		self.builder.get_object("sbAreaX2").set_value(x2)
		self.builder.get_object("sbAreaY2").set_value(y2)
		self.builder.get_object("cbAreaOSDEnabled").set_active(self.editor.osd)
		self.builder.get_object("cbAreaClickEnabled").set_active(self.pressing_pad_clicks())
		for row in cbAreaType.get_model():
			if key == row[1]:
				cbAreaType.set_active_iter(row.iter)
				break
		self._recursing = False

	def on_btCircularAxis_clicked(self, *a):
		def cb(action):
			self.circular_axis = action
			btCircularAxis = self.builder.get_object("btCircularAxis")
			btCircularAxis.set_label(action.describe(Action.AC_PAD))
			self.editor.set_action(self.make_circular_action())

		b = SimpleChooser(self.app, "axis", cb)
		b.set_title(_("Select Axis"))
		b.display_action(Action.AC_STICK, self.circular_axis)
		b.show(self.editor.window)

	def on_btCircularButton_clicked(self, button, *a):
		index = 0 if button == self.builder.get_object("btCircularButton0") else 1

		def cb(action):
			self.circular_buttons[index] = action.button
			btCircularButton = self.builder.get_object("btCircularButton%s" % (index,))
			btCircularButton.set_label(action.describe(Action.AC_PAD))
			self.editor.set_action(self.make_circular_action())

		b = SimpleChooser(self.app, "buttons", cb)
		b.set_title(_("Select Button"))
		b.display_action(Action.AC_STICK, self.circular_axis)
		b.show(self.editor.window)

	def on_btClearCircularAxis_clicked(self, *a):
		btCircularAxis = self.builder.get_object("btCircularAxis")
		self.circular_axis = NoAction()
		btCircularAxis.set_label(self.circular_axis.describe(Action.AC_PAD))
		self.editor.set_action(self.make_circular_action())

	def on_btClearCircularButtons_clicked(self, *a):
		btCircularButton0 = self.builder.get_object("btCircularButton0")
		btCircularButton1 = self.builder.get_object("btCircularButton1")
		self.circular_buttons = [None, None]
		btCircularButton0.set_label(NoAction().describe(Action.AC_PAD))
		btCircularButton1.set_label(NoAction().describe(Action.AC_PAD))
		self.editor.set_action(self.make_circular_action())

	def on_btSingleButton_clicked(self, *a):
		def cb(action):
			self.button = action
			btSingleButton = self.builder.get_object("btSingleButton")
			btSingleButton.set_label(self.button.describe(Action.AC_PAD))
			self.editor.set_action(self.button)

		b = SimpleChooser(self.app, "buttons", cb)
		b.set_title(_("Select Button"))
		b.display_action(Action.AC_STICK, self.circular_axis)
		b.show(self.editor.window)

	def on_cbAreaOSDEnabled_toggled(self, *a):
		self.editor.builder.get_object("cbOSD").set_active(self.builder.get_object("cbAreaOSDEnabled").get_active())

	def pressing_pad_clicks(self):
		"""Returns True if currently edited pad is set to press left mouse
		button when pressed.
		(yes, this is used somewhere)
		"""
		side = getattr(SCButtons, self.editor.get_id())
		c_action = self.app.current.buttons[side]
		if isinstance(c_action, ButtonAction):
			return c_action.button == Keys.BTN_LEFT
		return False

	def on_ok(self, action):
		if isinstance(action.strip(), AreaAction):
			# Kinda hacky way to set action on LPAD press or RPAD press
			# when user selects Mouse Area as ouput and checks
			# 'Pressing the Pad Clicks' checkbox
			side = getattr(SCButtons, self.editor.get_id())
			clicks = self.pressing_pad_clicks()

			if self.builder.get_object("cbAreaClickEnabled").get_active():
				if not clicks:
					# Turn pad press into mouse clicks
					self.app.set_action(self.app.current, side, ButtonAction(Keys.BTN_LEFT))
			elif clicks:
				# Clear action created above if checkbox is uncheck
				self.app.set_action(self.app.current, side, NoAction())

	def on_mouse_options_changed(self, *a):
		if self._recursing:
			return
		action = self.make_mouse_action()
		self.editor.set_action(action)

	def make_mouse_action(self):
		"""Loads values from UI into trackball-related action
		"""
		cbMouseOutput = self.builder.get_object("cbMouseOutput")
		a_str = cbMouseOutput.get_model().get_value(cbMouseOutput.get_active_iter(), 2)
		return self.parser.restart(a_str).parse()

	def make_circular_action(self):
		"""Constructs Circular Modifier
		"""
		if self.circular_axis and any(self.circular_buttons):
			return CircularModifier(MultiAction(self.circular_axis, ButtonAction(*self.circular_buttons)))
		if any(self.circular_buttons):
			return CircularModifier(ButtonAction(*self.circular_buttons))
		return CircularModifier(self.circular_axis)

	def make_area_action(self):
		"""Loads values from UI into new AreaAction or subclass.
		"""
		# Prepare
		cbAreaType = self.builder.get_object("cbAreaType")
		# Read numbers
		x1 = self.builder.get_object("sbAreaX1").get_value()
		y1 = self.builder.get_object("sbAreaY1").get_value()
		x2 = self.builder.get_object("sbAreaX2").get_value()
		y2 = self.builder.get_object("sbAreaY2").get_value()
		# Determine exact action type by looking into Area Type checkbox
		# (this part may seem little crazy)
		# ... numbers
		key = cbAreaType.get_model().get_value(cbAreaType.get_active_iter(), 1)
		if "-" in key:
			if key[-2] == "1":
				# Before-last character ius "1", that means that X coords are
				# counted from other side and has to be negated
				x1, x2 = -x1, -x2
			if key[-1] == "1":
				# Key ends with "1". Same thing as above but for Y coordinate
				y1, y2 = -y1, -y2
		if "size" in key:
			x1, y1, x2, y2 = x1 / 100.0, y1 / 100.0, x2 / 100.0, y2 / 100.0
		# ... class
		if "window-" in key:
			cls = WinAreaAction
			self.relative_area = False
		elif key == "screensize":
			cls = RelAreaAction
			self.relative_area = True
		elif key == "windowsize":
			cls = RelWinAreaAction
			self.relative_area = True
		else:  # "screen" in key
			cls = AreaAction
			self.relative_area = False
		if not self.relative_area:
			x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)

		return cls(x1, y1, x2, y2)

	def get_button_title(self):
		return _("Joystick / Mouse")

	def handles(self, mode, action):
		if isinstance(action, (NoAction, MouseAction, CircularModifier, InvalidAction, AreaAction, ButtonAction)):
			return True
		if isinstance(action, BallModifier):
			if isinstance(action.action, XYAction):
				return isinstance(action.action.x, (AxisAction, MouseAction)) and isinstance(
					action.action.x, (AxisAction, MouseAction),
				)
			return isinstance(action.action, MouseAction)
		if isinstance(action, XYAction):
			p = [None, None]
			for x in (0, 1):
				if len(action.actions[0].strip().parameters) >= x:
					if len(action.actions[x].strip().parameters) > 0:
						p[x] = action.actions[x].strip().parameters[0]
			if (p[0] == Axes.ABS_X and p[1] == Axes.ABS_Y) or (p[0] == Axes.ABS_RX and p[1] == Axes.ABS_RY):
				return True
			if (p[0] == Axes.ABS_HAT0X and p[1] == Axes.ABS_HAT0Y) or (p[0] == Rels.REL_HWHEEL and p[1] == Rels.REL_WHEEL):
				return True
		return False

	def on_area_options_changed(self, *a):
		if self._recursing:
			return
		action = self.make_area_action()
		self.editor.set_action(action)
		self.update_osd_area(action)
		for x in ("sbAreaX1", "sbAreaX2", "sbAreaY1", "sbAreaY2"):
			spin = self.builder.get_object(x)
			if self.relative_area:
				spin.get_adjustment().set_upper(100)
			else:
				spin.get_adjustment().set_upper(1000)
			self.on_sbArea_output(spin)

	def on_sbArea_output(self, button, *a):
		if self.relative_area:
			button.set_text("%s %%" % (button.get_value()))
		else:
			button.set_text("%s px" % (int(button.get_value())))

	def on_sbArea_focus_out_event(self, button, *a):
		GLib.idle_add(self.on_sbArea_output, button)

	def on_sbArea_changed(self, button, *a):
		self.on_sbArea_output(button)
		self.on_area_options_changed(button)

	def on_lblSetupTrackball_activate_link(self, trash, link):
		self.editor.on_link(link)

	def on_cbAxisOutput_changed(self, *a):
		cbAxisOutput = self.builder.get_object("cbAxisOutput")
		stActionData = self.builder.get_object("stActionData")
		key = cbAxisOutput.get_model().get_value(cbAxisOutput.get_active_iter(), 2)
		if key == "area":
			stActionData.set_visible_child(self.builder.get_object("grArea"))
			action = self.make_area_action()
			self.update_osd_area(action)
		elif key == "button":
			stActionData.set_visible_child(self.builder.get_object("vbButton"))
			self.button = self.button or ButtonAction(Keys.BTN_GAMEPAD)
			action = self.button
		elif key == "circular":
			stActionData.set_visible_child(self.builder.get_object("grCircular"))
			action = self.make_circular_action()
		elif key == "mouse":
			stActionData.set_visible_child(self.builder.get_object("vbMose"))
			if not self._recursing and self.editor.friction == 0:
				# When switching to mouse, enable trackball by default
				self.editor.friction = 10
			action = self.make_mouse_action()
		else:
			stActionData.set_visible_child(self.builder.get_object("nothing"))
			action = cbAxisOutput.get_model().get_value(cbAxisOutput.get_active_iter(), 0)
			action = self.parser.restart(action).parse()
			self.update_osd_area(None)

		self.editor.set_action(action)


class FakeMapper:
	"""Class that pretends to be mapper used when calling update_osd_area.
	It has two purposes: To provide get_xdisplay() method that does what it says
	and get_active_window() that returns any other window but window that
	belongs to SC-Controller gui application.
	"""

	def __init__(self, editor):
		self._xdisplay = X.Display(hash(GdkX11.x11_get_default_xdisplay()))
		self.editor = editor

	def get_xdisplay(self):
		return self._xdisplay

	def get_current_window(self):
		"""Gets last active window that was not part of SC-Controller.
		Uses _NET_CLIENT_LIST_STACKING property of root window, value provided
		by window manager. If that fail (because of no or not ICCM compilant WM),
		simply returns root window.
		"""
		root = X.get_default_root_window(self._xdisplay)
		NET_WM_WINDOW_TYPE_NORMAL = X.intern_atom(self._xdisplay, bytes("_NET_WM_WINDOW_TYPE_NORMAL", "utf-8"), False)
		my_windows = [x.get_xid() for x in Gdk.Screen.get_default().get_toplevel_windows()]
		nitems, prop = X.get_window_prop(
			self._xdisplay, root, bytes("_NET_CLIENT_LIST_STACKING", "utf-8"), max_size=0x8000,
		)

		if nitems > 0:
			for i in reversed(range(nitems)):
				window = cast(prop, POINTER(X.XID))[i]
				if window in my_windows:
					# skip over my own windows
					continue
				if not X.is_window_visible(self._xdisplay, window):
					# skip minimized and invisible windows
					continue
				tp = X.get_window_prop(self._xdisplay, window, bytes("_NET_WM_WINDOW_TYPE", "utf-8"))[-1]
				if tp is not None:
					tpval = cast(tp, POINTER(X.Atom)).contents.value
					X.free(tp)
					if tpval != NET_WM_WINDOW_TYPE_NORMAL:
						# skip over non-normal windows
						continue
				X.free(prop)
				return window
		if prop is not None:
			X.free(prop)

		# Failed to get property or there is not any usable window
		return root
