"""SC-Controller - OSD Menu.

Display menu that user can navigate through and print chosen item id to stdout
"""

from __future__ import annotations

import logging
import os
import sys
from typing import TYPE_CHECKING
from xml.etree import ElementTree as ET

from gi.repository import Gdk, GdkPixbuf, GdkX11, Gtk

import scc.osd.osk_actions
from scc.actions import Action
from scc.config import Config
from scc.constants import (
	CPAD,
	LSTICK,
	STICK_PAD_MAX,
	ControllerFlags,
	SCButtons,
	SCLeftRight,
	SCPads,
	SCSidesOSD,
	SCTriggers,
)
from scc.gui.daemon_manager import DaemonManager
from scc.gui.keycode_to_key import KEY_TO_KEYCODE
from scc.gui.svg_widget import SVGEditor, SVGWidget
from scc.lib import xwrappers as X
from scc.modifiers import ModeModifier
from scc.osd import OSDWindow
from scc.osd.slave_mapper import SlaveMapper
from scc.osd.timermanager import TimerManager
from scc.parser import TalkingActionParser
from scc.paths import get_config_path, get_share_path
from scc.profile import Profile
from scc.tools import circle_to_square, clamp, find_button_image, find_profile
from scc.uinput import Keys

if TYPE_CHECKING:
	from typing import Any

	from gi.repository.Gtk import Image

	from scc.gui.daemon_manager import ControllerManager

log = logging.getLogger("osd.keyboard")

SPECIAL_KEYS = {
	# Maps keycode to unicode character representing some
	# very special keys
	8: "←",
	9: "⇥",
	13: "↲",
	27: "␛",
	32: "␣",
}


