/// The ONE player in this process, and the ONE pump that reads it.
///
/// Round fifty-one made this a singleton, and the reason is not tidiness:
/// `Player::spawn_with_device` opens its cpal output stream EAGERLY, at the
/// top of `audio_thread`, so two `Player`s is two live AAudio output
/// streams — on Android each asking for an 8192-frame buffer — and two
/// decoders feeding one queue. The symptom would be a doubled, phasing
/// track on Kyle's own phone with his own music, which is the worst thing
/// this whole Android Auto plan could produce.
///
/// Whoever gets here first builds it: `playback::init` on a normal launch,
/// or `headless::start` in a process Android Auto started with no Activity
/// in it at all. `Player` already derives `Clone` and is nothing but a
/// command-channel handle, so `PlayerState.player` is a clone of the same
/// audio thread and not one line of the twenty-odd `players.player.…` call
/// sites had to change.
///
/// The pump is part of the singleton for a duller reason: it owns the event
/// `Receiver`, and a `Receiver` cannot be cloned.
pub struct ProcessPlayer {
    pub player: Player,
    /// The transport mirror the pump writes and `player_state` reads. Shared
    /// rather than owned by `PlayerState`, because it exists before there is
    /// a `PlayerState` to own it.
    pub snapshot: Arc<Mutex<PlaybackSnapshot>>,
}

static PROCESS: OnceLock<ProcessPlayer> = OnceLock::new();
/// The app handle, once Tauri has run. `None` means a service-started
/// process with no Activity — no events to emit, no OS card to drive, and
/// the headless fallback is allowed to be the queue.
static APP: Mutex<Option<tauri::AppHandle>> = Mutex::new(None);

/// The process player, created on the first ask. `device` is honoured only
/// by that first ask; a later adopter re-applies its own preference.
pub fn process_player(device: Option<String>) -> &'static ProcessPlayer {
    let (process, _created) = process_player_created(device);
    process
}

fn process_player_created(device: Option<String>) -> (&'static ProcessPlayer, bool) {
    let mut created = false;
    let process = PROCESS.get_or_init(|| {
        created = true;
        let (player, events) = Player::spawn_with_device(device);
        let snapshot = Arc::new(Mutex::new(PlaybackSnapshot {
            status: "idle".into(),
            ..Default::default()
        }));
        spawn_pump(events, snapshot.clone());
        ProcessPlayer { player, snapshot }
    });
    (process, created)
}

/// Is there a webview in this process? True from `init` onward, for the life
/// of the process — a WebView destroyed from Recents is deliberately NOT
/// detected here, because no signal for it has ever been read back from this
/// phone and guessing would hand the queue to two drivers.
pub fn has_app() -> bool {
    APP.lock().map(|a| a.is_some()).unwrap_or(false)
}

fn app_handle() -> Option<tauri::AppHandle> {
    APP.lock().ok().and_then(|a| a.clone())
}

/// Emit to the webview if there is one. `false` means there was not.
pub fn emit_to_app<S: Serialize + Clone>(event: &str, payload: S) -> bool {
    match app_handle() {
        Some(handle) => handle.emit(event, payload).is_ok(),
        None => false,
    }
}

/// The transport as of the audio thread's last event, readable before there
/// is any `PlayerState` — which is what the headless path needs and what
/// `player_state` has always answered.
pub fn snapshot() -> PlaybackSnapshot {
    match PROCESS.get() {
        Some(process) => process
            .snapshot
            .lock()
            .map(|s| s.clone())
            .unwrap_or_default(),
        None => PlaybackSnapshot::default(),
    }
}

/// Create the audio thread and forward its events to the webview.
pub fn init(app: &tauri::AppHandle) {
    let output_device = crate::config::load(app).audio_output_device;
    let (process, created) = process_player_created(output_device.clone());
    if let Ok(mut slot) = APP.lock() {
        *slot = Some(app.clone());
    }
    if created {
        crate::headless::log("player created");
    } else {
        // ADOPTED a player the headless path built. It had no `AppHandle`
        // to read `audio_output_device` with, so the preference is applied
        // now. On Android that field is always None and this never runs;
        // on a desktop it is the difference between following the user's
        // routing and following the system default.
        if output_device.is_some() {
            if let Err(e) = process.player.set_output_device(output_device) {
                log::warn!("could not re-apply the configured output device: {e}");
            }
        }
        crate::headless::log("player adopted");
    }
    // BEFORE anything the webview does. The fallback queue and
    // `armPreload()` must never both be appending to one rodio sink, and
    // this is the earliest point in the Activity's life at which the
    // handover can be made. It stops the DRIVER, not the music: the rows
    // survive for `headless_state()` to hand over a second later.
    crate::headless::stand_down();

    app.manage(PlayerState {
        player: process.player.clone(),
        streams: Mutex::new(HashMap::new()),
        stream_caches: Mutex::new(HashMap::new()),
        waveforms: Mutex::new(WaveformLru::new(WAVEFORM_CACHE_MAX_ENTRIES)),
        progressive_waveforms: Mutex::new(WaveformLru::new(PROGRESSIVE_WAVEFORM_CACHE_MAX_ENTRIES)),
        waveform_jobs: Mutex::new(HashMap::new()),
        preloads: Mutex::new(HashMap::new()),
        deferred_saves: Mutex::new(HashMap::new()),
        last_transport: process.snapshot.clone(),
    });
}

