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

//! Text rendering for [`GlesCanvas`]: shelf-packed glyph atlas +
//! per-glyph draw call. The line is shaped through
//! [`crate::text_shaping::shape_line`] (Unicode BiDi + per-sub-run
//! rustybuzz / HarfBuzz) and each shaped glyph is rasterised by
//! glyph index via `fontdue::Font::rasterize_indexed`, so the cache
//! is keyed on `(glyph_id, size_bits, font_id)` and Arabic
//! connected forms / Devanagari clusters / CJK shaped glyphs are
//! cached correctly without colliding with the source codepoints.
//!
//! The atlas format is picked per ES profile
//! ([`GlesCanvas::atlas_format`]): `GL_R8` / `GL_RED` on ES3,
//! `GL_LUMINANCE` on ES2. The fragment shader samples `.r` and is
//! identical for both — `GL_RED` puts the coverage byte in `.r`
//! directly, `GL_LUMINANCE` replicates it into `.r=.g=.b`.

use std::sync::Arc;

use fontdue::Font;
use glow::HasContext;

use crate::types::Color;

use super::{ ATLAS_SIZE, GlesCanvas, GlyphEntry };

const GLYPH_CACHE_SOFT_CAP: usize = 8192;

fn font_id( font: &Arc<Font> ) -> usize
{
	Arc::as_ptr( font ) as usize
}

impl GlesCanvas
{
	/// Draw a single shaped line of `text` with the canvas default font and the
	/// system fallback chain, baseline at `( x, y )` in surface px. `size` is in
	/// logical px and scaled by `dpi_scale` before rasterisation. Glyphs are
	/// shelf-packed into the GPU atlas and the whole line is flushed in one batched
	/// draw call.
	pub fn draw_text( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color )
	{
		self.draw_text_inner( text, x, y, size, color, None );
	}

	/// Like [`Self::draw_text`] but leads the resolver with `font` instead of the
	/// canvas default, falling back to the system chain for codepoints it does not
	/// cover.
	pub fn draw_text_with_font( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: &Arc<Font> )
	{
		self.draw_text_inner( text, x, y, size, color, Some( font ) );
	}

