from PySide6.QtCore import Qt
from PySide6.QtGui import QKeySequence, QShortcut
from PySide6.QtWidgets import (
	QGridLayout,
	QHBoxLayout,
	QMessageBox,
	QPushButton,
	QWidget,
)
from typing import Any, Callable, Optional
from cotation.acoustic.display.qt_graph_display import QtGraphDisplay
from cotation.acoustic.struct.praat import tiers
from cotation.acoustic.struct.praat.markers import IntervalMarker
from cotation.acoustic.display.io_widget.info import Info
from cotation.acoustic.struct.pm.parselmouth_audio_file import ParselmouthAudioFile
from tools.audio_manager import AudioManager


class NavWidget(QWidget):
	previous: QPushButton
	validate: QPushButton
	next: QPushButton

	def __init__(self):
		super().__init__()

		self.previous = QPushButton("Précédent")

		self.validate = QPushButton("Valider")
		self.validate.setStyleSheet("background-color: lightgreen;")

		self.next = QPushButton("Suivant")
		self.next.setStyleSheet("background-color: lightblue;")

		layout = QHBoxLayout()

		layout.addWidget(self.previous)
		layout.addWidget(self.validate)
		layout.addWidget(self.next)

		self.setLayout(layout)


class OneFileTab(QWidget):
	audio_data: ParselmouthAudioFile
	graph_display: QtGraphDisplay

	information_pannel: Info
	is_playing_sound: bool

	# Callback functions
	_go_previous_callback: Optional[Callable[[], Any]]
	_go_next_callback: Optional[Callable[[], Any]]
	_validated_callback: Optional[Callable[[], Any]]

	def __init__(
		self,
		audio_data: ParselmouthAudioFile,
		information_panel: Info,
		graph_display: QtGraphDisplay,
	):
		super().__init__()

		self.audio_data = audio_data
		self.information_pannel = information_panel
		self.graph_display = graph_display

		self.is_playing_sound = False
		self._go_previous_callback = None
		self._go_next_callback = None
		self._validated_callback = None

		nav = NavWidget()
		nav.previous.clicked.connect(self._handle_go_previous)
		nav.next.clicked.connect(self._handle_go_next)
		nav.validate.clicked.connect(self.validation)

		self.information_pannel.set_resize_callback(
			self.change_fixed_selection_interval_size
		)

		# self.graph_display.interval_change_finished.connect(
		# 	self.interval_change_finished
		# )

		layout = QGridLayout()

		layout.addWidget(self.graph_display, 0, 0, 2, 1)
		layout.addWidget(self.information_pannel, 0, 1)
		layout.addWidget(nav, 1, 1, 1, 1)

		layout.setColumnStretch(0, 2)
		layout.setRowStretch(0, 1)

		self.setLayout(layout)

		self.play_shortcut = QShortcut(QKeySequence(Qt.Key_Tab), self)
		self.stop_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)

		self.play_shortcut.activated.connect(self.start_sound)
		self.stop_shortcut.activated.connect(self.stop_sound)

	def get_selected_interval(self) -> IntervalMarker:
		"""
		Retrieves the currently selected interval from the graph display.

		Returns:
			IntervalMarker: A new interval marker representing the selection bounds.
		"""
		return IntervalMarker.new_interval(*self.graph_display.get_selection_bounds())

	def set_selected_interval(self, interval: IntervalMarker):
		"""
		Sets the selected interval in the graph display.

		Parameters:
			interval (IntervalMarker): The interval to set as selected, with start and end times.

		Returns:
			None
		"""
		self.graph_display.set_selection_bounds(
			float(interval.start_time), float(interval.end_time)
		)

	def get_all_filled(self) -> bool:
		"""
		Checks if all required fields in the information panel are filled.

		Returns:
			bool: True if all fields are filled, False otherwise.
		"""
		return self.information_pannel.get_all_filled()

	def validation(self):
		"""
		Performs validation by checking if all fields are filled and if the graph display selection has moved.

		If validation fails, displays a warning message box to the user.
		If validation succeeds and a validated callback is set, invokes the callback.

		Returns:
			None
		"""
		is_valid = self.get_all_filled() and self.graph_display.was_moved

		if not is_valid:
			QMessageBox.warning(
				self, "Champs invalides.", "Tout les champs n'ont pas été remplis!"
			)
			return

		if self._validated_callback:
			self._validated_callback()

	def change_fixed_selection_interval_size(self, new_duration: int):
		"""
		Changes the fixed size of the selected interval in the graph display.

		Updates the display to use the new duration, then resets the currently selected interval
		to start at the same time but span the updated duration.

		Parameters:
			new_duration (int): The new duration (in the same time unit as the interval)
								for the fixed selection interval.

		Returns:
			None
		"""
		self.graph_display.set_selection_fixed_size(new_duration)
		start = float(self.get_selected_interval().start_time)
		self.set_selected_interval(
			tiers.IntervalMarker.new_interval(start, start + float(new_duration))
		)

	# Audio data takes a 2D array -> Parselmouth.Sound.values.T
	def start_sound(self):
		"""
		Starts audio playback for the currently selected interval in the graph display.

		If audio is already playing, it stops the playback first. Then it retrieves the sound data,
		verifies it's mono (single channel), extracts the selected range from the graph display,
		and plays the audio within that range using the `AudioManager`.

		Triggers the playback animation and registers callbacks for playback progress and completion.

		Returns:
			None
		"""
		if self.is_playing_sound:
			self.stop_sound()
			return

		self.is_playing_sound = True

		sound = self.audio_data.get_sound()
		sound_data = sound.amplitudes.T

		assert sound_data.shape[1] == 1

		lbound, ubound = self.graph_display.get_index_selection_range()

		AudioManager.play_wav_from_data(
			sound_data[lbound:ubound],
			sound.sample_rate,
			blocksize=int(sound.sample_rate / 5),
			callback=self.graph_display.play_fill.advance,
			end_callback=self.stop_sound,
		)

		self.graph_display.start_play_animation()

	def stop_sound(self):
		"""
		Stops audio playback if it is currently playing.

		Sets the playing state to False, stops the audio via `AudioManager`,
		and halts the playback animation on the graph display.

		Returns:
			None
		"""
		if not self.is_playing_sound:
			return

		self.is_playing_sound = False

		AudioManager.stop()
		self.graph_display.stop_play_animation()

	def set_go_previous_callback(self, callback: Callable[[], Any]) -> None:
		"""
		Sets the callback function to be called when the "go previous" action is triggered.

		Parameters:
			callback (Callable[[], Any]): A function with no arguments to handle the "go previous" event.

		Returns:
			None
		"""
		self._go_previous_callback = callback

	def set_go_next_callback(self, callback: Callable[[], Any]) -> None:
		"""
		Sets the callback function to be called when the "go next" action is triggered.

		Parameters:
			callback (Callable[[], Any]): A function with no arguments to handle the "go next" event.

		Returns:
			None
		"""
		self._go_next_callback = callback

	def set_validated_callback(self, callback: Callable[[], Any]) -> None:
		"""
		Sets the callback function to be called when a validation event occurs.

		Parameters:
			callback (Callable[[], Any]): A function with no arguments to handle the validation event.

		Returns:
			None
		"""
		self._validated_callback = callback

	def _handle_go_previous(self) -> None:
		"""
		Invokes the "go previous" callback if it has been set.

		Returns:
			None
		"""
		if self._go_previous_callback:
			self._go_previous_callback()

	def _handle_go_next(self) -> None:
		"""
		Invokes the "go next" callback if it has been set.

		Returns:
			None
		"""
		if self._go_next_callback:
			self._go_next_callback()
