//! Screen capture on `Forge`: window thumbnails for the switcher, full/region
//! screenshots to the clipboard or a portal file, and screencast recording,
//! each gated behind a consent dialog. Operates on `Forge`.

use super::*;

impl Forge {
    pub(super) fn clean_stale_thumbnails() {
        let uid = unsafe { libc::getuid() };
        let dir = format!("/run/user/{uid}");
        let Ok(entries) = std::fs::read_dir(&dir) else {
            return;
        };
        for entry in entries.flatten() {
            if let Some(name) = entry.file_name().to_str() {
                if name.starts_with("forge-thumb-") && name.contains(".rgba") {
                    let _ = std::fs::remove_file(entry.path());
                }
            }
        }
    }

    pub fn capture_window_thumbnail(
        &mut self,
        window_id: u64,
        width: u32,
        height: u32,
    ) -> Result<serde_json::Value, String> {
        use smithay::backend::allocator::Fourcc;
        use smithay::backend::renderer::gles::GlesRenderbuffer;
        use smithay::backend::renderer::Bind;
        use smithay::utils::{Physical, Size, Transform};
        use smithay::utils::Buffer as BufferSpace;

        let thumbnail = self.windows.thumbnail_for_id(window_id)
            .ok_or_else(|| format!("no thumbnail for window {window_id}"))?;

        let thumb_size = thumbnail.size();
        if thumb_size.w <= 0 || thumb_size.h <= 0 {
            return Err(format!("window {window_id} has zero-size thumbnail"));
        }

        let scale_x = width as f64 / thumb_size.w as f64;
        let scale_y = height as f64 / thumb_size.h as f64;
        let scale   = scale_x.min(scale_y);
        let out_w   = ((thumb_size.w as f64 * scale).round() as i32).max(1);
        let out_h   = ((thumb_size.h as f64 * scale).round() as i32).max(1);

        let renderer = self.backend.renderer()
            .ok_or("no renderer available")?;

        let mut elements: Vec<crate::drawing::ForgeElement> = Vec::new();
        let phys_bounds = smithay::utils::Rectangle::new(
            smithay::utils::Point::<i32, Physical>::from((0, 0)),
            Size::<i32, Physical>::from((out_w, out_h)),
        );
        crate::drawing::ForgeElement::add_element(
            &mut elements,
            thumbnail,
            smithay::utils::Point::<i32, Physical>::from((0, 0)),
            phys_bounds,
            None,
            scale,
        );

        let buffer_size = Size::<i32, BufferSpace>::from((out_w, out_h));
        let render_size = Size::<i32, Physical>::from((out_w, out_h));
        let (mut rb, _sync): (GlesRenderbuffer, _) = crate::drawing::render_elements_offscreen(
            renderer,
            Fourcc::Abgr8888,
            buffer_size,
            render_size,
            scale,
            Transform::Normal,
            [0.0, 0.0, 0.0, 0.0],
            &elements,
            &[phys_bounds],
        ).map_err(|e| format!("render offscreen: {e:?}"))?;

        (out_w as usize)
            .checked_mul(out_h as usize)
            .and_then(|n| n.checked_mul(4))
            .ok_or("thumbnail dimensions overflow")?;

        // The offscreen render comes out top-down: read as-is.
        let target = renderer.bind(&mut rb)
            .map_err(|e| format!("bind renderbuffer: {e:?}"))?;
        let pixels = crate::screenshot::read_target_rgba(
            renderer,
            &target,
            smithay::utils::Rectangle::<i32, BufferSpace>::from_size(Size::from((out_w, out_h))),
        )?;

        let uid = unsafe { libc::getuid() };
        let path = format!("/run/user/{uid}/forge-thumb-{window_id}.rgba");
        let tmp = format!("{path}.tmp");
        std::fs::write(&tmp, &pixels)
            .map_err(|e| format!("write {tmp}: {e}"))?;
        std::fs::rename(&tmp, &path)
            .map_err(|e| format!("rename {path}: {e}"))?;

        Ok(serde_json::json!({
            "path": path,
            "width": out_w,
            "height": out_h,
            "format": "rgba",
        }))
    }

