//! Frame composition and pacing on `Forge`: `create_frame` (the per-frame
//! render entry), the stall/redraw controls, and the `FramePacer` that predicts
//! render deadlines. Operates on `Forge`.

use super::*;

impl Forge {
    #[cfg_attr(feature = "profiling", profiling::function)]
    pub fn create_frame(&mut self) {
        crate::output::purge_retired_globals(&self.display_handle);
        if !self.session.display_on {
            self.render.stalled = true;
            return;
        }

        self.render.frame_pacer.begin(&self.event_loop);
        self.render.stalled = false;
        self.backend.cancel_scheduled_redraws();

        // The pointer is clamped to the logical area on motion, but the area
        // itself moves under it: raising the scale shrinks it (1920x1080 at
        // 3.0 leaves 640x360), and a position left outside is drawn
        // off-screen — the cursor vanishes until the mouse is nudged. Same
        // for a mode change or a hotplug, so this sits on the frame path
        // rather than on each of the causes.
        let rects: Vec<smithay::utils::Rectangle<f64, smithay::utils::Logical>> =
            self.windows.output_rects().iter().map(|r| r.to_f64()).collect();
        self.input.touch_state.clamp_to_rects(&rects);

        let transaction_deadline = self.windows.update_transaction();

        if let Some((surface, _)) = self.windows.focus() {
            if self.last_focus.as_ref() != Some(&surface) {
                info!("create_frame: focus changed Some->Some, calling set_focus");
                self.last_focus = Some(surface.clone());
                self.set_focus(Some(surface));
            }
        } else if self.last_focus.is_some() {
            info!("create_frame: focus dropped to None, calling set_focus(None)");
            self.last_focus = None;
            self.set_focus(None);
        }

        let inhibited = self
            .session
            .idle_inhibitors
            .iter()
            .any(|s| self.windows.surface_visible(s))
            || crate::screensaver_dbus::inhibited();
        self.protocol.idle_notifier_state.set_is_inhibited(inhibited);
        crate::powermanager_dbus::set_inhibited(inhibited, self.backend.has_external_display());
        self.report_session_state();

        // Auto-dismiss OSD overlay
        if self.shell.osd.maybe_expire() {
            self.windows.set_dirty();
        }

        let cursor_pos = if self.windows.canvas().is_mobile() {
            None
        } else if self.shell.screenshot.is_active() {
            Some(self.input.touch_state.cursor_pos())
        } else if matches!(self.input.cursor_status, CursorImageStatus::Hidden) {
            None
        } else if self.input.draw_cursor {
            Some(self.input.touch_state.cursor_pos())
        } else {
            self.input.touch_state.position()
        };
        let cursor_moved = mem::replace(&mut self.input.cursor_position, cursor_pos) != cursor_pos;

        let image = if self.shell.screenshot.is_active() {
            CursorImage::Named(self.shell.screenshot.cursor_at(self.input.touch_state.cursor_pos()))
        } else {
            match &self.input.cursor_status {
                CursorImageStatus::Named(icon) => CursorImage::Named(*icon),
                CursorImageStatus::Surface(surface) if surface.is_alive() => {
                    let hotspot = compositor::with_states(surface, |states| {
                        states
                            .data_map
                            .get::<CursorImageSurfaceData>()
                            .and_then(|attrs| attrs.lock().ok())
                            .map(|attrs| attrs.hotspot)
                            .unwrap_or_default()
                    });
                    CursorImage::Surface {
                        surface: surface.clone(),
                        hotspot,
                    }
                }
                _ => CursorImage::Named(cursor_icon::CursorIcon::Default),
            }
        };
        let cursor = cursor_pos.map(|position| crate::cursor::CursorRender { position, image });

        if self.windows.damaged() || cursor_moved || !self.windows.x11_windows.is_empty() {
            if let Some(renderer) = self.backend.renderer() {
                self.windows.import_buffers(renderer);
            }

            let mut ctx = crate::render_context::RenderContext {
                text: &mut self.render.text_renderer,
                mpris: &mut self.shell.mpris,
                polkit: &mut self.shell.polkit,
                osd: &mut self.shell.osd,
                topbar: &mut self.shell.topbar,
                sys: &self.shell.sys_state,
                switcher: &mut self.shell.app_switcher,
                screenshot: &mut self.shell.screenshot,
            };
            let frame =
                self.backend.render(&mut self.windows, cursor, &mut ctx, self.pipewire.as_mut());

            if let Some(png) = self.windows.screenshot_result.take() {
                self.set_clipboard_png(png);
                info!("Screenshot copied to clipboard ({} bytes)", self.render.clipboard_png.as_ref().map_or(0, |b| b.len()));
                crate::screenshot::play_shutter();
            }

            if let Some(fence) = frame.gpu_fence {
                self.render.frame_pacer.end(&self.event_loop, fence);
            }

            if !frame.rendered {
                let interval = self.windows.canvas().frame_interval();
                self.backend.schedule_redraw(interval);
            } else if let Some(locker) = self.session.locker.take() {
                locker.lock();
            }
        } else if let Some(deadline) = transaction_deadline {
            self.backend.schedule_redraw(deadline);
            self.render.stalled = true;
        } else {
            self.render.stalled = true;
        }

        self.windows.request_frames(self.shell.app_switcher.is_active() || self.shell.screenshot.is_active());
    }

