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

//! Raster-image draw path for [`GlesCanvas`]. Uploads the RGBA
//! bytes as a premultiplied-alpha texture (cached by content
//! fingerprint so repeated draws of the same buffer do not
//! re-upload) and composites it through the texture shader,
//! honouring the canvas' `global_alpha` via the opacity uniform.
//!
//! The cache is keyed by `(size, fingerprint)` where the fingerprint
//! is a 64-bit hash sampled from the RGBA bytes. This avoids the
//! address-reuse trap of a pointer-based key — when an `Arc<Vec<u8>>`
//! gets dropped and the allocator hands the same heap address to a
//! *different* buffer on the next frame, a pointer-keyed cache would
//! serve the stale texture for the new content. Content-keying makes
//! that impossible: identical bytes → identical key, regardless of
//! where they live in memory.
//!
//! The cache is bounded to [`IMAGE_CACHE_MAX_BYTES`] of estimated GPU
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
//! eviction, so a stream of distinct buffers (a photo carousel, video
//! thumbnails) recycles textures instead of growing GPU memory for the
//! canvas' lifetime. The most recent entry is never evicted, so a
//! single texture larger than the whole budget still draws — the cache
//! then holds that one entry until something replaces it.

use std::collections::hash_map::DefaultHasher;
use std::hash::{ Hash, Hasher };

use glow::HasContext;

use crate::types::Rect;

use super::helpers::{ ortho_rect, upload_rgba_texture };
use super::GlesCanvas;

/// Byte budget for the image texture cache. 32 MiB holds a phone-sized
/// wallpaper (~10 MB at 1080×2400) plus a working set of icons and
/// thumbnails; sized for mobile GPUs where this memory competes with
/// the compositor.
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;

/// Compute a 64-bit fingerprint of an RGBA buffer for the texture
/// cache. Hashes the full byte slice for small buffers (icons,
/// thumbnails — below 16 KB ≈ 64×64 RGBA), and falls back to a
/// strided 8 × 512-byte sample for anything larger so a wallpaper
/// blit does not pay an 8 MB hash on every frame. Both modes
/// distinguish the chevron-icon-style cases that motivated the move
/// to content-keying — the SVG-rasterised buffers differ across
/// most of their interior bytes, not just at the corners.
fn fingerprint_rgba( bytes: &[u8] ) -> u64
{
	const FULL_HASH_THRESHOLD: usize = 16 * 1024;
	const SAMPLE_CHUNKS:       usize = 8;
	const SAMPLE_CHUNK_BYTES:  usize = 512;

	let mut h = DefaultHasher::new();
	let n     = bytes.len();
	n.hash( &mut h );
	if n <= FULL_HASH_THRESHOLD
	{
		bytes.hash( &mut h );
	} else {
		let stride = n / SAMPLE_CHUNKS;
		for i in 0..SAMPLE_CHUNKS
		{
			let pos = ( i * stride ).min( n - SAMPLE_CHUNK_BYTES );
			bytes[ pos..pos + SAMPLE_CHUNK_BYTES ].hash( &mut h );
		}
	}
	h.finish()
}

impl GlesCanvas
{
	/// Blit RGBA image data scaled to dest rect with opacity.
	///
	/// Defensive: rejects buffers whose declared `img_w × img_h × 4` does not
	/// match `rgba_data.len()`. The mismatch path logs a one-line warning
	/// and returns without uploading or drawing — the same boundary that
	/// the internal `upload_rgba_texture` helper enforces, raised one
	/// level so the cache key is never seeded with a bogus mapping.
	pub fn draw_image_data( &mut self, rgba_data: &[u8], img_w: u32, img_h: u32, dest: Rect, opacity: f32 )
	{
		if !crate::render::helpers::validate_rgba_dims( "GlesCanvas", rgba_data, img_w, img_h )
		{
			return;
		}

		self.activate_target();
		// Content-fingerprint key — see the module doc for the
		// rationale. The (w, h) prefix means a pathological pair of
		// buffers with identical bytes but different declared sizes
		// stays distinct (cannot happen for valid input, defence in
		// depth).
		let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );

		if !self.image_cache.contains_key( &cache_key )
		{
			let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
			self.image_cache.insert( cache_key, ( tex, img_w, img_h ) );
			self.image_cache_bytes += ( img_w as usize ) * ( img_h as usize ) * 4;
			self.image_cache_lru.push_back( cache_key );
			self.evict_image_cache_overflow();
		} else if let Some( pos ) = self.image_cache_lru.iter().position( |k| *k == cache_key )
		{
			self.image_cache_lru.remove( pos );
			self.image_cache_lru.push_back( cache_key );
		}

