from __future__ import annotations

from collections.abc import Iterable, Iterator
from enum import IntEnum, StrEnum
from typing import Any, ClassVar, Self

from pydantic import NonNegativeInt as uint, PrivateAttr

from mortis.aff.types.models import AFFEventConfig, LongNoteEvent, SkyEvent, TapLikeEvent
from mortis.aff.types.coord import ArcCoord, Coordinate
from mortis.aff.types.easings import ArcEasing, BaseEasing, get_easing_x, get_easing_y
from mortis.aff.types.dffloat import dffloat2


__all__ = ['ArcType', 'ArcColor', 'Arctap', 'Arc', 'ScaledArctap']

class ArcType(StrEnum):

	Solid = Snake = False_ = 'false'
	Void = Trace = True_ = 'true'
	Designant = 'designant'


class ArcColor(IntEnum):

	Blue = 0
	Red = 1
	Green = 2
	Gray = Grey = 3


class Arctap(SkyEvent, TapLikeEvent):

	__aff_config__: ClassVar[AFFEventConfig] = AFFEventConfig(
		is_event=True,
		trailing='',
		commands=('arctap', 'at'),
		argument_order=('time',)
	)
	__converters__: ClassVar[tuple[str, ...]] = 'from_int', 'from_str'

	time: uint

	_parent: Arc | None = PrivateAttr(default=None)

	@property
	def parent(self) -> Arc | None:
		return self._parent
	
	@parent.setter
	def parent(self, value: Arc | None) -> None:
		if value is None:
			del self.parent
		else:
			self.bind_to(value)
	
	@parent.deleter
	def parent(self) -> None:
		self.unbind()	

	def bind_to(self, arc: Arc) -> None:
		if not isinstance(arc, Arc):
			raise TypeError('arc must be an instance of Arc')
		
		if self._parent is arc:
			return

		if self._parent is not None:
			self.unbind()

		if self.time > arc.end_time or self.time < arc.begin_time:
			raise ValueError(f'Arctap time {self.time} is not within the range of arc: {arc.time_range}')

		arc._arctaps.append(self)
		arc._arctaps.sort(key=lambda at: at.time)
		self._parent = arc

	def unbind(self) -> None:
		if self._parent is None:
			return
		
		idx = self._parent.find_arctap(self)
		del self._parent._arctaps[idx]
		self._parent = None

	def position(self) -> tuple[float, float]:
		if self._parent is None:
			raise ValueError('Arctap is not bound to any arc')
		return self._parent.position_at(self.time)
	
	@classmethod
	def from_int(cls, value: int) -> Self:
		try:
			return cls(time=int(value))
		except (ValueError, TypeError) as e:
			raise ValueError(e)
	
	def copy(self) -> Self:
		return self.__copy__()
	
	def deepcopy(self) -> Self:
		return self.__deepcopy__()
	
	def __copy__(self) -> Self:
		return self.__class__(time=self.time)
	
	def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Self:
		instance = self.__class__(time=self.time)
		instance.parent = self.parent
		return instance


