from __future__ import annotations

from collections.abc import Callable
from typing import TYPE_CHECKING, Any

from archinstall.tui import Chars, FrameProperties, FrameStyle, MenuItem, MenuItemGroup, PreviewStyle, ResultType, SelectMenu, Tui

from ..output import error, unicode_ljust

if TYPE_CHECKING:
	_: Any


class Selector:
	def __init__(
		self,
		description: str,
		func: Callable[[Any], Any] | None = None,
		display_func: Callable | None = None,
		default: Any | None = None,
		enabled: bool = False,
		dependencies: list[str] = [],
		dependencies_not: list[str] = [],
		exec_func: Callable | None = None,
		preview_func: Callable | None = None,
		mandatory: bool = False,
		no_store: bool = False
	):
		"""
		Create a new menu selection entry

		:param description: Text that will be displayed as the menu entry
		:type description: str

		:param func: Function that is called when the menu entry is selected
		:type func: Callable

		:param display_func: After specifying a setting for a menu item it is displayed
		on the right side of the item as is; with this function one can modify the entry
		to be displayed; e.g. when specifying a password one can display **** instead
		:type display_func: Callable

		:param default: Default value for this menu entry
		:type default: Any

		:param enabled: Specify if this menu entry should be displayed
		:type enabled: bool

		:param dependencies: Specify dependencies for this menu entry; if the dependencies
		are not set yet, then this item is not displayed; e.g. disk_layout depends on selectiong
		harddrive(s) first
		:type dependencies: list

		:param dependencies_not: These are the exclusive options; the menu item will only be
		displayed if non of the entries in the list have been specified
		:type dependencies_not: list

		:param exec_func: A function with the name and the result of the selection as input parameter and which returns boolean.
		Can be used for any action deemed necessary after selection. If it returns True, exits the menu loop, if False,
		menu returns to the selection screen. If not specified it is assumed the return is False
		:type exec_func: Callable

		:param preview_func: A callable which invokws a preview screen
		:type preview_func: Callable

		:param mandatory: A boolean which determines that the field is mandatory, i.e. menu can not be exited if it is not set
		:type mandatory: bool

		:param no_store: A boolean which determines that the field should or shouldn't be stored in the data storage
		:type no_store: bool
		"""
		self._display_func = display_func
		self._no_store = no_store

		self.description = description
		self.func = func
		self.current_selection = default
		self.enabled = enabled
		self.dependencies = dependencies
		self.dependencies_not = dependencies_not
		self.exec_func = exec_func
		self.preview_func = preview_func
		self.mandatory = mandatory
		self.default = default

	def do_store(self) -> bool:
		return self._no_store is False

	def set_enabled(self, status: bool = True) -> None:
		self.enabled = status

	def update_description(self, description: str) -> None:
		self.description = description

	def menu_text(self, padding: int = 0) -> str:
		if self.description == '':  # special menu option for __separator__
			return ''

		current = ''

		if self._display_func:
			current = self._display_func(self.current_selection)
		else:
			if self.current_selection is not None:
				current = str(self.current_selection)

		if current:
			padding += 5
			description = unicode_ljust(str(self.description), padding, ' ')
		else:
			description = self.description
			current = ''

		return f'{description} {current}'

	def set_current_selection(self, current: Any | None) -> None:
		self.current_selection = current

	def has_selection(self) -> bool:
		if not self.current_selection:
			return False
		return True

	def get_selection(self) -> Any:
		return self.current_selection

	def is_empty(self) -> bool:
		if self.current_selection is None:
			return True
		elif isinstance(self.current_selection, str | list | dict) and len(self.current_selection) == 0:
			return True
		return False

	def is_enabled(self) -> bool:
		return self.enabled

	def is_mandatory(self) -> bool:
		return self.mandatory

	def set_mandatory(self, value: bool) -> None:
		self.mandatory = value


class AbstractMenu:
	def __init__(
		self,
		item_group: MenuItemGroup,
		data_store: dict[str, Any],
		auto_cursor: bool = True,
		allow_reset: bool = False,
		reset_warning: str | None = None
	):
		self._menu_item_group = item_group
		self._data_store = data_store
		self.auto_cursor = auto_cursor
		self._allow_reset = allow_reset
		self._reset_warning = reset_warning

		self.is_context_mgr = False

		self._sync_all_from_ds()

	def __enter__(self, *args: Any, **kwargs: Any) -> AbstractMenu:
		self.is_context_mgr = True
		return self

	def __exit__(self, *args: Any, **kwargs: Any) -> None:
		# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
		# TODO: skip processing when it comes from a planified exit
		if len(args) >= 2 and args[1]:
			error(args[1])
			Tui.print("Please submit this issue (and file) to https://github.com/archlinux/archinstall/issues")
			raise args[1]

		self._sync_all_to_ds()

	def _sync_all_from_ds(self) -> None:
		for item in self._menu_item_group.menu_items:
			if item.key is not None:
				if (store_value := self._data_store.get(item.key, None)) is not None:
					item.value = store_value

	def _sync_all_to_ds(self) -> None:
		for item in self._menu_item_group.menu_items:
			if item.key:
				self._data_store[item.key] = item.value

	def _sync(self, item: MenuItem) -> None:
		if not item.key:
			return

		store_value = self._data_store.get(item.key, None)

		if store_value is not None:
			item.value = store_value
		elif item.value is not None:
			self._data_store[item.key] = item.value

	def set_enabled(self, key: str, enabled: bool) -> None:
		if (item := self._menu_item_group.find_by_key(key)) is not None:
			item.enabled = enabled
			return None

		raise ValueError(f'No selector found: {key}')

	def disable_all(self) -> None:
		for item in self._menu_item_group.items:
			item.enabled = False

	def run(self) -> Any | None:
		self._sync_all_from_ds()

		while True:
			result = SelectMenu(
				self._menu_item_group,
				allow_skip=False,
				allow_reset=self._allow_reset,
				reset_warning_msg=self._reset_warning,
				preview_style=PreviewStyle.RIGHT,
				preview_size='auto',
				preview_frame=FrameProperties('Info', FrameStyle.MAX),
			).run()

			match result.type_:
				case ResultType.Selection:
					item: MenuItem = result.item()

					if item.action is None:
						break
				case ResultType.Reset:
					self._data_store = {}
					return None

		self._sync_all_to_ds()
		return None


class AbstractSubMenu(AbstractMenu):
	def __init__(
		self,
		item_group: MenuItemGroup,
		data_store: dict[str, Any],
		auto_cursor: bool = True,
		allow_reset: bool = False
	):
		back_text = f'{Chars.Right_arrow} ' + str(_('Back'))
		item_group.menu_items.append(MenuItem(text=back_text))

		super().__init__(
			item_group,
			data_store=data_store,
			auto_cursor=auto_cursor,
			allow_reset=allow_reset
		)