		// Snap to integer pixels. With GL_LINEAR sampling, a
		// fractional `dest.x` / `dest.y` makes every fragment sample
		// at sub-texel offset — bilinear blends adjacent texels and
		// the result reads as ~1 px softer than the source. At
		// integer offset every fragment center maps to a texel
		// centre and the bilinear collapses to identity, so a 1:1
		// sampled icon renders crisp.
		let dest = Rect
		{
			x:      dest.x.round(),
			y:      dest.y.round(),
			width:  dest.width.round(),
			height: dest.height.round(),
		};

		if let Some( ( tex, _, _ ) ) = self.image_cache.get( &cache_key )
		{
			let mvp   = ortho_rect( self.width, self.height, dest );
			let alpha = opacity * self.global_alpha;
			// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
			// `self.image_cache` so it outlives the call. Eviction ran
			// before this borrow and never removes the most-recent key,
			// which is `cache_key`.
			unsafe
			{
				self.gl.use_program( Some( self.tex_program ) );
				self.gl.uniform_matrix_4_f32_slice( Some( &self.u_tex_mvp ), false, &mvp );
				self.gl.uniform_1_f32( Some( &self.u_tex_opacity ), alpha );
				self.gl.active_texture( glow::TEXTURE0 );
				self.gl.bind_texture( glow::TEXTURE_2D, Some( *tex ) );
				self.gl.uniform_1_i32( Some( &self.u_tex_sampler ), 0 );
				self.gl.bind_vertex_array( Some( self.quad_vao ) );
				self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
				self.gl.bind_vertex_array( None );
				self.gl.bind_texture( glow::TEXTURE_2D, None );
			}
		}
	}

	/// Delete least-recently-drawn cached textures until the estimated
	/// byte total fits [`IMAGE_CACHE_MAX_BYTES`]. The back of the LRU —
	/// the entry the current draw is about to use — is never removed.
	fn evict_image_cache_overflow( &mut self )
	{
		while self.image_cache_bytes > IMAGE_CACHE_MAX_BYTES && self.image_cache_lru.len() > 1
		{
			let Some( key ) = self.image_cache_lru.pop_front() else { break };
			if let Some( ( tex, w, h ) ) = self.image_cache.remove( &key )
			{
				self.image_cache_bytes = self.image_cache_bytes
					.saturating_sub( ( w as usize ) * ( h as usize ) * 4 );
				// SAFETY: `tex` was created through `self.gl` in the insert
				// path above and just left the map, so it is deleted exactly
				// once. Deleting a bound texture is defined in GLES (the
				// binding reverts to 0); no draw is in flight here.
				unsafe { self.gl.delete_texture( tex ); }
			}
		}
	}

	/// Draw an externally-owned GL texture into `dest`.
	///
	/// The caller owns the texture and is responsible for keeping it valid
	/// for the duration of this call. No upload, no caching — used to
	/// composite content rendered by another GL producer (web engine,
	/// video decoder, …) into the LTK widget tree.
	pub fn draw_external_texture( &mut self, texture: glow::Texture, dest: Rect, opacity: f32 )
	{
		self.activate_target();
		let dest = Rect
		{
			x:      dest.x.round(),
			y:      dest.y.round(),
			width:  dest.width.round(),
			height: dest.height.round(),
		};
		let mvp   = ortho_rect( self.width, self.height, dest );
		let alpha = opacity * self.global_alpha;
		// SAFETY: caller-owned texture must outlive this call. We only
		// sample it; we never delete or reassign the GL name.
		unsafe
		{
			self.gl.use_program( Some( self.tex_program ) );
			self.gl.uniform_matrix_4_f32_slice( Some( &self.u_tex_mvp ), false, &mvp );
			self.gl.uniform_1_f32( Some( &self.u_tex_opacity ), alpha );
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( texture ) );
			self.gl.uniform_1_i32( Some( &self.u_tex_sampler ), 0 );
			self.gl.bind_vertex_array( Some( self.quad_vao ) );
			self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
			self.gl.bind_vertex_array( None );
			self.gl.bind_texture( glow::TEXTURE_2D, None );
		}
	}
}
