// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Per-frame drawing pipeline.
//!
//! The run loop hands each configured surface to [`draw_frame`] once per
//! vblank; this module walks it through a decision tree:
//!
//! * **Skip** — no content changed and no interaction state moved, so the
//!   previously committed buffer is still correct.
//! * **Partial** — only focus / hover / pressed changed. Install a clip
//!   mask covering the paint rects of the affected widgets, repaint only
//!   under the clip, damage Wayland with exactly those rects.
//! * **Full** — something substantive changed (app message, animation
//!   tick, configure, text edit, scroll, slider drag). Clear + redraw
//!   the entire view, let damage tracking tighten the commit.
//!
//! Each of those paths has a software variant (CPU + SHM pool) and a
//! GLES variant (FBO + EGL swap). The four resulting functions live in
//! [`software`] and [`gles`]; routing and surface-level orchestration
//! live in [`crate::event_loop::frame`].
//!
//! # Submodule layout
//!
//! * [`software`] — `draw_surface_full` / `draw_surface_partial`
//! * [`gles`] — `draw_surface_full_gpu` / `draw_surface_partial_gpu`
//! * [`damage`] — `compute_interaction_dirty_rects`, `compute_damage`,
//!   `clamp_rect_to`
//! * [`chrome`] — `draw_titlebar`, `apply_input_region`
//! * [`layout`] — `layout_and_draw` (the recursive element walker)

use std::collections::HashMap;

use crate::render::Canvas;
use crate::types::Rect;
use crate::widget::LaidOutWidget;

pub( crate ) mod software;
pub( crate ) mod gles;
pub( crate ) mod damage;
pub( crate ) mod chrome;
pub( crate ) mod layout;

pub( crate ) use damage::{ compute_damage, compute_interaction_dirty_rects };
pub( crate ) use layout::layout_and_draw;

/// Per-frame draw state threaded through [`layout_and_draw`]. Captures
/// the interaction snapshot (focus / hover / pressed), scratch space
/// for the widget-rect list the frame will produce, and the scroll
/// offsets / sub-canvases carried across frames.
///
/// `scroll_canvases` arrives populated (the previous frame's
/// sub-canvases) so `layout_and_draw` can re-use them for Scroll
/// viewports whose size did not change. `scroll_rects` and
/// `widget_rects` start empty and get filled as the element tree is
/// walked.
pub( crate ) struct DrawCtx<Msg: Clone>
{
	pub focused_idx:   Option<usize>,
	pub hovered_idx:   Option<usize>,
	pub pressed_idx:   Option<usize>,
	pub cursor_state:  HashMap<usize, usize>,
	pub selection_anchor: HashMap<usize, usize>,
	pub widget_rects:  Vec<LaidOutWidget<Msg>>,
	pub debug_layout:  bool,
	pub scroll_offsets:  HashMap<usize, ( f32, f32 )>,
	pub scroll_rects:    Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
	pub scroll_canvases: HashMap<usize, Canvas>,
	/// Per-scroll navigation map: list of `(flat_idx, content_y, height)`
	/// for every interactive item the scroll's child laid out, in
	/// document order, **including items currently scrolled off-screen**.
	/// Keyboard arrow handlers read this to step the runtime's
	/// `hovered_idx` item-by-item without needing to know how the popup
	/// content was composed. The Y is in pre-translation, pre-offset
	/// coordinates (i.e. relative to the start of the scroll's child
	/// content) so the keyboard auto-scroll can compute the offset
	/// needed to bring an item into view without depending on the
	/// current scroll position.
	pub scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
	/// Snapshot of the previous frame's `widget_rects`. Read by
	/// [`crate::widget::anchored_overlay::AnchoredOverlay`] at draw time
	/// to look up the rect of an anchor widget by [`crate::WidgetId`] and
	/// re-position itself relative to that rect. Drivers populate this
	/// before invoking the recursive layout / draw walk.
	pub previous_widget_rects: Vec<LaidOutWidget<Msg>>,
	/// Non-interactive widgets exposed only to the accessibility
	/// tree (text labels, images, separators, progress bars).
	pub accessible_extras: Vec<crate::a11y::tree::AccessibleExtra>,
	/// Depth counter for containers marked `a11y_live`.
	pub live_depth: u32,
}

