
/// Push H.264 or MJPEG frames into the HDMI display pipeline.
///
/// Inputs: the negotiated camera configuration.
/// Outputs: a live `HdmiSink` ready to display frames.
/// Why: HDMI output uses a different sink selection and conversion chain than
/// the USB gadget, so it warrants a dedicated implementation.
pub struct HdmiSink {
    appsrc: gst_app::AppSrc,
    pipe: gst::Pipeline,
    clock_aligned: AtomicBool,
    next_pts_us: AtomicU64,
    frame_step_us: u64,
    presentation_delay_us: u64,
}

#[cfg(any(not(coverage), test))]
fn hdmi_queue_buffers() -> u32 {
    crate::video_support::env_u32("LESAVKA_HDMI_QUEUE_BUFFERS", 1).max(1)
}

#[cfg(any(not(coverage), test))]
fn hdmi_presentation_delay_us() -> u64 {
    // The HDMI + capture-adapter path still lands video earlier than the UAC
    // path on Tethys-style consumers, even after the stale audio holdback was
    // removed. Start with a measured default that keeps A/V within one video
    // frame instead of requiring per-host hand tuning.
    crate::video_support::env_u32("LESAVKA_HDMI_PRESENTATION_DELAY_US", 180_000) as u64
}

impl HdmiSink {
    /// Build a new HDMI sink pipeline.
    ///
    /// Inputs: the selected camera configuration, including optional connector
    /// metadata for `kmssink`.
    /// Outputs: a sink ready to receive `VideoPacket`s.
    /// Why: display output must honor connector pinning and decoder selection
    /// while keeping the relay code agnostic of GStreamer details.
    #[cfg(coverage)]
    pub fn new(cfg: &CameraConfig) -> anyhow::Result<Self> {
        gst::init()?;

        let pipeline = gst::Pipeline::new();
        let src = gst::ElementFactory::make("appsrc")
            .build()?
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc");
        src.set_is_live(true);
        src.set_format(gst::Format::Time);
        src.set_property("do-timestamp", &false);

        let sink = build_hdmi_sink(cfg)?;
        crate::media_timing::prepare_pipeline_clock_sync(&pipeline);
        pipeline.add_many(&[src.upcast_ref(), &sink])?;
        gst::Element::link_many(&[src.upcast_ref(), &sink])?;
        pipeline.set_state(gst::State::Playing)?;

        let frame_step_us = (1_000_000u64 / u64::from(cfg.fps.max(1))).max(1);
        Ok(Self {
            appsrc: src,
            pipe: pipeline,
            clock_aligned: AtomicBool::new(false),
            next_pts_us: AtomicU64::new(0),
            frame_step_us,
            presentation_delay_us: 0,
        })
    }

