import io
import cv2
import base64
import logging
import numpy as np
from PIL import Image
from pynput import keyboard
from eymos import Service, log


class DependencyLoader:
	TKINTER = False

	@staticmethod
	def is_tkinter():
		"""Check if tkinter is installed."""
		return DependencyLoader.TKINTER


try:
	import tkinter as tk
	from tkinter import Canvas
	from PIL import ImageTk
	DependencyLoader.TKINTER = True
except ImportError:
	pass


class ImageSize:
	COVER = 0
	CONTAIN = 1
	STRETCH = 2


class WindowService(Service):
	DEFAULT_FPS = 60
	DEFAULT_RESOLUTION = [480, 270]
	APP_LOGO = " "

	def __init__(self, name: str, config: dict, services: dict):
		"""Initialize the service.
		Args:
			name (str): The name of the service.
			config (dict): The system configuration.
			services (dict, optional): The services to use. Defaults to {}.
		"""
		# Initialize the service attributes
		self.__fps = None
		self.__title = None
		self.__resizable = None
		self.__always_on_top = None
		self.__background = None
		self.__border = None
		self.__width = None
		self.__height = None
		self.__draggable = None
		self.__toggle_visibility = None
		self.__frame = None
		self.__tk_root = None
		self.__tk_icon = None
		self.__tk_image = None
		self.__tk_photo = None
		self.__tk_canvas = None
		self.__tk_canvas_img = None
		self.__tk_is_visible = None

		# Call the parent class constructor
		super().__init__(name, config, services)

	def init(self):
		"""Initialize the service."""
		self.__fps = self._config.get('fps', self.DEFAULT_FPS)
		self._loop_delay = 1 / self.__fps
		self.__title = self._config.get('title', 'EymOS')
		self.__resizable = self._config.get('resizable', False)
		self.__always_on_top = self._config.get('always_on_top', True)
		self.__background = self._config.get('background', 'black')
		self.__border = self._config.get('border', False)
		self.__width = self._config.get('width', self.DEFAULT_RESOLUTION[0])
		self.__height = self._config.get('height', self.DEFAULT_RESOLUTION[1])
		self.__draggable = self._config.get('draggable', True)
		self.__toggle_visibility = self._config.get('toggle_visibility', {'key': 'h', 'modifiers': ['ctrl']})
		self.__frame = 0
		self.__tk_root = None
		self.__tk_icon = self.APP_LOGO
		self.__tk_image = None
		self.__tk_photo = None
		self.__tk_canvas = None
		self.__tk_canvas_img = None
		self.__tk_is_visible = True

		# Initialize the tkinter window
		self.__tk_init()

	def destroy(self):
		"""Destroy the service."""
		self.__fps = None
		self.__title = None
		self.__resizable = None
		self.__always_on_top = None
		self.__background = None
		self.__border = None
		self.__width = None
		self.__height = None
		self.__draggable = None
		self.__toggle_visibility = None
		self.__frame = None
		self.__tk_icon = None
		self.__tk_image = None
		self.__tk_photo = None
		self.__tk_canvas = None
		self.__tk_canvas_img = None
		self.__tk_is_visible = None

		# Destroy the tkinter window from manager
		if self._manager:
			self._manager.on_stop(self.__tk_root.quit)

	def loop(self):
		"""Service loop."""
		if not DependencyLoader.is_tkinter():
			return

		# Update the tkinter window
		if self.__tk_root is not None:
			self.__tk_update()

	def mainloop(self):
		"""Start the tkinter main loop."""
		# Check if tkinter is installed
		if self.__tk_root is None:
			return

		# Start the tkinter main loop
		self.__tk_root.mainloop()

	def draw(self, image: np.ndarray, size: int = ImageSize.COVER):
		"""Draw the image on the window.
		Args:
			image (np.ndarray): The image to draw.
		"""
		# Draw the image on the tkinter canvas
		self.__tk_draw(image, size)

	def __tk_init(self):
		"""Initializes tkinter for PCs to emulate the screen."""
		# Check if tkinter is installed
		if not DependencyLoader.is_tkinter():
			log('Tkinter is not installed. The window service is disabled.', logging.WARNING)
			return

		# Create the tkinter window
		geometry = f'{self.__width}x{self.__height}'
		self.__tk_root = tk.Tk()
		self.__tk_root.title(self.__title)
		self.__tk_root.geometry(geometry)
		self.__tk_root.resizable(self.__resizable, self.__resizable)

		# Set the window always on top
		if self.__always_on_top:
			self.__tk_root.lift()

		# Remove the window border
		if not self.__border:
			self.__tk_root.overrideredirect(True)
			self.__tk_root.config(bg=self.__background)

		# Create the tkinter canvas
		self.__tk_image = Image.new('RGB', (self.__width, self.__height), 'black')
		self.__tk_photo = ImageTk.PhotoImage(self.__tk_image)
		self.__tk_canvas = Canvas(self.__tk_root, width=self.__width, height=self.__height, bg=self.__background, highlightthickness=0)
		self.__tk_canvas_img = self.__tk_canvas.create_image(0, 0, anchor='nw', image=self.__tk_photo)
		self.__tk_canvas.pack()

		# Set the window icon
		if self.__tk_icon:
			icon_data = base64.b64decode(self.__tk_icon)
			icon_image = Image.open(io.BytesIO(icon_data))
			self.__tk_icon = ImageTk.PhotoImage(icon_image)
			self.__tk_root.iconphoto(False, self.__tk_icon)

		# Enable dragging
		if self.__draggable:
			self.__tk_drag(self.__tk_root)

		# Enable visibility toggle
		if self.__toggle_visibility:
			self.__tk_visibility(self.__tk_root)

	def __tk_drag(self, root):
		"""Enable dragging for the tkinter window.
		Args:
			root (tk.Tk): The tkinter window.
		"""
		# Check if tkinter is installed
		if not DependencyLoader.is_tkinter():
			return

		# Initialize the start coordinates
		coords = {"x": 0, "y": 0}

		# Function to start dragging
		def start_drag(event, coords):
			coords["x"] = event.x
			coords["y"] = event.y

		# Function to stop dragging
		def stop_drag(event, coords):
			coords["x"] = None
			coords["y"] = None

		# Function to drag the window
		def drag(event, coords):
			x = root.winfo_x() + event.x - coords["x"]
			y = root.winfo_y() + event.y - coords["y"]
			root.geometry(f'+{x}+{y}')

		# Remove the window border
		if not self.__border:
			self.__tk_root.overrideredirect(True)

		# Bind the mouse events
		root.bind('<ButtonPress-1>', lambda event: start_drag(event, coords))
		root.bind('<ButtonRelease-1>', lambda event: stop_drag(event, coords))
		root.bind('<B1-Motion>', lambda event: drag(event, coords))

	def __tk_visibility(self, root):
		"""Enable visibility toggle for the tkinter window.
		Args:
			root (tk.Tk): The tkinter window.
		"""
		# Check if tkinter is installed
		if not DependencyLoader.is_tkinter():
			return

		# Check if the toggle key is set
		if not self.__toggle_visibility:
			return

		# Initialize the modifier keys
		modifiers = self.__toggle_visibility.get('modifiers', [])
		key_char = self.__toggle_visibility.get('key', 'h')

		# Initialize the pressed keys
		keys = {"ctrl": False, "shift": False, "alt": False, "meta": False}

		# Function to check pressed keys
		def on_press(key, keys):
			if keyboard.Key.ctrl_l == key or keyboard.Key.ctrl_r == key:
				keys["ctrl"] = True
			elif keyboard.Key.shift_l == key or keyboard.Key.shift_r == key:
				keys["shift"] = True
			elif keyboard.Key.alt_l == key or keyboard.Key.alt_r == key:
				keys["alt"] = True
			elif keyboard.Key.cmd_l == key or keyboard.Key.cmd_r == key:
				keys["meta"] = True
			is_pressed = True
			for modifier in modifiers:
				if not keys[modifier]:
					is_pressed = False
					break
			if type(key) != keyboard.KeyCode:
				return
			if is_pressed and key_char == key.char:
				self.__tk_is_visible = not self.__tk_is_visible
				if self.__tk_is_visible:
					log('The window is visible')
					root.deiconify()
				else:
					log('The window is hidden')
					root.withdraw()

		# Function to check released keys
		def on_release(key, keys):
			if keyboard.Key.ctrl_l == key or keyboard.Key.ctrl_r == key:
				keys["ctrl"] = False
			elif keyboard.Key.shift_l == key or keyboard.Key.shift_r == key:
				keys["shift"] = False
			elif keyboard.Key.alt_l == key or keyboard.Key.alt_r == key:
				keys["alt"] = False
			elif keyboard.Key.cmd_l == key or keyboard.Key.cmd_r == key:
				keys["meta"] = False

		# Start the listener
		listener = keyboard.Listener(on_press=lambda key: on_press(key, keys), on_release=lambda key: on_release(key, keys))
		listener.start()

	def __tk_update(self):
		"""Update the tkinter window."""
		# Check if the window is visible or the canvas is not set
		if not self.__tk_is_visible or self.__tk_canvas is None:
			return

		# Update the frame
		self.__frame += 1

		# Update width and height
		width = self.__tk_root.winfo_width()
		height = self.__tk_root.winfo_height()
		if width != self.__width:
			self.__width = width
		if height != self.__height:
			self.__height = height

		# Set the always on top
		if self.__always_on_top:
			self.__tk_root.attributes('-topmost', True)

		# Draw the image
		self.__tk_draw()

		# Update the window
		self.__tk_root.update_idletasks()
		self.__tk_root.update()

	def __tk_clear(self):
		"""Clear the tkinter canvas."""
		# Draw a black image
		self.__tk_image = Image.new('RGB', (self.__width, self.__height), 'black')

		# Convert the image to tkinter
		self.__tk_photo = ImageTk.PhotoImage(self.__tk_image)

		# Update the canvas
		self.__tk_canvas.itemconfig(self.__tk_canvas_img, image=self.__tk_photo)

	def __tk_draw(self, image: np.ndarray = None, size: int = ImageSize.COVER):
		"""Draw the image on the tkinter canvas.
		Args:
			image (Image, optional): The image to draw. Defaults to None.
		"""
		# Check if the image is set
		if image is None:
			return

		# Resize the image
		if size == ImageSize.COVER:
			image = self.__image_resize_cover(image, self.__width, self.__height)
		elif size == ImageSize.CONTAIN:
			image = self.__image_resize_contain(image, self.__width, self.__height)
		elif size == ImageSize.STRETCH:
			image = self.__image_resize_stretch(image, self.__width, self.__height)

		# Convert the image to PIL
		image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
		image = Image.fromarray(image)

		# Convert the image to tkinter
		self.__tk_photo = ImageTk.PhotoImage(image)

		# Update the canvas
		self.__tk_canvas.itemconfig(self.__tk_canvas_img, image=self.__tk_photo)

	@staticmethod
	def __image_resize_cover(image: np.ndarray, width: int, height: int) -> np.ndarray:
		"""Resize the image to cover the window.
		Args:
			image (np.ndarray): The image to resize.
			width (int): The width of the window.
			height (int): The height of the window.
		Returns:
			np.ndarray: The resized image.
		"""
		# Get the image size
		image_width, image_height = image.shape[1], image.shape[0]

		# Calculate the resize ratio
		ratio = max(width / image_width, height / image_height)

		# Resize the image
		image = cv2.resize(image, (int(image_width * ratio), int(image_height * ratio)))

		# Calculate the cropping
		crop_x = (image.shape[1] - width) // 2
		crop_y = (image.shape[0] - height) // 2

		# Crop the image
		image = image[crop_y:crop_y + height, crop_x:crop_x + width]

		# Return the image
		return image

	@staticmethod
	def __image_resize_contain(image: np.ndarray, width: int, height: int) -> np.ndarray:
		"""Resize the image to contain the window.
		Args:
			image (np.ndarray): The image to resize.
			width (int): The width of the window.
			height (int): The height of the window.
		Returns:
			np.ndarray: The resized image.
		"""
		# Get the image size
		image_width, image_height = image.shape[1], image.shape[0]

		# Calculate the resize ratio
		ratio = min(width / image_width, height / image_height)

		# Resize the image
		image = cv2.resize(image, (int(image_width * ratio), int(image_height * ratio)))

		# Calculate the padding
		padding = (height - image.shape[0]) // 2

		# Add the padding
		image = cv2.copyMakeBorder(image, padding, padding, 0, 0, cv2.BORDER_CONSTANT, value=[0, 0, 0])

		# Return the image
		return image

	@staticmethod
	def __image_resize_stretch(image: np.ndarray, width: int, height: int) -> np.ndarray:
		"""Resize the image to stretch the window.
		Args:
			image (np.ndarray): The image to resize.
			width (int): The width of the window.
			height (int): The height of the window.
		Returns:
			np.ndarray: The resized image.
		"""
		# Resize the image
		image = cv2.resize(image, (width, height))

		# Return the image
		return image
