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

//! Typography tokens: a resolved text style (family + weight + size + …) as
//! the theme knows it.
//!
//! The struct is deliberately named [`TextStyle`] rather than `Typography`
//! to avoid colliding with the [`super::typography`] constants module that
//! ships alongside it.
//!
//! # Resolution
//!
//! A [`TextStyle`] carries a [`FontRef`] (by name) and optionally a slot
//! reference for its default colour. Both are resolved lazily by the theme's
//! slot table: the string IDs live here, the actual font bytes and
//! [`crate::types::Color`] value come from the font registry and the slot
//! store respectively. This keeps the struct copy-cheap and JSON-friendly.

// ─── Font reference ──────────────────────────────────────────────────────────

/// Reference to a font family registered in the theme's `fonts` block.
///
/// A family name like `"sora"` maps to a [`super::FontFamilyDef`] that
/// lists the actual `.ttf` paths for each weight/style. Keeping the
/// reference by name lets multiple text styles share a family without
/// duplicating the source list.
#[ derive( Debug, Clone, PartialEq, Eq, Hash ) ]
pub enum FontRef
{
	/// Reference by family id, looked up in the theme's `fonts` block.
	Named( String ),
}

// ─── Style axes ──────────────────────────────────────────────────────────────

/// Italic vs upright.
#[ derive( Debug, Clone, Copy, PartialEq, Eq, Hash ) ]
pub enum FontStyle
{
	Normal,
	Italic,
}

impl Default for FontStyle
{
	fn default() -> Self { FontStyle::Normal }
}

/// Case transform applied at render time.
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum TextTransform
{
	/// Render the string unchanged.
	None,
	/// Uppercase every character.
	Uppercase,
	/// Lowercase every character.
	Lowercase,
	/// Uppercase the first letter of each word, leave the rest untouched.
	Capitalize,
}

impl Default for TextTransform
{
	fn default() -> Self { TextTransform::None }
}

/// Underline / strikethrough decoration.
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum TextDecoration
{
	None,
	Underline,
	Strikethrough,
}

impl Default for TextDecoration
{
	fn default() -> Self { TextDecoration::None }
}

// ─── Line height ─────────────────────────────────────────────────────────────

/// How the vertical advance between lines is specified.
///
/// Both forms are supported because design-tool exports usually emit absolute
/// pixel heights, while hand-authored themes more often prefer relative
/// line-heights that scale with the font size.
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum LineHeight
{
	/// Absolute pixel box height for the line.
	Px( f32 ),
	/// Multiplier of the font size. `1.5` means line-height is 1.5× the size.
	Multiplier( f32 ),
}

impl LineHeight
{
	/// Resolve to an absolute pixel height given the font size.
	pub fn resolve( self, font_size: f32 ) -> f32
	{
		match self
		{
			LineHeight::Px( h )         => h,
			LineHeight::Multiplier( m ) => font_size * m,
		}
	}
}

// ─── TextStyle ───────────────────────────────────────────────────────────────

/// A resolved text style: family, weight, size, line-height and the visual
/// modifiers that go with them.
///
/// `color` is a slot id (string) rather than a resolved [`crate::types::Color`]
/// because the slot store is what knows how to resolve names to values — and
/// widgets can override the colour with [`.color()`] at the call-site, which
/// always wins over the style's default.
#[ derive( Debug, Clone, PartialEq ) ]
pub struct TextStyle
{
	/// Font family, looked up in the theme's `fonts` block.
	pub family:         FontRef,
	/// Weight as a CSS numeric value (100..=900).
	pub weight:         u16,
	/// Italic vs upright.
	pub style:          FontStyle,
	/// Font size in CSS pixels.
	pub size:           f32,
	/// Line height.
	pub line_height:    LineHeight,
	/// Letter spacing in `em` (fraction of the font size). `0.0` by default.
	pub letter_spacing: f32,
	/// Case transform applied before shaping.
	pub transform:      TextTransform,
	/// Underline / strikethrough.
	pub decoration:     TextDecoration,
	/// Default colour slot id. `None` means "inherit from the widget's own
	/// colour setting". When `Some`, widgets that don't override with
	/// [`.color()`] get this.
	pub color:          Option<String>,
}

impl TextStyle
{
	/// Convenience constructor that fills the non-essential fields with
	/// sensible defaults (upright, no transform, no decoration, no default
	/// colour, no letter spacing).
	pub fn new( family: FontRef, weight: u16, size: f32, line_height: LineHeight ) -> Self
	{
		Self
		{
			family,
			weight,
			style:          FontStyle::Normal,
			size,
			line_height,
			letter_spacing: 0.0,
			transform:      TextTransform::None,
			decoration:     TextDecoration::None,
			color:          None,
		}
	}
}

// ─── Tests ───────────────────────────────────────────────────────────────────

#[ cfg( test ) ]
mod tests
{
	use super::*;

	#[ test ]
	fn new_constructor_sets_sensible_defaults()
	{
		let s = TextStyle::new
		(
			FontRef::Named( "sora".to_string() ),
			400,
			16.0,
			LineHeight::Px( 24.0 ),
		);
		assert_eq!( s.weight, 400 );
		assert_eq!( s.size,   16.0 );
		assert_eq!( s.line_height.resolve( 16.0 ), 24.0 );
		assert_eq!( s.transform, TextTransform::None );
		assert_eq!( s.letter_spacing, 0.0 );
		assert!( s.color.is_none() );
	}

	#[ test ]
	fn caption_l_upcases_at_render_time()
	{
		let s = TextStyle
		{
			family:         FontRef::Named( "sora".to_string() ),
			weight:         400,
			style:          FontStyle::Normal,
			size:           16.0,
			line_height:    LineHeight::Px( 20.0 ),
			letter_spacing: 0.0,
			transform:      TextTransform::Uppercase,
			decoration:     TextDecoration::None,
			color:          None,
		};
		assert_eq!( s.transform, TextTransform::Uppercase );
	}

	#[ test ]
	fn line_height_multiplier_scales_with_size()
	{
		let lh = LineHeight::Multiplier( 1.5 );
		assert_eq!( lh.resolve( 16.0 ), 24.0 );
		assert_eq!( lh.resolve( 12.0 ), 18.0 );
	}

	#[ test ]
	fn defaults_are_normal_and_none()
	{
		assert_eq!( FontStyle::default(),      FontStyle::Normal );
		assert_eq!( TextTransform::default(),  TextTransform::None );
		assert_eq!( TextDecoration::default(), TextDecoration::None );
	}
}
