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

//! Backend-neutral free helpers for the GLES renderer: MVP matrix
//! construction, shader compilation, FBO / texture allocation, small
//! typed-handle extractors. Visible only within `crate::gles_render` —
//! callers always go through `GlesCanvas`'s public methods. System-font
//! resolution lives in `crate::system_fonts`.

use glow::HasContext;

use crate::types::Rect;

use super::GlesVersion;

pub( super ) fn ortho_rect( vp_w: u32, vp_h: u32, rect: Rect ) -> [f32; 16]
{
	let w = vp_w as f32;
	let h = vp_h as f32;
	let sx = rect.width  * 2.0 / w;
	let sy = rect.height * 2.0 / h;
	let tx = rect.x * 2.0 / w - 1.0;
	let ty = 1.0 - rect.y * 2.0 / h - sy;

	[
		sx,  0.0, 0.0, 0.0,
		0.0, sy,  0.0, 0.0,
		0.0, 0.0, 1.0, 0.0,
		tx,  ty,  0.0, 1.0,
	]
}

/// Upload `data` as a 2D RGBA texture, premultiplying alpha into the
/// upload buffer.
///
/// `data` is straight-alpha (the format CPU PNG / JPG decoders
/// produce). GL_LINEAR sampling interpolates RGB and A independently
/// across texels: at the antialiased edge of an icon — say a fully
/// opaque black texel next to a fully transparent white texel —
/// straight-alpha interpolation midway gives `( 0.5, 0.5, 0.5, 0.5 )`
/// which composes onto the destination as a 50 % gray halo, while
/// premultiplied interpolation gives `( 0, 0, 0, 0.5 )` and composes
/// as transparent black. Premultiplying once at upload eliminates
/// the halo on every later draw.
///
/// Defensive: when the declared `w × h × 4` size does not match
/// `data.len()`, the function refuses the upload, logs once via
/// stderr, and substitutes a 1×1 transparent placeholder. The GL
/// driver would otherwise read past the slice end inside
/// `tex_image_2d`, since it trusts the dimensions over the slice
/// length.
pub( super ) fn upload_rgba_texture( gl: &glow::Context, version: GlesVersion, data: &[u8], w: i32, h: i32 ) -> glow::Texture
{
	let expected = ( w as i64 ).saturating_mul( h as i64 ).saturating_mul( 4 );
	let valid    = w > 0 && h > 0 && expected >= 0 && expected as usize == data.len();
	let placeholder: [u8; 4] = [ 0, 0, 0, 0 ];
	let ( safe_w, safe_h, safe_data ): ( i32, i32, &[u8] ) = if valid
	{
		( w, h, data )
	}
	else
	{
		eprintln!(
			"[ltk] upload_rgba_texture: refusing malformed upload — \
			 {w}×{h} declared, {} bytes provided, expected {}",
			data.len(),
			expected.max( 0 ),
		);
		( 1, 1, &placeholder[..] )
	};

	let mut premul = Vec::with_capacity( safe_data.len() );
	for px in safe_data.chunks_exact( 4 )
	{
		let a = px[3] as u32;
		// `(c * a + 127) / 255` — round-to-nearest integer scale.
		// Plain `c * a / 255` truncates, leaving fully-opaque pixels
		// with rgb < their straight value (visibly darker icons).
		premul.push( ( ( px[0] as u32 * a + 127 ) / 255 ) as u8 );
		premul.push( ( ( px[1] as u32 * a + 127 ) / 255 ) as u8 );
		premul.push( ( ( px[2] as u32 * a + 127 ) / 255 ) as u8 );
		premul.push( px[3] );
	}
	// `RGBA8` (sized) on GLES 3 forces 8-bit-per-channel storage; the
	// unsized `RGBA` token leaves the format up to the driver and some
	// mobile GPUs pick a 4-bits-per-channel or 565+A4 layout for it,
	// which shows up as banded / colour-quantised icons. ES 2 has no
	// `RGBA8` constant, so we fall back to the unsized form there
	// (matching `alloc_fbo_tex`).
	let internal_format = match version
	{
		GlesVersion::V3 => glow::RGBA8 as i32,
		GlesVersion::V2 => glow::RGBA  as i32,
	};
	// SAFETY: caller's GL context is current. The most important
	// invariant is on the upload size: the validity check above
	// guarantees `safe_w * safe_h * 4 == safe_data.len() == premul.len()`,
	// or replaces the upload with a 1×1 transparent placeholder when the
	// caller provided malformed dimensions. Without this guard the GLES
	// driver trusts the dimensions and reads past the slice end inside
	// `tex_image_2d` (the original UB this defensive code prevents).
	// `RGBA + UNSIGNED_BYTE` is universally supported. We unbind on
	// exit to avoid stranding TEXTURE_2D bound to the new texture.
	unsafe
	{
		let tex = gl.create_texture().unwrap();
		gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32 );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32 );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 );
		gl.tex_image_2d(
			glow::TEXTURE_2D, 0, internal_format,
			safe_w, safe_h, 0, glow::RGBA, glow::UNSIGNED_BYTE,
			glow::PixelUnpackData::Slice( Some( &premul ) ),
		);
		gl.bind_texture( glow::TEXTURE_2D, None );
		tex
	}
}

