from functools import total_ordering

try:
	from typing import override
except ImportError:
	from typing_extensions import override  # noqa: F401

from dataclasses import dataclass


@total_ordering
@dataclass
class Marker:
	position: float
	name: str = ""
	rounding_digits: int = 5

	def __post_init__(self):
		self.position = float(self.position)

	def __lt__(self, other: "Marker") -> bool:
		return self.position < other.position

	def __eq__(self, other: object) -> bool:
		if not isinstance(other, Marker):
			return NotImplemented
		self_pos = round(self.position, self.rounding_digits)
		other_pos = round(other.position, self.rounding_digits)

		return self_pos == other_pos

	def __hash__(self) -> int:
		return hash(id(self))

	def __float__(self) -> float:
		return self.position

	def __str__(self) -> str:
		return f"{self.name} - Position: {self.position}"

	def has_name(self) -> bool:
		"""
		Check whether this object has a non-empty name.

		Returns:
			bool: True if the name is not an empty string, False otherwise.
		"""
		return self.name != ""

	def compare_position(self, other_position: float) -> bool:
		"""
		Compare the object's position to another position with rounding precision.

		Parameters:
			other_position (float): The position value to compare against.

		Returns:
			bool: True if the two positions are equal when rounded to the defined
				number of digits; False otherwise.
		"""
		self_pos = round(self.position, self.rounding_digits)
		other_pos = round(other_position, self.rounding_digits)

		return self_pos == other_pos


class MarkerList:
	"""
	Container class to manage a collection of Marker objects.

	Provides methods to add, remove, and access markers while ensuring
	uniqueness by position and maintaining sorted order.

	Attributes:
	    elements (list[Marker]): The list storing Marker instances.
	"""

	elements: list[Marker]

	def __init__(self):
		"""
		Initialize the marker container.

		Creates an empty list to store Marker instances.
		"""
		self.elements = []

	def __repr__(self) -> str:
		"""
		Return a string representation of the marker container.

		Returns:
			str: A string showing all stored markers.
		"""
		return str(self.elements)

	def __contains__(self, element: Marker) -> bool:
		"""
		Check if a marker with the same position exists in the container.

		Args:
			element (Marker): The marker to check for.

		Returns:
			bool: True if a marker with the same position exists, False otherwise.
		"""
		return element.position in (m.position for m in self.elements)

	def add_marker(self, marker: Marker) -> Marker:
		"""
		Add a new marker to the container.

		If a marker with the same position already exists, its name is updated instead.

		Args:
			marker (Marker): The marker to add.

		Returns:
			Marker: The added or updated marker.
		"""
		if marker in self:
			m = self.elements[self.elements.index(marker)]
			m.name = marker.name
			return m

		self.elements.append(marker)
		self.notify_marker_changed()

		return marker

	def remove_marker(self, marker: Marker) -> Marker:
		"""
		Remove a marker from the container.

		Args:
			marker (Marker): The marker to remove.

		Returns:
			Marker: The removed marker.
		"""
		self.elements.remove(marker)
		self.notify_marker_changed()
		return marker

	def remove_marker_by_idx(self, marker_idx: int) -> Marker:
		"""
		Remove a marker by its index in the list.

		Args:
			marker_idx (int): The index of the marker to remove.

		Returns:
			Marker: The removed marker.
		"""
		res = self.elements.pop(marker_idx)
		self.notify_marker_changed()
		return res

	def get_marker(self, marker_idx: int) -> Marker:
		"""
		Retrieve a marker by its index.

		Args:
			marker_idx (int): The index of the desired marker.

		Returns:
			Marker: The marker at the specified index.
		"""
		return self.elements[marker_idx]

	def get_marker_idx(self, marker: Marker) -> int:
		"""
		Get the index of a specific marker.

		Args:
			marker (Marker): The marker to locate.

		Returns:
			int: The index of the marker in the list.
		"""
		return self.elements.index(marker)

	def get_markers(self) -> list[Marker]:
		"""
		Return a shallow copy of all stored markers.

		Returns:
			list[Marker]: A list of Marker objects.
		"""
		return self.elements.copy()

	def notify_marker_changed(self):
		"""
		Sort the list of markers based on their natural order (e.g., position).

		Called after any change to the marker list to maintain consistency.
		"""
		self.elements.sort()


