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

use std::sync::Arc;
use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::reexports::client::protocol::{ wl_shm, wl_surface::WlSurface };
use smithay_client_toolkit::shm::Shm;

use crate::app::App;
use crate::egl_context::EglContext;
use crate::types::{ Color, Rect };
use crate::widget::Element;

use super::AppData;
use super::surface::{ SurfaceFocus, SurfaceState };
use crate::draw::gles::{ draw_surface_full_gpu, draw_surface_partial_gpu };
use crate::draw::software::{ draw_surface_full, draw_surface_partial };
use crate::draw::damage::compute_interaction_dirty_rects;

/// Pick the best wl_shm format for our RGBA-premultiplied pixmap.
/// Abgr8888 matches tiny-skia's byteorder on little-endian systems (plain
/// memcpy). Falls back to mandatory Argb8888 when not advertised.
/// Returns `(format, swap_rb)`.
pub( crate ) fn pick_shm_format( shm: &Shm ) -> ( wl_shm::Format, bool )
{
	if shm.formats().contains( &wl_shm::Format::Abgr8888 )
	{
		( wl_shm::Format::Abgr8888, false )
	} else {
		( wl_shm::Format::Argb8888, true )
	}
}

/// Returns `true` when this call performed the very first commit on the main
/// surface — the run loop uses that to fire `App::on_first_frame_committed`
/// exactly once, after the borrows held by draw_frame have been released.
pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
{
	let main_view = data.cached_view.as_ref().expect( "view cache populated" );
	let overlays  = data.cached_overlays.as_ref().expect( "overlays cache populated" );

	let main_bg      = data.app.background_color();
	let main_region  = data.app.input_region();
	let debug_layout = data.debug_layout;
	let ( format, swap_rb ) = pick_shm_format( &data.shm );
	let egl_ctx = data.egl_context.as_ref();
	let qh      = &data.qh;

	let mut first_commit_now = false;
	if data.main.configured && data.main.needs_redraw && !data.main.frame_pending
	{
		let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, SurfaceFocus::Main ); };
		draw_surface::<A::Message>(
			&mut data.main,
			&data.compositor_state,
			egl_ctx,
			main_view,
			main_bg,
			main_region.as_deref(),
			debug_layout,
			format,
			swap_rb,
			&req_frame,
		);
		data.main.needs_redraw = false;
		data.main.last_draw    = std::time::Instant::now();
		if !data.first_frame_committed
		{
			data.first_frame_committed = true;
			first_commit_now = true;
		}
	}

	for ( _, ss ) in data.secondaries.iter_mut()
	{
		if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; }
		let scale = ss.scale_factor.max( 1 ) as u32;
		let view  = data.app.secondary_view( ss.width * scale, ss.height * scale )
			.unwrap_or_else( || crate::spacer().into() );
		// No frame callback: these surfaces are static between invalidations,
		// so there is no per-frame pacing to keep — and a callback that never
		// gets requested again would leave `frame_pending` stuck.
		let req_frame = | _: &WlSurface | {};
		draw_surface::<A::Message>(
			ss,
			&data.compositor_state,
			egl_ctx,
			&view,
			main_bg,
			None,
			debug_layout,
			format,
			swap_rb,
			&req_frame,
		);
		ss.needs_redraw  = false;
		ss.frame_pending = false;
		ss.last_draw     = std::time::Instant::now();
	}

	for spec in overlays
	{
		if let Some( ss ) = data.overlays.get_mut( &spec.id )
		{
			if !ss.configured || !ss.needs_redraw || ss.frame_pending { continue; }
			let focus     = SurfaceFocus::Overlay( spec.id );
			let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, focus ); };
			let bg        = Color::rgba( 0.0, 0.0, 0.0, 0.0 );
			draw_surface::<A::Message>(
				ss,
				&data.compositor_state,
				egl_ctx,
				&spec.view,
				bg,
				spec.input_region.as_deref(),
				debug_layout,
				format,
				swap_rb,
				&req_frame,
			);
			ss.needs_redraw = false;
			ss.last_draw    = std::time::Instant::now();
		}
	}

	first_commit_now
}

fn draw_surface<Msg: Clone>(
	ss:            &mut SurfaceState<Msg>,
	compositor:    &CompositorState,
	egl_ctx:       Option<&Arc<EglContext>>,
	view:          &Element<Msg>,
	bg:            Color,
	input_region:  Option<&[Rect]>,
	debug_layout:  bool,
	shm_format:    wl_shm::Format,
	swap_rb:       bool,
	request_frame: &dyn Fn( &WlSurface ),
)
{
	let scale = ss.scale_factor.max( 1 ) as u32;
	let w = ss.width;
	let h = ss.height;
	if w == 0 || h == 0 { return; }
	let pw = w * scale;
	let ph = h * scale;

	let canvas_ready = ss.canvas.as_ref()
		.map( |c| c.size() == ( pw, ph ) )
		.unwrap_or( false );
	let partial_eligible = !ss.content_dirty
		&& canvas_ready
		&& !ss.frame.widget_rects.is_empty();

	if partial_eligible
	{
		let dirty_rects = compute_interaction_dirty_rects(
			&ss.frame.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,
		);
		if dirty_rects.is_empty() { return; }
		let total: f32 = dirty_rects.iter().map( |r| r.width * r.height ).sum();
		if total < pw as f32 * ph as f32 * 0.5
		{
			if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() )
			{
				draw_surface_partial_gpu(
					ss, compositor, ctx, view, bg, input_region,
					dirty_rects, pw, ph, scale, request_frame,
				);
			} else {
				draw_surface_partial(
					ss, compositor, view, bg, input_region,
					shm_format, swap_rb, dirty_rects, pw, ph, scale, request_frame,
				);
			}
			return;
		}
	}

	if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() )
	{
		draw_surface_full_gpu(
			ss, compositor, ctx, view, bg, input_region, debug_layout,
			pw, ph, scale, request_frame,
		);
	} else {
		draw_surface_full(
			ss, compositor, view, bg, input_region, debug_layout,
			shm_format, swap_rb, pw, ph, scale, request_frame,
		);
	}
}