class KeyboardImage(Gtk.DrawingArea):
	LINE_WIDTH = 2

	__gsignals__ = {}

	def __init__(self, image):
		Gtk.DrawingArea.__init__(self)
		self.connect("size-allocate", self.on_size_allocate)
		self.connect("draw", self.on_draw)

		areas = []
		self.color_button1 = 0.8, 0, 0, 1  # Just random mess,
		self.color_button1_border = 1, 0, 0, 1  # config overrides it anyway
		self.color_button2 = 0.8, 0.8, 0, 1
		self.color_button2_border = 1, 1, 0, 1
		self.color_hilight = 0, 1, 1, 1
		self.color_pressed = 1, 1, 1, 1
		self.color_text = 1, 1, 1, 1

		self.overlay = SVGWidget(image, False)
		self.tree = ET.fromstring(self.overlay.current_svg.encode("utf-8"))
		SVGWidget.find_areas(self.tree, None, areas, get_colors=True)

		self._hilight = ()
		self._pressed = ()
		self._button_images = {}
		self._help_areas = [self.get_limit("HELP_LEFT"), self.get_limit("HELP_RIGHT")]
		self._help_lines = ([], [])

		# TODO: It would be cool to use user-set font here, but cairo doesn't
		# have glyph replacement and most of default fonts (Ubuntu, Cantarell,
		# similar shit) misses pretty-much everything but letters, notably ↲
		#
		# For that reason, DejaVu Sans is hardcoded for now. On systems
		# where DejaVu Sans is not available, Cairo will automatically fallback
		# to default font.
		self.font_face = "DejaVu Sans"
		# self.font_face = Gtk.Label(label="X").get_style().font_desc.get_family()
		log.debug("Using font %s", self.font_face)

		self.buttons = [Button(self.tree, area) for area in areas]
		background = SVGEditor.find_by_id(self.tree, "BACKGROUND")
		self.set_size_request(*SVGEditor.get_size(background))
		self.overlay.edit().keep("overlay").commit()
		self.overlay.hilight({})
		# with open("/tmp/a.svg", "w") as file:
		# 	file.write(self.overlay.current_svg.encode("utf-8"))

	def hilight(self, hilight, pressed):
		self._hilight = hilight
		self._pressed = pressed
		self.queue_draw()

	def set_help(self, left, right):
		self._help_lines = (left, right)
		self.queue_draw()

	def set_labels(self, labels) -> None:
		for b in self.buttons:
			label = labels.get(b)
			if type(label) in (int,):
				pass
			elif label:
				# b.label = label.encode("utf-8")
				b.label = label
		self.queue_draw()

	def get_limit(self, id: str):
		a = SVGEditor.find_by_id(self.tree, id)
		width, height = 0.0, 0.0
		# if not hasattr(a, "parent"): a.parent = None

		x, y = SVGEditor.get_translation(a, absolute=True)
		if "width" in a.attrib:
			width = float(a.attrib["width"])
		if "height" in a.attrib:
			height = float(a.attrib["height"])
		return x, y, width, height

	@staticmethod
	def increase_contrast(buf):
		"""Takes input image, which is assumed to be grayscale RGBA and turns it
		into "symbolic" image by inverting colors of pixels where opacity is
		greater than threshold.
		"""
		pixels = [x for x in buf.get_pixels()]
		bpp = 4 if buf.get_has_alpha() else 3
		w, h = buf.get_width(), buf.get_height()
		stride = buf.get_rowstride()
		for i in range(0, len(pixels), bpp):
			if pixels[i + 3] > 64:
				pixels[i + 0] = 255 - pixels[i + 0]
				pixels[i + 1] = 255 - pixels[i + 1]
				pixels[i + 2] = 255 - pixels[i + 2]

		pixels = b"".join([chr(x).encode("latin-1") for x in pixels])
		rv = GdkPixbuf.Pixbuf.new_from_data(
			pixels,
			buf.get_colorspace(),
			buf.get_has_alpha(),
			buf.get_bits_per_sample(),
			w,
			h,
			stride,
			None,
		)
		rv.pixels = pixels  # Has to be kept in memory
		return rv

	def get_button_image(self, x, size):
		"""Loads and returns button image as pixbuf.
		Pixbufs are cached.
		"""
		if x not in self._button_images:
			path, bw = find_button_image(x, prefer_bw=True)
			if path is None:
				self._button_images[x] = None
				return None
			buf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, size, size)
			buf = self.increase_contrast(buf)
			self._button_images[x] = buf
		i = self._button_images[x]
		return i

	def on_draw(self, self2, ctx):
		ctx.select_font_face(self.font_face, 0, 0)

		ctx.set_line_width(self.LINE_WIDTH)
		ctx.set_font_size(48)
		ascent, descent, height, max_x_advance, max_y_advance = ctx.font_extents()

		# Buttons
		for button in self.buttons:
			if button in self._pressed:
				ctx.set_source_rgba(*self.color_pressed)
			elif button in self._hilight:
				ctx.set_source_rgba(*self.color_hilight)
			elif button.dark:
				ctx.set_source_rgba(*self.color_button2)
			else:
				ctx.set_source_rgba(*self.color_button1)
			# filled rectangle
			x, y, w, h = button
			ctx.move_to(x, y)
			ctx.line_to(x + w, y)
			ctx.line_to(x + w, y + h)
			ctx.line_to(x, y + h)
			ctx.line_to(x, y)
			ctx.fill()

			# border
			ctx.set_source_rgba(*self.color_button1_border)
			ctx.move_to(x, y)
			ctx.line_to(x + w, y)
			ctx.line_to(x + w, y + h)
			ctx.line_to(x, y + h)
			ctx.line_to(x, y)
			ctx.stroke()

			# label
			if button.label:
				ctx.set_source_rgba(*self.color_text)
				extents = ctx.text_extents(button.label)
				x_bearing, y_bearing, width, trash, x_advance, y_advance = extents
				ctx.move_to(x + w * 0.5 - width * 0.5 - x_bearing, y + h * 0.5 + height * 0.3)
				ctx.show_text(button.label)
				ctx.stroke()

		# Overlay
		Gdk.cairo_set_source_pixbuf(ctx, self.overlay.get_pixbuf(), 0, 0)
		ctx.paint()

		# Help
		ctx.set_source_rgba(*self.color_text)
		ctx.set_font_size(16)
		ascent, descent, height, max_x_advance, max_y_advance = ctx.font_extents()
		for left_right in (0, 1):
			x, y, w, h = self._help_areas[left_right]
			lines = self._help_lines[left_right]
			xx = x if left_right == 0 else x + w
			yy = y
			for icon, line in lines:
				yy += height
				if yy > y + h:
					break
				image = self.get_button_image(icon, height * 0.9)
				if image is None:
					continue
				iw, ih = image.get_width(), image.get_height()

				if left_right == 1:  # Right align
					extents = ctx.text_extents(line)
					x_bearing, y_bearing, width, trash, x_advance, y_advance = extents
					ctx.save()
					ctx.translate(xx - height + (height - iw) * 0.5, 1 + yy - (ascent + ih) * 0.5)
					Gdk.cairo_set_source_pixbuf(ctx, image, 0, 0)
					ctx.paint()
					ctx.restore()
					ctx.move_to(xx - x_bearing - width - 5 - height, yy)
				else:
					ctx.save()
					ctx.translate(1 + xx + (height - iw) * 0.5, 1 + yy - (ascent + ih) * 0.5)
					Gdk.cairo_set_source_pixbuf(ctx, image, 0, 0)
					ctx.paint()
					ctx.restore()
					ctx.move_to(xx + 5 + height, yy)

				ctx.show_text(line)
				ctx.stroke()

	def on_size_allocate(self, *a):
		pass