    #[cfg(not(coverage))]
    pub fn new(cfg: &CameraConfig) -> anyhow::Result<Self> {
        gst::init()?;

        let pipeline = gst::Pipeline::new();
        let source_width = cfg.width as i32;
        let source_height = cfg.height as i32;
        let (display_width, display_height) = cfg.hdmi_display_size();
        let width = display_width as i32;
        let height = display_height as i32;
        let fps = cfg.fps.max(1) as i32;

        let src = gst::ElementFactory::make("appsrc")
            .build()?
            .downcast::<gst_app::AppSrc>()
            .expect("appsrc");
        src.set_is_live(true);
        src.set_format(gst::Format::Time);
        src.set_property("do-timestamp", false);

        let raw_caps = gst::Caps::builder("video/x-raw")
            .field("width", width)
            .field("height", height)
            .field("framerate", gst::Fraction::new(fps, 1))
            .build();
        let capsfilter = gst::ElementFactory::make("capsfilter")
            .property("caps", &raw_caps)
            .build()?;

        let queue_depth = hdmi_queue_buffers();
        let presentation_delay_us = hdmi_presentation_delay_us();
        let queue = gst::ElementFactory::make("queue")
            .property("max-size-buffers", queue_depth)
            .property("max-size-bytes", 0u32)
            .property("max-size-time", 0u64)
            .build()?;
        let convert = gst::ElementFactory::make("videoconvert").build()?;
        let rate = gst::ElementFactory::make("videorate").build()?;
        let scale = gst::ElementFactory::make("videoscale").build()?;
        let sink = build_hdmi_sink(cfg)?;
        crate::media_timing::prepare_pipeline_clock_sync(&pipeline);
        tracing::info!(
            target: "lesavka_server::video",
            queue_depth,
            presentation_delay_us,
            "📺 HDMI sink queue depth armed"
        );

        if (display_width, display_height) != (cfg.width, cfg.height) {
            tracing::info!(
                target: "lesavka_server::video",
                source_width = cfg.width,
                source_height = cfg.height,
                display_width,
                display_height,
                "📺 HDMI sink scaling camera uplink to adapter mode"
            );
        }

        match cfg.codec {
            CameraCodec::H264 => {
                let caps_h264 = gst::Caps::builder("video/x-h264")
                    .field("stream-format", "byte-stream")
                    .field("alignment", "au")
                    .build();
                src.set_caps(Some(&caps_h264));
                let h264parse = gst::ElementFactory::make("h264parse").build()?;
                let decoder_name = require_h264_decoder()?;
                let decoder = gst::ElementFactory::make(decoder_name)
                    .build()
                    .with_context(|| format!("building decoder element {decoder_name}"))?;

                pipeline.add_many([
                    src.upcast_ref(),
                    &queue,
                    &h264parse,
                    &decoder,
                    &rate,
                    &convert,
                    &scale,
                    &capsfilter,
                    &sink,
                ])?;
                gst::Element::link_many([
                    src.upcast_ref(),
                    &queue,
                    &h264parse,
                    &decoder,
                    &rate,
                    &convert,
                    &scale,
                    &capsfilter,
                    &sink,
                ])?;
            }
            CameraCodec::Hevc => {
                let caps_hevc = gst::Caps::builder("video/x-h265")
                    .field("stream-format", "byte-stream")
                    .field("alignment", "au")
                    .build();
                src.set_caps(Some(&caps_hevc));
                let h265parse = gst::ElementFactory::make("h265parse")
                    .property("disable-passthrough", true)
                    .property("config-interval", -1i32)
                    .build()?;
                let decoder_name = require_hevc_decoder()?;
                let decoder = gst::ElementFactory::make(decoder_name)
                    .build()
                    .with_context(|| format!("building HEVC decoder element {decoder_name}"))?;

                pipeline.add_many([
                    src.upcast_ref(),
                    &queue,
                    &h265parse,
                    &decoder,
                    &rate,
                    &convert,
                    &scale,
                    &capsfilter,
                    &sink,
                ])?;
                gst::Element::link_many([
                    src.upcast_ref(),
                    &queue,
                    &h265parse,
                    &decoder,
                    &rate,
                    &convert,
                    &scale,
                    &capsfilter,
                    &sink,
                ])?;
            }
            CameraCodec::Mjpeg => {
                let caps_mjpeg = gst::Caps::builder("image/jpeg")
                    .field("parsed", true)
                    .field("width", source_width)
                    .field("height", source_height)
                    .field("framerate", gst::Fraction::new(fps, 1))
                    .build();
                src.set_caps(Some(&caps_mjpeg));
                let jpegdec = gst::ElementFactory::make("jpegdec").build()?;

                pipeline.add_many([
                    src.upcast_ref(),
                    &queue,
                    &jpegdec,
                    &rate,
                    &convert,
                    &scale,
                    &capsfilter,
                    &sink,
                ])?;
                gst::Element::link_many([
                    src.upcast_ref(),
                    &queue,
                    &jpegdec,
                    &rate,
                    &convert,
                    &scale,
                    &capsfilter,
                    &sink,
                ])?;
            }
        }

        pipeline.set_state(gst::State::Playing)?;
        let frame_step_us = (1_000_000u64 / u64::from(cfg.fps.max(1))).max(1);
        Ok(Self {
            appsrc: src,
            pipe: pipeline,
            clock_aligned: AtomicBool::new(false),
            next_pts_us: AtomicU64::new(0),
            frame_step_us,
            presentation_delay_us,
        })
    }

