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

//! GLES (EGL + FBO) draw paths.
//!
//! The GPU counterparts to [`super::software::draw_surface_full`] and
//! [`super::software::draw_surface_partial`]. Same DrawCtx flow; the
//! differences are:
//!
//! * Canvas is `Canvas::new_gles` backed by a persistent FBO rather
//!   than a CPU pixmap → FBO pixels survive across frames naturally,
//!   so the partial path only needs a scissor.
//! * Presentation goes through `egl_ctx.swap_buffers_with_damage`
//!   which attaches + damages + commits atomically. The partial path
//!   translates the top-left dirty rects into EGL's bottom-left
//!   convention before passing them in.
//! * No `wl_shm` pool; no `pick_shm_format`; no byte-order swap.
//!
//! The Y-flip for the swap-with-damage call lives here (only the GPU
//! path needs it — SHM buffers are already top-down).

use std::sync::Arc;

use smithay_client_toolkit::compositor::CompositorState;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;

use crate::egl_context::EglContext;
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, layout_and_draw };
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar };

/// GPU full-redraw path. Mirrors [`super::software::draw_surface_full`]
/// but writes into the EGL surface's persistent FBO instead of an SHM
/// buffer:
///
///   1. `eglMakeCurrent` so subsequent GL calls hit this surface.
///   2. (Lazily) build the [`Canvas::Gles`], or resize its FBO if the
///      surface dimensions changed.
///   3. Clear, run layout+draw, blit the FBO to the default framebuffer.
///   4. Set buffer_scale + input_region (state attached to the implicit
///      commit done by `eglSwapBuffers`).
///   5. `eglSwapBuffers` — performs the wl attach/damage/commit atomically.
pub( crate ) fn draw_surface_full_gpu<Msg: Clone>(
	ss:            &mut SurfaceState<Msg>,
	compositor:    &CompositorState,
	egl_ctx:       &Arc<EglContext>,
	view:          &Element<Msg>,
	bg:            Color,
	input_region:  Option<&[Rect]>,
	debug_layout:  bool,
	pw:            u32,
	ph:            u32,
	scale:         u32,
	request_frame: &dyn Fn( &WlSurface ),
)
{
	let Some( es ) = ss.egl_surface.as_ref() else { return };
	if egl_ctx.make_current( es ).is_err() { return; }

	let canvas = ss.canvas.get_or_insert_with( ||
	{
		let mut c = Canvas::new_gles(
			Arc::clone( egl_ctx.gl() ), egl_ctx.version, 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 );

	canvas.present();

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

	let wl_surface = ss.surface.wl_surface();
	apply_input_region( wl_surface, compositor, input_region, scale );
	// Frame callback request must precede the implicit commit done by
	// `swap_buffers_with_damage`, so the compositor can attach it to this
	// frame and not the previous one.
	request_frame( wl_surface );
	// Full-surface damage. (0,0,w,h) is identical in EGL bottom-left and
	// top-left coords, so no Y-flip needed.
	let _ = egl_ctx.swap_buffers_with_damage( es, &[ ( 0, 0, pw as i32, ph as i32 ) ] );
	ss.frame_pending = true;
}

/// GPU partial-redraw path. Same flow as
/// [`super::software::draw_surface_partial`] (clip mask → repaint
/// background + widgets → present), but the clip is implemented with
/// `glScissor` (bbox union) inside the canvas, and presentation goes
/// through `eglSwapBuffers`. The FBO is preserved between frames so
/// the unclipped pixels naturally carry over.
pub( crate ) fn draw_surface_partial_gpu<Msg: Clone>(
	ss:            &mut SurfaceState<Msg>,
	compositor:    &CompositorState,
	egl_ctx:       &Arc<EglContext>,
	view:          &Element<Msg>,
	bg:            Color,
	input_region:  Option<&[Rect]>,
	dirty_rects:   Vec<Rect>,
	pw:            u32,
	ph:            u32,
	scale:         u32,
	request_frame: &dyn Fn( &WlSurface ),
)
{
	let Some( es ) = ss.egl_surface.as_ref() else { return };
	if egl_ctx.make_current( es ).is_err() { return; }

	let canvas = ss.canvas.as_mut().expect( "partial path requires existing canvas" );
	if canvas.size() != ( pw, ph )
	{
		canvas.resize( pw, ph );
		canvas.set_dpi_scale( scale as f32 );
	}
	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 ) };

	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.
	draw_fallback_banner( canvas, pw, sf );

	canvas.clear_clip();
	canvas.present();

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

	let wl_surface = ss.surface.wl_surface();
	apply_input_region( wl_surface, compositor, input_region, scale );
	request_frame( wl_surface );
	// Convert top-left dirty rects to EGL bottom-left coords for the
	// swap-with-damage call.
	let damage: Vec<( i32, i32, i32, i32 )> = dirty_rects.iter().map( |r|
	{
		let x = r.x.floor() as i32;
		let w = r.width.ceil()  as i32;
		let h = r.height.ceil() as i32;
		let y_top    = r.y.floor() as i32;
		let y_bottom = ph as i32 - y_top - h;
		( x, y_bottom.max( 0 ), w.max( 0 ), h.max( 0 ) )
	} ).collect();
	let _ = egl_ctx.swap_buffers_with_damage( es, &damage );
	ss.frame_pending = true;
}
