//! Dedicated KMS-commit thread.
//!
//! Page-flips and vblank delivery live here, off the input/event loop, so that
//! N outputs with independent vblank phases never serialise against input or
//! each other. The renderer stays on the main thread (`GlesRenderer` is
//! `!Send`); only the `Committer` half of each output — all `Send` — crosses
//! over, plus the device's `DrmDeviceNotifier` (the vblank source, per-device).
//! See `/tmp/fase2-kms-design.md` for the full design.
//!
//! The thread runs its own calloop event loop with two sources: the command
//! channel from the main thread and the DRM vblank notifier. It owns one
//! `Committer` per CRTC. On each vblank it acknowledges the flip and reports
//! `Presented` back to the main thread, which does `mark_presented` and frame
//! pacing (both must stay on main: Wayland objects and the GLES fence live there).

use std::collections::HashMap;
use std::thread::{self, JoinHandle};

use calloop::channel::{self, Channel, Sender};
use calloop::{EventLoop, LoopHandle, RegistrationToken};
use smithay::backend::allocator::gbm::GbmBuffer;
use smithay::backend::allocator::Slot;
use smithay::backend::drm::{DrmDeviceNotifier, DrmEvent, DrmEventMetadata};
use smithay::reexports::drm::control::crtc;
use smithay::reexports::drm::control::framebuffer;
use tracing::{error, info};

use crate::present::Committer;

/// Commands sent from the main thread to the KMS thread.
pub enum KmsCmd {
    /// Hand the device's vblank notifier and the commit half of each of its
    /// outputs to the thread. Sent once per DRM device when it is added.
    SetDevice {
        notifier: DrmDeviceNotifier,
        outputs: Vec<(crtc::Handle, Committer)>,
    },
    /// Add one output's commit half (hotplug: a newly connected mirror).
    AddOutput { crtc: crtc::Handle, committer: Committer },
    /// Drop one output (hotplug disconnect).
    RemoveOutput { crtc: crtc::Handle },
    /// Page-flip a rendered buffer. The `slot` is held until the matching vblank.
    Flip {
        crtc: crtc::Handle,
        fb: framebuffer::Handle,
        slot: Slot<GbmBuffer>,
    },
    /// The session was paused (VT switch / suspend): stop flipping every output,
    /// since DRM master is gone on the main thread. Sent before `drm.pause()`.
    Pause,
    /// The session is active again: allow flips. Sent before the `ResetState`s.
    Resume,
    /// Re-sync a surface after a VT switch / session resume (forces a modeset).
    ResetState { crtc: crtc::Handle },
    /// Drop the device's notifier and all its outputs (device removed).
    ClearDevice,
    /// Stop the thread's event loop and return.
    Shutdown,
}

/// Events sent from the KMS thread back to the main thread.
pub enum KmsEvent {
    /// A queued flip's vblank arrived; carries the metadata `mark_presented`
    /// needs and signals the main thread to pace and schedule the next frame.
    Presented {
        crtc: crtc::Handle,
        metadata: Option<DrmEventMetadata>,
    },
    /// A flip failed; the main thread must resync (reset buffer ages) and
    /// reschedule a full repaint.
    FlipFailed { crtc: crtc::Handle },
}

/// Main-thread handle to the KMS thread. Dropping it stops the thread.
pub struct KmsHandle {
    cmd: Sender<KmsCmd>,
    join: Option<JoinHandle<()>>,
}

impl KmsHandle {
    pub fn send(&self, cmd: KmsCmd) {
        if self.cmd.send(cmd).is_err() {
            error!("KMS thread channel closed");
        }
    }

    /// A clonable command sender, for code paths that hold an output but not the
    /// handle (the per-device `OutputDevice` dispatching flips).
    pub fn sender(&self) -> Sender<KmsCmd> {
        self.cmd.clone()
    }
}

impl Drop for KmsHandle {
    fn drop(&mut self) {
        let _ = self.cmd.send(KmsCmd::Shutdown);
        if let Some(join) = self.join.take() {
            let _ = join.join();
        }
    }
}

/// Send a flip command on a raw sender, logging if the thread is gone.
pub fn send_on(sender: &Sender<KmsCmd>, cmd: KmsCmd) {
    if sender.send(cmd).is_err() {
        error!("KMS thread channel closed");
    }
}

