// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{path::Path, time::Duration, rc::Rc, cell::RefCell};

use kira::{
	AudioManager, AudioManagerSettings, DefaultBackend, Tween, StartTime, Easing,
	Decibels, listener::ListenerHandle, sound::{PlaybackState,
	static_sound::{StaticSoundData, StaticSoundHandle}},
	track::{SpatialTrackBuilder, SpatialTrackHandle, TrackBuilder, TrackHandle},
};
use glam::{Vec2, Vec3, Quat};
use strum_macros::{EnumCount as EnumCountMacro, EnumIter};
use strum::{EnumCount, IntoEnumIterator};

use crate::app::{config::Config, filesystem::{Filesystem, FsBase}};
use crate::world::{powerup::PowerupType, config::MAX_SIZE};
use crate::protocol::message::GameStateTransition;
use crate::utils::maths::{self, glam_fix::Fix};

#[repr(usize)]
#[derive(EnumCountMacro, EnumIter)]
enum SfxVariant {
	Shot,
	Hit,
	Kill,
	BlockBullet,
	AmmoCrate,
	Speed,
	Reload,
	Regeneration,
	Damage,
	Forcefield,
	Teleportation,
	Thrust,
	BlockPlayer,
	GameStart,
	GameEnd,
	GameWin,
	FlagCapture,
	FlagDrop,
	Checkpoint,
}

impl SfxVariant {
	fn name(self) -> &'static str {
		match self {
			SfxVariant::Shot => "shot",
			SfxVariant::Hit => "hit",
			SfxVariant::Kill => "kill",
			SfxVariant::BlockBullet => "block_bullet",
			SfxVariant::AmmoCrate => "ammo_crate",
			SfxVariant::Speed => "speed",
			SfxVariant::Reload => "reload",
			SfxVariant::Regeneration => "regen",
			SfxVariant::Damage => "damage",
			SfxVariant::Forcefield => "forcefield",
			SfxVariant::Teleportation => "teleportation",
			SfxVariant::Thrust => "thrust",
			SfxVariant::BlockPlayer => "block_player",
			SfxVariant::GameStart => "game_start",
			SfxVariant::GameEnd => "game_end",
			SfxVariant::GameWin => "game_win",
			SfxVariant::FlagCapture => "flag_capture",
			SfxVariant::FlagDrop => "flag_drop",
			SfxVariant::Checkpoint => "checkpoint",
		}
	}
}

#[derive(Debug)]
pub enum Sfx {
	Shot(f32),
	Hit,
	Kill,
	BlockBullet,
	AmmoCrate,
	Powerup(PowerupType), // For powerups and effect zones
	GameStateTransition(GameStateTransition),
	FlagCapture,
	FlagDrop,
	Checkpoint,
}

impl Sfx {
	fn get_speed(&self) -> f32 {
		if let Sfx::Shot(reload) = self && *reload > 1.0 {
			3.0 - 4.0 / (*reload + 1.0)
		} else {
			1.0
		}
	}

	fn index(&self) -> usize {
		(match self {
			Sfx::Shot(_) => SfxVariant::Shot,
			Sfx::Hit => SfxVariant::Hit,
			Sfx::Kill => SfxVariant::Kill,
			Sfx::BlockBullet => SfxVariant::BlockBullet,
			Sfx::AmmoCrate => SfxVariant::AmmoCrate,
			Sfx::Powerup(PowerupType::Speed) => SfxVariant::Speed,
			Sfx::Powerup(PowerupType::Reload) => SfxVariant::Reload,
			Sfx::Powerup(PowerupType::Health) => SfxVariant::Regeneration,
			Sfx::Powerup(PowerupType::Damage) => SfxVariant::Damage,
			Sfx::Powerup(PowerupType::Forcefield) => SfxVariant::Forcefield,
			Sfx::Powerup(PowerupType::Teleportation) => SfxVariant::Teleportation,
			Sfx::GameStateTransition(GameStateTransition::Start) => SfxVariant::GameStart,
			Sfx::GameStateTransition(GameStateTransition::End) => SfxVariant::GameEnd,
			Sfx::GameStateTransition(GameStateTransition::Win) => SfxVariant::GameWin,
			Sfx::FlagCapture => SfxVariant::FlagCapture,
			Sfx::FlagDrop => SfxVariant::FlagDrop,
			Sfx::Checkpoint => SfxVariant::Checkpoint,
		}) as usize
	}
}

