from __future__ import annotations

from collections.abc import Iterator
from typing import Any, Self

from pydantic import BaseModel, NonNegativeInt as uint, PrivateAttr, ValidationError, model_validator

from mortis.aff.events.arc import Arc, ScaledArctap
from mortis.aff.events.camera import Camera
from mortis.aff.events.hold import Hold, HoldFloat
from mortis.aff.events.scenecontrol import SceneControl
from mortis.aff.events.tap import Tap, TapFloat
from mortis.aff.events.timing import Timing
from mortis.aff.types.models import AFFEvent
from mortis.aff.lexer.analyse import analyse_command, analyse_timinggroup_footer, analyse_timinggroup_header
from mortis.aff.lexer.token import tokenize
from mortis.globcfg import GlobalConfig
from mortis.utils import get_default_model_cfg


__all__ = ['parse_event', 'TimingGroup']

__registered_aff_events__: list[type[AFFEvent]] = []

def get_registered_aff_events():
	global __registered_aff_events__
	if not __registered_aff_events__:
		__registered_aff_events__ = [Timing, Arc, Tap, TapFloat, Hold, HoldFloat, SceneControl, Camera]
		if GlobalConfig.uses_scaled_arctap:
			__registered_aff_events__.insert(1, ScaledArctap)
	
	return __registered_aff_events__


def parse_event(
	command: str,
	args: tuple,
	extras: tuple | None = None,
) -> AFFEvent:
	
	errors: dict[str, Exception] = {}
	registered = get_registered_aff_events()
	for eventcls in registered:
		if command in eventcls.commands:
			try:
				return eventcls.from_args(args, extras)
			except Exception as e:
				errors[eventcls.__name__] = e
	
	input = {'command': command, 'args': args, 'extras': extras}
	raise ValidationError.from_exception_data(
		title = f'parsing AFF event from {input}',
		line_errors = [{
			'type': 'value_error',
			'loc': ('parse_aff_event', ),
			'input': input,
			'ctx': {'error': errors},
		}]
	)

__timing_group_attrs__ = ('noinput', 'fadingholds', 'anglex', 'angley')
class TimingGroup(BaseModel):

	model_config = get_default_model_cfg()

	noinput: bool | None = None
	fadingholds: bool | None = None
	anglex: uint | None = None
	angley: uint | None = None

	_events: list[AFFEvent] = PrivateAttr(default_factory=list)


	@property
	def args(self) -> dict[str, bool | uint | None]:
		return {
			name: current
			for name in __timing_group_attrs__
			if (current := getattr(self, name)) is not None
		}
	
	@args.setter
	def args(self, args: Any) -> None:
		if isinstance(args, dict):
			valid_args = self.load_args_from_dict(args)
		elif isinstance(args, str):
			valid_args = self.load_args_from_str(args)
		elif args is None:
			del self.args
			return
		else:
			raise TypeError(f'Invalid type for TimingGroup args: {type(args).__name__!r}')
		
		for name, value in valid_args.items():
			setattr(self, name, value)

	@args.deleter
	def args(self) -> None:
		for name in __timing_group_attrs__:
			setattr(self, name, None)
	

	@classmethod
	def load_args_from_dict(cls, args: dict[str, Any]) -> dict[str, bool | uint | None]:
		valid_args = {}
		for name in args:
			if name in __timing_group_attrs__:
				valid_args[name] = args[name]
			else:
				raise ValueError(f'Unknown TimingGroup argument: {name!r}')
		
		return valid_args
	
	@classmethod
	def load_args_from_str(cls, args_str: str) -> dict[str, bool | uint | None]:
		args: dict[str, bool | uint | None] = {}
		segments: list[str] = [s.strip() for s in args_str.split(',')]

		for seg in segments:
			if seg == 'noinput':
				args['noinput'] = True

			elif seg == 'fadingholds':
				args['fadingholds'] = True

			elif seg.startswith('anglex'):
				value_str = seg[len('anglex'):].strip()
				args['anglex'] = int(value_str)

			elif seg.startswith('angley'):
				value_str = seg[len('angley'):].strip()
				args['angley'] = int(value_str)
			
			else:
				raise ValueError(f'Unknown TimingGroup argument: {seg!r}')
		
		return args
	

	def dump_args_to_dict(self) -> dict:
		return {
			attr: current
			for attr in __timing_group_attrs__
			if (current := getattr(self, attr)) is not None
		}

	def dump_args_to_str(self) -> str:		
		segments = []
		for attr, current in self.dump_args_to_dict().items():
			if current is True:
				segments.append(attr)
			else:
				segments.append(f'{attr}{current}')
		return '_'.join(segments)

	
	def iter_events(self) -> Iterator[AFFEvent]:
		yield from self._events

	@property
	def events(self) -> tuple[AFFEvent, ...]:
		return tuple(self._events)

	def find_event(self, event: AFFEvent) -> int:
		for i, ev in enumerate(self._events):
			if ev is event:
				return i
		return -1

	def __contains__(self, event: AFFEvent) -> bool:
		return self.find_event(event) != -1

	def add_event(self, event: AFFEvent) -> None:
		if not isinstance(event, AFFEvent):
			raise TypeError(f'Event must be of type {AFFEvent.__name__}')
		
		if event in self:
			raise ValueError('Event is already in this group')
		self._events.append(event)
	
	def remove_event(self, event: AFFEvent) -> None:
		idx = self.find_event(event)
		if idx == -1:
			raise ValueError('Event is not found in this group')
		del self._events[idx]
	
	def clear_events(self) -> None:
		self._events = []

	@property
	def required_hitsounds(self) -> set[str]:
		hitsounds = set()
		for event in self.events:
			if not isinstance(event, Arc):
				continue
			hitsound = event.unwrap_hitsound()
			if hitsound is not None:
				hitsounds.add(hitsound)
		return hitsounds
	
	@classmethod
	def from_str(cls, contents: str, *, events_only: bool=False) -> Self:
		instance = cls()

		lines = contents.split('\n')
		tokens_list = list(map(tokenize, lines))
		if events_only:
			evtoks_list = tokens_list

		else:
			header_toks = tokens_list[0]
			evtoks_list = tokens_list[1:-1]
			footer_toks = tokens_list[-1]

			header_args = analyse_timinggroup_header(header_toks)
			analyse_timinggroup_footer(footer_toks)
			cls.args = header_args
		
		for evtoks in evtoks_list:
			command, args, extras = analyse_command(evtoks)
			event = parse_event(command, args, extras, )
			instance.add_event(event)
		
		return instance

	def to_str(self, *, events_only: bool=False) -> str:
		event_lines = list(map(str, self._events))
		if events_only:
			result = event_lines

		else:
			result = [
				f'timinggroup({self.dump_args_to_str()})' + '{'
			]
			result.extend(f'  {x}' for x in event_lines)
			result.append('};')
		
		return '\n'.join(result)

	def __str__(self) -> str:
		return self.to_str()


	@model_validator(mode='after')
	def _after_validation(self) -> Self:
		if self.noinput is False:
			self.noinput = None

		if self.fadingholds is False:
			self.fadingholds = None

		if self.anglex is not None:
			self.anglex %= 3600
		
		if self.angley is not None:
			self.angley %= 3600

		return self