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

//! Text rendering for [`SoftwareCanvas`]. The line is shaped through
//! [`crate::text_shaping::shape_line`] (BiDi reordering + per-sub-run
//! rustybuzz / HarfBuzz shaping) and each shaped glyph is rasterised
//! by glyph index via `fontdue::Font::rasterize_indexed`. The cache
//! is keyed on `(glyph_id, size, font_id)` so Arabic connected
//! forms, Devanagari clusters, and CJK shaped glyphs cluster
//! correctly without colliding with the per-codepoint cache the
//! old path used.

use std::sync::Arc;

use fontdue::Font;

use crate::types::Color;

use super::{ GlyphEntry, GlyphKey, SoftwareCanvas };

const GLYPH_CACHE_SOFT_CAP: usize = 8192;

impl SoftwareCanvas
{
	fn evict_if_full( &mut self, key: &GlyphKey )
	{
		if !self.glyph_cache.contains_key( key )
			&& self.glyph_cache.len() >= GLYPH_CACHE_SOFT_CAP
		{
			let drop_n = self.glyph_cache.len() / 2;
			let victims: Vec<_> = self.glyph_cache.keys().copied().take( drop_n ).collect();
			for k in victims
			{
				self.glyph_cache.remove( &k );
			}
		}
	}

	/// Rasterise glyph `glyph_id` in `font` at `scaled` px, caching
	/// the result under `(glyph_id, size_bits, font_id)`. Used by
	/// both the public `draw_text` and `draw_text_with_font` paths
	/// after shaping has resolved every codepoint to a glyph index.
	fn rasterize_indexed_cached( &mut self, font: &Arc<Font>, glyph_id: u16, scaled: f32, font_id: usize ) -> &GlyphEntry
	{
		let key = GlyphKey { glyph_id, size_bits: scaled.to_bits(), font_id };
		self.evict_if_full( &key );
		if !self.glyph_cache.contains_key( &key )
		{
			let ( metrics, bitmap ) = font.rasterize_indexed( glyph_id, scaled );
			self.glyph_cache.insert( key, GlyphEntry { metrics, bitmap } );
		}
		self.glyph_cache.get( &key ).expect( "inserted above on miss" )
	}

	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 );
	}

	/// Draw `text` using the explicitly supplied font instead of the
	/// canvas default + fallback chain.
	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>> )
	{
		let scaled  = size * self.dpi_scale;
		let line_h  = scaled * 1.5;
		let ph      = self.pixmap.height() as f32;
		let pw      = self.pixmap.width()  as f32;
		if y + line_h < 0.0 || y - line_h > ph { return; }
		if x > pw { return; }
		if self.has_clip() && !self.strip_intersects_clip( y - line_h, y + 0.5 * line_h )
		{
			return;
		}

		// Resolve the font handle (font + raw bytes + face index) per
		// codepoint. When the caller supplied a preferred font (a
		// theme-resolved bold / italic), we still consult the
		// fallback chain whenever the preferred face does not own
		// the requested glyph, so a Sora-Bold label that includes a
		// CJK character still picks up Noto Sans CJK for that one
		// codepoint.
		let canvas_handle =
		{
			let h = self.font_handle();
			match font
			{
				Some( f ) if Arc::ptr_eq( f, &h.font ) => h,
				Some( f )                              => crate::system_fonts::FontHandle
				{
					font:  Arc::clone( f ),
					bytes: Arc::new( Vec::new() ),
					face:  0,
				},
				None                                   => h,
			}
		};
		let resolve = |ch: char| -> Option<crate::system_fonts::FontHandle>
		{
			if canvas_handle.font.lookup_glyph_index( ch ) != 0 && !canvas_handle.bytes.is_empty()
			{
				return Some( canvas_handle.clone() );
			}
			crate::system_fonts::lookup_handle( ch ).or_else( ||
				if !canvas_handle.bytes.is_empty() { Some( canvas_handle.clone() ) } else { None }
			)
		};

		// `shape_line` returns glyphs in visual order with HarfBuzz
		// advance widths and offsets. An empty result (rustybuzz
		// refused the font, missing bytes for the preferred face)
		// falls through to no-op — we'd rather not paint than risk
		// a corrupted line.
		let shaped = crate::text_shaping::shape_line_cached(
			text, scaled, Arc::as_ptr( &canvas_handle.font ) as usize, resolve,
		);
		if shaped.is_empty() { return; }

		// Resolve every glyph's font into an `Arc<Font>` for
		// rasterization — we keep a per-font-id index into a small
		// vec of `Arc<Font>` so the rasterizer step does not have to
		// re-walk the resolve fn (mut self borrow conflicts).
		let mut fonts: Vec<( usize, Arc<Font> )> = Vec::new();
		let primary_id = Arc::as_ptr( &canvas_handle.font ) as usize;
		if !canvas_handle.bytes.is_empty()
		{
			fonts.push( ( primary_id, Arc::clone( &canvas_handle.font ) ) );
		}
		for g in shaped.iter()
		{
			if fonts.iter().any( |( id, _ )| *id == g.font_id ) { continue; }
			// Walk the fallback chain to find an Arc<Font> with this
			// id — this only ever runs once per (font, line) pair
			// because we cache by id in `fonts`.
			let mut found = None;
			// Try every char in the text — the resolve fn is monotone
			// per char so checking the chars yields every distinct
			// font that the shaper saw.
			for ch in text.chars()
			{
				if let Some( h ) = crate::system_fonts::lookup_handle( ch )
				{
					let id = Arc::as_ptr( &h.font ) as usize;
					if id == g.font_id { found = Some( h.font ); break; }
				}
			}
			if let Some( f ) = found
			{
				fonts.push( ( g.font_id, f ) );
			}
		}

		let mut layout: Vec<( GlyphKey, f32, f32 )> = Vec::with_capacity( shaped.len() );
		{
			let mut cursor_x = x;
			for g in shaped.iter()
			{
				let Some( ( _, font_arc ) ) = fonts.iter().find( |( id, _ )| *id == g.font_id ) else
				{
					cursor_x += g.x_advance;
					continue;
				};
				let glyph_id = g.glyph_id as u16;
				let _ = self.rasterize_indexed_cached( font_arc, glyph_id, scaled, g.font_id );
				let key = GlyphKey { glyph_id, size_bits: scaled.to_bits(), font_id: g.font_id };
				layout.push( ( key, cursor_x + g.x_offset, g.y_offset ) );
				cursor_x += g.x_advance;
			}
		}

		let w = self.pixmap.width() as i32;
		let h = self.pixmap.height() as i32;
		let cr = (color.r * 255.0) as u8;
		let cg = (color.g * 255.0) as u8;
		let cb = (color.b * 255.0) as u8;
		let color_a = color.a * self.global_alpha;

		let pixels    = self.pixmap.data_mut();
		let cache     = &self.glyph_cache;
		let mask_data = self.clip_mask.as_ref().map( |m| ( m.data(), m.width() as i32 ) );

		for ( key, cursor_x, glyph_y_offset ) in layout
		{
			let entry   = cache.get( &key ).expect( "warmed above" );
			let metrics = &entry.metrics;
			let bitmap  = &entry.bitmap;
			if metrics.width == 0 || metrics.height == 0 { continue; }
			// Round the pen position to the nearest pixel (matching the GLES
			// backend) so a glyph at a fractional x/y lands on the same integer
			// origin on both backends; the integer glyph metrics are added after.
			let gx = cursor_x.round() as i32 + metrics.xmin;
			let gy = ( y - glyph_y_offset ).round() as i32
				- metrics.ymin as i32
				- metrics.height as i32
				+ 1;
			for ( i, &alpha ) in bitmap.iter().enumerate()
			{
				if alpha == 0 { continue; }
				let px = gx + (i % metrics.width) as i32;
				let py = gy + (i / metrics.width) as i32;
				if px < 0 || py < 0 || px >= w || py >= h { continue; }
				if let Some( ( md, mw ) ) = mask_data
				{
					if md[ ( py * mw + px ) as usize ] == 0 { continue; }
				}
				let idx = (py as usize * w as usize + px as usize) * 4;
				let a   = (alpha as f32 / 255.0) * color_a;
				let inv = 1.0 - a;
				pixels[idx]     = (cr as f32 * a + pixels[idx]     as f32 * inv) as u8;
				pixels[idx + 1] = (cg as f32 * a + pixels[idx + 1] as f32 * inv) as u8;
				pixels[idx + 2] = (cb as f32 * a + pixels[idx + 2] as f32 * inv) as u8;
				let a_dst = pixels[idx + 3] as f32 / 255.0;
				pixels[idx + 3] = ( ( a + a_dst * inv ) * 255.0 ) as u8;
			}
		}
	}

	pub fn measure_text( &self, text: &str, size: f32 ) -> f32
	{
		self.measure_with_font( text, size, None )
	}

	pub fn measure_text_with_font( &self, text: &str, size: f32, font: &Arc<Font> ) -> f32
	{
		self.measure_with_font( text, size, Some( font ) )
	}

	fn measure_with_font( &self, text: &str, size: f32, font: Option<&Arc<Font>> ) -> f32
	{
		let scaled = size * self.dpi_scale;
		let canvas_handle =
		{
			let h = self.font_handle();
			match font
			{
				Some( f ) if Arc::ptr_eq( f, &h.font ) => h,
				Some( f )                              => crate::system_fonts::FontHandle
				{
					font:  Arc::clone( f ),
					bytes: Arc::new( Vec::new() ),
					face:  0,
				},
				None                                   => h,
			}
		};
		let resolve = |ch: char| -> Option<crate::system_fonts::FontHandle>
		{
			if canvas_handle.font.lookup_glyph_index( ch ) != 0 && !canvas_handle.bytes.is_empty()
			{
				return Some( canvas_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( &canvas_handle.font ) as usize, resolve,
		);
		if shaped.is_empty()
		{
			// Fallback: rustybuzz could not shape (no bytes for the
			// preferred font, no fallback covers the codepoints).
			// Sum per-codepoint advances so layout still makes a
			// vaguely useful decision.
			return text.chars().map( |ch|
			{
				let f = font.map( Arc::clone ).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,
		}
	}
}
