//! MPRIS client for the QS media card.
//!
//! Connects to the freedesktop "Media Player Remote Interfacing
//! Specification" (`org.mpris.MediaPlayer2.*`) on the session DBus to
//! drive the now-playing card on the QS panel. Replaces the original
//! `playerctl`-subprocess-with-10-second-polling implementation —
//! that one only ever saw a single player and missed track changes by
//! up to ten seconds.
//!
//! Three threads:
//! * [`bus`] — owns a [`zbus::blocking::Connection`], discovers
//!   players via `ListNames()` / `NameOwnerChanged`, subscribes to
//!   each player's `PropertiesChanged` on
//!   `org.mpris.MediaPlayer2.Player`, applies an active-player
//!   policy (any `Playing` → any `Paused` → none, alphabetical by
//!   bus name within each tier), and pushes [`MediaState`] updates to
//!   the UI thread. Also serves outgoing transport commands
//!   ([`MprisCommand`]) by calling DBus methods on the chosen player.
//! * [`art`] — pulls `mpris:artUrl` content into RGBA byte buffers.
//!   `file://` URLs are read directly; HTTP(S) URLs are fetched via
//!   `ureq` with a per-URL on-disk cache under `$XDG_CACHE_HOME/
//!   crustace/mpris-art/`. Decoded covers are sent back to the bus
//!   thread, which folds them into the active state and re-emits.
//! * The UI thread itself reads completed states with
//!   [`MprisPoller::try_recv`] from `App::poll_external`, exactly as
//!   before — the integration point did not change.
//!
//! `MediaState::art_rgba` is an [`Arc<Vec<u8>>`] kept stable across
//! frames, which lets ltk's GLES `image_cache` (keyed on the input
//! pointer — see `ltk/src/gles_render/image.rs`) reuse the GPU
//! texture without reuploading on every render. Same trick the QS
//! tint cache uses for icons.

use std::sync::Arc;
use std::sync::mpsc::{ self, Sender };

mod art;
mod bus;

/// Snapshot of the currently-active MPRIS player. Sent over the
/// channel returned by [`MprisPoller::try_recv`] every time the
/// player set, the active player's playback state, the current
/// track's metadata, or the cover art changes.
///
/// `Default` produces an empty / not-playing state — used by the
/// view code as the "no media" placeholder.
#[ derive( Clone, Default ) ]
pub struct MediaState
{
	/// `true` when the active player's `PlaybackStatus == "Playing"`.
	pub playing:    bool,
	/// `xesam:title` from the player's current metadata.
	pub title:      String,
	/// First entry of `xesam:artist` (an array). Empty string when
	/// the player exposes no artist for the current track (web radio,
	/// some podcast players, …).
	pub artist:     String,
	/// Full DBus bus name of the active player, e.g.
	/// `org.mpris.MediaPlayer2.firefox.instance_1234`. The transport
	/// methods on [`MprisPoller`] take this so the same shell-wide
	/// state can target any reproduceable player.
	pub player:     String,
	/// `mpris:artUrl` from the active track's metadata. Kept around
	/// so the view can tell when artwork is absent (no download
	/// pending → empty card) vs pending (URL set, `art_rgba` still
	/// `None` while the worker fetches it).
	#[ allow( dead_code ) ]
	pub art_url:    Option<String>,
	/// Decoded cover RGBA8 buffer, ready to feed into
	/// `ltk::img_widget`. The `Arc` is stable across renders for the
	/// same `(player, art_url)` pair so the GLES texture cache hits.
	pub art_rgba:   Option<Arc<Vec<u8>>>,
	/// `(width, height)` of [`Self::art_rgba`] in pixels.
	pub art_dims:   Option<( u32, u32 )>,
	/// Whether the active player will do anything if `Next()` is
	/// called — mirrored from `org.mpris.MediaPlayer2.Player.CanGoNext`.
	/// Drives the disabled-state styling of the transport buttons in
	/// the QS card. `false` for the "no active player" sentinel so
	/// both arrow buttons stay greyed.
	pub can_go_next:     bool,
	/// Whether the active player will do anything if `Previous()` is
	/// called — mirrored from
	/// `org.mpris.MediaPlayer2.Player.CanGoPrevious`.
	pub can_go_previous: bool,
}

/// Outgoing transport command. Sent from the UI thread to the bus
/// thread via [`MprisPoller::play_pause`] etc. The bus thread
/// translates each variant into the corresponding DBus method call
/// on the named player.
pub( crate ) enum MprisCommand
{
	/// `org.mpris.MediaPlayer2.Player.PlayPause` on the named player.
	PlayPause( String ),
	/// `org.mpris.MediaPlayer2.Player.Next`.
	Next( String ),
	/// `org.mpris.MediaPlayer2.Player.Previous`.
	Previous( String ),
}

/// Public handle to the MPRIS subsystem. One per shell — owned by
/// `Crustace::mpris_poller`. Spawning is non-blocking; the bus and
/// art threads run independently and stay alive for the process
/// lifetime.
pub struct MprisPoller
{
	state_rx: mpsc::Receiver<MediaState>,
	cmd_tx:   Sender<MprisCommand>,
}

impl MprisPoller
{
	/// Bring up the bus + art threads. Returns immediately; the
	/// first [`MediaState`] is delivered once the bus thread has
	/// listed the active players and read their initial metadata.
	pub fn new() -> Self
	{
		let ( state_tx, state_rx ) = mpsc::channel::<MediaState>();
		let ( cmd_tx,   cmd_rx   ) = mpsc::channel::<MprisCommand>();
		let ( art_req_tx, art_req_rx )   = mpsc::channel::<art::ArtRequest>();
		let ( art_done_tx, art_done_rx ) = mpsc::channel::<art::ArtResult>();

		art::spawn_worker( art_req_rx, art_done_tx );
		bus::spawn_worker( state_tx, cmd_rx, art_req_tx, art_done_rx );

		Self { state_rx, cmd_tx }
	}

	/// Drain the channel and return the latest [`MediaState`], if
	/// any update arrived since the last call. Mirrors the original
	/// poller's signature so `App::poll_external` stays the same.
	/// Outer `Option`: `Some` if any update arrived in this batch.
	/// Inner `Option`: `Some(state)` for "active player exists" or
	/// `None` for "nothing playing on the bus right now"; preserved
	/// from the original API so the view code's `media_state.is_some()`
	/// check still distinguishes the two.
	pub fn try_recv( &self ) -> Option<Option<MediaState>>
	{
		let mut latest = None;
		while let Ok( state ) = self.state_rx.try_recv()
		{
			latest = Some( state );
		}
		latest.map( |s|
		{
			// Empty title + empty artist + no art = "no active
			// player" sentinel from the bus thread.
			if s.title.is_empty() && s.artist.is_empty() && s.player.is_empty()
			{
				None
			}
			else
			{
				Some( s )
			}
		} )
	}

	/// Toggle play/pause on the named player. Non-blocking — the
	/// command is queued on the bus thread, which makes the actual
	/// DBus call. Same semantics for [`Self::next_track`] and
	/// [`Self::previous_track`].
	pub fn play_pause( &self, player: &str )
	{
		let _ = self.cmd_tx.send( MprisCommand::PlayPause( player.to_string() ) );
	}

	pub fn next_track( &self, player: &str )
	{
		let _ = self.cmd_tx.send( MprisCommand::Next( player.to_string() ) );
	}

	pub fn previous_track( &self, player: &str )
	{
		let _ = self.cmd_tx.send( MprisCommand::Previous( player.to_string() ) );
	}
}