    /// Take a screenshot using grim, save to ~/Pictures, and show a notification.
    /// Open the interactive drag-to-select region overlay. The capture runs
    /// when the user releases the drag (see `input.rs`), copying the result to
    /// the clipboard as PNG.
    pub fn take_screenshot(&mut self) {
        let default = self.windows.focused_window_capture_rect();
        self.shell.screenshot.begin(default);
        self.request_redraw();
    }

    fn full_output_region(&self) -> Rectangle<i32, Logical> {
        Rectangle::new(Point::from((0, 0)), self.windows.canvas().size())
    }

    /// Capture the whole output to the clipboard.
    pub fn screenshot_full_to_clipboard(&mut self) {
        let region = self.full_output_region();
        self.screenshot_region_to_clipboard(region);
    }

    /// Queue a region capture. The actual readback runs inside the next render
    /// pass (see `OutputDevice::render`); `create_frame` then places the PNG on
    /// the clipboard.
    pub fn screenshot_region_to_clipboard(&mut self, region: Rectangle<i32, Logical>) {
        self.windows.pending_screenshot = Some((region, crate::windows::ScreenshotSink::Clipboard));
        self.windows.set_dirty();
        self.request_redraw();
    }

    pub(super) fn set_clipboard_png(&mut self, bytes: Vec<u8>) {
        use smithay::wayland::selection::data_device::set_data_device_selection;
        let arc: Arc<[u8]> = bytes.into();
        self.render.clipboard_png = Some(arc);
        set_data_device_selection(&self.display_handle, &self.seat, vec!["image/png".into()], ());
        if let Some(xwm) = self.xwm.as_mut() {
            if let Err(err) = xwm.new_selection(SelectionTarget::Clipboard, Some(vec!["image/png".into()])) {
                warn!("failed to advertise screenshot to Xwayland: {err}");
            }
        }
    }

    /// Toggle screencast recording to the default `$HOME/Videos` location.
    pub fn toggle_screencast(&mut self) {
        if self.screencast.is_some() {
            self.stop_screencast();
        } else if let Some(path) = crate::screenshot::paths::default_screencast_path() {
            self.start_screencast(&path);
        }
        self.windows.set_dirty();
    }

    /// Start recording to `path` with wf-recorder, unless one is already running.
    /// This is the single control point; the portal routes here after consent.
    pub fn start_screencast(&mut self, path: &std::path::Path) {
        if self.screencast.is_some() {
            return;
        }
        let mut cmd = std::process::Command::new("wf-recorder");
        cmd.arg("-f").arg(path);
        match cmd.spawn() {
            Ok(child) => {
                info!("Screencast recording to {}", path.display());
                self.screencast = Some(child);
            }
            Err(e) => warn!("wf-recorder failed to start: {e}"),
        }
    }

    /// Stop the running screencast, if any, with SIGINT so wf-recorder
    /// finalises the file, then reap it.
    pub fn stop_screencast(&mut self) {
        if let Some(child) = self.screencast.take() {
            unsafe { libc::kill(child.id() as libc::pid_t, libc::SIGINT); }
            crate::spawn::reap(child);
            info!("Screencast recording stopped");
        }
    }

    /// Ask the user to approve a portal screenshot; resolved via `resolve_confirm`.
    pub fn request_screenshot_consent(
        &mut self,
        app_id: String,
        reply: async_channel::Sender<crate::portal_dbus::PortalReply>,
    ) {
        let who = if app_id.is_empty() { "An application".into() } else { app_id };
        self.windows.confirm.push(crate::confirm::ConfirmRequest::portal(
            format!("{who} wants to take a screenshot"),
            Vec::new(),
            crate::confirm::ConfirmKind::Screenshot(reply),
        ));
        self.sync_modal_focus();
        self.windows.set_dirty();
        self.request_redraw();
    }

