//! Call-audio backend for the Librem 5 (i.MX8 + wm8962 codec + Broadmobi BM818
//! modem), driven entirely through PipeWire.
//!
//! On this board PipeWire owns both cards: the codec appears as the
//! `platform-sound` card (earpiece vs speaker are *card profiles*, not ports of
//! one sink) and the modem as `platform-sound-wwan` (its own sink+source). So
//! call audio is set up the PipeWire-native way, replicating what callaudiod +
//! wys do, but ourselves:
//!
//! - **Bridge** (wys' job): `pw-link` the modem capture → codec sink and the
//!   codec mic → modem playback. Linking resumes the otherwise-suspended nodes,
//!   so audio starts flowing.
//! - **Earpiece/Speaker** (callaudiod's job): `pactl set-card-profile` between
//!   `HiFi (Handset1, Mic2)` and `HiFi (Mic2, Speaker)`. The codec sink node
//!   changes with the profile, so we re-link after every switch.
//! - **Mute**: `pactl set-source-mute` on the handset mic.
//!
//! Everything is configurable via `EYDOS_AUDIO_*` env vars (see `Config`) so the
//! node/profile names can be tuned on a device without recompiling. The work
//! runs on a dedicated thread; the trait methods only enqueue commands, so the
//! UI never blocks on `pactl`/`pw-link`.

use std::path::Path;
use std::sync::mpsc;
use std::thread::{ self, JoinHandle };
use std::time::Duration;

use super::pw::{ self, env, env_bool };
use super::{ AudioError, CallAudio };
use crate::types::CallId;

#[ derive( Clone ) ]
struct Config
{
	codec_card:       String,
	speaker_profile:  String,
	earpiece_profile: String,
	speaker_sink:     String,
	earpiece_sink:    String,
	mic_source:       String,
	modem_source:     String,
	modem_sink:       String,
	/// Start a call on the loudspeaker. Default false (earpiece): the earpiece
	/// avoids the speakerphone acoustic echo (the far end hearing itself) and
	/// is the correct phone default. The UI speaker button flips this live via
	/// set_speaker. The self-healing bring-up makes the profile switch robust.
	start_on_speaker: bool,
}

impl Config
{
	fn from_env() -> Self
	{
		Self
		{
			codec_card:       env( "EYDOS_AUDIO_CODEC_CARD",       "alsa_card.platform-sound" ),
			speaker_profile:  env( "EYDOS_AUDIO_SPEAKER_PROFILE",  "HiFi (Mic2, Speaker)" ),
			earpiece_profile: env( "EYDOS_AUDIO_EARPIECE_PROFILE", "HiFi (Handset1, Mic2)" ),
			speaker_sink:     env( "EYDOS_AUDIO_SPEAKER_SINK",     "alsa_output.platform-sound.HiFi__Speaker__sink" ),
			earpiece_sink:    env( "EYDOS_AUDIO_EARPIECE_SINK",    "alsa_output.platform-sound.HiFi__Handset1__sink" ),
			mic_source:       env( "EYDOS_AUDIO_MIC_SOURCE",       "alsa_input.platform-sound.HiFi__Mic2__source" ),
			modem_source:     env( "EYDOS_AUDIO_MODEM_SOURCE",     "alsa_input.platform-sound-wwan.stereo-fallback" ),
			modem_sink:       env( "EYDOS_AUDIO_MODEM_SINK",       "alsa_output.platform-sound-wwan.stereo-fallback" ),
			start_on_speaker: env_bool( "EYDOS_AUDIO_START_SPEAKER", false ),
		}
	}
}

#[ derive( Debug ) ]
enum Cmd
{
	Begin,
	End,
	Speaker( bool ),
	Mute( bool ),
}

pub struct Librem5Audio
{
	tx:      mpsc::Sender<Cmd>,
	_worker: JoinHandle<()>,
}

impl Librem5Audio
{
	/// Build the backend and start its worker, regardless of hardware. Use via
	/// the `EYDOS_AUDIO_BACKEND=librem5` override; prefer [`Self::detect`].
	pub fn new() -> Self
	{
		let cfg = Config::from_env();
		let ( tx, rx ) = mpsc::channel();
		let worker = thread::Builder::new()
			.name( "eydos-audio".into() )
			.spawn( move || worker_main( cfg, rx ) )
			.expect( "failed to spawn audio worker thread" );
		Self { tx, _worker: worker }
	}