	fn draw_text_inner( &mut self, text: &str, x: f32, y: f32, size: f32, color: Color, font: Option<&Arc<Font>> )
	{
		self.activate_target();
		let scaled   = size * self.dpi_scale;
		let size_key = ( scaled * 10.0 ) as u32;

		unsafe
		{
			self.gl.active_texture( glow::TEXTURE0 );
			self.gl.bind_texture( glow::TEXTURE_2D, Some( self.atlas_texture ) );
		}

		// Resolve every glyph + its font up front so we can decide
		// whether the atlas needs a single reset before we start
		// emitting quads. Doing the reset mid-draw would orphan the
		// `(atlas_x, atlas_y)` recorded in `glyph_cache` for glyphs
		// already pushed earlier in this same call.
		let canvas_handle = self.font_handle();
		let prefer_handle = font.cloned().map( |f|
		{
			if Arc::ptr_eq( &f, &canvas_handle.font )
			{
				canvas_handle.clone()
			}
			else
			{
				// Caller-supplied face without bytes — leave bytes
				// empty so the resolve function below knows it
				// cannot be shaped through HarfBuzz and must fall
				// back to the system chain.
				crate::system_fonts::FontHandle
				{
					font:  f,
					bytes: Arc::new( Vec::new() ),
					face:  0,
				}
			}
		} ).unwrap_or_else( || canvas_handle.clone() );
		let resolve = |ch: char| -> Option<crate::system_fonts::FontHandle>
		{
			if prefer_handle.font.lookup_glyph_index( ch ) != 0 && !prefer_handle.bytes.is_empty()
			{
				return Some( prefer_handle.clone() );
			}
			crate::system_fonts::lookup_handle( ch ).or_else( ||
				if !canvas_handle.bytes.is_empty() { Some( canvas_handle.clone() ) } else { None }
			)
		};
		let shaped = crate::text_shaping::shape_line_cached(
			text, scaled, Arc::as_ptr( &prefer_handle.font ) as usize, resolve,
		);
		if shaped.is_empty() { return; }

		// Resolve each glyph's `font_id` to an actual `Arc<Font>` for
		// rasterization. The `shape_line` step kept the id as the
		// `Arc<Font>::as_ptr` of the resolved handle, so we walk
		// every codepoint and ask the same resolver — exactly once
		// per distinct id thanks to the dedup loop.
		let mut fonts: Vec<( usize, Arc<Font> )> = Vec::new();
		if !canvas_handle.bytes.is_empty()
		{
			fonts.push( ( font_id( &canvas_handle.font ), Arc::clone( &canvas_handle.font ) ) );
		}
		for g in shaped.iter()
		{
			if fonts.iter().any( |( id, _ )| *id == g.font_id ) { continue; }
			let mut found = None;
			for ch in text.chars()
			{
				if let Some( h ) = crate::system_fonts::lookup_handle( ch )
				{
					if font_id( &h.font ) == g.font_id { found = Some( h.font ); break; }
				}
			}
			if let Some( f ) = found
			{
				fonts.push( ( g.font_id, f ) );
			}
		}

		// Phase 1 — rasterise every missing glyph into the atlas.
		// Permitted to reset the atlas (and re-rasterise everything we
		// already inserted in this same pass) at most once: a single
		// string longer than the atlas capacity falls back to skipping
		// the tail rather than oscillating.
		let mut reset_used = false;
		let mut i          = 0;
		while i < shaped.len()
		{
			let g  = &shaped[ i ];
			let glyph_id = g.glyph_id as u16;
			let key      = ( glyph_id, size_key, g.font_id );
			if self.glyph_cache.contains_key( &key )
			{
				i += 1;
				continue;
			}
			let Some( ( _, font_arc ) ) = fonts.iter().find( |( id, _ )| *id == g.font_id ) else
			{
				i += 1;
				continue;
			};
			let ( metrics, bitmap ) = font_arc.rasterize_indexed( glyph_id, scaled );
			if metrics.width == 0 || metrics.height == 0
			{
				self.glyph_cache.insert( key, GlyphEntry
				{
					metrics,
					tex_w:   0,
					tex_h:   0,
					atlas_x: 0,
					atlas_y: 0,
				} );
				i += 1;
				continue;
			}
			let ( w, h ) = ( metrics.width as u32, metrics.height as u32 );

			if self.glyph_cache.len() >= GLYPH_CACHE_SOFT_CAP && !reset_used
			{
				self.atlas_reset();
				reset_used = true;
				i = 0;
				continue;
			}
			let pos = match self.atlas_alloc( w, h )
			{
				Some( p ) => p,
				None if !reset_used =>
				{
					self.atlas_reset();
					reset_used = true;
					i = 0;
					continue;
				}
				None =>
				{
					self.glyph_cache.insert( key, GlyphEntry
					{
						metrics,
						tex_w:   0,
						tex_h:   0,
						atlas_x: 0,
						atlas_y: 0,
					} );
					i += 1;
					continue;
				}
			};
			unsafe
			{
				self.gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 1 );
				self.gl.tex_sub_image_2d(
					glow::TEXTURE_2D, 0,
					pos.0 as i32, pos.1 as i32,
					w as i32, h as i32,
					self.atlas_format, glow::UNSIGNED_BYTE,
					glow::PixelUnpackData::Slice( Some( &bitmap ) ),
				);
				self.gl.pixel_store_i32( glow::UNPACK_ALIGNMENT, 4 );
			}
			self.glyph_cache.insert( key, GlyphEntry
			{
				metrics,
				tex_w:   w as i32,
				tex_h:   h as i32,
				atlas_x: pos.0,
				atlas_y: pos.1,
			} );
			i += 1;
		}

		// Phase 2 — collect every quad's vertices in one buffer and
		// flush them with a single draw call. Saves N-1
		// program/uniform/draw round-trips per text run.
		let atlas_size = ATLAS_SIZE as f32;
		let surface_w  = self.width  as f32;
		let surface_h  = self.height as f32;
		let mut verts: Vec<f32> = Vec::with_capacity( shaped.len() * 24 );
		let mut cx = x;
		for g in shaped.iter()
		{
			let glyph_id = g.glyph_id as u16;
			let key      = ( glyph_id, size_key, g.font_id );
			let Some( entry ) = self.glyph_cache.get( &key ) else
			{
				cx += g.x_advance;
				continue;
			};
			if entry.tex_w == 0 || entry.tex_h == 0
			{
				cx += g.x_advance;
				continue;
			}
			let pen_x = cx + g.x_offset;
			let pen_y = y - g.y_offset;
			let gx    = ( pen_x + entry.metrics.xmin as f32 ).round();
			let gy    = ( pen_y - entry.metrics.height as f32 - entry.metrics.ymin as f32 + 1.0 ).round();
			let gw    = entry.tex_w as f32;
			let gh    = entry.tex_h as f32;

			let x0_ndc =       gx * 2.0 / surface_w - 1.0;
			let x1_ndc = ( gx + gw ) * 2.0 / surface_w - 1.0;
			let y0_ndc = 1.0 -       gy * 2.0 / surface_h;
			let y1_ndc = 1.0 - ( gy + gh ) * 2.0 / surface_h;

			let u0 = entry.atlas_x as f32 / atlas_size;
			let v0 = entry.atlas_y as f32 / atlas_size;
			let u1 = ( entry.atlas_x as f32 + gw ) / atlas_size;
			let v1 = ( entry.atlas_y as f32 + gh ) / atlas_size;

			verts.extend_from_slice( &[
				x0_ndc, y0_ndc, u0, v0,
				x1_ndc, y0_ndc, u1, v0,
				x0_ndc, y1_ndc, u0, v1,
				x1_ndc, y0_ndc, u1, v0,
				x1_ndc, y1_ndc, u1, v1,
				x0_ndc, y1_ndc, u0, v1,
			] );

			cx += g.x_advance;
		}