/// The one event pump. Spawned by the singleton's initializer, so it exists
/// whether or not Tauri ever runs in this process; the Tauri half of its
/// body is skipped while there is no app handle.
fn spawn_pump(events: std::sync::mpsc::Receiver<PlayerEvent>, snapshot: Arc<Mutex<PlaybackSnapshot>>) {
    std::thread::Builder::new()
        .name("player-events".into())
        .spawn(move || {
            let mut last_position_emit = Instant::now() - UI_POSITION_INTERVAL;
            for ev in events {
                let ui = match ev {
                    PlayerEvent::Buffering => {
                        last_position_emit = Instant::now() - UI_POSITION_INTERVAL;
                        PlayerUiEvent::Buffering
                    }
                    PlayerEvent::Playing => {
                        last_position_emit = Instant::now() - UI_POSITION_INTERVAL;
                        PlayerUiEvent::Playing
                    }
                    PlayerEvent::Paused => PlayerUiEvent::Paused,
                    PlayerEvent::Stopped => PlayerUiEvent::Stopped,
                    PlayerEvent::TrackEnded { gapless } => {
                        last_position_emit = Instant::now() - UI_POSITION_INTERVAL;
                        PlayerUiEvent::TrackEnded { gapless }
                    }
                    PlayerEvent::Position { secs, duration } => {
                        let now = Instant::now();
                        if now.duration_since(last_position_emit) < UI_POSITION_INTERVAL {
                            continue;
                        }
                        last_position_emit = now;
                        PlayerUiEvent::Position { secs, duration }
                    }
                    PlayerEvent::Error(message) => PlayerUiEvent::Error { message },
                };
                // Mirror the transport before emitting, so a webview that
                // reloads mid-track can ask what it missed. TrackEnded is
                // deliberately not recorded: the frontend owns the queue and
                // decides what follows, and the next Buffering/Stopped says
                // what actually happened.
                // The OS media card rides this same mirror (see smtc.rs). The
                // snapshot IS the transport the UI is about to be told about,
                // so the card cannot drift from the window, and driving it
                // from here costs no IPC at all. Copied out rather than used
                // under the lock: the card's updates are cross-process calls
                // and have no business holding the transport mutex.
                let mut card = None;
                if let Ok(mut last) = snapshot.lock() {
                    match &ui {
                        PlayerUiEvent::Buffering => {
                            last.status = "buffering".into();
                            last.position = 0.0;
                        }
                        PlayerUiEvent::Playing => last.status = "playing".into(),
                        PlayerUiEvent::Paused => last.status = "paused".into(),
                        PlayerUiEvent::Stopped | PlayerUiEvent::Error { .. } => {
                            *last = PlaybackSnapshot {
                                status: "idle".into(),
                                ..Default::default()
                            };
                        }
                        PlayerUiEvent::Position { secs, duration } => {
                            last.position = *secs;
                            if duration.is_some() {
                                last.duration = *duration;
                            }
                        }
                        PlayerUiEvent::TrackEnded { .. } => {}
                    }
                    card = Some((last.status.clone(), last.position, last.duration));
                }
                // The headless fallback, which is a no-op unless IT is the
                // driver. A track that ends with nobody watching is the
                // whole claim of this round: the album keeps going.
                match &ui {
                    PlayerUiEvent::TrackEnded { .. } => crate::headless::on_track_ended(),
                    PlayerUiEvent::Buffering => crate::headless::on_transport("buffering"),
                    PlayerUiEvent::Playing => crate::headless::on_transport("playing"),
                    PlayerUiEvent::Paused => crate::headless::on_transport("paused"),
                    PlayerUiEvent::Stopped | PlayerUiEvent::Error { .. } => {
                        crate::headless::on_transport("idle")
                    }
                    PlayerUiEvent::Position { .. } => {}
                }
                let handle = app_handle();
                if let (Some(handle), Some((status, position, duration))) = (&handle, card) {
                    crate::smtc::set_status(handle, &status);
                    crate::smtc::set_timeline(handle, position, duration);
                    // The taskbar preview's middle button wears play or pause
                    // off the same truth.
                    crate::thumbbar::set_playing(handle, status == "playing");
                }
                if let Some(handle) = handle {
                    let _ = handle.emit("soulseek:player", ui);
                }
            }
        })
        .expect("spawn player event forwarder");
}
