//! Incoming-call ringtone.
//!
//! Loops a sound to the default sink while a call is ringing. This is a UI
//! notification, separate from the modem voice path (`audio::CallAudio`): it
//! plays an ordinary OGG to whatever PipeWire's default output is (the
//! loudspeaker), so it stops before `begin_call` wires the earpiece.
//!
//! Playback shells out to `paplay` (preferred — the session already drives
//! PipeWire/Pulse) and falls back to `pw-play`; both decode OGG/Vorbis. The
//! file is found at the install path, then the dev tree.

use std::path::PathBuf;
use std::process::{ Child, Command };
use std::sync::Arc;
use std::sync::atomic::{ AtomicBool, Ordering };
use std::thread::{ self, JoinHandle };
use std::time::Duration;

pub struct Ringtone
{
	playing: Option<( Arc<AtomicBool>, JoinHandle<()> )>,
}

impl Ringtone
{
	pub fn new() -> Self
	{
		Self { playing: None }
	}

	/// Start looping the ringtone. Idempotent: a no-op while already ringing.
	pub fn start( &mut self )
	{
		if self.playing.is_some()
		{
			return;
		}
		let Some( file ) = ring_file() else
		{
			eprintln!( "eydos-calls: ringtone file not found; ringing silently" );
			return;
		};

		let stop = Arc::new( AtomicBool::new( false ) );
		let stop_w = stop.clone();
		let worker = thread::Builder::new()
			.name( "eydos-calls-ringtone".into() )
			.spawn( move || play_loop( &file, &stop_w ) )
			.expect( "failed to spawn ringtone thread" );
		self.playing = Some( ( stop, worker ) );
	}

	/// Stop the ringtone. Idempotent.
	pub fn stop( &mut self )
	{
		if let Some( ( stop, worker ) ) = self.playing.take()
		{
			stop.store( true, Ordering::Relaxed );
			let _ = worker.join();
		}
	}
}

impl Default for Ringtone
{
	fn default() -> Self
	{
		Self::new()
	}
}

impl Drop for Ringtone
{
	fn drop( &mut self )
	{
		self.stop();
	}
}

fn play_loop( file: &str, stop: &AtomicBool )
{
	let player = if which( "paplay" ) { "paplay" } else { "pw-play" };
	while !stop.load( Ordering::Relaxed )
	{
		let mut child = match Command::new( player ).arg( file ).spawn()
		{
			Ok( c )  => c,
			Err( e ) =>
			{
				eprintln!( "eydos-calls: ringtone {player} failed: {e}" );
				return;
			}
		};
		wait_or_stop( &mut child, stop );
	}
}

/// Wait for one playback to finish, killing it promptly if `stop` is set.
/// Returns when the child has exited (either naturally or after a kill).
fn wait_or_stop( child: &mut Child, stop: &AtomicBool )
{
	loop
	{
		if stop.load( Ordering::Relaxed )
		{
			let _ = child.kill();
			let _ = child.wait();
			return;
		}
		match child.try_wait()
		{
			Ok( Some( _ ) ) => return,
			Ok( None )      => thread::sleep( Duration::from_millis( 100 ) ),
			Err( _ )        => { let _ = child.kill(); return; }
		}
	}
}

fn ring_file() -> Option<String>
{
	let mut candidates: Vec<PathBuf> = vec!
	[
		PathBuf::from( "/usr/share/eydos-calls/sounds/ring.ogg" ),
	];
	if let Ok( exe ) = std::env::current_exe()
	{
		// Relocatable install: <prefix>/bin/eydos-calls → <prefix>/share/...
		if let Some( prefix ) = exe.parent().and_then( |p| p.parent() )
		{
			candidates.push( prefix.join( "share/eydos-calls/sounds/ring.ogg" ) );
		}
	}
	// Dev tree (cargo run / running from the repo).
	candidates.push( PathBuf::from( "res/sounds/ring.ogg" ) );
	candidates.push( PathBuf::from( concat!( env!( "CARGO_MANIFEST_DIR" ), "/res/sounds/ring.ogg" ) ) );

	candidates.into_iter()
		.find( |p| p.exists() )
		.map( |p| p.to_string_lossy().into_owned() )
}

fn which( bin: &str ) -> bool
{
	std::env::var_os( "PATH" )
		.map( |path| std::env::split_paths( &path ).any( |dir| dir.join( bin ).is_file() ) )
		.unwrap_or( false )
}