class Button:
	def __init__(self, tree, area):
		self.contains = area.contains
		self.name = area.name
		self.label = None
		self.x, self.y = area.x, area.y
		self.w, self.h = area.w, area.h
		self.dark = area.color[2] < 0.5  # Dark button is less than 50% blue

	def __iter__(self):
		return iter((self.x, self.y, self.w, self.h))


class Keyboard(OSDWindow, TimerManager):
	EPILOG = """Exit codes:
   0  - clean exit, user closed keyboard
   1  - error, invalid arguments
   2  - error, failed to access sc-daemon, sc-daemon reported error or died while keyboard is displayed.
   3  - erorr, failed to lock input stick, pad or button(s)
	"""
	OSK_PROF_NAME = ".scc-osd.keyboard"

	BUTTON_MAP = {
		SCButtons.A.name: Keys.KEY_ENTER,
		SCButtons.B.name: Keys.KEY_ESC,
		SCButtons.LB.name: Keys.KEY_BACKSPACE,
		SCButtons.RB.name: Keys.KEY_SPACE,
		SCButtons.LGRIP.name: Keys.KEY_LEFTSHIFT,
		SCButtons.RGRIP.name: Keys.KEY_RIGHTALT,
	}
	MODIFIER_MASKS = {
		Keys.KEY_LEFTSHIFT: Gdk.ModifierType.SHIFT_MASK,
		Keys.KEY_RIGHTSHIFT: Gdk.ModifierType.SHIFT_MASK,
		Keys.KEY_LEFTCTRL: Gdk.ModifierType.CONTROL_MASK,
		Keys.KEY_RIGHTCTRL: Gdk.ModifierType.CONTROL_MASK,
		Keys.KEY_LEFTALT: Gdk.ModifierType.MOD1_MASK,  # TODO(Martin): Change to ALT_MASK for Gdk4
		Keys.KEY_RIGHTALT: Gdk.ModifierType.MOD1_MASK,  # TODO(Martin): Change to ALT_MASK for Gdk4
	}

	def __init__(self, config=None) -> None:
		self.kbimage = os.path.join(get_config_path(), "keyboard.svg")
		if not os.path.exists(self.kbimage):
			# Prefer image in ~/.config/scc, but load default one as fallback
			self.kbimage = os.path.join(get_share_path(), "images", "keyboard.svg")

		TimerManager.__init__(self)
		OSDWindow.__init__(self, "osd-keyboard")
		self.daemon: DaemonManager | None = None
		self.mapper = None
		self.keymap = Gdk.Keymap.get_default()
		self.keymap.connect("state-changed", self.on_keymap_state_changed)
		Action.register_all(sys.modules["scc.osd.osk_actions"], prefix="OSK")
		self.profile = Profile(TalkingActionParser())
		self.config = config or Config()
		if isinstance(Gdk.Display.get_default(), GdkX11.X11Display):
			self.x11_dpy = X.Display(hash(GdkX11.x11_get_default_xdisplay()))
			self.group = None
		else:
			self.x11_dpy = None
			self.group = 0
		self.limits: dict[SCSidesOSD, Any] = {}
		self.background = None

		cursor = os.path.join(get_share_path(), "images", "menu-cursor.svg")
		self.cursors: dict[SCSidesOSD, Image] = {}
		self.cursors[SCLeftRight.LEFT] = Gtk.Image.new_from_file(cursor)
		self.cursors[SCLeftRight.LEFT].set_name("osd-keyboard-cursor")
		self.cursors[SCLeftRight.RIGHT] = Gtk.Image.new_from_file(cursor)
		self.cursors[SCLeftRight.RIGHT].set_name("osd-keyboard-cursor")
		self.cursors[SCPads.CPAD] = Gtk.Image.new_from_file(cursor)
		self.cursors[SCPads.CPAD].set_name("osd-keyboard-cursor")

		self._eh_ids = []
		self._controller: ControllerManager | None = None
		self._stick = 0, 0
		self._hovers = {self.cursors[SCLeftRight.LEFT]: None, self.cursors[SCLeftRight.RIGHT]: None}
		self._pressed = {self.cursors[SCLeftRight.LEFT]: None, self.cursors[SCLeftRight.RIGHT]: None}
		self._pressed_areas = {}

		self.c = Gtk.Box()
		self.c.set_name("osd-keyboard-container")

		self.f = Gtk.Fixed()

	def _create_background(self) -> None:
		self.background = KeyboardImage(self.args.image)
		self.recolor()

		self.limits = {}
		self.limits[SCLeftRight.LEFT] = self.background.get_limit("LIMIT_LPAD")
		self.limits[SCLeftRight.RIGHT] = self.background.get_limit("LIMIT_RPAD")
		self.limits[SCPads.CPAD] = self.background.get_limit("LIMIT_CPAD")
		self._pack()

	def _pack(self) -> None:
		self.f.add(self.background)
		self.f.add(self.cursors[SCLeftRight.LEFT])
		self.f.add(self.cursors[SCLeftRight.RIGHT])
		self.f.add(self.cursors[SCPads.CPAD])
		self.c.add(self.f)
		self.add(self.c)

	def recolor(self) -> None:
		# TODO: keyboard description is probably not needed anymore
		_get = lambda a: SVGWidget.color_to_float(self.config["osk_colors"].get(a, ""))
		self.background.color_button1 = _get("button1")
		self.background.color_button1_border = _get("button1_border")
		self.background.color_button2 = _get("button2")
		self.background.color_button2_border = _get("button2_border")
		self.background.color_hilight = _get("hilight")
		self.background.color_pressed = _get("pressed")
		self.background.color_text = _get("text")

	def use_daemon(self, d: DaemonManager) -> None:
		"""Allows (re)using already existing DaemonManager instance in same process"""
		self.daemon = d
		self._cononect_handlers()
		self.on_daemon_connected(self.daemon)

	def redraw_background(self, *a) -> None:
		"""Forces a repaint of the keyboard background image.

		Called by the OSD daemon after recolor()/update_labels() when the
		OSD color configuration changes while the keyboard is visible.
		"""
		if self.background is not None:
			self.background.queue_draw()

	def on_keymap_state_changed(self, x11keymap) -> None:
		if not self.timer_active("labels"):
			self.timer("labels", 0.1, self.update_labels)

	def set_help(self) -> None:
		"""Updates help shown on keyboard image.

		Keyboard bindings don't change on the fly, so this is done only
		right after start or when daemon is reconfigured.
		"""
		if self._controller is None:
			# Not yet connected
			return
		gui_config = self._controller.load_gui_config(os.path.join(get_share_path(), "images"))
		l_lines, r_lines, used = [], [], set()

		def add_action(side, button: int, a: Action) -> None:
			if not a:
				return
			if isinstance(a, scc.osd.osk_actions.OSKCursorAction):
				if a.side != CPAD:
					return
			if isinstance(a, ModeModifier):
				mapper = getattr(self, "mapper", None)
				# Before the mapper is created no modes can be held,
				# so the default action is the accurate initial description.
				add_action(side, button, a.select(mapper) if mapper is not None else a.default)
				return
			desc = a.describe(Action.AC_OSK)
			if desc in used:
				if isinstance(a, scc.osd.osk_actions.OSKPressAction):
					# Special case, both triggers are set to "press a key"
					pass
				else:
					return
			icon = self._controller.get_button_name(gui_config, button)
			side.append((icon, desc))
			used.add(desc)

		def add_button(side, b: int) -> None:
			add_action(side, b, self.profile.buttons[b])

		if self._controller.get_flags() & ControllerFlags.NO_GRIPS == 0:
			add_button(l_lines, SCButtons.LGRIP)
			add_button(r_lines, SCButtons.RGRIP)
		add_action(l_lines, SCButtons.LT, self.profile.triggers[SCTriggers.LT])
		add_action(r_lines, SCButtons.RT, self.profile.triggers[SCTriggers.RT])
		for b in (SCButtons.LB, SCButtons.Y, SCButtons.X):
			add_button(l_lines, b)
		for b in (SCButtons.RB, SCButtons.B, SCButtons.A):
			add_button(r_lines, b)

		if self._controller.get_flags() & ControllerFlags.HAS_CPAD != 0:
			for lst in (l_lines, r_lines):
				while len(lst) > 3:
					lst.pop()
				while len(lst) < 3:
					lst.append((None, ""))
			add_action(r_lines, CPAD, self.profile.pads[CPAD])
		add_action(l_lines, SCButtons.LSTICKPRESS, self.profile.lstick)

		self.background.set_help(l_lines, r_lines)

	def update_labels(self) -> None:
		"""Updates keyboard labels

		X11 - based on active X keymap
		Wayland - based on internal modifier state
		"""
		labels = {}
		# Get current layout group
		if self.x11_dpy is not None:
			self.group = X.get_xkb_state(self.x11_dpy).group
		# Get state of shift/alt/ctrl key
		mt = Gdk.ModifierType(self.keymap.get_modifier_state())
		# On Wayland a client does not necessarily observe modifier state from the uinput keyboard it created.
		# Include modifiers held by the OSK's own virtual keyboard so its labels still match the keys it will emit.
		if self.x11_dpy is None and self.mapper is not None:
			for key in self.mapper.keyboard._pressed:
				mt |= self.MODIFIER_MASKS.get(
					key, Gdk.ModifierType(0),
				)  # TODO(Martin): Change to GDK_NO_MODIFIER_MASK for Gdk4
		for button in self.background.buttons:
			if getattr(Keys, button.name, None) in KEY_TO_KEYCODE:
				keycode = KEY_TO_KEYCODE[getattr(Keys, button.name)]
				translation = self.keymap.translate_keyboard_state(keycode, mt, self.group)
				if hasattr(translation, "keyval"):
					code = Gdk.keyval_to_unicode(translation.keyval)
				else:
					code = Gdk.keyval_to_unicode(translation[1])
				if code >= 33:  # Printable chars, w/out space
					labels[button] = chr(code).strip()
				else:
					labels[button] = SPECIAL_KEYS.get(code)
		self.background.set_labels(labels)

	def _add_arguments(self) -> None:
		OSDWindow._add_arguments(self)
		self.argparser.add_argument("image", type=str, nargs="?", default=self.kbimage, help="keyboard image to use")

	def parse_arguments(self, argv) -> bool:
		if not OSDWindow.parse_arguments(self, argv):
			return False
		return True

	def _cononect_handlers(self) -> None:
		self._eh_ids += [
			(self.daemon, self.daemon.connect("dead", self.on_daemon_died)),
			(self.daemon, self.daemon.connect("error", self.on_daemon_died)),
			(self.daemon, self.daemon.connect("reconfigured", self.on_reconfigured)),
			(self.daemon, self.daemon.connect("alive", self.on_daemon_connected)),
		]

	def run(self) -> None:
		self.daemon = DaemonManager()
		self._cononect_handlers()
		OSDWindow.run(self)

	def load_profile(self) -> None:
		self.profile.load(find_profile(Keyboard.OSK_PROF_NAME)).compress()
		self.set_help()

	def on_reconfigured(self, *a) -> None:
		self.load_profile()
		log.debug("Reloaded profile")

	def on_daemon_connected(self, *a) -> None:
		def success(*a) -> None:
			log.info("Sucessfully locked input")

		c = self.choose_controller(self.daemon)
		if c is None or not c.is_connected():
			# There is no controller connected to daemon
			self.on_failed_to_lock("Controller not connected")
			return

		self._eh_ids += [
			(c, c.connect("event", self.on_event)),
			(c, c.connect("lost", self.on_controller_lost)),
		]

		# TODO: Single-handed mode for PS4 postponed
		button_locks = [
			"LPADPRESS" if b == SCButtons.LPAD else "RPADPRESS" if b == SCButtons.RPAD else b.name for b in SCButtons
		]
		locks = [SCPads.LPAD, SCPads.RPAD, LSTICK, "LSTICKPRESS", *button_locks]
		if (c.get_flags() & ControllerFlags.HAS_CPAD) == 0:
			# Two pads, two hands
			locks = [SCPads.LPAD, SCPads.RPAD, LSTICK, "LSTICKPRESS", *button_locks]
			self.cursors[CPAD].hide()
		else:
			# Single-handed mode
			locks = [CPAD, "CPADPRESS", LSTICK, "LSTICKPRESS", *button_locks]
			self._hovers[self.cursors[SCLeftRight.RIGHT]] = None
			self._hovers = {self.cursors[CPAD]: None}
			self._pressed = {self.cursors[CPAD]: None}
			self.cursors[SCLeftRight.LEFT].hide()
			self.cursors[SCLeftRight.RIGHT].hide()

			# There is no configurable nor default mapping for CPDAD,
			# so situable mappings are hardcoded here
			self.profile.pads[CPAD] = scc.osd.osk_actions.OSKCursorAction(SCPads.CPAD)
			self.profile.pads[CPAD].speed = [0.85, 1.2]
			self.profile.buttons[SCButtons.CPADPRESS] = scc.osd.osk_actions.OSKPressAction(CPAD)

			for i in (SCTriggers.LT, SCTriggers.RT):
				if isinstance(self.profile.triggers[i], scc.osd.osk_actions.OSKPressAction):
					self.profile.triggers[i] = scc.osd.osk_actions.OSKPressAction(CPAD)

		self._controller = c
		c.lock(success, self.on_failed_to_lock, *locks)
		self.set_help()

	def quit(self, code: int = -1) -> None:
		if self.get_controller():
			self.get_controller().unlock_all()
		for source, eid in self._eh_ids:
			source.disconnect(eid)
		self._eh_ids = []
		del self.mapper
		OSDWindow.quit(self, code)

	def show(self, *a) -> None:
		if self.background is None:
			self._create_background()
		OSDWindow.show(self, *a)
		self.load_profile()
		self.mapper = SlaveMapper(self.profile, None, keyboard=b"SCC OSD Keyboard", mouse=b"SCC OSD Mouse")
		self.mapper.set_special_actions_handler(self)
		self.set_cursor_position(0, 0, self.cursors[SCLeftRight.LEFT], self.limits[SCLeftRight.LEFT])
		self.set_cursor_position(0, 0, self.cursors[SCLeftRight.RIGHT], self.limits[SCLeftRight.RIGHT])
		self.set_cursor_position(0, 0, self.cursors[SCPads.CPAD], self.limits[SCPads.CPAD])
		self.timer("labels", 0.1, self.update_labels)

	def on_event(self, daemon, what, data) -> None:
		"""Called when button press, button release or stick / pad update is sent by the daemon."""
		# Controller events can already be queued when the OSK is closing and quit() has disposed of its mapper.
		# Localize self.mapper to prevent race ceonditions
		mapper = getattr(self, "mapper", None)
		if mapper is None:
			return
		if self.x11_dpy is not None:
			group = X.get_xkb_state(self.x11_dpy).group
			if self.group != group:
				self.group = group
				self.timer("labels", 0.1, self.update_labels)
		else:
			old_modifiers = mapper.keyboard._pressed & self.MODIFIER_MASKS.keys()
		old_buttons = mapper.buttons
		mapper.handle_event(daemon, what, data)
		if getattr(self, "mapper", None) is not mapper:
			return
		if old_buttons != mapper.buttons:
			self.set_help()
		if self.x11_dpy is None:
			new_modifiers = mapper.keyboard._pressed & self.MODIFIER_MASKS.keys()
			if old_modifiers != new_modifiers:
				self.timer("labels", 0.01, self.update_labels)

	def on_sa_close(self, *a) -> None:
		"""Called by CloseOSDKeyboardAction"""
		self.quit(0)

	def on_sa_cursor(self, mapper, action, x, y) -> None:
		self.set_cursor_position(
			x * action.speed[0],
			y * action.speed[1],
			self.cursors[action.side],
			self.limits[action.side],
		)

	def on_sa_move(self, mapper, action, x, y) -> None:
		self._stick = x, y
		if not self.timer_active("lstick"):
			self.timer("lstick", 0.05, self._move_window)

	def on_sa_press(self, mapper, action, pressed) -> None:
		self.key_from_cursor(self.cursors[action.side], pressed)

	def set_cursor_position(self, x, y, cursor, limit) -> None:
		"""Moves cursor image."""
		if cursor not in self._hovers or self._controller is None:
			return
		w = limit[2] - (cursor.get_allocation().width * 0.5)
		h = limit[3] - (cursor.get_allocation().height * 0.5)
		x = x / float(STICK_PAD_MAX)
		y = y / float(STICK_PAD_MAX) * -1.0

		if self._controller.get_flags() & ControllerFlags.LPAD_RPAD_IS_CIRCLE:
			x, y = circle_to_square(x, y)

		x = clamp(
			cursor.get_allocation().width * 0.5,
			(limit[0] + w * 0.5) + x * w * 0.5,
			self.get_allocation().width - cursor.get_allocation().width,
		)

		y = clamp(
			cursor.get_allocation().height * 0.5,
			(limit[1] + h * 0.5) + y * h * 0.5,
			self.get_allocation().height - cursor.get_allocation().height,
		)

		cursor.position = int(x), int(y)
		self.f.move(cursor, x - cursor.get_allocation().width * 0.5, y - cursor.get_allocation().height * 0.5)
		for button in self.background.buttons:
			if button.contains(x, y):
				if button != self._hovers[cursor]:
					self._hovers[cursor] = button
					if self._pressed[cursor] is not None:
						self.mapper.keyboard.releaseEvent([self._pressed[cursor]])
						self.key_from_cursor(cursor, True)
					if not self.timer_active("update"):
						self.timer("update", 0.01, self.update_background)
					break

	def update_background(self, *whatever) -> None:
		"""Updates hilighted keys on bacgkround image."""
		self.background.hilight(
			set([a for a in self._hovers.values() if a]),
			set([a for a in self._pressed_areas.values() if a]),
		)

	def _move_window(self, *a) -> None:
		"""Called by timer while stick is tilted to move window around the screen."""
		x, y = self._stick
		x = x * 50.0 / STICK_PAD_MAX
		y = y * -50.0 / STICK_PAD_MAX
		self.move_relative(x, y)
		if abs(self._stick[0]) > 100 or abs(self._stick[1]) > 100:
			self.timer("lstick", 0.05, self._move_window)

	def key_from_cursor(self, cursor, pressed) -> None:
		"""Sends keypress/keyrelease event to emulated keyboard, based on position of cursor on OSD keyboard."""
		x, y = cursor.position

		if pressed:
			for button in self.background.buttons:
				if button.contains(x, y):
					if button.name.startswith("KEY_") and hasattr(Keys, button.name):
						key = getattr(Keys, button.name)
						if self._pressed[cursor] is not None:
							self.mapper.keyboard.releaseEvent([self._pressed[cursor]])
						self.mapper.keyboard.pressEvent([key])
						self._pressed[cursor] = key
						self._pressed_areas[cursor] = button
					break
		elif self._pressed[cursor] is not None:
			self.mapper.keyboard.releaseEvent([self._pressed[cursor]])
			self._pressed[cursor] = None
			del self._pressed_areas[cursor]
		if not self.timer_active("update"):
			self.timer("update", 0.01, self.update_background)


def main() -> None:
	import gi

	gi.require_version("Gtk", "3.0")
	gi.require_version("Rsvg", "2.0")
	gi.require_version("GdkX11", "3.0")

	from scc.tools import init_logging

	init_logging()

	k = Keyboard()
	if not k.parse_arguments(sys.argv):
		sys.exit(1)
	k.run()


if __name__ == "__main__":
	import signal

	def sigint(*a):
		print("\n*break*")
		sys.exit(-1)

	signal.signal(signal.SIGINT, sigint)
	main()
