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

//! Software (CPU + `wl_shm` pool) draw paths.
//!
//! Two functions, both symmetric with their GPU counterparts in
//! [`super::gles`]:
//!
//! * [`draw_surface_full`] — full redraw. Allocate a buffer, clear the
//!   canvas, run layout+draw, write pixels to the SHM buffer, damage
//!   the whole surface (or fine-grained rects from [`compute_damage`]),
//!   commit.
//! * [`draw_surface_partial`] — clip-masked repaint. The canvas
//!   pixmap from the previous frame is still valid; install a clip
//!   covering only the dirty rects, repaint the background + title bar
//!   + widget tree under the clip, and damage Wayland with exactly
//!   those rects.
//!
//! Both paths share the DrawCtx / `layout_and_draw` / `draw_titlebar`
//! / `apply_input_region` helpers with the GPU path.

use std::sync::Arc;

use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::reexports::client::protocol::{ wl_shm, wl_surface::WlSurface };

use crate::event_loop::SurfaceState;
use crate::render::Canvas;
use crate::types::{ Color, Rect };
use crate::widget::Element;

use super::{ build_draw_ctx, commit_draw_ctx, compute_damage, layout_and_draw };
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar };

/// Full redraw path: clear the canvas, run layout+draw for every widget, then
/// emit either tight per-widget damage or a single full-surface damage rect
/// depending on what [`compute_damage`] derives.
pub( crate ) fn draw_surface_full<Msg: Clone>(
	ss:            &mut SurfaceState<Msg>,
	compositor:    &CompositorState,
	view:          &Element<Msg>,
	bg:            Color,
	input_region:  Option<&[Rect]>,
	debug_layout:  bool,
	shm_format:    wl_shm::Format,
	swap_rb:       bool,
	pw:            u32,
	ph:            u32,
	scale:         u32,
	request_frame: &dyn Fn( &WlSurface ),
)
{
	let Some( pool ) = ss.pool.as_mut() else { return };
	let stride = pw * 4;

	let ( buffer, canvas_buf ) = match pool.create_buffer(
		pw as i32, ph as i32, stride as i32, shm_format,
	)
	{
		Ok( r )  => r,
		Err( _ ) => return,
	};

	let canvas = ss.canvas.get_or_insert_with( || {
		let mut c = Canvas::new( pw, ph );
		c.set_dpi_scale( scale as f32 );
		if let Some( reg ) = crate::theme::build_font_registry()
		{
			c.set_font_registry( Arc::new( reg ) );
		}
		c
	} );
	if canvas.size() != ( pw, ph )
	{
		canvas.resize( pw, ph );
		canvas.set_dpi_scale( scale as f32 );
	}
	canvas.clear_clip();

	let sf          = scale as f32;
	let tb_h        = ss.titlebar_height * sf;
	let screen_rect = Rect { x: 0.0, y: tb_h, width: pw as f32, height: (ph as f32 - tb_h).max( 0.0 ) };

	if bg.a > 0.0 { canvas.fill( bg ); } else { canvas.clear(); }

	ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf );

	let mut ctx = build_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, debug_layout );
	layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );

	if ctx.debug_layout
	{
		for w in &ctx.widget_rects
		{
			canvas.stroke_rect( w.rect, Color::rgb( 1.0, 0.0, 0.0 ), 1.5, 0.0 );
		}
	}

	if let Some( ref menu ) = ss.context_menu
	{
		super::draw_context_menu( canvas, menu );
	}

	// Fallback-theme warning. Painted last so it sits on top of every
	// widget; no-op when the real theme is loaded.
	draw_fallback_banner( canvas, pw, sf );

	let damage_rects = if ss.content_dirty
	{
		Vec::new()
	} else {
		compute_damage(
			&ss.frame.widget_rects,
			&ctx.widget_rects,
			ss.frame.prev_focused,
			ss.frame.prev_hovered,
			ss.frame.prev_pressed,
			ss.focused_idx,
			ss.hovered_idx,
			ss.gesture.pressed_idx,
			pw, ph,
		)
	};

	commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx );
	ss.content_dirty = false;

	canvas.write_to_wayland_buf( canvas_buf, swap_rb );

	let wl_surface = ss.surface.wl_surface();
	buffer.attach_to( wl_surface ).expect( "attach" );

	if damage_rects.is_empty()
	{
		wl_surface.damage_buffer( 0, 0, pw as i32, ph as i32 );
	} else {
		for r in &damage_rects
		{
			wl_surface.damage_buffer( r.x as i32, r.y as i32, r.width as i32, r.height as i32 );
		}
	}

	apply_input_region( wl_surface, compositor, input_region, scale );
	// Request a frame callback BEFORE commit so the compositor schedules it
	// against this exact frame; sets `frame_pending` so the run loop won't
	// try to draw this surface again until the callback fires.
	request_frame( wl_surface );
	wl_surface.commit();
	ss.frame_pending = true;
}