class Arc(SkyEvent, LongNoteEvent):

	__aff_config__: ClassVar[AFFEventConfig] = AFFEventConfig(
		is_event=True,
		uses_extra_args=True,
		commands=('arc', ),
		argument_order=(
			'begin_time', 'end_time',
			'begin_x', 'end_x',
			'easing',
			'begin_y', 'end_y',
			'color', 'hitsound', 'type_',
			'smoothness'
		)
	)
	
	begin_time: uint
	end_time: uint
	begin_x: ArcCoord
	end_x: ArcCoord
	easing: ArcEasing
	begin_y: ArcCoord
	end_y: ArcCoord
	color: ArcColor
	hitsound: str
	type_: ArcType
	smoothness: dffloat2 | None = None

	_arctaps: list[Arctap] = []
	
	@property
	def easing_x(self) -> BaseEasing:
		return get_easing_x(self.easing)(self.begin_time, self.end_time, self.begin_x, self.end_x)
	
	@property
	def easing_y(self) -> BaseEasing:
		return get_easing_y(self.easing)(self.begin_time, self.end_time, self.begin_y, self.end_y)
	

	def wrap_hitsound(self, hitsound: str | None) -> None:
		self.hitsound = 'none' if hitsound is None else hitsound.replace('.', '_')
	
	def unwrap_hitsound(self) -> str | None:
		return None if self.hitsound == 'none' else self.hitsound.replace('_', '.')
	

	def iter_arctaps(self) -> Iterator[Arctap]:
		yield from self._arctaps
	
	@property
	def arctaps(self) -> tuple[Arctap, ...]:
		return tuple(self._arctaps)
	
	def find_arctap(self, arctap: Arctap) -> int:
		for i, at in enumerate(self._arctaps):
			if at is arctap: # check identity (`is`) because we are maintaining refs
				return i
		return -1
	
	def add_arctap(self, arctap: Arctap) -> None:
		if arctap in self:
			raise ValueError('Arctap is already added to this arc')
		arctap.bind_to(self)
	
	def remove_arctap(self, arctap: Arctap) -> None:
		if arctap not in self:
			raise ValueError('Arctap is not found in this arc')
		arctap.unbind()
	
	def clear_arctaps(self) -> None:
		for at in self._arctaps:
			at.unbind()
	
	def extend_arctaps(self, arctaps: Iterable[Arctap]) -> None:
		for at in arctaps:
			self.add_arctap(at)

	def _get_extras(self) -> Any:
		return self._arctaps
	
	def _set_extras(self, extras: Any) -> None:
		try:
			arctaps = list(map(Arctap.from_any, list(extras)))
			for arctap in arctaps:
				arctap.bind_to(self)
		except (ValueError, TypeError) as e:
			raise ValueError(e)
	
	def position_at(self, time: int) -> tuple[float, float]:
		x = self.easing_x.at(time)
		y = self.easing_y.at(time)
		return (x, y)

	def __contains__(self, arctap: Arctap) -> bool:
		return self.find_arctap(arctap) != -1
	
	@classmethod
	def from_scaled_arctap(cls, sat: ScaledArctap) -> Self:
		return cls(
			begin_time = sat.time,
			end_time = sat.time,
			begin_x = sat.begin_x,
			end_x = sat.end_x,
			easing = ArcEasing.S,
			begin_y = sat.y,
			end_y = sat.y,
			color = ArcColor.Grey,
			hitsound = sat.hitsound,
			type_ = ArcType.Solid,
		)
	
	def to_scaled_arctap(self) -> ScaledArctap:
		return ScaledArctap.from_arc(self)
	
	def is_scaled_arctap(self) -> bool:
		try:
			self.to_scaled_arctap()
			return True
		except ValueError:
			return False

	def _after_validation(self) -> Self:
		super()._after_validation()

		if all([
			self.begin_time == self.end_time,
			self.begin_x == self.end_x,
			self.begin_y == self.end_y
		]):
			raise ValueError(f'The start and end points of arc cannot be the same')

		return self
	
	def clip(
		self,
		begin: uint,
		end: uint,
		/, *, 
		preserve_arctaps: bool = True
	) -> Arc:
		
		begin = int(begin)
		end = int(end)
		
		arc = self.model_copy()
		arc.time_range = begin, end

		if any([
			begin < self.begin_time,
			end > self.end_time
		]):
			raise ValueError(f'Clip is out of the range of the arc')
		
		begin_x, begin_y = self.position_at(begin)
		end_x, end_y = self.position_at(end)

		arc.clear_arctaps()
		arc.begin_x, arc.begin_y = ArcCoord(begin_x), ArcCoord(begin_y)
		arc.end_x, arc.end_y = ArcCoord(end_x), ArcCoord(end_y)

		if preserve_arctaps:
			arc.extend_arctaps(
				at for at in self._arctaps
				if at.time >= begin and at.time <= end
			)

		return arc
	
	def copy(self) -> Self:
		return self.__copy__()
	
	def deepcopy(self) -> Self:
		return self.__deepcopy__()
	
	def __copy__(self) -> Self:
		cls = self.__class__
		data = {
			name: getattr(self, name)
			for name in cls.fields_order
		}
		return cls(**data)
	
	def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Self:
		instance = self.__copy__()
		for at in self.arctaps:
			copied_at = at.__deepcopy__()
			copied_at.bind_to(instance)

		return instance
	
	def mirror(self) -> None:
		self.begin_x = ArcCoord(1.0 - self.begin_x)
		self.end_x = ArcCoord(1.0 - self.end_x)


class ScaledArctap(SkyEvent, TapLikeEvent):

	__aff_config__: ClassVar[AFFEventConfig] = AFFEventConfig(
		is_event=True,
		uses_extra_args=True,
		commands=('arc', ),
		argument_order=('time', 'begin_x', 'end_x', 'y', 'hitsound')
	)
	
	time: uint
	begin_x: ArcCoord
	end_x: ArcCoord
	y: ArcCoord
	hitsound: str

	@classmethod
	def from_arc(cls, arc: Arc) -> Self:
		if not all([
			arc.begin_time == arc.end_time,
			arc.begin_y == arc.end_y,
			arc.color == ArcColor.Grey,
			arc.type_ == ArcType.Solid,
			len(arc.arctaps) == 0
		]):
			raise ValueError(f'Failed to convert this arc to a scaled arctap')
		
		return cls(
			time = arc.begin_time,
			begin_x = arc.begin_x,
			end_x = arc.end_x,
			y = arc.begin_y,
			hitsound = arc.hitsound
		)

	def to_arc(self) -> Arc:
		return Arc.from_scaled_arctap(self)
	
	@classmethod
	def from_str(cls, line: str) -> Self:
		arc = Arc.from_str(line)
		return cls.from_arc(arc)

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

	@classmethod
	def from_dict(cls, data: dict) -> Self:
		arc = Arc.from_dict(data)
		return cls.from_arc(arc)
	
	@classmethod
	def from_analysed(cls, command: str, args: tuple, extras: tuple | None = None) -> Self:
		arc = Arc.from_analysed(command, args, extras)
		return cls.from_arc(arc)
	
	@classmethod
	def from_args(cls, args: tuple, extras: tuple | None = None) -> Self:
		arc = Arc.from_args(args, extras)
		return cls.from_arc(arc)
	
	def mirror(self) -> None:
		self.begin_x = Coordinate.mirror_arc(self.begin_x)
		self.end_x = Coordinate.mirror_arc(self.end_x)