    /// Push one client frame into the HDMI pipeline.
    ///
    /// Inputs: the next `VideoPacket` from the gRPC camera stream.
    /// Outputs: none; the frame is forwarded to the appsrc when possible.
    /// Why: display playback uses the same local monotonic PTS policy as UVC to
    /// avoid visible glitches when remote timestamps jitter.
    #[cfg(coverage)]
    pub fn push(&self, pkt: VideoPacket) {
        let buf = gst::Buffer::from_slice(pkt.data);
        let _ = self.appsrc.push_buffer(buf);
    }

    #[cfg(not(coverage))]
    pub fn push(&self, pkt: VideoPacket) {
        let mut buf = gst::Buffer::from_slice(pkt.data);
        if let Some(meta) = buf.get_mut() {
            let pts_us = crate::video_support::reserve_local_pts(
                &self.next_pts_us,
                pkt.pts.saturating_add(self.presentation_delay_us),
                self.frame_step_us,
            );
            if !self
                .clock_aligned
                .swap(true, std::sync::atomic::Ordering::SeqCst)
            {
                crate::media_timing::align_pipeline_to_session_clock(&self.pipe, pts_us);
            }
            let ts = gst::ClockTime::from_useconds(pts_us);
            meta.set_pts(Some(ts));
            meta.set_dts(Some(ts));
            meta.set_duration(Some(gst::ClockTime::from_useconds(self.frame_step_us)));
        }
        if let Err(err) = self.appsrc.push_buffer(buf) {
            tracing::warn!(target:"lesavka_server::video", %err, "📺⚠️  HDMI appsrc push failed");
        }
    }
}

impl Drop for HdmiSink {
    fn drop(&mut self) {
        let _ = self.pipe.set_state(gst::State::Null);
    }
}

#[cfg(coverage)]
fn build_hdmi_sink(_cfg: &CameraConfig) -> anyhow::Result<gst::Element> {
    if let Ok(name) = std::env::var("LESAVKA_HDMI_SINK") {
        let sink = gst::ElementFactory::make(&name)
            .build()
            .context("building HDMI sink")?;
        crate::media_timing::enable_sink_clock_sync(&sink);
        return Ok(sink);
    }

    let sink = gst::ElementFactory::make("fakesink")
        .build()
        .context("building fallback HDMI sink")?;
    crate::media_timing::enable_sink_clock_sync(&sink);
    Ok(sink)
}

#[cfg(not(coverage))]
fn build_hdmi_sink(cfg: &CameraConfig) -> anyhow::Result<gst::Element> {
    if let Ok(name) = std::env::var("LESAVKA_HDMI_SINK") {
        let normalized = name.trim().to_ascii_lowercase();
        if normalized == "fbdev" || normalized == "fbdevsink" {
            return build_fbdev_hdmi_sink();
        }

        let sink = gst::ElementFactory::make(&name)
            .build()
            .context("building HDMI sink")?;
        crate::media_timing::enable_sink_clock_sync(&sink);
        return Ok(sink);
    }

    if gst::ElementFactory::find("kmssink").is_some() {
        unblank_framebuffer(&hdmi_fbdev_device());
        let sink = gst::ElementFactory::make("kmssink").build()?;
        if sink.has_property("driver-name", None) {
            let driver = std::env::var("LESAVKA_HDMI_DRIVER").unwrap_or_else(|_| "vc4".to_string());
            sink.set_property("driver-name", &driver);
        }
        if let Some(connector) = cfg.hdmi.as_ref().and_then(|hdmi| hdmi.id) {
            if sink.has_property("connector-id", None) {
                sink.set_property("connector-id", connector as i32);
            } else {
                tracing::warn!(
                    target: "lesavka_server::video",
                    %connector,
                    "kmssink does not expose connector-id property; using default connector"
                );
            }
        }
        if sink.has_property("force-modesetting", None) {
            sink.set_property("force-modesetting", true);
        }
        if sink.has_property("restore-crtc", None) {
            let restore = read_bool_env("LESAVKA_HDMI_RESTORE_CRTC").unwrap_or(false);
            sink.set_property("restore-crtc", restore);
        }
        if sink.has_property("skip-vsync", None) {
            let skip = read_bool_env("LESAVKA_HDMI_SKIP_VSYNC").unwrap_or(false);
            sink.set_property("skip-vsync", skip);
        }
        crate::media_timing::enable_sink_clock_sync(&sink);
        return Ok(sink);
    }

    let sink = gst::ElementFactory::make("autovideosink")
        .build()
        .context("building HDMI sink")?;
    crate::media_timing::enable_sink_clock_sync(&sink);
    Ok(sink)
}