#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, EnumCountMacro, EnumIter)]
pub enum LoopingSfx {
	Thrust,
	BlockPlayer,
}

impl LoopingSfx {
	fn index(self) -> usize {
		(match self {
			LoopingSfx::Thrust => SfxVariant::Thrust,
			LoopingSfx::BlockPlayer => SfxVariant::BlockPlayer,
		}) as usize
	}
}

/**
 * A wrapper of an inner struct that manages sounds, so that any failure to
 * initialise things (such as if a file isn't found or PulseAudio is disabled in
 * the Flatpak permissions) produces an object that doesn't do anything. This is
 * better than both lazily using unwrap and returning an Option<Sounds> which
 * requires `if let` for every call on the outside.
 */
pub struct Sounds(Option<SoundsInner>);

struct SoundsInner {
	pos: Vec2,
	vel: Vec2,

	config: Rc<RefCell<Config>>,
	manager: AudioManager<DefaultBackend>,
	listener: ListenerHandle,
	spatial_tracks: Vec<(SpatialTrackHandle, StaticSoundHandle)>, // Could call `persist_until_sounds_finish` but it doesn't seem to perform as well when there are many ongoing sound effects

	/*
	 * Spatial audio used when implementing non-spatial audio, but the track's
	 * position is at (0, 0, 0) and the listener's position is at (0, 0, z).
	 *
	 * This is to reproduce the exact amplitude if the sound was spatial but
	 * centred at the camera's x and y coordinates.
	 */
	non_spatial_listener: ListenerHandle,
	non_spatial_track: SpatialTrackHandle,

	looping_tracks: Box<[LoopingTrack]>, // Size is LoopingSfx::COUNT

	sound_effects: Box<[StaticSoundData]> // Size is SfxVariant::COUNT
}

pub struct LoopingSounds([Vec<LoopingSoundInfo>; LoopingSfx::COUNT]);

impl LoopingSounds {
	pub fn new() -> LoopingSounds {
		LoopingSounds(Default::default())
	}

	pub fn play(&mut self, sfx: LoopingSfx, pos: Vec2, vel: Vec2, amplitude: f32) {
		if amplitude > 0.0 {
			self.0[sfx as usize].push(LoopingSoundInfo { pos, vel, amplitude });
		}
	}
}

struct LoopingSoundInfo {
	pos: Vec2,
	vel: Vec2,
	amplitude: f32,
}

struct LoopingTrack {
	_track: TrackHandle,
	sound: StaticSoundHandle,
}

const SOUND_SPEED: f32 = 500.0;
const TOGGLE_MUTE_TIME: f32 = 0.25;
const MAX_PLAYBACK_SPEED: f32 = 32.0;

const MAX_SPATIAL_DIST: f32 = 500.0;
const SPATIALISATION_STRENGTH: f32 = 0.75;

impl Sounds {
	pub fn new(fs: &Filesystem, config: Rc<RefCell<Config>>) -> Sounds {
		Sounds(SoundsInner::try_new(fs, config).inspect_err(|err| log::warn!("failed initialising sound: {err}")).ok())
	}

	pub fn set_muted(&mut self, muted: bool) { self.with_inner(|sounds| sounds.set_muted(muted)); }
	pub fn play(&mut self, sfx: Sfx) { self.with_inner(|sounds| sounds.play(sfx)); }
	pub fn play_spatial(&mut self, sfx: Sfx, pos: Vec2, vel: Vec2) { self.with_inner(|sounds| sounds.play_spatial(sfx, pos, vel)); }
	pub fn update(&mut self, dt: f32, pos: Vec2, zoom: f32, jolted: bool, looping: LoopingSounds) { self.with_inner(|sounds| sounds.update(dt, pos, zoom, jolted, looping)); }

	fn with_inner(&mut self, f: impl FnOnce(&mut SoundsInner)) {
		if let Some(sounds) = &mut self.0 {
			f(sounds);
		}
	}
}

