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

//! Unicode text processing: BiDi visual reordering folded into
//! HarfBuzz shaping.
//!
//! [`shape_line`] is the main entry point. It takes a logical-order
//! string, a font resolver ("for this codepoint, which
//! `FontHandle` should we use?") and the pixel size, runs the
//! Unicode Bidirectional Algorithm over the input, splits each
//! BiDi run into sub-runs that share a font, calls [`shape_run`]
//! per sub-run, and returns a [`PositionedGlyph`] sequence in
//! visual order. Renderers consume that sequence directly: each
//! `PositionedGlyph` carries the per-font `glyph_id`, the visual
//! advance, and ink offsets, which is exactly what
//! `fontdue::Font::rasterize_indexed` needs to render Arabic
//! connected forms, Devanagari clusters and CJK shaped glyphs
//! correctly.
//!
//! [`shape_run`] is the lower-level entry point — useful in tests
//! and when a caller has already done its own bidi / sub-run
//! splitting.

/// Single shaped glyph returned by [`shape_run`]. Horizontal-only —
/// vertical-script metrics (`y_advance`) and the source-string
/// `cluster` index that rustybuzz also returns are dropped because
/// no caller in the crate consumes them; add them back when a
/// caller actually needs vertical layout or text-edit
/// cluster-aware caret navigation.
pub struct ShapedGlyph
{
	/// Glyph index in the font (not a Unicode codepoint).
	pub glyph_id:  u32,
	/// Horizontal advance in 26.6 fixed-point font units, already scaled
	/// to the requested pixel size.
	pub x_advance: f32,
	/// Horizontal glyph offset from the pen position.
	pub x_offset:  f32,
	/// Vertical glyph offset from the baseline.
	pub y_offset:  f32,
}

/// Per-glyph entry returned by [`shape_line`]. Carries the font that
/// owns the glyph (so the rasterizer caches per font, not just per
/// glyph index) and the visual position relative to the start of the
/// line.
pub struct PositionedGlyph
{
	pub glyph_id:  u32,
	pub font_id:   usize,
	pub x_advance: f32,
	pub x_offset:  f32,
	pub y_offset:  f32,
}

/// Shape `text` for visual rendering. Runs the Unicode Bidirectional
/// Algorithm over the input, then asks `resolve_font(ch)` for the
/// font handle that owns each codepoint, groups consecutive
/// codepoints sharing a font / direction into sub-runs, and
/// calls [`shape_run`] per sub-run. Output is in visual order:
/// concatenating `x_advance` of each glyph yields the rendered line
/// width.
///
/// `resolve_font` is the per-character fallback resolver (typically
/// "primary font if it has the glyph, otherwise walk the system
/// fallback chain"). Returning `None` lets the caller skip the
/// codepoint entirely.
pub fn shape_line<F>( text: &str, px: f32, mut resolve_font: F ) -> Vec<PositionedGlyph>
where
	F: FnMut( char ) -> Option<crate::system_fonts::FontHandle>,
{
	if text.is_empty() { return vec![]; }
	let bidi = unicode_bidi::BidiInfo::new( text, None );
	if bidi.paragraphs.is_empty() { return vec![]; }
	let para = &bidi.paragraphs[0];
	let ( levels, runs ) = bidi.visual_runs( para, para.range.clone() );

	let mut out: Vec<PositionedGlyph> = Vec::with_capacity( text.chars().count() );
	for run in runs
	{
		let is_rtl = levels.get( run.start ).map( |l| l.is_rtl() ).unwrap_or( false );
		let run_text = &text[ run ];
		// Split the BiDi run into sub-runs that share a single font.
		// `resolve_font` is asked once per codepoint; consecutive
		// codepoints whose handle has the same `font` pointer are
		// shaped together.
		let mut current_handle: Option<crate::system_fonts::FontHandle> = None;
		let mut sub_run = String::new();
		let flush = | handle_opt: &Option<crate::system_fonts::FontHandle>, sub_run: &mut String, out: &mut Vec<PositionedGlyph> |
		{
			if sub_run.is_empty() { return; }
			let Some( h ) = handle_opt.as_ref() else { sub_run.clear(); return };
			let glyphs = shape_run( sub_run, &h.bytes, h.face, px, is_rtl );
			let font_id = std::sync::Arc::as_ptr( &h.font ) as usize;
			for g in glyphs
			{
				out.push( PositionedGlyph
				{
					glyph_id:  g.glyph_id,
					font_id,
					x_advance: g.x_advance,
					x_offset:  g.x_offset,
					y_offset:  g.y_offset,
				} );
			}
			sub_run.clear();
		};
		for ch in run_text.chars()
		{
			let handle = resolve_font( ch );
			let same = match ( &current_handle, &handle )
			{
				( Some( a ), Some( b ) ) => std::sync::Arc::ptr_eq( &a.font, &b.font ),
				( None,      None      ) => true,
				_                        => false,
			};
			if !same
			{
				flush( &current_handle, &mut sub_run, &mut out );
				current_handle = handle.clone();
			}
			sub_run.push( ch );
		}
		flush( &current_handle, &mut sub_run, &mut out );
	}
	out
}