#[cfg(not(coverage))]
fn build_fbdev_hdmi_sink() -> anyhow::Result<gst::Element> {
    let device = hdmi_fbdev_device();
    unblank_framebuffer(&device);

    let sink = gst::ElementFactory::make("fbdevsink")
        .property("device", &device)
        .build()
        .context("building framebuffer HDMI sink")?;
    crate::media_timing::enable_sink_clock_sync(&sink);

    tracing::info!(
        target: "lesavka_server::video",
        %device,
        "📺 HDMI sink using framebuffer scanout"
    );

    Ok(sink)
}

#[cfg(not(coverage))]
fn hdmi_fbdev_device() -> String {
    std::env::var("LESAVKA_HDMI_FBDEV")
        .ok()
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| "/dev/fb0".to_string())
}

#[cfg(not(coverage))]
fn unblank_framebuffer(device: &str) {
    let Some(name) = Path::new(device)
        .file_name()
        .and_then(|value| value.to_str())
    else {
        return;
    };
    if !name.starts_with("fb") {
        return;
    }

    let blank_path = format!("/sys/class/graphics/{name}/blank");
    match fs::write(&blank_path, b"0\n") {
        Ok(()) => tracing::debug!(
            target: "lesavka_server::video",
            %device,
            %blank_path,
            "📺 HDMI framebuffer unblanked"
        ),
        Err(error) => tracing::debug!(
            target: "lesavka_server::video",
            %device,
            %blank_path,
            %error,
            "📺 HDMI framebuffer unblank skipped"
        ),
    }
}

fn read_bool_env(name: &str) -> Option<bool> {
    let value = std::env::var(name).ok()?;
    match value.trim().to_ascii_lowercase().as_str() {
        "1" | "true" | "yes" | "on" => Some(true),
        "0" | "false" | "no" | "off" => Some(false),
        _ => None,
    }
}

#[cfg(test)]
mod hdmi_queue_tests {
    use super::{hdmi_presentation_delay_us, hdmi_queue_buffers};

    #[test]
    fn hdmi_queue_depth_defaults_to_one_frame() {
        temp_env::with_var_unset("LESAVKA_HDMI_QUEUE_BUFFERS", || {
            assert_eq!(hdmi_queue_buffers(), 1);
        });
    }

    #[test]
    fn hdmi_queue_depth_accepts_positive_env_override_only() {
        temp_env::with_var("LESAVKA_HDMI_QUEUE_BUFFERS", Some("3"), || {
            assert_eq!(hdmi_queue_buffers(), 3);
        });
        temp_env::with_var("LESAVKA_HDMI_QUEUE_BUFFERS", Some("0"), || {
            assert_eq!(hdmi_queue_buffers(), 1);
        });
        temp_env::with_var("LESAVKA_HDMI_QUEUE_BUFFERS", Some("nope"), || {
            assert_eq!(hdmi_queue_buffers(), 1);
        });
    }

    #[test]
    fn hdmi_presentation_delay_defaults_to_measured_holdback_and_accepts_override() {
        temp_env::with_var_unset("LESAVKA_HDMI_PRESENTATION_DELAY_US", || {
            assert_eq!(hdmi_presentation_delay_us(), 180_000);
        });
        temp_env::with_var("LESAVKA_HDMI_PRESENTATION_DELAY_US", Some("185000"), || {
            assert_eq!(hdmi_presentation_delay_us(), 185_000);
        });
        temp_env::with_var("LESAVKA_HDMI_PRESENTATION_DELAY_US", Some("nope"), || {
            assert_eq!(hdmi_presentation_delay_us(), 180_000);
        });
    }
}