/// Allocate a fresh color texture sized for the FBO. Internal format is sized
/// (`GL_RGBA8`) on ES3 and unsized (`GL_RGBA`) on ES2 — the unsized form is
/// required for color-renderable textures on ES2 drivers.
pub( super ) unsafe fn alloc_fbo_tex( gl: &glow::Context, version: GlesVersion, w: u32, h: u32 ) -> glow::Texture
{
	// SAFETY: caller guarantees the GL context bound to `gl` is current
	// on this thread. `tex_image_2d` with a `None` data slice allocates
	// uninitialised storage of size `w*h*4` bytes (RGBA8 / unsized RGBA);
	// the size and format combination is valid on every GLES2 / GLES3
	// driver. The bind / unbind pair leaves TEXTURE_2D unbound on exit
	// so we don't strand a binding the caller might rely on.
	unsafe
	{
		let tex = gl.create_texture().expect( "create_texture" );
		gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
		let internal_format = match version
		{
			GlesVersion::V3 => glow::RGBA8 as i32,
			GlesVersion::V2 => glow::RGBA  as i32,
		};
		gl.tex_image_2d(
			glow::TEXTURE_2D, 0, internal_format,
			w as i32, h as i32, 0, glow::RGBA, glow::UNSIGNED_BYTE,
			glow::PixelUnpackData::Slice( None ),
		);
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::NEAREST as i32 );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::NEAREST as i32 );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 );
		gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 );
		gl.bind_texture( glow::TEXTURE_2D, None );
		tex
	}
}


pub( super ) fn compile_program( gl: &glow::Context, vert_src: &str, frag_src: &str ) -> glow::Program
{
	compile_program_with_attribs( gl, vert_src, frag_src, &[ ( 0, "a_pos" ) ] )
}

pub( super ) fn compile_program_with_attribs( gl: &glow::Context, vert_src: &str, frag_src: &str, attribs: &[ ( u32, &str ) ] ) -> glow::Program
{
	unsafe
	{
		let program = gl.create_program().unwrap();
		let vs = gl.create_shader( glow::VERTEX_SHADER ).unwrap();
		gl.shader_source( vs, vert_src );
		gl.compile_shader( vs );
		assert!( gl.get_shader_compile_status( vs ), "VS: {}", gl.get_shader_info_log( vs ) );

		let fs = gl.create_shader( glow::FRAGMENT_SHADER ).unwrap();
		gl.shader_source( fs, frag_src );
		gl.compile_shader( fs );
		assert!( gl.get_shader_compile_status( fs ), "FS: {}", gl.get_shader_info_log( fs ) );

		gl.attach_shader( program, vs );
		gl.attach_shader( program, fs );
		for ( loc, name ) in attribs
		{
			gl.bind_attrib_location( program, *loc, name );
		}
		gl.link_program( program );
		assert!( gl.get_program_link_status( program ), "Link: {}", gl.get_program_info_log( program ) );

		gl.delete_shader( vs );
		gl.delete_shader( fs );
		program
	}
}

pub( super ) fn bytemuck_cast_slice( floats: &[f32] ) -> &[u8]
{
	// SAFETY: `f32` has the same allocation provenance / validity as
	// `[u8; 4]` for any bit pattern (every bit pattern is a valid byte;
	// `f32` admits NaN payloads but those are valid `u8` reads). The
	// returned slice's lifetime is tied to the input slice's lifetime
	// through the function signature so the read window cannot outlive
	// the underlying storage. `len * 4` cannot overflow `usize` because
	// it would require an `f32` slice exceeding `usize::MAX / 4` bytes
	// — physically impossible on any addressable target.
	unsafe
	{
		std::slice::from_raw_parts(
			floats.as_ptr() as *const u8,
			floats.len() * 4,
		)
	}
}

pub( super ) fn native_texture_id( texture: glow::Texture ) -> u32
{
	texture.0.get()
}

pub( super ) fn native_framebuffer_id( framebuffer: glow::Framebuffer ) -> u32
{
	framebuffer.0.get()
}