thread_local!
{
	/// Shaped-line cache. [`shape_line`] re-parses the font face
	/// (`rustybuzz::Face::from_slice`) and re-runs HarfBuzz on every call;
	/// during an animation the same labels are shaped every frame, at both
	/// measure and draw time. Caching the visual-order glyph run by
	/// `(font context, pixel size, text)` makes those repeats free. Keyed by
	/// the resolver's leading font (its `Arc` pointer) since the output
	/// depends on which face leads; the system fallback chain is
	/// process-global and stable, so it need not be in the key.
	static SHAPE_CACHE: std::cell::RefCell<
		std::collections::HashMap<( usize, u32, String ), std::sync::Arc<Vec<PositionedGlyph>>>
	> = std::cell::RefCell::new( std::collections::HashMap::new() );
}

/// Soft cap on cached shaped lines; cleared wholesale on overflow (the live
/// set of on-screen labels is far smaller than this).
const SHAPE_CACHE_CAP: usize = 8192;

/// Measure a single line of `text` at `size` px with the default UI font (and
/// the system fallback chain), returning `(width, line_height)` in pixels. For
/// laying text out without a live [`crate::Canvas`] — e.g. an embedded Android
/// measure pass that needs the same metrics the renderer will use.
pub fn measure_text( text: &str, size: f32 ) -> ( f32, f32 )
{
	let primary = crate::system_fonts::primary_handle();
	let line_h  = primary.font.horizontal_line_metrics( size ).map( |m| m.new_line_size ).unwrap_or( size * 1.3 );
	if text.is_empty()
	{
		return ( 0.0, line_h );
	}
	let resolver = primary.clone();
	let glyphs = shape_line( text, size, move | ch |
	{
		if resolver.font.lookup_glyph_index( ch ) != 0
		{
			Some( resolver.clone() )
		}
		else
		{
			crate::system_fonts::lookup_handle( ch ).or_else( || Some( resolver.clone() ) )
		}
	} );
	let width: f32 = glyphs.iter().map( | g | g.x_advance ).sum();
	( width, line_h )
}

/// Cached wrapper over [`shape_line`]. `font_ctx` identifies the resolver's
/// leading font so two fonts shaping the same text don't collide — pass
/// `std::sync::Arc::as_ptr( &handle.font ) as usize`. `resolve_font` runs only
/// on a cache miss.
pub fn shape_line_cached<F>( text: &str, px: f32, font_ctx: usize, resolve_font: F ) -> std::sync::Arc<Vec<PositionedGlyph>>
where
	F: FnMut( char ) -> Option<crate::system_fonts::FontHandle>,
{
	if text.is_empty() { return std::sync::Arc::new( Vec::new() ); }
	let key = ( font_ctx, px.to_bits(), text.to_owned() );
	if let Some( hit ) = SHAPE_CACHE.with( |c| c.borrow().get( &key ).cloned() )
	{
		return hit;
	}
	let shaped = std::sync::Arc::new( shape_line( text, px, resolve_font ) );
	SHAPE_CACHE.with( |c|
	{
		let mut c = c.borrow_mut();
		if c.len() >= SHAPE_CACHE_CAP { c.clear(); }
		c.insert( key, std::sync::Arc::clone( &shaped ) );
	} );
	shaped
}

/// Drop every cached shaped line. Called when the font registry changes so a
/// new face set can't be served stale glyph runs keyed by a reused pointer.
pub fn clear_shape_cache()
{
	SHAPE_CACHE.with( |c| c.borrow_mut().clear() );
}

/// Shape a text run using rustybuzz (HarfBuzz-compatible shaping) and
/// return the glyph sequence with ink positions.
///
/// `font_bytes` must be the raw bytes of the font file (TrueType or
/// OpenType); `face_index` is 0 for single-face files and the TTC
/// sub-face index otherwise. `px` is the point size in physical pixels.
/// `rtl` selects right-to-left shaping (set from the BiDi run level).
///
/// Returns an empty vec if the font cannot be parsed or the string is
/// empty.
pub fn shape_run( text: &str, font_bytes: &[u8], face_index: u32, px: f32, rtl: bool ) -> Vec<ShapedGlyph>
{
	use rustybuzz::{ UnicodeBuffer, Direction };

	if text.is_empty() { return vec![]; }
	let Some( face ) = rustybuzz::Face::from_slice( font_bytes, face_index ) else
	{
		return vec![];
	};

	let units_per_em = face.units_per_em() as f32;
	if units_per_em <= 0.0 { return vec![]; }
	let scale        = px / units_per_em;

	let mut buf = UnicodeBuffer::new();
	buf.push_str( text );
	buf.set_direction( if rtl { Direction::RightToLeft } else { Direction::LeftToRight } );

	let output = rustybuzz::shape( &face, &[], buf );
	let infos  = output.glyph_infos();
	let pos    = output.glyph_positions();

	infos.iter().zip( pos.iter() ).map( |( info, p )|
	{
		ShapedGlyph
		{
			glyph_id:  info.glyph_id,
			x_advance: p.x_advance as f32 * scale,
			x_offset:  p.x_offset  as f32 * scale,
			y_offset:  p.y_offset  as f32 * scale,
		}
	} ).collect()
}
