//! PipeWire integration. The whole PipeWire object graph (`MainLoop`,
//! `Context`, `Core`, and the screencast streams) is `!Send` and lives on the
//! render thread, where the `GlesRenderer` and the composed `Dmabuf`s it feeds
//! from already are. calloop drives it by polling the loop's fd and
//! dispatching with a zero timeout, so nothing here ever blocks the frame
//! loop. `cast` owns the stream state machine, `buffer` the dmabuf slots and
//! the unsafe spa_data access, `format` the POD construction.

mod buffer;
mod cast;
mod format;

pub use cast::Cast;

use std::os::fd::{AsFd, BorrowedFd};
use std::time::Duration;

use calloop::generic::Generic;
use calloop::{Interest, LoopHandle, Mode, PostAction};
use pipewire::context::Context;
use pipewire::core::{Core, Listener};
use pipewire::main_loop::MainLoop;
use smithay::backend::allocator::gbm::GbmDevice;
use smithay::backend::drm::DrmDeviceFd;
use tracing::{info, warn};

use crate::forge::Forge;

/// `MainLoop` is a refcounted handle; this clone owned by the calloop source
/// exposes the underlying pollable fd.
struct LoopFd(MainLoop);

impl AsFd for LoopFd {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.0.loop_().fd()
    }
}

pub struct PipeWire {
    main_loop: MainLoop,
    _context: Context,
    pub core: Core,
    _listener: Listener,
    /// The single active screencast, if any (one stream at a time).
    pub cast: Option<Cast>,
}

impl PipeWire {
    pub fn new(event_loop: &LoopHandle<'static, Forge>) -> Result<Self, Box<dyn std::error::Error>> {
        pipewire::init();

        let main_loop = MainLoop::new(None)?;
        let context = Context::new(&main_loop)?;
        let core = context.connect(None)?;

        let listener = core
            .add_listener_local()
            .error(|id, seq, res, message| {
                warn!("pipewire error: id={id} seq={seq} res={res}: {message}");
            })
            .register();

        let source = Generic::new(LoopFd(main_loop.clone()), Interest::READ, Mode::Level);
        event_loop.insert_source(source, |_, fd, _forge| {
            while fd.0.loop_().iterate(Duration::ZERO) > 0 {}
            Ok(PostAction::Continue)
        })?;

        info!("pipewire connected");
        Ok(Self { main_loop, _context: context, core, _listener: listener, cast: None })
    }

    /// Create a screencast stream for `output` at `size`, answering `reply`
    /// with the node id once the daemon assigns it — the id is not known at
    /// connect time, only when the stream reaches `Paused`. Replaces any
    /// previous cast (single stream).
    pub fn start_cast(
        &mut self,
        output: Option<String>,
        width: u32,
        height: u32,
        gbm: Option<GbmDevice<DrmDeviceFd>>,
        session: String,
        reply: async_channel::Sender<Option<u32>>,
    ) {
        self.cast = None;
        match Cast::new(&self.core, output, width, height, gbm, session, reply) {
            Ok(cast) => {
                self.cast = Some(cast);
                info!("screencast stream created ({width}x{height})");
            }
            Err(e) => {
                warn!("screencast stream failed: {e}");
            }
        }
        self.flush();
    }

    pub fn stop_cast(&mut self) {
        if self.cast.take().is_some() {
            info!("screencast stream destroyed");
        }
        self.flush();
    }

    /// Stop the active cast only when `session` owns it: any portal session
    /// can be closed, but it must never tear down another session's stream.
    pub fn stop_cast_for(&mut self, session: &str) {
        if self.cast.as_ref().is_some_and(|c| c.session == session) {
            self.stop_cast();
        }
    }

    /// Drive the loop once so protocol messages queued outside the fd source
    /// (stream create/destroy from the main thread) reach the daemon even when
    /// no incoming traffic wakes the fd.
    fn flush(&self) {
        while self.main_loop.loop_().iterate(Duration::ZERO) > 0 {}
    }
}
