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

//! Paint primitives: the "what do we fill a shape with" side of theming.
//!
//! A [`Paint`] is either a flat [`Color`], a [`LinearGradient`] or a
//! [`RadialGradient`]. Gradients carry their colour stops, a direction (angle
//! for linear, center+radius for radial) and the [`GradientSpace`] in which
//! stops are interpolated. Sampling happens downstream in the renderer; these
//! types are pure data and have no rendering logic of their own.
//!
//! # Stop positions
//!
//! Stops are represented as fractions (`0.0..=1.0` for the caller's mental
//! model) but the [`ColorStop::position`] field accepts **values outside
//! that range**. This is intentional: design-tool exports often emit
//! gradients whose stops fall outside `[0, 1]`, meaning the visible region
//! of the shape only covers a middle slice of the interpolation. The
//! renderer is expected to extrapolate linearly, not clamp.
//!
//! # Colour space
//!
//! Picking the right interpolation space matters for saturated gradients:
//! interpolating `#04D9FE → #8A38F5` in sRGB produces a muddy grey in the
//! middle, while Oklab keeps the chroma. The space is resolved at theme-load
//! time (stops converted once), not per pixel.

use crate::types::Color;

// ─── Gradient space ──────────────────────────────────────────────────────────

/// Colour space in which a gradient's stops are interpolated.
///
/// The default is [`GradientSpace::LinearRgb`]: cheap, physically correct, and
/// a clear win over naive sRGB interpolation. [`GradientSpace::Oklab`] is kept
/// as an opt-in for brand gradients with high-chroma endpoints where even
/// linear-light shows an undesirable darkening in the mid-point. [`GradientSpace::Srgb`]
/// exists primarily to reproduce designs that were authored against it
/// byte-for-byte.
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum GradientSpace
{
	/// Interpolate directly in sRGB gamma-encoded space. Cheap, but midpoints
	/// of saturated gradients look muddy.
	Srgb,
	/// Interpolate in linear-light RGB. Default. Physically correct and fast.
	LinearRgb,
	/// Interpolate in the Oklab perceptual colour space. Best for saturated
	/// brand gradients; slightly more expensive.
	Oklab,
}

impl Default for GradientSpace
{
	fn default() -> Self { GradientSpace::LinearRgb }
}

// ─── Colour stops ────────────────────────────────────────────────────────────

/// One stop of a gradient: a `position` along the gradient axis (for linear)
/// or along the radius (for radial), plus the [`Color`] at that point.
///
/// `position` is a fraction but **may fall outside `[0.0, 1.0]`**. See the
/// module-level note on extrapolation.
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub struct ColorStop
{
	/// Position along the gradient. `0.0` is the start, `1.0` is the end;
	/// values outside this range are allowed and will be extrapolated.
	pub position: f32,
	/// Colour at this position.
	pub color:    Color,
}

// ─── Linear gradient ─────────────────────────────────────────────────────────

/// A straight gradient swept along a vector set by an angle.
///
/// The angle convention follows CSS `linear-gradient`: `0deg` points from the
/// bottom edge towards the top, `90deg` from left to right, `180deg` from top
/// to bottom, and so on.
#[ derive( Debug, Clone, PartialEq ) ]
pub struct LinearGradient
{
	/// Direction of the gradient, in degrees (CSS convention).
	pub angle_deg: f32,
	/// Stops along the axis, in source order. The renderer does not require
	/// them to be sorted by position — it sorts internally — but keeping them
	/// in increasing order is conventional and makes diffs readable.
	pub stops:     Vec<ColorStop>,
	/// Space in which stops are interpolated. See [`GradientSpace`].
	pub space:     GradientSpace,
}

// ─── Radial gradient ─────────────────────────────────────────────────────────

/// A gradient radiating from a centre point out to a radius.
///
/// `center` and `radius` are expressed as **fractions of the bounding box**
/// (not pixels) so the gradient scales with the widget. `center: [0.5, 0.5]`
/// and `radius: 0.5` describes a circle inscribed in a square widget.
#[ derive( Debug, Clone, PartialEq ) ]
pub struct RadialGradient
{
	/// Centre of the gradient in box-relative coordinates, `[0.0, 1.0]`.
	pub center: [f32; 2],
	/// Radius of the gradient in box-relative units. `0.5` reaches the edge
	/// of a square box, `1.0` reaches the corner diagonally.
	pub radius: f32,
	/// Stops along the radius, in source order.
	pub stops:  Vec<ColorStop>,
	/// Space in which stops are interpolated. See [`GradientSpace`].
	pub space:  GradientSpace,
}

// ─── Paint ───────────────────────────────────────────────────────────────────

/// How a shape is filled: a flat colour or one of the gradient variants.
///
/// Theming consumers rarely construct [`Paint`] directly: a slot of kind
/// `color` is promoted to [`Paint::Solid`] automatically when the widget asks
/// for a paint. Only slots that actually declare a gradient in the theme JSON
/// round-trip through [`Paint::Linear`] / [`Paint::Radial`].
#[ derive( Debug, Clone, PartialEq ) ]
pub enum Paint
{
	/// A uniform fill with a single [`Color`].
	Solid( Color ),
	/// A linear gradient sweep.
	Linear( LinearGradient ),
	/// A radial gradient sweep.
	Radial( RadialGradient ),
}

impl Paint
{
	/// Convenience constructor for the common solid case.
	pub fn solid( color: Color ) -> Self
	{
		Paint::Solid( color )
	}
}

impl From<Color> for Paint
{
	fn from( c: Color ) -> Self { Paint::Solid( c ) }
}

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

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

	#[ test ]
	fn stops_positions_outside_unit_range_are_accepted()
	{
		// Construction must succeed; the renderer will extrapolate.
		let grad = LinearGradient
		{
			angle_deg: 152.77,
			stops: vec!
			[
				ColorStop { position: -1.1654, color: Color::hex( 0xFF, 0x93, 0xA9 ) },
				ColorStop { position:  1.2332, color: Color::WHITE },
			],
			space: GradientSpace::LinearRgb,
		};
		assert_eq!( grad.stops.len(), 2 );
		assert!( grad.stops[0].position < 0.0 );
		assert!( grad.stops[1].position > 1.0 );
	}

	#[ test ]
	fn default_gradient_space_is_linear_rgb()
	{
		assert_eq!( GradientSpace::default(), GradientSpace::LinearRgb );
	}

	#[ test ]
	fn paint_promotes_from_color()
	{
		let c = Color::hex( 0x04, 0xD9, 0xFE );
		let p: Paint = c.into();
		assert_eq!( p, Paint::Solid( c ) );
	}

	#[ test ]
	fn radial_gradient_uses_box_relative_coordinates()
	{
		// Documenting the contract: center + radius are fractions, not pixels.
		let g = RadialGradient
		{
			center: [ 0.5, 0.5 ],
			radius: 0.5,
			stops:  vec!
			[
				ColorStop { position: 0.0, color: Color::WHITE },
				ColorStop { position: 1.0, color: Color::TRANSPARENT },
			],
			space:  GradientSpace::LinearRgb,
		};
		assert_eq!( g.center, [ 0.5, 0.5 ] );
		assert_eq!( g.radius, 0.5 );
	}
}
