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

//! Single- or multi-line text: a font-sized, coloured, aligned string that
//! either stays on one line (truncating with an ellipsis on overflow) or
//! word-wraps to the layout width.

use std::sync::Arc;

use fontdue::Font;

use crate::theme::FontStyle;
use crate::types::{ Color, Length, Rect };
use crate::render::Canvas;
use super::Element;

#[ cfg( test ) ]
mod tests;

/// Horizontal alignment of text within its layout rect.
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum TextAlign
{
	/// Align to the left edge.
	Left,
	/// Centre within the rect.
	Center,
	/// Align to the right edge.
	Right,
}

/// A run of text rendered with a size, colour, alignment and optional font,
/// either word-wrapped or kept on one line with ellipsis truncation.
pub struct Text
{
	pub( crate ) content:  String,
	/// Font size as a [`Length`]. Resolved against the surface's logical
	/// viewport at layout time, so a `Length::Vmin( 5.0 )` heading scales
	/// with the screen instead of being frozen at a px constant.
	pub( crate ) size:     Length,
	pub( crate ) color:    Color,
	pub( crate ) align:    TextAlign,
	pub( crate ) wrap:     bool,
	/// When `true` (default), overflowing single-line text is truncated
	/// with an ellipsis. When `false`, the full string is painted even
	/// if it extends past the layout rect — useful for very short
	/// labels (calendar day numbers, day-of-week stubs) where a couple
	/// of pixels of overflow is invisible but "..." is noisy.
	pub( crate ) truncate: bool,
	/// Optional `(family, weight, style)` override resolved through
	/// the active theme's font registry on every draw. `None` keeps
	/// the canvas default font (Sora Regular in `ltk-theme-default`).
	pub( crate ) font:    Option<( String, u16, FontStyle )>,
	/// Multiplier applied to the font's natural line height when wrapping
	/// onto multiple lines. `1.0` (default) uses the font-declared leading;
	/// values above `1.0` open up the gap between wrapped lines.
	pub( crate ) line_height: f32,
}

impl Text
{
	/// A left-aligned, non-wrapping, ellipsis-truncated white label at the
	/// default 16 px size with the canvas default font.
	pub fn new( content: impl Into<String> ) -> Self
	{
		Self
		{
			content:  content.into(),
			size:     Length::px( 16.0 ),
			color:    Color::WHITE,
			align:    TextAlign::Left,
			wrap:     false,
			truncate: true,
			font:     None,
			line_height: 1.0,
		}
	}

	/// Resolve the font size against the canvas viewport. Internal
	/// helper: every method that needs an `f32` size for a `fontdue`
	/// call routes through this so the field can stay a `Length`.
	#[ inline ]
	fn resolved_size( &self, canvas: &Canvas ) -> f32
	{
		canvas.resolve_font( self.size )
	}

	/// Paint the full string even when it overflows, instead of truncating
	/// with an ellipsis.
	pub fn no_truncate( mut self ) -> Self
	{
		self.truncate = false;
		self
	}

	/// Set a `(family, weight, style)` override for this text node.
	/// The triple is resolved through [`Canvas::font_for`] at draw
	/// time, so missing weights fall through the registry's nearest-
	/// match precedence.
	pub fn font( mut self, family: impl Into<String>, weight: u16, style: FontStyle ) -> Self
	{
		self.font = Some( ( family.into(), weight, style ) );
		self
	}

	/// Shorthand for [`Self::font`] when the desired override is just
	/// a weight on the default Sora family (the family `ltk-theme-
	/// default` declares).
	pub fn weight( mut self, weight: u16 ) -> Self
	{
		self.font = Some( ( "sora".to_string(), weight, FontStyle::Normal ) );
		self
	}

	/// Set the font size.
	pub fn size( mut self, s: impl Into<Length> ) -> Self
	{
		self.size = s.into();
		self
	}

	/// Set the text colour.
	pub fn color( mut self, c: Color ) -> Self
	{
		self.color = c;
		self
	}

	/// Set the horizontal alignment.
	pub fn align( mut self, a: TextAlign ) -> Self
	{
		self.align = a;
		self
	}

	/// Shorthand for [`Self::align`] with [`TextAlign::Center`].
	pub fn align_center( mut self ) -> Self
	{
		self.align = TextAlign::Center;
		self
	}

	/// Enable word-wrapping. With `wrap = true` the text breaks on
	/// whitespace at `max_width` and the widget reports the natural
	/// height of all the resulting lines. With `wrap = false` (the
	/// default) the text stays on one line and is truncated with an
	/// ellipsis when it overflows.
	pub fn wrap( mut self, w: bool ) -> Self
	{
		self.wrap = w;
		self
	}

	/// Multiply the natural line height for wrapped text. `1.0` keeps the
	/// font's declared leading; e.g. `1.4` opens the gap between wrapped
	/// lines. Clamped to a `0.5` floor so lines never overlap.
	pub fn line_height( mut self, mult: f32 ) -> Self
	{
		self.line_height = mult.max( 0.5 );
		self
	}

	fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<Font>>
	{
		self.font.as_ref().map( |( family, weight, style )|
		{
			canvas.font_for( family, *weight, *style )
		} )
	}

	fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc<Font>> ) -> f32
	{
		let size = self.resolved_size( canvas );
		match font
		{
			Some( f ) => canvas.measure_text_with_font( text, size, f ),
			None      => canvas.measure_text( text, size ),
		}
	}

	fn measure_char( &self, ch: char, canvas: &Canvas, font: Option<&Arc<Font>> ) -> f32
	{
		let size = self.resolved_size( canvas );
		match font
		{
			Some( f ) => f.metrics( ch, size * canvas.dpi_scale() ).advance_width,
			None      => canvas.font_metrics( ch, size ).advance_width,
		}
	}

	fn paint( &self, canvas: &mut Canvas, text: &str, x: f32, y: f32, font: Option<&Arc<Font>> )
	{
		let size = self.resolved_size( canvas );
		match font
		{
			Some( f ) => canvas.draw_text_with_font( text, x, y, size, self.color, f ),
			None      => canvas.draw_text( text, x, y, size, self.color ),
		}
	}

	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
	{
		let size   = self.resolved_size( canvas );
		// `new_line_size = ascent - descent + line_gap` is the standard
		// typographic line height; the previous `ascent - descent` discarded
		// the font-declared leading and at large sizes left adjacent rows
		// visibly overlapping when stacked tight in a column.
		let line_h = canvas.font_line_metrics( size )
			.map( |m| m.new_line_size )
			.unwrap_or( size ) * self.line_height;
		let font = self.resolve_font( canvas );

		if self.wrap
		{
			let lines = wrap_lines( &self.content, size, max_width, canvas, font.as_ref() );
			let h = line_h * lines.len().max( 1 ) as f32;
			( max_width, h )
		}
		else
		{
			let w = ( self.measure( &self.content, canvas, font.as_ref() ) + 8.0 ).min( max_width );
			( w, line_h )
		}
	}

	pub fn draw( &self, canvas: &mut Canvas, rect: Rect, _focused: bool )
	{
		let size   = self.resolved_size( canvas );
		let ascent = canvas.font_line_metrics( size )
			.map( |m| m.ascent )
			.unwrap_or( size * 0.8 );
		let line_h = canvas.font_line_metrics( size )
			.map( |m| m.new_line_size )
			.unwrap_or( size ) * self.line_height;
		let font = self.resolve_font( canvas );

		if self.wrap
		{
			let lines = wrap_lines( &self.content, size, rect.width, canvas, font.as_ref() );
			for ( i, line ) in lines.iter().enumerate()
			{
				let line_w = self.measure( line, canvas, font.as_ref() );
				let slack  = ( rect.width - line_w ).max( 0.0 );
				let pad    = 4.0_f32.min( slack );
				let tx = match self.align
				{
					TextAlign::Left   => rect.x + pad,
					TextAlign::Center => rect.x + slack / 2.0,
					TextAlign::Right  => rect.x + rect.width - line_w - pad,
				};
				let ty = rect.y + ascent + line_h * i as f32;
				self.paint( canvas, line, tx, ty, font.as_ref() );
			}
			return;
		}

		let text_w = self.measure( &self.content, canvas, font.as_ref() );

		let display = if self.truncate && text_w > rect.width && rect.width > 0.0
		{
			let ellipsis = "...";
			let ell_w = self.measure( ellipsis, canvas, font.as_ref() );
			let budget = rect.width - ell_w;
			if budget <= 0.0
			{
				ellipsis.to_string()
			}
			else
			{
				let mut accum = 0.0_f32;
				let truncated: String = self.content.chars().take_while( |ch|
				{
					let cw = self.measure_char( *ch, canvas, font.as_ref() );
					accum += cw;
					accum <= budget
				} ).collect();
				format!( "{truncated}{ellipsis}" )
			}
		}
		else
		{
			self.content.clone()
		};

		let disp_w = self.measure( &display, canvas, font.as_ref() );
		let slack  = ( rect.width - disp_w ).max( 0.0 );
		let pad    = 4.0_f32.min( slack );
		let tx = match self.align
		{
			TextAlign::Left   => rect.x + pad,
			TextAlign::Center => rect.x + slack / 2.0,
			TextAlign::Right  => rect.x + rect.width - disp_w - pad,
		};

		let ty = rect.y + ascent;
		self.paint( canvas, &display, tx, ty, font.as_ref() );
	}
}

/// Greedy word-wrap: split `text` on whitespace and pack words into
/// lines whose total width stays under `max_width`. A word longer
/// than `max_width` overflows on its own line rather than being
/// hyphenated. Routes through the font override when one is set so
/// measurement and rendering agree on advance widths.
fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<Font>> ) -> Vec<String>
{
	if max_width <= 0.0 || text.is_empty()
	{
		return vec![ text.to_string() ];
	}
	let measure = |s: &str| -> f32
	{
		match font
		{
			Some( f ) => canvas.measure_text_with_font( s, size, f ),
			None      => canvas.measure_text( s, size ),
		}
	};
	let space_w = measure( " " );
	let mut lines = Vec::new();
	let mut current   = String::new();
	let mut current_w = 0.0_f32;
	for word in text.split_whitespace()
	{
		let word_w = measure( word );
		if current.is_empty()
		{
			current.push_str( word );
			current_w = word_w;
		}
		else if current_w + space_w + word_w <= max_width
		{
			current.push( ' ' );
			current.push_str( word );
			current_w += space_w + word_w;
		}
		else
		{
			lines.push( std::mem::take( &mut current ) );
			current.push_str( word );
			current_w = word_w;
		}
	}
	if !current.is_empty() { lines.push( current ); }
	if lines.is_empty() { lines.push( String::new() ); }
	lines
}

impl<Msg: Clone + 'static> From<Text> for Element<Msg>
{
	fn from( t: Text ) -> Self
	{
		Element::Text( t )
	}
}