impl SoundsInner {
	fn try_new(fs: &Filesystem, config: Rc<RefCell<Config>>) -> Result<SoundsInner, String> {
		let mut sound_effects = Vec::with_capacity(SfxVariant::COUNT);
		for sfx in SfxVariant::iter() {
			let path = fs.get(FsBase::Static, Path::new(&format!("sounds/{}.wav", sfx.name())));
			sound_effects.push(StaticSoundData::from_file(&path).map_err(|err| format!("failed loading file \"{}\": {err}", path.display()))?);
		}

		let mut settings = AudioManagerSettings::default();
		settings.capacities.sub_track_capacity = 256;
		let mut manager = AudioManager::new(settings).map_err(|err| format!("failed creating audio manager: {err}"))?;
		manager.main_track().set_volume(config.borrow().sound.get_volume(), new_tween(0.0));
		let listener = manager.add_listener(Vec3::ZERO, Quat::IDENTITY).map_err(|err| format!("failed adding listener: {err}"))?;
		let non_spatial_listener = manager.add_listener(Vec3::ZERO, Quat::IDENTITY).map_err(|err| format!("failed adding non-spatial listener: {err}"))?;
		let non_spatial_track = manager.add_spatial_sub_track(&non_spatial_listener, Vec3::ZERO, spatial_track_builder()).map_err(|err| format!("failed adding non-spatial track: {err}"))?;

		let mut looping_tracks = Vec::with_capacity(LoopingSfx::COUNT);
		for sfx in LoopingSfx::iter() {
			let mut track = manager.add_sub_track(TrackBuilder::new()).map_err(|err| format!("failed creating looping sound track: {err}"))?;
			let static_sound = sound_effects[sfx.index()].loop_region(..).volume(Decibels::SILENCE);
			let sound = track.play(static_sound).map_err(|err| format!("failed playing looping sound: {err}"))?;
			looping_tracks.push(LoopingTrack { _track: track, sound });
		}

		Ok(SoundsInner {
			pos: Vec2::ZERO,
			vel: Vec2::ZERO,
			config,
			manager,
			listener,
			non_spatial_listener,
			non_spatial_track,
			spatial_tracks: Vec::new(),
			looping_tracks: looping_tracks.into_boxed_slice(),
			sound_effects: sound_effects.into_boxed_slice(),
		})
	}

	fn set_muted(&mut self, muted: bool) {
		self.manager.main_track().set_volume(if muted { Decibels::SILENCE } else { self.config.borrow().sound.get_volume() }, new_tween(TOGGLE_MUTE_TIME));
	}

	fn play(&mut self, sfx: Sfx) {
		let sound = self.get_static_sound(sfx, 1.0);
		let _ = self.non_spatial_track.play(sound);
	}

	fn play_spatial(&mut self, sfx: Sfx, pos: Vec2, vel: Vec2) {
		let doppler = SoundsInner::get_doppler(self.pos, self.vel, pos, vel);
		let sound = self.get_static_sound(sfx, doppler);

		let Ok(mut track) = self.manager.add_spatial_sub_track(&self.listener, clamp(into_3d(pos)), spatial_track_builder()) else { return; };
		let Ok(sound) = track.play(sound) else { return; };
		self.spatial_tracks.push((track, sound));
	}

	fn get_doppler(l_pos: Vec2, l_vel: Vec2, s_pos: Vec2, s_vel: Vec2) -> f32 {
		/*
		 * Don't want the Doppler effect to be 3D as it feels weird when the zoom
		 * changes.
		 */
		let dir = (l_pos - s_pos).normalise_or_zero();
		let doppler = (SOUND_SPEED - dir.dot(l_vel)) / (SOUND_SPEED - dir.dot(s_vel));

		if doppler < 0.0 || doppler.is_nan() { // Supersonic speeds
			f32::INFINITY
		} else {
			doppler
		}
	}

	fn get_static_sound(&self, sfx: Sfx, speed: f32) -> StaticSoundData {
		self.sound_effects[sfx.index()].playback_rate((speed * sfx.get_speed()).min(MAX_PLAYBACK_SPEED) as f64)
	}

