use std::process::Command;

use super::cmd_stdout;

/// Volume (0.0–1.0) and mute state in a single `wpctl get-volume` call.
/// `wpctl` prints both on one line (e.g. `Volume: 0.45 [MUTED]`), so the
/// previous split into `volume()` + `muted()` spawned the identical
/// subprocess twice per refresh for data already in one output.
pub fn audio_state() -> ( f32, bool )
{
	parse_wpctl_volume( &cmd_stdout( "wpctl", &["get-volume", "@DEFAULT_AUDIO_SINK@"] ) )
}

/// Parse one line of `wpctl get-volume` output into `(volume, muted)`.
///
/// `wpctl` prints e.g. `Volume: 0.65` or `Volume: 0.00 [MUTED]`. The volume is
/// the second whitespace-separated token; a `[MUTED]` marker anywhere on the
/// line means the sink is muted. Falls back to `0.5` if the token is missing or
/// unparseable, matching the previous inline behaviour.
fn parse_wpctl_volume( s: &str ) -> ( f32, bool )
{
	let vol = s.split_whitespace().nth( 1 )
		.and_then( |v| v.parse::<f32>().ok() )
		.unwrap_or( 0.5 );
	( vol, s.contains( "[MUTED]" ) )
}

pub fn set_volume( val: f32 )
{
	let _ = Command::new( "wpctl" )
		.args( ["set-volume", "@DEFAULT_AUDIO_SINK@", &format!( "{:.2}", val )] ).output();
}

pub fn set_muted( m: bool )
{
	let val = if m { "1" } else { "0" };
	let _ = Command::new( "wpctl" ).args( ["set-mute", "@DEFAULT_AUDIO_SINK@", val] ).output();
}

#[ derive( Clone, PartialEq ) ]
pub struct AudioDevice
{
	pub id:      u32,
	pub name:    String,
	pub default: bool,
}

/// Playback sinks and capture sources with their PipeWire ids, from
/// `wpctl status`.
pub fn audio_devices() -> ( Vec<AudioDevice>, Vec<AudioDevice> )
{
	parse_wpctl_status( &cmd_stdout( "wpctl", &["status"] ) )
}

/// Extract the Audio-block sink and source entries from `wpctl status`.
///
/// The output is a tree drawn with box characters; `Sinks:`/`Sources:`
/// headers repeat under the top-level `Video` block, so entries only count
/// while inside `Audio`. An entry line is `<id>. <name> [vol: …]`, with a
/// `*` marker on the current default.
fn parse_wpctl_status( s: &str ) -> ( Vec<AudioDevice>, Vec<AudioDevice> )
{
	let mut sinks   = Vec::new();
	let mut sources = Vec::new();
	let mut in_audio = false;
	let mut section: Option<bool> = None;

	for raw in s.lines()
	{
		if !raw.starts_with( ' ' )
		{
			in_audio = raw.trim() == "Audio";
			section = None;
			continue;
		}

		let line = raw.trim_start_matches( [' ', '│', '├', '└', '─'] ).trim_end();
		if !in_audio { continue; }

		if line.ends_with( ':' )
		{
			section = match line
			{
				"Sinks:"   => Some( true ),
				"Sources:" => Some( false ),
				_          => None,
			};
			continue;
		}

		let Some( is_sink ) = section else { continue };
		let ( default, entry ) = match line.strip_prefix( '*' )
		{
			Some( rest ) => ( true, rest.trim_start() ),
			None         => ( false, line ),
		};
		let Some( ( id, rest ) ) = entry.split_once( ". " ) else { continue };
		let Ok( id ) = id.trim().parse::<u32>() else { continue };
		let name = rest.split( " [vol:" ).next().unwrap_or( rest ).trim().to_string();

		let dev = AudioDevice { id, name, default };
		if is_sink { sinks.push( dev ); } else { sources.push( dev ); }
	}
	( sinks, sources )
}

pub fn set_default_device( id: u32 )
{
	let _ = Command::new( "wpctl" ).args( ["set-default", &id.to_string()] ).output();
}