		if !verts.is_empty()
		{
			unsafe
			{
				self.gl.use_program( Some( self.glyph_batch_program ) );
				self.gl.uniform_4_f32( Some( &self.u_glyph_batch_color   ), color.r, color.g, color.b, color.a );
				self.gl.uniform_1_f32( Some( &self.u_glyph_batch_opacity ), self.global_alpha );
				self.gl.uniform_1_i32( Some( &self.u_glyph_batch_sampler ), 0 );
				self.gl.bind_vertex_array( Some( self.glyph_batch_vao ) );
				self.gl.bind_buffer( glow::ARRAY_BUFFER, Some( self.glyph_batch_vbo ) );
				self.gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, super::helpers::bytemuck_cast_slice( &verts ), glow::STREAM_DRAW );
				let vertex_count = ( verts.len() / 4 ) as i32;
				self.gl.draw_arrays( glow::TRIANGLES, 0, vertex_count );
				self.gl.bind_vertex_array( None );
			}
		}

		unsafe { self.gl.bind_texture( glow::TEXTURE_2D, None ); }
	}

	/// Advance width of one shaped line of `text` in surface px, using the canvas
	/// default font and the system fallback chain. Shapes through the same path as
	/// [`Self::draw_text`] (so kerning and fallback advances match) without drawing.
	pub fn measure_text( &self, text: &str, size: f32 ) -> f32
	{
		self.measure_inner( text, size, None )
	}

	/// Like [`Self::measure_text`] but measures with `font` leading the resolver,
	/// so text laid out at one weight and drawn at another stays aligned.
	pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
	{
		self.measure_inner( text, size, Some( font ) )
	}

	fn measure_inner( &self, text: &str, size: f32, font: Option<&Arc<Font>> ) -> f32
	{
		let scaled = size * self.dpi_scale;
		let canvas_handle = self.font_handle();
		let prefer_handle = font.cloned().map( |f|
		{
			if Arc::ptr_eq( &f, &canvas_handle.font )
			{
				canvas_handle.clone()
			}
			else
			{
				crate::system_fonts::FontHandle
				{
					font:  f,
					bytes: Arc::new( Vec::new() ),
					face:  0,
				}
			}
		} ).unwrap_or_else( || canvas_handle.clone() );
		let resolve = |ch: char| -> Option<crate::system_fonts::FontHandle>
		{
			if prefer_handle.font.lookup_glyph_index( ch ) != 0 && !prefer_handle.bytes.is_empty()
			{
				return Some( prefer_handle.clone() );
			}
			crate::system_fonts::lookup_handle( ch ).or_else( ||
				if !canvas_handle.bytes.is_empty() { Some( canvas_handle.clone() ) } else { None }
			)
		};
		let shaped = crate::text_shaping::shape_line_cached(
			text, scaled, Arc::as_ptr( &prefer_handle.font ) as usize, resolve,
		);
		if shaped.is_empty()
		{
			return text.chars().map( |ch|
			{
				let f = font.cloned().unwrap_or_else( || self.font_for_char( ch ) );
				f.metrics( ch, scaled ).advance_width
			} ).sum();
		}
		shaped.iter().map( |g| g.x_advance ).sum()
	}

	fn font_handle( &self ) -> crate::system_fonts::FontHandle
	{
		crate::system_fonts::FontHandle
		{
			font:  Arc::clone( &self.font ),
			bytes: Arc::clone( &self.font_bytes ),
			face:  self.font_face,
		}
	}

	fn atlas_alloc( &mut self, w: u32, h: u32 ) -> Option<( u32, u32 )>
	{
		const PAD: u32 = 1;
		if w + PAD > ATLAS_SIZE || h + PAD > ATLAS_SIZE { return None; }
		if self.atlas_cursor_x + w + PAD > ATLAS_SIZE
		{
			self.atlas_cursor_x   = 0;
			self.atlas_cursor_y  += self.atlas_row_height;
			self.atlas_row_height = 0;
		}
		if self.atlas_cursor_y + h + PAD > ATLAS_SIZE { return None; }
		let x = self.atlas_cursor_x;
		let y = self.atlas_cursor_y;
		self.atlas_cursor_x  += w + PAD;
		if h + PAD > self.atlas_row_height { self.atlas_row_height = h + PAD; }
		Some( ( x, y ) )
	}

	fn atlas_reset( &mut self )
	{
		self.glyph_cache.clear();
		self.atlas_cursor_x   = 0;
		self.atlas_cursor_y   = 0;
		self.atlas_row_height = 0;
	}

}