@dataclass
class IntervalMarker:
	start_time: Marker
	end_time: Marker

	@classmethod
	def new_interval(
		cls, start_time: float, end_time: float, interval_label: str = ""
	) -> "IntervalMarker":
		"""
		Create a new IntervalMarker instance using start and end times.

		Args:
			start_time (float): The start time of the interval.
			end_time (float): The end time of the interval.
			interval_label (str, optional): An optional label for the interval's start marker.

		Returns:
			IntervalMarker: A new instance initialized with the provided start and end markers.

		Note:
			The interval will be validated upon creation (see __post_init__).
		"""
		start = Marker(start_time, interval_label)
		end = Marker(end_time)

		return cls(start, end)

	def __post_init__(self):
		"""
		Validate the integrity of the interval after initialization.

		Raises:
			ValueError: If the start and end times are equal.
			ValueError: If the start time is after the end time.
		"""
		if self.start_time == self.end_time:
			msg = "The start and end time for the interval cannot be equal."
			raise ValueError(msg)

		if self.start_time > self.end_time:
			msg = "The start time for the interval cannot be after the end time."
			raise ValueError(msg)

	def __hash__(self) -> int:
		"""
		Compute a hash based on the interval's start and end times.

		Returns:
			int: A unique hash value representing the interval.
		"""
		return int(hash(self.start_time) + hash(self.end_time))

	def __repr__(self) -> str:
		"""
		Return a string representation of the interval.

		Returns:
			str: A string in the format "start_time end_time".
		"""
		return f"{self.start_time} {self.end_time}"

	def get_name(self) -> str:
		"""
		Gets the name associated with the start time.

		Returns:
			str: The current name of the start time.
		"""
		return self.start_time.name

	def set_name(self, new_name: str):
		"""
		Sets a new name for the start time.

		Args:
			new_name (str): The new name to assign.
		"""
		self.start_time.name = new_name


class IntervalMarkerList(MarkerList):
	"""
	A specialized MarkerList managing intervals defined by pairs of markers.

	This class supports adding and removing markers with interval-aware logic,
	allowing intervals to be represented as pairs of consecutive markers.

	Key behaviors:
	- Removing a named marker appends its name to the previous marker if not the first.
	- Intervals can be added only if no other markers exist between the interval boundaries.
	- Provides methods to retrieve intervals as IntervalMarker objects.

	Inherits from:
	    MarkerList: Base container managing a list of Marker objects.
	"""

	@override
	def remove_marker(self, marker: Marker) -> Marker:
		"""
		Remove a marker from the container.

		Finds the index of the given marker and removes it using the index-based method.

		Args:
			marker (Marker): The marker to remove.

		Returns:
			Marker: The removed marker.
		"""
		marker_idx = self.elements.index(marker)
		return self.remove_marker_by_idx(marker_idx)

	@override
	def remove_marker_by_idx(self, marker_idx: int) -> Marker:
		"""
		Remove a marker by its index, with special handling for unnamed markers.

		If the removed marker has a name and is not the first in the list, its name is
		appended to the previous marker’s name.

		Args:
			marker_idx (int): The index of the marker to remove.

		Returns:
			Marker: The removed marker.
		"""
		marker_to_remove = super().remove_marker_by_idx(marker_idx)

		if marker_idx == 0:
			return marker_to_remove

		if not marker_to_remove.has_name():
			return marker_to_remove

		previous_marker_idx = marker_idx % len(self.elements)
		self.elements[previous_marker_idx].name += marker_to_remove.name
		return marker_to_remove

	def add_interval(self, interval: IntervalMarker):
		"""
		Adds an interval to the collection, ensuring no existing markers lie between its start and end.

		Args:
			interval (IntervalMarker): The interval to add.

		Raises:
			ValueError: If there are markers between the interval's start and end.
		"""
		has_marker_between = any(
			(interval.start_time < m < interval.end_time for m in self.elements)
		)

		if has_marker_between:
			raise ValueError("Impossible to add interval")

		interval.start_time = self.add_marker(interval.start_time)
		interval.end_time = self.add_marker(interval.end_time)

	def get_interval(self, interval_idx: int) -> IntervalMarker:
		"""
		Retrieves an interval by its index, wrapping around if the index exceeds the number of elements.

		Args:
			interval_idx (int): The index of the interval to retrieve.

		Returns:
			IntervalMarker: The interval at the specified index.
		"""
		index = interval_idx % len(self.elements)

		start_time = self.get_marker(index)
		end_time = self.get_marker(index + 1)

		return IntervalMarker(start_time, end_time)

	def get_intervals(self) -> list[IntervalMarker]:
		"""
		Returns a list of all intervals defined by consecutive markers.

		Returns:
			list[IntervalMarker]: List of intervals formed by pairs of consecutive markers.
		"""
		markers = self.get_markers()

		m1 = markers
		m2 = markers[1:]
		intervals = map(IntervalMarker, m1, m2)

		return list(intervals)
