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

//! Shadow primitives: outer drop shadows, inner inset shadows, and the
//! blend modes used to composite them.
//!
//! # Units
//!
//! [`Shadow::blur`] and [`InsetShadow::blur`] store the **CSS blur radius**,
//! not the SVG `stdDeviation`. The relationship is `blur = 2 × stdDeviation`,
//! which is what browsers compute for `box-shadow: … blur …`. The shader
//! integrates against `sigma`, so it applies `sigma = blur / 2` internally
//! (see [`Shadow::sigma`]).
//!
//! # Order
//!
//! A theme's `shadows` list is stored **back-to-front**, mirroring SVG's
//! `feBlend` stacking order. The first entry is painted first (lowest layer),
//! the last entry is painted last (topmost). This is the inverse of CSS
//! `box-shadow` string order. Documented here so the renderer loop (`for
//! shadow in shadows { ... }`) produces the visually correct result without
//! reversing.

use crate::types::Color;

// ─── Blend modes ─────────────────────────────────────────────────────────────

/// How a shadow composites against the layers below it.
///
/// All modes assume **premultiplied** colour and alpha. The GPU pipeline is
/// expected to be premul-correct; the software pipeline must premultiply
/// before applying these formulas.
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum BlendMode
{
	/// Standard `src-over`: `result = src + dst × (1 − src.a)`.
	Normal,
	/// CSS `plus-lighter`: `result = min(1, src + dst)`, channel-wise on
	/// premultiplied values. Adds light; never darkens.
	PlusLighter,
	/// Overlay (multiply on dark base, screen on light base). Preserves the
	/// base's luminance while pushing local contrast.
	Overlay,
	/// Multiplicative blend: `result = src × dst`. Only darkens.
	Multiply,
	/// Screen blend: `result = 1 − (1 − src) × (1 − dst)`. Only lightens.
	Screen,
}

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

// ─── Outer shadow ────────────────────────────────────────────────────────────

/// An outer drop shadow cast by a shape.
///
/// Modelled after CSS `box-shadow`: an offset, a blur radius, an optional
/// spread that dilates (positive) or erodes (negative) the silhouette before
/// blurring, a colour, and a blend mode.
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub struct Shadow
{
	/// `[dx, dy]` offset in CSS pixels. Positive `dy` is downward.
	pub offset: [f32; 2],
	/// CSS blur radius in pixels (2 × SVG `stdDeviation`).
	pub blur:   f32,
	/// Spread in CSS pixels. Positive values dilate the silhouette before
	/// blurring; negative values erode it. Usually `0.0`.
	pub spread: f32,
	/// Shadow colour, including alpha.
	pub color:  Color,
	/// Compositing mode. Most drop shadows use [`BlendMode::Normal`].
	pub blend:  BlendMode,
}

impl Shadow
{
	/// Gaussian sigma derived from the CSS blur radius.
	///
	/// The shader integrates Gaussian kernels against `sigma`, but the public
	/// field stores the CSS blur radius for parity with CSS `box-shadow`.
	/// The relationship is `sigma = blur / 2`.
	pub fn sigma( &self ) -> f32 { self.blur * 0.5 }
}

// ─── Inner shadow ────────────────────────────────────────────────────────────

/// An inset shadow: a shadow painted **inside** the shape's silhouette, as
/// opposed to the outer drop shadow cast behind it.
///
/// Structurally identical to [`Shadow`]; kept as a separate type so the
/// renderer and theme JSON can't accidentally treat an inset as an outer
/// (and vice versa) — the dispatch is at the type level.
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub struct InsetShadow
{
	/// `[dx, dy]` offset in CSS pixels. Positive `dy` is downward.
	pub offset: [f32; 2],
	/// CSS blur radius in pixels (2 × SVG `stdDeviation`).
	pub blur:   f32,
	/// Spread in CSS pixels.
	pub spread: f32,
	/// Shadow colour, including alpha.
	pub color:  Color,
	/// Compositing mode against the layers below. Insets routinely use
	/// non-`Normal` modes (`PlusLighter` for highlights, `Overlay` for rim).
	pub blend:  BlendMode,
}

impl InsetShadow
{
	/// Gaussian sigma derived from the CSS blur radius. See [`Shadow::sigma`].
	pub fn sigma( &self ) -> f32 { self.blur * 0.5 }
}

// ─── Shadow reference ────────────────────────────────────────────────────────

/// How a [`crate::theme::Surface`] refers to its outer shadow stack: either
/// by name (reused across several surfaces — the common case for elevation
/// tokens) or inline (one-off, uncommon).
#[ derive( Debug, Clone, PartialEq ) ]
pub enum ShadowsRef
{
	/// Reference to another slot in the theme, by id.
	Named( String ),
	/// The shadow list, carried inline. Used when the surface is exotic
	/// enough that the stack isn't worth a dedicated slot.
	Inline( Vec<Shadow> ),
}

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

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

	#[ test ]
	fn sigma_is_half_of_css_blur()
	{
		// CSS blur 4 → SVG stdDev 2. The shader needs sigma == stdDev == 2.
		let s = Shadow
		{
			offset: [ 0.0, 2.0 ],
			blur:   4.0,
			spread: 0.0,
			color:  Color::rgba( 0.0, 0.0, 0.0, 0.04 ),
			blend:  BlendMode::Normal,
		};
		assert_eq!( s.sigma(), 2.0 );
	}

	#[ test ]
	fn inset_sigma_follows_same_convention()
	{
		let i = InsetShadow
		{
			offset: [ -3.6, -3.6 ],
			blur:   13.5,
			spread: 0.0,
			color:  Color::hex( 0x55, 0x55, 0x55 ),
			blend:  BlendMode::PlusLighter,
		};
		assert!( ( i.sigma() - 6.75 ).abs() < 1e-6 );
	}

	#[ test ]
	fn default_blend_mode_is_normal()
	{
		assert_eq!( BlendMode::default(), BlendMode::Normal );
	}

	#[ test ]
	fn shadows_ref_distinguishes_named_and_inline()
	{
		let n = ShadowsRef::Named( "shadows-2".to_string() );
		let i = ShadowsRef::Inline( vec!
		[
			Shadow
			{
				offset: [ 0.0, 4.0 ],
				blur:   10.0,
				spread: 0.0,
				color:  Color::rgba( 0.0, 0.0, 0.0, 0.08 ),
				blend:  BlendMode::Normal,
			},
		]);
		match n { ShadowsRef::Named( ref s ) => assert_eq!( s, "shadows-2" ), _ => panic!() }
		match i { ShadowsRef::Inline( v )    => assert_eq!( v.len(), 1 ),    _ => panic!() }
	}
}
