# encoding=utf-8

__author__ = "Roland Trouville"
__copyright__ = "Copyright 2015+, Consortium MonPaGe"
__license__ = "Creative Commons 4.0 By-Nc-Sa"
__maintainer__ = "Roland Trouville"
__email__ = "contact.monpage@gmail.com"
__status__ = "Production"

import math
import parselmouth  # type: ignore
from cotation.acoustic.struct.pm.parselmouth_intensity import ParselmouthIntensity
from cotation.acoustic.struct.pm.parselmouth_pitch import ParselmouthPitch
from cotation.acoustic.struct.pm.parselmouth_voice_report import ParselmouthVoiceReport
import datetime
import os
from abc import ABC, abstractmethod
from typing import Callable

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

import pyqtgraph
from cotation.acoustic.display.io_widget.info import Info
from cotation.acoustic.struct.praat.markers import IntervalMarker
from typing import Optional, Union

PRAAT_FULL_FILE: float = 0.0
PITCH_UNIT: str = "Hertz"
MINIMUM_INTERPOLATION_TYPE: str = "Parabolic"

VOICE_REPORT_DEFAULT = (1.3, 1.6, 0.03, 0.45)

# @property transparantly have getter and setter.
# class.x instead of class.(get,set)_x()

DEFAULT_SOUND_FILE_PATTERN = "{}_{}_Module{}.wav"

# TODO find a better name