/// Volume (0.0–1.0) and mute state of the default capture source.
pub fn source_state() -> ( f32, bool )
{
	parse_wpctl_volume( &cmd_stdout( "wpctl", &["get-volume", "@DEFAULT_AUDIO_SOURCE@"] ) )
}

pub fn set_source_volume( val: f32 )
{
	let _ = Command::new( "wpctl" )
		.args( ["set-volume", "@DEFAULT_AUDIO_SOURCE@", &format!( "{:.2}", val )] ).output();
}

pub fn set_source_muted( m: bool )
{
	let val = if m { "1" } else { "0" };
	let _ = Command::new( "wpctl" ).args( ["set-mute", "@DEFAULT_AUDIO_SOURCE@", val] ).output();
}

#[ cfg( test ) ]
mod tests
{
	use super::parse_wpctl_volume;

	#[ test ]
	fn parses_unmuted_volume()
	{
		let ( vol, muted ) = parse_wpctl_volume( "Volume: 0.65" );
		assert!( ( vol - 0.65 ).abs() < 1e-6 );
		assert!( !muted );
	}

	#[ test ]
	fn parses_muted_volume()
	{
		let ( vol, muted ) = parse_wpctl_volume( "Volume: 0.00 [MUTED]" );
		assert!( ( vol - 0.0 ).abs() < 1e-6 );
		assert!( muted );
	}

	#[ test ]
	fn falls_back_when_token_missing()
	{
		// No numeric token after the label: fall back to 0.5, unmuted.
		let ( vol, muted ) = parse_wpctl_volume( "Volume:" );
		assert!( ( vol - 0.5 ).abs() < 1e-6 );
		assert!( !muted );
	}

	#[ test ]
	fn detects_mute_marker_regardless_of_position()
	{
		let ( _, muted ) = parse_wpctl_volume( "Volume: 0.42 [MUTED] (extra)" );
		assert!( muted );
	}

	const WPCTL_STATUS: &str = "\
PipeWire 'pipewire-0' [1.4.2, liberux@bach, cookie:2626005118]
 └─ Clients:
        33. pipewire                            [1.4.2, liberux@bach, pid:1142]
        34. WirePlumber                         [1.4.2, liberux@bach, pid:1141]

Audio
 ├─ Devices:
 │      48. Built-in Audio                      [alsa]
 │
 ├─ Sinks:
 │      54. Built-in Audio USB-C DisplayPort Output [vol: 0.40]
 │      55. Built-in Audio Headphones playback  [vol: 1.00]
 │  *   56. Built-in Audio HDMI Digital Stereo Output [vol: 1.00]
 │
 ├─ Sources:
 │      57. Built-in Audio headset Mic          [vol: 1.00]
 │
 ├─ Filters:
 │
 └─ Streams:

Video
 ├─ Devices:
 │      52. Iris Decoder                        [v4l2]
 │
 ├─ Sinks:
 │      99. Video Fake Sink                     [vol: 1.00]
 │
 └─ Streams:

Settings
 └─ Default Configured Devices:
";

	#[ test ]
	fn parses_audio_sinks_and_sources()
	{
		let ( sinks, sources ) = super::parse_wpctl_status( WPCTL_STATUS );
		assert_eq!( sinks.len(), 3 );
		assert_eq!( sinks[0].id, 54 );
		assert_eq!( sinks[0].name, "Built-in Audio USB-C DisplayPort Output" );
		assert!( !sinks[0].default );
		assert!( sinks[2].default );
		assert_eq!( sinks[2].name, "Built-in Audio HDMI Digital Stereo Output" );
		assert_eq!( sources.len(), 1 );
		assert_eq!( sources[0].id, 57 );
		assert_eq!( sources[0].name, "Built-in Audio headset Mic" );
	}

	#[ test ]
	fn ignores_video_block_entries()
	{
		let ( sinks, _ ) = super::parse_wpctl_status( WPCTL_STATUS );
		assert!( sinks.iter().all( |d| d.id != 99 ) );
	}

	#[ test ]
	fn survives_empty_output()
	{
		let ( sinks, sources ) = super::parse_wpctl_status( "" );
		assert!( sinks.is_empty() );
		assert!( sources.is_empty() );
	}
}