	fn update(&mut self, dt: f32, pos: Vec2, zoom: f32, jolted: bool, looping: LoopingSounds) {
		let tween = new_tween(dt);
		self.vel = if jolted { Vec2::ZERO } else { (pos - self.pos) / maths::limit_dt(dt) };
		self.pos = pos;

		let z = zoom / 2.0;
		let listener_pos = clamp(pos.extend(z));

		for (i, LoopingTrack { _track, sound }) in self.looping_tracks.iter_mut().enumerate() {
			let mut amplitude = Vec2::ZERO;
			let mut speed = 0.0;
			let mut weight_sum = 0.0;
			for info in &looping.0[i] {
				let dpos = clamp(into_3d(info.pos)) - listener_pos;
				let a = SoundsInner::get_spatial_amplitude(dpos) * Decibels(Decibels::SILENCE.0 * (1.0 - info.amplitude)).as_amplitude();
				amplitude += a;

				let weight = a.x + a.y;
				speed += SoundsInner::get_doppler(self.pos, self.vel, info.pos, info.vel) * weight;
				weight_sum += weight;
			}

			// Converts from this amplitude on the two channels into a volume and panning that the sound handle accepts
			let mut panning = 1.0 - 2.0 / ((amplitude.y / amplitude.x).powi(2) + 1.0);
			if panning.is_nan() { panning = 0.0; }
			let volume = (20.0 * (amplitude.y / (panning + 1.0).sqrt()).log10()).max(Decibels::SILENCE.0);

			speed /= weight_sum;
			if speed.is_nan() {
				speed = 1.0;
			} else {
				speed = speed.clamp(0.0/* Shouldn't be possible to get less than this */, MAX_PLAYBACK_SPEED);
			}

			sound.set_volume(volume, tween);
			sound.set_panning(panning, tween);
			sound.set_playback_rate(speed as f64, tween);
		}

		self.listener.set_position(clamp(pos.extend(z)), tween);
		self.non_spatial_listener.set_position(clamp(Vec2::ZERO.extend(z)), tween);
		self.spatial_tracks.retain(|(_, sound)| sound.state() == PlaybackState::Playing);
	}

	/**
	 * Calculates the amplitude of a spatial sound being played given some input
	 * position (sound pos - listener pos). The returned value is a `Vec2` whose
	 * x and y components are the left and right amplitudes respectively.
	 *
	 * Using the algorithm from:
	 * https://docs.rs/kira/0.10.8/src/kira/track/sub.rs.html#296
	 */
	fn get_spatial_amplitude(dpos: Vec3) -> Vec2 {
		const EAR_DIST: f32 = 0.1;
		const COS_FRAC_PI_8: f32 = 0.9238795;
		const SIN_FRAC_PI_8: f32 = 0.38268343;
		const MIN_EAR_AMPLITUDE: f32 = 1.0 - SPATIALISATION_STRENGTH;

		// The default values `kira` uses with the default identity orientation
		const LEFT_EAR_DIR: Vec3 = Vec3::new(-COS_FRAC_PI_8, 0.0, -SIN_FRAC_PI_8);
		const RIGHT_EAR_DIR: Vec3 = Vec3::new(COS_FRAC_PI_8, 0.0, -SIN_FRAC_PI_8);

		let dist = dpos.length();
		let rel_dist = (dist / MAX_SPATIAL_DIST).min(1.0);

		let attenuation = (1.0 - rel_dist).powi(2); // Same as attenuation function
		let attenuation_amplitude = Decibels(Decibels::SILENCE.0 * (1.0 - attenuation)).as_amplitude();

		let (left_dir, right_dir) = ((dpos + Vec3::X * EAR_DIST).normalise_or_zero(), (dpos - Vec3::X * EAR_DIST).normalise_or_zero());
		let spatial_amplitude = MIN_EAR_AMPLITUDE + (1.0 - MIN_EAR_AMPLITUDE) * (Vec2::new(LEFT_EAR_DIR.dot(left_dir), RIGHT_EAR_DIR.dot(right_dir)) + 1.0) / 2.0;

		attenuation_amplitude * spatial_amplitude
	}
}

fn new_tween(time: f32) -> Tween {
	Tween { start_time: StartTime::Immediate, duration: Duration::from_secs_f32(time), easing: Easing::Linear }
}

fn spatial_track_builder() -> SpatialTrackBuilder {
	SpatialTrackBuilder::new()
		.distances((0.0, MAX_SPATIAL_DIST))
		.attenuation_function(Some(Easing::InPowi(2)))
		.spatialization_strength(SPATIALISATION_STRENGTH)
}

fn into_3d(pos: Vec2) -> Vec3 {
	pos.extend(0.0)
}

const MAX_POS: f32 = MAX_SIZE * 2.0;

/// Prevents loud audio "popping" whenever spatial positions input to `kira`
/// aren't finite.
fn clamp(mut pos: Vec3) -> Vec3 {
	pos = pos.clamp(Vec3::splat(-MAX_POS), Vec3::splat(MAX_POS));
	if pos.is_nan() {
		pos = Vec3::ZERO;
	}
	pos
}

#[cfg(test)]
#[test]
fn max_pos_is_finite() {
	assert!(MAX_POS.is_finite());
}
