"""SC-Controller - OSD Menu

Display menu that user can navigate through
"""
from __future__ import annotations

import json
import logging
import os
import sys
from math import atan2, cos, sin
from math import pi as PI
from typing import TYPE_CHECKING

import cairo

from scc.config import Config
from scc.constants import STICK, STICK_PAD_MAX
from scc.gui.svg_widget import SVGEditor, SVGWidget
from scc.lib import xwrappers as X
from scc.menu_data import Separator, Submenu
from scc.osd import OSDWindow
from scc.osd.menu import Menu, MenuIcon
from scc.paths import get_share_path
from scc.tools import degdiff, find_icon

if TYPE_CHECKING:
	from gi.repository import Gtk

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


class RadialMenu(Menu):
	RECOLOR_BACKGROUNDS = ("background", "menuitem_hilight_border", "text")
	RECOLOR_STROKES = ("border", "menuitem_border")
	MIN_DISTANCE = 3000  # Minimal cursor distance from center (in px^2)
	ICON_SIZE = 96

	def __init__(self) -> None:
		Menu.__init__(self, "osd-radial-menu")
		self.angle = 0
		self.rotation = 0
		self.scale = 1.0
		self.items_with_icon = []
		if self.xdisplay is None:
			# Compositors normally provide an RGBA visual, but the API can
			# return null if it is not supported, so retain a rectangular fallback.
			visual = self.get_screen().get_rgba_visual()
			if visual is not None:
				self.set_visual(visual)
				self.set_app_paintable(True)
				self.connect("draw", self._on_draw_clip_circle)

	def create_parent(self) -> SVGWidget:
		background = os.path.join(get_share_path(), "images", "radial-menu.svg")
		self.b = SVGWidget(background)
		self.b.connect("size-allocate", self.on_size_allocate)
		self.recolor()
		return self.b

	def recolor(self):
		config = Config()
		source_colors = {}
		try:
			# Try to read json file and bail out if it fails
			desc = os.path.join(get_share_path(), "images", "radial-menu.svg.json")
			source_colors = json.loads(open(desc).read())["colors"]
		except Exception as e:
			log.warning("Failed to load keyboard description")
			log.warning(e)
			return
		editor = self.b.edit()

		for k in RadialMenu.RECOLOR_BACKGROUNDS:
			if k in config["osd_colors"] and k in source_colors:
				editor.recolor_background(source_colors[k], config["osd_colors"][k])
		editor.recolor_background(source_colors["background"], config["osd_colors"]["background"])

		for k in RadialMenu.RECOLOR_STROKES:
			if k in config["osd_colors"] and k in source_colors:
				print("REC", source_colors[k], config["osd_colors"][k])
				editor.recolor_strokes(source_colors[k], config["osd_colors"][k])

		editor.commit()

	def on_size_allocate(self, trash, allocation):
		"""(Re)centers all icons when menu is displayed or size is changed"""
		cx = allocation.width * self.scale * 0.5
		cy = allocation.height * self.scale * 0.5
		radius = min(cx, cy) * 2 / 3
		for i in self.items_with_icon:
			angle, icon = float(i.a) * PI / 180.0, i.icon_widget
			x, y = cx + sin(angle) * radius, cy - cos(angle) * radius
			x = x - (self.ICON_SIZE * self.scale * 0.5)
			y = y - (self.ICON_SIZE * self.scale * 0.5)
			i.icon_widget.get_parent().move(i.icon_widget, x, y)

	def get_window_size(self):
		w, h = Menu.get_window_size(self)
		if self.scale != 1.0:
			w = int(w * self.scale)
			h = int(h * self.scale)
		return w, h

	def _add_arguments(self):
		Menu._add_arguments(self)
		self.argparser.add_argument("--rotation", type=float, default=0, help="rotates input by angle (default: 0)")

	def parse_argumets(self, argv):
		self.editor = self.b.edit()
		rv = Menu.parse_argumets(self, argv)
		self.rotation = self.args.rotation
		if rv:
			self.enable_cursor()
		return rv

	def generate_widget(self, item):
		if isinstance(item, (Separator, Submenu)) or item.id is None:
			# Labels and separators, radial menu can't show these
			return None
		e = self.editor.clone_element("menuitem_template")
		SVGEditor.set_text(e, item.label)
		e.attrib["id"] = "menuitem_" + item.id
		return e

	def pack_items(self, trash, items):
		if self._size > 0 and self._size < 100:
			self.scale = self._size / 100.0
			root = SVGEditor.get_element(self.editor, "root")
			SVGEditor.scale(root, self.scale)
		pb = self.b.get_pixbuf()
		# Image width is not scaled as everything bellow operates
		# in 'root' object coordinate space
		image_width = pb.get_width()

		index = 0
		item_offset = 360.0 / len(self.items)
		a1 = (-90.0 - item_offset * 0.5) * PI / 180.0
		a2 = (-90.0 + item_offset * 0.5) * PI / 180.0
		for i in self.items_with_icon:
			i.icon_widget.get_parent().remove_child(i.icon_widget)
		self.items_with_icon = []
		for i in items:
			# Set size of each arc
			if SVGEditor.get_element(i.widget, "arc") is not None:
				l = SVGEditor.get_element(i.widget, "arc")
				radius = float(l.attrib["radius"])  # TODO: Find how to get value of 'sodipodi:rx'
				l.attrib["d"] = l.attrib["d-template"] % (
					radius * cos(a1) + image_width / 2,
					radius * sin(a1) + image_width / 2,
					radius * cos(a2) + image_width / 2,
					radius * sin(a2) + image_width / 2,
				)
			# Rotate arc to correct position
			i.a = (360.0 / float(len(self.items))) * float(index)
			SVGEditor.rotate(i.widget, i.a, image_width * 0.5, image_width * 0.5)
			# Check if there is any icon
			icon_file, has_colors = find_icon(i.icon, False) if hasattr(i, "icon") else (None, False)
			if icon_file:
				# Icon - hide all text and place MenuIcon widget on top of image
				self.editor.remove_element(SVGEditor.get_element(i.widget, "menuitem_text"))
				self.editor.remove_element(SVGEditor.get_element(i.widget, "line0"))
				self.editor.remove_element(SVGEditor.get_element(i.widget, "line2"))
				i.icon_widget = MenuIcon(icon_file, has_colors)
				i.icon_widget.set_name("osd-radial-menu-icon")
				i.icon_widget.set_size_request(self.ICON_SIZE * self.scale, self.ICON_SIZE * self.scale)
				self.b.get_parent().put(i.icon_widget, 200, 200)
				self.items_with_icon.append(i)
			else:
				# No icon - rotate text in arc to other direction to keep it horisontal
				if SVGEditor.get_element(i.widget, "menuitem_text") is not None:
					l = SVGEditor.get_element(i.widget, "menuitem_text")
					l.attrib["id"] = "text_" + i.id
					l.attrib["transform"] = "%s rotate(%s)" % (l.attrib["transform"], -i.a)
				# Place up to 3 lines of item label
				label = i.label.split("\n")
				first_line = 0
				if len(label) == 1:
					self.editor.remove_element(SVGEditor.get_element(i.widget, "line0"))
					self.editor.remove_element(SVGEditor.get_element(i.widget, "line2"))
					first_line = 1
				elif len(label) == 2:
					self.editor.remove_element(SVGEditor.get_element(i.widget, "line0"))
					first_line = 1
				for line in range(len(label)):
					l = SVGEditor.get_element(i.widget, "line%s" % (first_line + line,))
					if l is None:
						break
					SVGEditor.set_text(l, label[line])
			# Continue with next menu item
			i.index = index

			index += 1

		self.editor.remove_element("menuitem_template")
		self.editor.commit()
		del self.editor

	def _on_draw_clip_circle(self, widget: Gtk.Widget, cr: cairo.Context[cairo.Surface]) -> bool:
		"""Wayland circular shaping

		Clear the surface to transparent and clip all drawing (background, arcs, icons)
		to a centered circle inscribed in the window.

		The transparent corners give the same round appearance the X SHAPE extension produces on X11.

		Returns False so the default handler then draws the children within this clip.
		"""
		cr.set_source_rgba(0, 0, 0, 0)
		cr.set_operator(cairo.OPERATOR_SOURCE)
		cr.paint()
		cr.set_operator(cairo.OPERATOR_OVER)
		w = widget.get_allocated_width()
		h = widget.get_allocated_height()
		cr.arc(w / 2.0, h / 2.0, min(w, h) / 2.0, 0, 2 * PI)
		cr.clip()
		return False

	def show(self) -> None:
		OSDWindow.show(self)

		pb = self.b.get_pixbuf()

		width = int(pb.get_width() * self.scale * self.get_scale_factor())
		height = int(pb.get_height() * self.scale * self.get_scale_factor())
		self.f.move(self.cursor, int(width / 2), int(height / 2))

		if self.xdisplay is None:
			# Clipping to circle on Wayland is handled in __init__()
			return

		win = X.XID(self.get_window().get_xid())
		pixmap = X.create_pixmap(self.xdisplay, win, width, height, 1)

		gc = X.create_gc(self.xdisplay, pixmap, 0, None)
		X.set_foreground(self.xdisplay, gc, 0)
		X.fill_rectangle(self.xdisplay, pixmap, gc, 0, 0, width, height)
		X.set_foreground(self.xdisplay, gc, 1)
		X.set_background(self.xdisplay, gc, 1)

		r = int(width * 0.985)
		x = int((width - r) / 2)

		X.fill_arc(self.xdisplay, pixmap, gc, x, x, r, r, 0, 360 * 64)

		X.flush_gc(self.xdisplay, gc)
		X.flush(self.xdisplay)

		X.shape_combine_mask(self.xdisplay, win, X.SHAPE_BOUNDING, 0, 0, pixmap, X.SHAPE_SET)

		X.flush(self.xdisplay)

	def select(self, i) -> None:
		if type(i) == int:
			i = self.items[i]
		if self._selected and hasattr(self._selected, "icon_widget"):
			if self._selected.icon_widget:
				self._selected.icon_widget.set_name("osd-radial-menu-icon")
		self._selected = i
		if hasattr(self._selected, "icon_widget") and self._selected.icon_widget:
			self._selected.icon_widget.set_name("osd-radial-menu-icon-selected")
		self.b.hilight(
			{
				"menuitem_" + i.id: "#" + self.config["osd_colors"]["menuitem_hilight"],
				"text_" + i.id: "#" + self.config["osd_colors"]["menuitem_hilight_text"],
			},
		)

	def on_event(self, daemon, what, data):
		if self._submenu:
			return self._submenu.on_event(daemon, what, data)
		if what == self._control_with:
			x, y = data
			# Special case, both confirm_with and cancel_with can be set to STICK
			if self._cancel_with == STICK and self._control_with == STICK:
				if self._control_equals_cancel(daemon, x, y):
					return None

			if self.rotation:
				rx = x * cos(self.rotation) - y * sin(self.rotation)
				ry = x * sin(self.rotation) + y * cos(self.rotation)
				x, y = rx, ry

			max_w = self.get_allocation().width * self.scale - (self.cursor.get_allocation().width * 1.0)
			max_h = self.get_allocation().height * self.scale - (self.cursor.get_allocation().height * 1.0)
			cx = ((x * 0.75 / (STICK_PAD_MAX * 2.0)) + 0.5) * max_w
			cy = (0.5 - (y * 0.75 / (STICK_PAD_MAX * 2.0))) * max_h

			cx -= self.cursor.get_allocation().width * 0.5
			cy -= self.cursor.get_allocation().height * 0.5
			self.f.move(self.cursor, int(cx), int(cy))

			if abs(x) + abs(y) > RadialMenu.MIN_DISTANCE:
				angle = atan2(x, y) * 180.0 / PI
				half_width = 180.0 / len(self.items)
				for i in self.items:
					if abs(degdiff(i.a, angle)) < half_width:
						if self._selected != i:
							if self.feedback and self.controller:
								self.controller.feedback(*self.feedback)
							self.select(i)
		else:
			return Menu.on_event(self, daemon, what, data)
		return None


if __name__ == "__main__":
	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()

	m = RadialMenu()
	if not m.parse_argumets(sys.argv):
		sys.exit(1)
	m.run()
	if m.get_exit_code() == 0:
		print(m.get_selected_item_id())
	sys.exit(m.get_exit_code())