/// Build the per-frame [`DrawCtx`] from the surface's [`FrameState`], moving the
/// carried maps (cursor / selection / scroll) out of `frame` into the context.
/// The interaction indices are passed by value (they live on `SurfaceState`, not
/// `frame`). `debug_layout` is `false` on the partial paths. Shared by all four
/// frame paths; [`commit_draw_ctx`] writes the produced state back.
pub( crate ) fn build_draw_ctx<Msg: Clone>(
	frame:        &mut crate::event_loop::FrameState<Msg>,
	focused_idx:  Option<usize>,
	hovered_idx:  Option<usize>,
	pressed_idx:  Option<usize>,
	debug_layout: bool,
) -> DrawCtx<Msg>
{
	DrawCtx
	{
		focused_idx,
		hovered_idx,
		pressed_idx,
		cursor_state:    std::mem::take( &mut frame.cursor_state ),
		selection_anchor: std::mem::take( &mut frame.selection_anchor ),
		widget_rects:    Vec::new(),
		debug_layout,
		scroll_offsets:  std::mem::take( &mut frame.scroll_offsets ),
		scroll_rects:    Vec::new(),
		scroll_canvases: std::mem::take( &mut frame.scroll_canvases ),
		scroll_navigable_items: std::mem::take( &mut frame.scroll_navigable_items ),
		previous_widget_rects: frame.widget_rects.clone(),
		accessible_extras: Vec::new(),
		live_depth: 0,
	}
}

/// Write the finished frame's [`DrawCtx`] back into the [`FrameState`]: snapshot
/// the interaction indices into `prev_*` and move the produced rect lists and
/// carried maps back. Shared by all four frame paths; the caller must have
/// already consumed anything it needs from `ctx` (e.g. `compute_damage`) before
/// calling this, and resets `content_dirty` itself (it lives on `SurfaceState`).
pub( crate ) fn commit_draw_ctx<Msg: Clone>(
	frame:       &mut crate::event_loop::FrameState<Msg>,
	focused_idx: Option<usize>,
	hovered_idx: Option<usize>,
	pressed_idx: Option<usize>,
	mut ctx:     DrawCtx<Msg>,
)
{
	frame.prev_focused = focused_idx;
	frame.prev_hovered = hovered_idx;
	frame.prev_pressed = pressed_idx;

	frame.widget_rects    = ctx.widget_rects;
	frame.scroll_rects    = ctx.scroll_rects;
	frame.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases );
	frame.cursor_state    = ctx.cursor_state;
	frame.selection_anchor = ctx.selection_anchor;
	frame.scroll_offsets  = ctx.scroll_offsets;
	frame.accessible_extras = ctx.accessible_extras;
	frame.scroll_navigable_items = ctx.scroll_navigable_items;
}

/// Paint the built-in Copy / Cut / Paste context menu on top of the
/// finished surface content. Called from the software and GLES draw
/// paths right before `present()` so the menu sits above everything
/// the widget tree painted, matching the convention every other
/// toolkit follows for runtime-internal popups.
pub( crate ) fn draw_context_menu(
	canvas: &mut crate::render::Canvas,
	menu:   &crate::event_loop::context_menu::ContextMenu,
)
{
	let palette = crate::theme::palette();
	let bg      = palette.surface;
	let border  = palette.divider;
	let text    = palette.text_primary;
	let muted   = palette.text_secondary;
	let hi      = palette.surface_alt;

	let r = menu.rect;
	canvas.fill_rect( r, bg, 8.0 );
	canvas.stroke_rect( r, border, 1.0, 8.0 );

	let ( ys, row_h ) = menu.row_ys();
	// Row order: Copy / Cut / Paste / Delete. Labels go through
	// `rust_i18n::t!()` so the menu picks up the active locale; the
	// `enabled` flag mirrors the gating in `handle_context_menu_press`.
	let labels: [ ( String, bool ); 4 ] =
	[
		( rust_i18n::t!( "context_menu.copy"   ).to_string(), menu.has_selection ),
		( rust_i18n::t!( "context_menu.cut"    ).to_string(), menu.has_selection ),
		( rust_i18n::t!( "context_menu.paste"  ).to_string(), menu.can_paste     ),
		( rust_i18n::t!( "context_menu.delete" ).to_string(), menu.has_selection ),
	];

	// Subtle accent band on the row matching the *primary* action so
	// the menu reads as "Paste is the default" when there is no
	// selection (the common case for a paste-into-empty-field click)
	// and "Copy is the default" when a selection is active. Just a
	// hint, not a binding — every row still works on its own click.
	let primary_idx = if menu.has_selection { 0 } else { 2 };
	let primary_band = crate::types::Rect
	{
		x: r.x + 4.0, y: ys[ primary_idx ] + 2.0,
		width: r.width - 8.0, height: row_h - 4.0,
	};
	canvas.fill_rect( primary_band, hi, 6.0 );

	for ( i, ( label, enabled ) ) in labels.iter().enumerate()
	{
		let color = if *enabled { text } else { muted };
		canvas.draw_text(
			label,
			r.x + 16.0,
			ys[ i ] + row_h * 0.5 + 5.0,
			14.0,
			color,
		);
	}

	// Thin separator between every pair of rows.
	let sep_color = palette.divider;
	for y in ys.iter().skip( 1 )
	{
		canvas.draw_line( r.x + 8.0, *y, r.x + r.width - 8.0, *y, sep_color, 1.0 );
	}
}