    #[cfg(feature = "winit")]
    pub fn is_stalled(&self) -> bool {
        self.render.stalled
    }

    /// True when time-based side-effects still need to run on the 100 ms
    /// heartbeat tick even though nothing is visually dirty (e.g. an OSD
    /// overlay is on-screen and waiting to expire).
    #[cfg(feature = "winit")]
    pub fn needs_heartbeat(&self) -> bool {
        self.shell.osd.visible
    }

    pub fn unstall(&mut self) {
        if self.render.stalled {
            self.create_frame();
        }
    }

    pub fn request_redraw(&mut self) {
        self.windows.set_dirty();
        self.unstall();
    }

    pub fn force_redraw(&mut self, immediate: bool) {
        self.backend.reset_buffer_ages();
        self.windows.set_dirty();

        if immediate {
            self.create_frame();
        } else {
            self.unstall();
        }
    }

    /// Drop the chrome rasters whose cache key does not carry the text
    /// scale. The SSD and the app switcher rebuild themselves — their keys
    /// do carry it.
    pub fn invalidate_text_caches(&mut self) {
        self.shell.topbar.invalidate_text();
        self.shell.osd.invalidate();
        self.shell.mpris.invalidate();
        self.shell.polkit.invalidate();
    }

    pub fn invalidate_keyboard_focus(&mut self) {
        let has_focus = self.windows.focus().is_some();
        info!(
            "invalidate_keyboard_focus: windows.focus={has_focus}, kb={}",
            self.seat.get_keyboard().is_some()
        );
        if let Some(kb) = self.seat.get_keyboard() {
            kb.set_focus(self, None, SERIAL_COUNTER.next_serial());
        }
        self.last_focus = None;
    }
}

pub struct FramePacer {
    history: [u64; FRAME_HISTORY_SIZE],
    frame_count: u64,
    cumulative_ns: u64,
    pending_token: Option<RegistrationToken>,
    frame_start: Instant,
}

impl Default for FramePacer {
    fn default() -> Self {
        Self {
            history: [0; FRAME_HISTORY_SIZE],
            frame_count: 0,
            cumulative_ns: 0,
            pending_token: None,
            frame_start: Instant::now(),
        }
    }
}

impl FramePacer {
    fn begin(&mut self, event_loop: &LoopHandle<'static, Forge>) {
        if let Some(token) = self.pending_token.take() {
            event_loop.remove(token);
        }
        self.frame_start = Instant::now();
    }

    fn end(&mut self, event_loop: &LoopHandle<'static, Forge>, fence: OwnedFd) {
        let source = Generic::new(fence, Interest::READ, Mode::OneShot);
        match event_loop.insert_source(source, |_, _, forge| {
            let elapsed = forge.render.frame_pacer.frame_start.elapsed();
            let interval = forge.windows.canvas().frame_interval();
            forge.render.frame_pacer.record(elapsed, interval);
            Ok(PostAction::Remove)
        }) {
            Ok(token) => self.pending_token = Some(token),
            Err(e) => error!("Frame timer registration failed: {e}"),
        }
    }

    fn record(&mut self, render_time: Duration, frame_interval: Duration) {
        let ns = cmp::min(render_time, frame_interval).as_nanos() as u64;
        self.history.rotate_right(1);
        self.history[0] = ns;
        self.cumulative_ns += ns;
        self.frame_count += 1;
    }

    pub fn predict(&self) -> Option<Duration> {
        (self.frame_count > 0).then(|| {
            let avg_ns = self.cumulative_ns / self.frame_count;
            let recent_max = self
                .history
                .iter()
                .copied()
                .take(self.frame_count as usize)
                .max()
                .unwrap_or_default();
            let base_ns = recent_max.max(avg_ns);
            Duration::from_nanos(base_ns * PREDICTION_MARGIN_PCT / 100)
        })
    }
}