class CotationAcousticAnalysis(ABC):
	speaker_code: str
	module_code: str
	session_date: str
	data_dir: str
	sound_file_pattern: str
	file_full_dir_path: str

	_interval: Optional[IntervalMarker]

	# Add by RT on 16/6/25
	is_signal_selection_movable: bool = True
	is_signal_selection_manually_resizable: bool = True
	signal_selection_max_length: float = 4
	signal_selection_min_length: float = 1
	signal_selection_default_duration: float = 2

	entry_date: Optional[datetime.datetime] = None
	on_interval_change: list[Callable[[], None]]

	pitch: Optional[ParselmouthPitch]

	intensity: Optional[ParselmouthIntensity]
	voice_report: Optional[ParselmouthVoiceReport]

	duration: float

	min_max_pitch_range: Optional[tuple[float, float]]

	def __init__(
		self,
		speaker_code: str,
		module_code: str,
		session_date: str,
		data_dir: str = "data/participant",
		sound_file_pattern: str = DEFAULT_SOUND_FILE_PATTERN,
	):
		self.speaker_code = speaker_code
		self.module_code = module_code
		self.session_date = session_date
		self.data_dir = data_dir
		self.sound_file_pattern = sound_file_pattern
		self.file_full_dir_path = os.path.join(
			self.data_dir,
			self.speaker_code,
			self.session_date,
			f"Module{self.module_code.split('_')[0]}",
		)

		self._interval = None
		self.on_interval_change = []
		self.min_max_pitch_range = None

		# self.signal_selection_max_length=self.default_interval_duration
		# self.signal_selection_min_length=self.default_interval_duration

	def get_interval(self) -> IntervalMarker:
		"""
		Return the current interval marker.
		"""
		return self._interval

	def set_interval(self, inter: IntervalMarker):
		"""
		Set the current interval marker and notify all registered callbacks of the change.
		"""
		self._interval = inter

		for callback in self.on_interval_change:
			callback()

	def get_audio_file_name(self) -> str:
		"""
		Return the formatted audio file name using speaker code, session date, and module code.
		"""
		return self.sound_file_pattern.format(
			self.speaker_code, self.session_date, self.module_code
		)

	# We want interval_duration to be read only as it is calculated
	def get_interval_duration(self) -> float:
		"""
		Calculate and return the duration of the selected interval.
		Raises an error if no interval is selected.
		"""
		if self.get_interval() is None:
			raise ValueError("No interval selected.")
		return float(self.get_interval().end_time) - float(
			self.get_interval().start_time
		)

	def get_default_interval_duration(self) -> float:
		"""
		Return the default duration for signal selection.
		"""
		return self.signal_selection_default_duration

	@abstractmethod
	def get_results(self) -> dict[str, Union[float, str]]:
		"""
		Abstract method to return the results as a dictionary mapping string keys to float or string values.
		"""
		pass

	@abstractmethod
	def get_io(self) -> Info:
		"""
		Abstract method to return the input/output interface information.
		"""
		pass

	@abstractmethod
	def update_io(self):
		"""
		Abstract method to update the input/output interface.
		"""
		pass

	def get_required_plots(self) -> tuple[list[pyqtgraph.PlotDataItem], float, float]:
		"""
		Return a tuple containing a list of required plot items and their y-axis limits.
		Defaults to an empty list and no limits.
		"""
		return ([],)

	def reload_plots(self):
		"""
		Reload or refresh the plots. Default implementation does nothing.
		"""
		pass

	def load_data(self):
		"""
		Load acoustic analysis data including pitch, voice report, intensity, and pitch range,
		then reload the plots.
		"""
		(self.pitch, self.voice_report, self.intensity, self.min_max_pitch_range) = (
			CotationAcousticAnalysis.load(
				self.get_interval(),
				os.path.join(self.file_full_dir_path, self.get_audio_file_name()),
				self.min_max_pitch_range,
			)
		)
		self.reload_plots()

	@staticmethod
	def compute_range(sound: parselmouth.Sound, interval: IntervalMarker):
		"""
		Compute the pitch range and estimated F0 range from a given sound and interval.

		Returns:
		- Tuple of (minimum F0, maximum F0)
		- Tuple of (minimum F0 range, maximum F0 range) adjusted based on quantiles
		"""
		pitch = sound.to_pitch(time_step=0.01, pitch_floor=60, pitch_ceiling=600)
		ppitch = ParselmouthPitch(
			sound, pitch, float(interval.start_time), float(interval.end_time)
		)

		minimum_f0 = ppitch.get_minimum()
		maximum_f0 = ppitch.get_maximum()

		q15 = ppitch.get_quantile(0.15)
		q65 = ppitch.get_quantile(0.65)

		min_f0_range = 10.0 * math.floor((0.83 * q15) / 10)
		max_f0_range = 10.0 * math.ceil((1.92 * q65) / 10)

		return (minimum_f0, maximum_f0), (min_f0_range, max_f0_range)

	@staticmethod
	def load(
		interval: IntervalMarker,
		sound_file_path: str,
		min_max_f0_range: Optional[tuple[float, float]] = None,
	) -> tuple[ParselmouthPitch, ParselmouthVoiceReport, ParselmouthIntensity, tuple]:
		"""
		Load acoustic features from a sound file and interval with optional pitch range.

		Steps:
		1. Compute pitch ranges.
		2. Generate pitch, voice report, and intensity objects.
		3. Return the calculated pitch, voice report, intensity, and pitch range tuple.
		"""
		sound = parselmouth.Sound(sound_file_path)

		# Step 1
		# TODO
		(minimum_f0, maximum_f0), (min_f0_range, max_f0_range) = (
			CotationAcousticAnalysis.compute_range(sound, interval)
		)

		if min_max_f0_range is not None:
			(min_f0_range, max_f0_range) = min_max_f0_range

		# Step 2

		pitch = sound.to_pitch(0.01, min_f0_range, max_f0_range)

		calcpitch = ParselmouthPitch(
			sound, pitch, float(interval.start_time), float(interval.end_time)
		)

		calc_voicereport = ParselmouthVoiceReport(
			sound, pitch, min_f0_range, max_f0_range, interval
		)

		# On ne calcule pas l'intensity que sur ce qui est selectionné (source: script praat)
		# intensity = ParselmouthIntensity(
		# 	sound, float(interval.start_time), float(interval.end_time)
		# )

		intensity = ParselmouthIntensity(sound, 0)

		return calcpitch, calc_voicereport, intensity, (min_f0_range, max_f0_range)