    /// Ask the user to approve a portal screencast; resolved via `resolve_confirm`.
    pub fn request_screencast_consent(
        &mut self,
        app_id: String,
        session: String,
        reply: async_channel::Sender<Option<u32>>,
    ) {
        let who = if app_id.is_empty() { "An application".into() } else { app_id };
        // Offer an output picker only when there is a choice to make.
        let mut options = Vec::new();
        if !self.windows.extra_outputs().is_empty() {
            options.push(self.windows.primary_output().smithay_output().name());
            options.extend(
                self.windows.extra_outputs().iter().map(|o| o.smithay_output().name()),
            );
        }
        self.windows.confirm.push(crate::confirm::ConfirmRequest::portal(
            format!("{who} wants to record the screen"),
            options,
            crate::confirm::ConfirmKind::Screencast { session, reply },
        ));
        self.sync_modal_focus();
        self.windows.set_dirty();
        self.request_redraw();
    }

    /// Ask the user to approve a portal color pick; resolved via `resolve_confirm`.
    pub fn request_pickcolor_consent(
        &mut self,
        app_id: String,
        reply: async_channel::Sender<crate::portal_dbus::PortalReply>,
    ) {
        let who = if app_id.is_empty() { "An application".into() } else { app_id };
        self.windows.confirm.push(crate::confirm::ConfirmRequest::portal(
            format!("{who} wants to pick a screen color"),
            Vec::new(),
            crate::confirm::ConfirmKind::PickColor(reply),
        ));
        self.sync_modal_focus();
        self.windows.set_dirty();
        self.request_redraw();
    }

    /// Resolve the frontmost modal dialog with the user's choice.
    pub fn resolve_confirm(&mut self, allow: bool) {
        let selection = self.windows.confirm.selected();
        let Some(req) = self.windows.confirm.take_front() else {
            return;
        };
        match req.kind {
            crate::confirm::ConfirmKind::Screenshot(reply) => {
                if allow {
                    let region = self.full_output_region();
                    self.windows.pending_screenshot =
                        Some((region, crate::windows::ScreenshotSink::PortalFile(reply)));
                } else {
                    let _ = reply.try_send(crate::portal_dbus::PortalReply::Denied);
                }
            }
            crate::confirm::ConfirmKind::Screencast { session, reply } => {
                // options[0] is the primary; a later index picks an extend
                // head by connector name, validated against the live set.
                let output = req
                    .options
                    .get(selection)
                    .filter(|_| selection > 0)
                    .filter(|name| {
                        self.windows
                            .extra_outputs()
                            .iter()
                            .any(|o| o.smithay_output().name() == **name)
                    })
                    .cloned();
                let size = match output.as_deref() {
                    Some(name) => self
                        .windows
                        .extra_outputs()
                        .iter()
                        .find(|o| o.smithay_output().name() == name)
                        .map(|o| o.canvas().physical_resolution())
                        .unwrap_or_else(|| self.windows.canvas().physical_resolution()),
                    None => self.windows.canvas().physical_resolution(),
                };
                let gbm = self.backend.gbm();
                match (allow, self.pipewire.as_mut()) {
                    (true, Some(pw)) => {
                        pw.start_cast(output, size.w as u32, size.h as u32, gbm, session, reply);
                    }
                    _ => {
                        let _ = reply.try_send(None);
                    }
                }
            }
            crate::confirm::ConfirmKind::Notice => (),
            crate::confirm::ConfirmKind::PowerOff => {
                if allow {
                    crate::logind_dbus::power_off();
                }
            }
            crate::confirm::ConfirmKind::BatteryCritical => {
                if allow {
                    crate::powermanager_dbus::postpone_suspend();
                } else {
                    crate::powermanager_dbus::request_suspend();
                }
            }
            crate::confirm::ConfirmKind::PickColor(reply) => {
                if allow {
                    // Sample the pixel under the pointer from the next composed
                    // frame (the dialog is gone by then). Known limit: with the
                    // cursor composed into the scene (multi-head), the sample
                    // can hit the cursor image itself.
                    let pos = self.input.touch_state.cursor_pos().to_i32_round();
                    let region = Rectangle::new(pos, (1, 1).into());
                    self.windows.pending_screenshot =
                        Some((region, crate::windows::ScreenshotSink::PickColor(reply)));
                } else {
                    let _ = reply.try_send(crate::portal_dbus::PortalReply::Denied);
                }
            }
        }
        self.sync_modal_focus();
        self.windows.set_dirty();
        self.request_redraw();
    }
}
