import pathlib as p
import sys
import threading as th
import traceback
import warnings as ws
from collections.abc import Callable

from colored import Back, Fore, Style

from .tcr_classfuncs import get_classname
from .tcr_console import console
from .tcr_iterable import cut_at
from .tcr_print import FMTC

C_WHITE = Fore.white + Style.bold
C_NUMBER = FMTC.NUMBER
C_WARNING = Fore.yellow + Style.bold
C_ERROR = Fore.red + Style.bold
C_EXCEPTION = FMTC.INTERNAL_EXCEPTION


def default_err_or_warning_printhook(s: str, warning: Warning | Exception, filename: str, lineno: int, *, tb=None) -> None:
	"""Default printhook for tcr.WarningCatcher."""
	path = p.Path(filename)

	AROUND: int = 2
	"""How many lines around the line of the warning to show."""
	CUTOFF: int = 70
	"""How many (max) characters can be shown per line until they ge..."""

	is_warning = isinstance(warning, Warning)

	color = C_WARNING if is_warning else C_ERROR

	if not path.exists():
		lines = ""
	else:
		lines = path.read_text().split("\n")
		lines = lines[max(0, lineno - (AROUND + 1)) : lineno + AROUND]
		while all(x.startswith(" ") for x in lines):
			lines = [x[1:] for x in lines]
		lines = [f'{C_NUMBER}{lineno - AROUND + i}{color} {">" if i == AROUND else "|"}{FMTC._} {cut_at(x, CUTOFF)}' for i, x in enumerate(lines)]
		lines = "\n".join(lines)
		lines = f"\n{lines}"

	if tb:
		traceback.print_tb(tb)

	func = console.warn if is_warning else console.error
	func(f'{color}{filename}{C_WHITE}:{C_NUMBER}{lineno}{C_WHITE}: {C_EXCEPTION}{get_classname(warning)}{C_WHITE}: {color}{str(warning).replace("<locals>.", "")}{lines}')


def _errorcatcher_excepthook(type, value, traceback):
	frame = traceback.tb_frame
	filename = frame.f_code.co_filename
	lineno = frame.f_lineno
	default_err_or_warning_printhook(str(value), value, filename, lineno, tb=traceback)


class ErrorCatcher:
	def __init__(self, **kwargs) -> None:
		self.install(**kwargs)

	@staticmethod
	def install():
		sys.excepthook = _errorcatcher_excepthook


class WarningCatcher:
	"""Catches any warnings (for example "Coroutine X was not awaited") and forwards them as text to the selected (or default: tcr.console.warn) printhook.

	Usage:
	```py
	tcr.WarningCatcher()
	# OR
	tcr.WarningCatcher.install() # Tgus
	```
	"""

	@staticmethod
	def install(**kwargs):
		"""Alias for tcr.WarningCatcher(**kwargs). Use it for better typehints."""
		_WarningCatcher.install(**kwargs)

	def __init__(
		self,
		printhook: Callable[[str, Warning, str, int], None] = default_err_or_warning_printhook,
	) -> None:
		"""Catches any warnings (for example "Coroutine X was not awaited") and forwards them as text to the selected (or default: tcr.console.warn) printhook.

		Usage:
		```py
		tcr.WarningCatcher()
		```

		Args:
		        - printhook: Callable[[str, Warning, str, int], None], Defines a printer function, for more info see this module's default_showwarning_printhook function and https://docs.python.org/3/library/warnings.html#warnings.showwarning.
		"""
		_WarningCatcher.install(
			printhook=printhook,
		)


class _WarningCatcher(th.Thread):
	_initiated = False
	_installed = False

	def __init__(self, *, printhook):
		if _WarningCatcher._initiated:
			raise RuntimeError("WarningCatcher can only be initiated once.")
		super().__init__()
		_WarningCatcher._initiated = True
		self.__printhook = printhook
		self.daemon = True
		self._stop_event = th.Event()

	def run(self):
		def showwarning(message, category, filename, lineno, file=None, line=None):
			self.__printhook(str(message), message, filename, lineno)

		with ws.catch_warnings():
			ws.showwarning = showwarning
			while not self._stop_event.is_set():
				self._stop_event.wait(0.1)  # Check every 0.1 second for the stop signal

	def stop(self):
		self._stop_event.set()

	@classmethod
	def install(cls, **kwargs):
		if _WarningCatcher._installed:
			raise RuntimeError("WarningCatcher can only be installed once.")
		_WarningCatcher._installed = True

		catcher = cls(**kwargs)
		catcher.start()
		return catcher