	/// `Some` only on a Librem 5: both the modem and codec ALSA cards must be
	/// present. Keeps the backend from loading on unrelated hardware.
	pub fn detect() -> Option<Self>
	{
		let present = Path::new( "/proc/asound/Modem" ).exists()
			&& Path::new( "/proc/asound/L5" ).exists();
		present.then( Self::new )
	}

	fn send( &self, cmd: Cmd ) -> Result<(), AudioError>
	{
		self.tx
			.send( cmd )
			.map_err( |e| AudioError::Backend( format!( "audio worker: {e}" ) ) )
	}
}

impl CallAudio for Librem5Audio
{
	fn begin_call( &mut self, _id: CallId ) -> Result<(), AudioError>
	{
		self.send( Cmd::Begin )
	}

	fn end_call( &mut self, _id: CallId ) -> Result<(), AudioError>
	{
		self.send( Cmd::End )
	}

	fn set_mute( &mut self, _id: CallId, muted: bool ) -> Result<(), AudioError>
	{
		self.send( Cmd::Mute( muted ) )
	}

	fn set_speaker( &mut self, _id: CallId, speaker: bool ) -> Result<(), AudioError>
	{
		self.send( Cmd::Speaker( speaker ) )
	}
}

// ── Worker thread ────────────────────────────────────────────────

fn worker_main( cfg: Config, rx: mpsc::Receiver<Cmd> )
{
	let mut active  = false;
	let mut speaker = cfg.start_on_speaker;

	while let Ok( cmd ) = rx.recv()
	{
		match cmd
		{
			Cmd::Begin =>
			{
				speaker = cfg.start_on_speaker;
				route( &cfg, speaker );
				set_mute( &cfg, false );
				active = true;
			}
			Cmd::End =>
			{
				unlink_all( &cfg );
				set_mute( &cfg, false );
				// Leave the codec on the speaker profile for media playback.
				set_profile( &cfg, true );
				active = false;
			}
			Cmd::Speaker( s ) =>
			{
				speaker = s;
				if active
				{
					route( &cfg, speaker );
				}
			}
			Cmd::Mute( m ) =>
			{
				if active
				{
					set_mute( &cfg, m );
				}
			}
		}
	}
}

/// Apply the earpiece/speaker profile and (re)create the modem↔codec links.
/// Switching the profile recreates the codec nodes, so we always tear down and
/// rebuild both legs.
fn route( cfg: &Config, speaker: bool )
{
	set_profile( cfg, speaker );
	let sink = if speaker { &cfg.speaker_sink } else { &cfg.earpiece_sink };
	unlink_all( cfg );

	// The BM818's I2S is a single full-duplex SAI: bringing up its second
	// direction can drop the first, so linking both back-to-back left calls
	// one-way (which leg survived alternated). Bring both up self-healingly —
	// (re)create whichever leg isn't streaming until both wwan streams RUNNING.
	for _ in 0..15
	{
		let rx_up = pw::running( &cfg.modem_source );  // network → us
		let tx_up = pw::running( &cfg.modem_sink );    // us → network
		if rx_up && tx_up
		{
			return;
		}
		if !rx_up
		{
			pw::link_once( &cfg.modem_source, sink );
		}
		if !tx_up
		{
			pw::link_once( &cfg.mic_source, &cfg.modem_sink );
		}
		thread::sleep( Duration::from_millis( 250 ) );
	}
	eprintln!( "eydos-calls: call audio did not reach full-duplex (a direction may be silent)" );
}

fn set_profile( cfg: &Config, speaker: bool )
{
	let profile = if speaker { &cfg.speaker_profile } else { &cfg.earpiece_profile };
	pw::set_card_profile( &cfg.codec_card, profile );
}

fn set_mute( cfg: &Config, muted: bool )
{
	pw::set_source_mute( &cfg.mic_source, muted );
}

fn unlink_all( cfg: &Config )
{
	pw::unlink( &cfg.modem_source, &cfg.speaker_sink );
	pw::unlink( &cfg.modem_source, &cfg.earpiece_sink );
	pw::unlink( &cfg.mic_source, &cfg.modem_sink );
}