/// Spawn the KMS thread. Returns the main-thread handle and the channel of
/// events the thread emits, which the caller registers in the main event loop.
pub fn spawn() -> (KmsHandle, Channel<KmsEvent>) {
    let (cmd_tx, cmd_rx) = channel::channel::<KmsCmd>();
    let (evt_tx, evt_rx) = channel::channel::<KmsEvent>();

    let join = thread::Builder::new()
        .name("forge-kms".into())
        .spawn(move || run(cmd_rx, evt_tx))
        .expect("spawn KMS thread");

    (KmsHandle { cmd: cmd_tx, join: Some(join) }, evt_rx)
}

/// Per-thread state: the commit half of every output, the device vblank source,
/// and the channel back to the main thread.
struct KmsThread {
    outputs: HashMap<crtc::Handle, Committer>,
    notifier_token: Option<RegistrationToken>,
    loop_handle: LoopHandle<'static, KmsThread>,
    evt: Sender<KmsEvent>,
}

impl KmsThread {
    /// Register the device vblank notifier on this thread's loop and adopt its
    /// outputs. A previously registered notifier is dropped first.
    fn set_device(
        &mut self,
        notifier: DrmDeviceNotifier,
        outputs: Vec<(crtc::Handle, Committer)>,
    ) {
        self.clear_device();
        for (crtc, committer) in outputs {
            self.outputs.insert(crtc, committer);
        }
        match self.loop_handle.insert_source(notifier, |event, metadata, state: &mut KmsThread| {
            if let DrmEvent::VBlank(crtc) = event {
                if let Some(committer) = state.outputs.get_mut(&crtc) {
                    committer.frame_submitted();
                }
                let _ = state.evt.send(KmsEvent::Presented { crtc, metadata: metadata.take() });
            }
        }) {
            Ok(token) => self.notifier_token = Some(token),
            Err(e) => error!("KMS thread notifier registration failed: {e}"),
        }
    }

    fn clear_device(&mut self) {
        if let Some(token) = self.notifier_token.take() {
            self.loop_handle.remove(token);
        }
        self.outputs.clear();
    }

    fn flip(&mut self, crtc: crtc::Handle, fb: framebuffer::Handle, slot: Slot<GbmBuffer>) {
        let Some(committer) = self.outputs.get_mut(&crtc) else { return };
        if let Err(e) = committer.flip(fb, slot) {
            // No vblank will come for a failed flip; tell main so it resyncs and
            // reschedules instead of waiting forever. The slot is dropped here on
            // error (freeing the buffer back to the swapchain).
            tracing::debug!("flip failed on {crtc:?}: {e}");
            let _ = self.evt.send(KmsEvent::FlipFailed { crtc });
        }
    }
}

fn run(cmd_rx: Channel<KmsCmd>, evt_tx: Sender<KmsEvent>) {
    let mut event_loop = match EventLoop::<KmsThread>::try_new() {
        Ok(el) => el,
        Err(e) => {
            error!("KMS thread event loop init failed: {e}");
            return;
        }
    };
    let signal = event_loop.get_signal();
    let handle = event_loop.handle();

    let registered = handle.insert_source(cmd_rx, move |event, _, state: &mut KmsThread| {
        if let channel::Event::Msg(cmd) = event {
            match cmd {
                KmsCmd::SetDevice { notifier, outputs } => state.set_device(notifier, outputs),
                KmsCmd::AddOutput { crtc, committer } => {
                    state.outputs.insert(crtc, committer);
                }
                KmsCmd::RemoveOutput { crtc } => {
                    state.outputs.remove(&crtc);
                }
                KmsCmd::Flip { crtc, fb, slot } => state.flip(crtc, fb, slot),
                KmsCmd::Pause => {
                    for committer in state.outputs.values_mut() {
                        committer.pause();
                    }
                }
                KmsCmd::Resume => {
                    for committer in state.outputs.values_mut() {
                        committer.resume();
                    }
                }
                KmsCmd::ResetState { crtc } => {
                    if let Some(committer) = state.outputs.get_mut(&crtc) {
                        if let Err(e) = committer.reset_state() {
                            error!("KMS reset_state failed on {crtc:?}: {e}");
                        }
                    }
                }
                KmsCmd::ClearDevice => state.clear_device(),
                KmsCmd::Shutdown => signal.stop(),
            }
        }
    });
    if let Err(e) = registered {
        error!("KMS thread command channel registration failed: {e}");
        return;
    }

    let mut state = KmsThread {
        outputs: HashMap::new(),
        notifier_token: None,
        loop_handle: handle,
        evt: evt_tx,
    };
    info!("KMS thread started");
    if let Err(e) = event_loop.run(None, &mut state, |_| {}) {
        error!("KMS thread event loop error: {e}");
    }
    info!("KMS thread stopped");
}