/// Partial redraw: install a clip mask covering only `dirty_rects`, repaint
/// the background + title bar + widget tree under that mask (so unchanged
/// pixels from the previous frame stay), and damage Wayland with exactly
/// those rects.
pub( crate ) fn draw_surface_partial<Msg: Clone>(
	ss:            &mut SurfaceState<Msg>,
	compositor:    &CompositorState,
	view:          &Element<Msg>,
	bg:            Color,
	input_region:  Option<&[Rect]>,
	shm_format:    wl_shm::Format,
	swap_rb:       bool,
	dirty_rects:   Vec<Rect>,
	pw:            u32,
	ph:            u32,
	scale:         u32,
	request_frame: &dyn Fn( &WlSurface ),
)
{
	let Some( pool ) = ss.pool.as_mut() else { return };
	let stride = pw * 4;

	let ( buffer, canvas_buf ) = match pool.create_buffer(
		pw as i32, ph as i32, stride as i32, shm_format,
	)
	{
		Ok( r )  => r,
		Err( _ ) => return,
	};

	// Canvas pixels from the previous frame are still valid for the unclipped
	// region and serve as our background — do not call clear()/fill() here.
	let canvas = ss.canvas.as_mut().expect( "partial path requires existing canvas" );
	canvas.set_clip_rects( &dirty_rects );

	let sf          = scale as f32;
	let tb_h        = ss.titlebar_height * sf;
	let screen_rect = Rect { x: 0.0, y: tb_h, width: pw as f32, height: (ph as f32 - tb_h).max( 0.0 ) };

	// Repaint surface background under the clip so the dirty rects start from
	// a known state instead of leaking the previous widget's pixels.
	if bg.a > 0.0 { canvas.fill( bg ); }
	else
	{
		canvas.clear_rects_transparent( &dirty_rects );
	}

	ss.titlebar_close_rect = draw_titlebar( canvas, &ss.titlebar_title, pw, tb_h, sf );

	let mut ctx = build_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, false );
	layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );

	if let Some( ref menu ) = ss.context_menu
	{
		super::draw_context_menu( canvas, menu );
	}

	// Fallback-theme warning. Painted last so it sits on top of every
	// widget; no-op when the real theme is loaded. If the banner's row
	// happens to fall outside the partial-redraw clip, the previous
	// frame's banner is still on the canvas (pixmap preserved), which
	// is the correct behaviour.
	draw_fallback_banner( canvas, pw, sf );

	canvas.clear_clip();

	commit_draw_ctx( &mut ss.frame, ss.focused_idx, ss.hovered_idx, ss.gesture.pressed_idx, ctx );
	ss.content_dirty = false;

	canvas.write_to_wayland_buf( canvas_buf, swap_rb );

	let wl_surface = ss.surface.wl_surface();
	buffer.attach_to( wl_surface ).expect( "attach" );

	for r in &dirty_rects
	{
		wl_surface.damage_buffer( r.x as i32, r.y as i32, r.width as i32, r.height as i32 );
	}

	apply_input_region( wl_surface, compositor, input_region, scale );
	request_frame( wl_surface );
	wl_surface.commit();
	ss.frame_pending = true;
}
