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

use std::sync::Arc;
use crate::types::{ Point, Rect };
use crate::render::Canvas;
use super::Element;

mod theme;
#[ cfg( test ) ]
mod tests;

/// Which axis a slider tracks. Used by input dispatch to pick the right
/// `value_from_*_in_rect` formula for a [`Slider`] (horizontal) or
/// [`crate::widget::vslider::VSlider`] (vertical).
#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ]
pub enum SliderAxis
{
	Horizontal,
	Vertical,
}

/// Compute the slider value `[0.0, 1.0]` from a pointer position within a
/// slider's layout rect, dispatching to the axis-specific formula.
///
/// Exposed alongside [`value_from_x_in_rect`] so `input.rs` can drive both
/// [`Slider`] and [`crate::widget::vslider::VSlider`] through the same call
/// site by consulting the [`SliderAxis`] stored in the widget's handler
/// snapshot.
pub fn value_from_pos_in_rect( rect: Rect, pos: Point, axis: SliderAxis, thumb_px: f32 ) -> f32
{
	match axis
	{
		// The vertical branch maps against the full rect height and takes no
		// thumb inset, so `thumb_px` is unused there.
		SliderAxis::Horizontal => value_from_x_in_rect( rect, pos.x, thumb_px ),
		SliderAxis::Vertical   => crate::widget::vslider::value_from_y_in_rect( rect, pos.y ),
	}
}

/// Intersect `inner` with the `saved` outer clip and return the rect
/// list to install with [`crate::render::Canvas::set_clip_rects`].
///
/// `saved` is the snapshot returned by
/// [`crate::render::Canvas::clip_bounds`]: empty means "no clip is
/// installed" (paint everywhere), non-empty is the outer scissor list
/// — typically the partial-redraw damage rects.
///
/// `Slider` / [`crate::widget::vslider::VSlider`] use this when they
/// need to clip the active-side fill paint to a band tighter than the
/// widget rect. Calling `set_clip_rects( &[ band ] )` directly would
/// REPLACE the outer clip, causing the fill to repaint outside the
/// damage region during partial redraws and clobbering pixels (most
/// visibly the thumb's white interior left from the previous frame).
/// Routing through this helper preserves the outer clip by
/// intersecting with it.
///
/// Returns `Vec::new()` when there is no overlap; callers must skip
/// the paint entirely in that case — installing an empty rect list
/// means "no clip" which would paint outside the damage region.
pub( crate ) fn intersect_clip( saved: &[ Rect ], inner: Rect ) -> Vec<Rect>
{
	if saved.is_empty()
	{
		return vec![ inner ];
	}
	saved.iter().filter_map( |r|
	{
		let x0 = inner.x.max( r.x );
		let y0 = inner.y.max( r.y );
		let x1 = ( inner.x + inner.width  ).min( r.x + r.width  );
		let y1 = ( inner.y + inner.height ).min( r.y + r.height );
		if x1 <= x0 || y1 <= y0
		{
			None
		}
		else
		{
			Some( Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } )
		}
	} ).collect()
}

/// Compute the slider value `[0.0, 1.0]` from a tap/drag x position within a
/// slider's layout rect. Pure — depends only on `theme::THUMB_SIZE`. Lifted
/// out of [`Slider`] so input handlers can call it directly from
/// [`crate::widget::LaidOutWidget`] without needing the [`Element`] tree.
pub fn value_from_x_in_rect( rect: Rect, x: f32, thumb_px: f32 ) -> f32
{
	let pad         = thumb_px / 2.0;
	let track_start = rect.x + pad;
	let track_end   = rect.x + rect.width - pad;
	let track_w     = ( track_end - track_start ).max( 1.0 );
	( ( x - track_start ) / track_w ).clamp( 0.0, 1.0 )
}

/// The slider thumb's design pixel size — the raw theme constant, used as a
/// fallback when a widget-scaling-resolved size is not available (e.g. a
/// handler snapshot built outside layout). Public for `test_support`, so the
/// pure `value_from_*` helpers can be exercised with the real design thumb.
pub fn thumb_design_px() -> f32 { theme::THUMB_SIZE }

/// Resolve the thumb size through the widget-scaling mode. The layout pass
/// stores this in the [`crate::widget::WidgetHandlers::Slider`] snapshot so
/// the input path maps clicks against the same thumb the renderer drew.
pub( crate ) fn resolved_thumb_px( canvas: &Canvas ) -> f32
{
	canvas.geom_px( theme::THUMB_SIZE )
}

/// A horizontal slider for selecting a value in a range.
///
/// The track defaults to a theme-resolved surface; pass
/// [`Self::track_paint`] to override with a custom
/// [`Paint`](crate::theme::Paint) — typically a multi-stop linear
/// gradient for a hue / spectrum picker. When `track_paint` is set
/// the active-side fill is suppressed (the spectrum already
/// communicates the position visually through colour).
///
/// ```rust,no_run
/// # use ltk::{ slider, Slider };
/// # #[ derive( Clone ) ] enum Msg { SetBrightness( f32 ) }
/// # struct App { brightness: f32 }
/// # impl App { fn _ex( &self ) -> Slider<Msg> {
/// slider( self.brightness )
///     .on_change( |v| Msg::SetBrightness( v ) )
/// # }}
/// ```
pub struct Slider<Msg: Clone>
{
	/// Current value in `[0.0, 1.0]`.
	pub( crate ) value:     f32,
	/// Callback invoked with the new value when the slider is dragged.
	/// `Arc` (not `Box`) so the layout pass can clone it into the per-leaf
	/// handler snapshot for O(1) dispatch on input events.
	pub( crate ) on_change: Option<Arc<dyn Fn(f32) -> Msg>>,
	/// Callback invoked once with the final value when the drag ends, so an
	/// app can keep an expensive commit off the per-motion path.
	pub( crate ) on_release: Option<Arc<dyn Fn(f32) -> Msg>>,
	/// Theme slot id for the track background pill. Defaults to the
	/// generic `surface-slider-track`; override per-instance to opt
	/// into the `-flat` (no per-surface backdrop) variant when the
	/// slider already lives inside a panel-wide blur, or any other
	/// custom slot.
	pub( crate ) track_surface: &'static str,
	/// Theme slot id for the rising / leftward fill. Same shape as
	/// [`Self::track_surface`] but for the active portion.
	pub( crate ) fill_surface:  &'static str,
	/// Paint the thumb with the active palette's `accent` colour and
	/// a thicker white border (accent inner pill + 4 px white ring)
	/// instead of the default white-on-text-primary thumb.
	pub( crate ) accent_thumb:  bool,
	/// Override the track paint with a custom
	/// [`Paint`](crate::theme::Paint) — typically a
	/// [`Paint::Linear`](crate::theme::Paint::Linear) for spectrum /
	/// hue pickers. When set, the active-side fill is suppressed
	/// because a spectrum already conveys position information
	/// through colour and the thumb shows where the user is.
	pub( crate ) track_paint:   Option<crate::theme::Paint>,
}

impl<Msg: Clone> Slider<Msg>
{
	/// Create a slider with the given value (clamped to `[0.0, 1.0]`).
	pub fn new( value: f32 ) -> Self
	{
		Self
		{
			value: value.clamp( 0.0, 1.0 ),
			on_change: None,
			on_release: None,
			track_surface: theme::SURFACE_TRACK,
			fill_surface:  theme::SURFACE_FILL,
			accent_thumb:  false,
			track_paint:   None,
		}
	}

	/// Override the track paint with a custom
	/// [`Paint`](crate::theme::Paint) — e.g. a multi-stop linear
	/// gradient for a hue / spectrum picker. Disables the active-
	/// side fill (the spectrum already encodes the position
	/// visually).
	pub fn track_paint( mut self, paint: crate::theme::Paint ) -> Self
	{
		self.track_paint = Some( paint );
		self
	}

	/// Set the callback invoked when the slider value changes.
	pub fn on_change( mut self, f: impl Fn(f32) -> Msg + 'static ) -> Self
	{
		self.on_change = Some( Arc::new( f ) );
		self
	}

	/// Set the callback invoked once when the drag ends, with the final
	/// value. Pair it with [`Self::on_change`] to move the slider without
	/// paying for the commit on every motion event.
	pub fn on_release( mut self, f: impl Fn(f32) -> Msg + 'static ) -> Self
	{
		self.on_release = Some( Arc::new( f ) );
		self
	}

	/// Override the theme slot id used to paint the track background
	/// — pass `surface-slider-track-flat` (or any other surface) to
	/// drop the per-instance backdrop blur when the slider already
	/// lives inside a panel-wide blur. See
	/// [`crate::widget::vslider::VSlider::track_surface`] for the use
	/// case.
	pub fn track_surface( mut self, id: &'static str ) -> Self
	{
		self.track_surface = id;
		self
	}

	/// Override the theme slot id used to paint the fill. Mirrors
	/// [`Self::track_surface`].
	pub fn fill_surface( mut self, id: &'static str ) -> Self
	{
		self.fill_surface = id;
		self
	}

	/// Switch the thumb to the accent-pill style: accent-coloured inner
	/// + 4 px white border. Default thumb stays white-on-text-primary
	/// for shells that haven't opted in.
	pub fn accent_thumb( mut self, on: bool ) -> Self
	{
		self.accent_thumb = on;
		self
	}

	/// Return the preferred `(width, height)`.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		( max_width, canvas.geom_px( theme::HEIGHT ) )
	}

	/// Compute the value `[0.0, 1.0]` from a tap/drag x position within `rect`.
	/// Uses the design thumb size; the live input path resolves the thumb
	/// through the widget-scaling mode via the handler snapshot instead.
	pub fn value_from_x( &self, rect: Rect, x: f32 ) -> f32
	{
		value_from_x_in_rect( rect, x, thumb_design_px() )
	}

	/// Thumb border stroke is centered on the thumb edge (which touches the
	/// widget's left/right edges at value 0 / 1), so half the stroke width
	/// plus ~1 px of antialiasing sits outside `rect`.
	pub fn paint_bounds( &self, rect: Rect ) -> Rect
	{
		let border_w = if self.accent_thumb { theme::ACCENT_THUMB_BORDER_W } else { theme::THUMB_BORDER_W };
		rect.expand( ( border_w * 0.5 + 1.0 ).max( surface_shadow_margin( self.track_surface ) ) )
	}

	/// Draw the slider into `canvas` at `rect`.
	///
	/// Track + fill paint through theme slots ([`Self::track_surface`]
	/// / [`Self::fill_surface`]) when those slots resolve in the
	/// active theme — the track pill spans the full inner width and
	/// the fill is anchored to the same pill but scissor-clipped to
	/// the left band so insets stay positioned against the track rect
	/// rather than a shrinking fill rect. Falls back to
	/// `theme::track_bg()` / `theme::track_fill()` when the active
	/// theme has no entry for either slot — a bare-bones third-party
	/// theme still paints a usable slider.
	pub fn draw( &self, canvas: &mut Canvas, rect: Rect, _focused: bool )
	{
		// Resolve the thumb / track through the widget-scaling mode. The
		// pad MUST match `value_from_x_in_rect`, which the input path feeds
		// the same mode-resolved thumb size via the handler snapshot.
		let thumb       = canvas.geom_px( theme::THUMB_SIZE );
		let track_h     = canvas.geom_px( theme::TRACK_H );
		let pad         = thumb / 2.0;
		let track_y     = rect.y + (rect.height - track_h) / 2.0;
		let track_start = rect.x + pad;
		let track_end   = rect.x + rect.width - pad;
		let track_w     = track_end - track_start;
		let radius_bg   = track_h / 2.0;

		// Background track — full pill across the inner span.
		let track_rect = Rect
		{
			x:      track_start,
			y:      track_y,
			width:  track_w,
			height: track_h,
		};
		if let Some( paint ) = self.track_paint.as_ref()
		{
			// Custom paint override (hue spectrum, custom gradient, …)
			// short-circuits the theme-slot resolution. The
			// active-side fill is suppressed below because a
			// spectrum already encodes the position via colour.
			canvas.fill_paint_rect( track_rect, paint, radius_bg );
		}
		else if let Some( ( surf, outer ) ) = crate::theme::resolve_surface( self.track_surface )
		{
			canvas.fill_surface
			(
				track_rect,
				&surf.fill,
				&outer,
				&surf.inset_shadows,
				radius_bg,
			);
		}
		else
		{
			canvas.fill_rect( track_rect, theme::track_bg(), radius_bg );
		}

		// Filled portion. Paint the surface across the FULL track rect
		// and clip the visible band to the left-aligned slice. This
		// keeps inset shadows / glass rims anchored to the track pill
		// instead of to a shrinking fill rect — the highlights don't
		// shift as the user drags.
		//
		// Suppressed entirely when `track_paint` is set: a custom
		// spectrum already conveys position through colour, and a
		// solid accent band painted over part of it would obscure
		// half the spectrum and read as a rendering bug.
		let fill_w = track_w * self.value;
		if self.track_paint.is_none() && fill_w > 0.5
		{
			let visible = Rect
			{
				x:      track_start,
				y:      track_y,
				width:  fill_w,
				height: track_h,
			};
			let saved_clip = canvas.clip_bounds();
			let band       = intersect_clip( &saved_clip, visible );
			if !band.is_empty()
			{
				canvas.set_clip_rects( &band );
				if let Some( ( surf, _ ) ) = crate::theme::resolve_surface( self.fill_surface )
				{
					canvas.fill_paint_rect( track_rect, &surf.fill, radius_bg );
					// Drop insets with negative-Y offset — they live
					// near the top of the track pill and would slice
					// the fill at the active band edge. See
					// `VSlider::draw` for the full rationale.
					for inset in surf.inset_shadows.iter().filter( |s| s.offset[1] >= 0.0 )
					{
						canvas.fill_shadow_inset( track_rect, inset, radius_bg );
					}
				}
				else
				{
					canvas.fill_rect( visible, theme::track_fill(), radius_bg );
				}
				canvas.set_clip_rects( &saved_clip );
			}
		}

		// Thumb.
		let thumb_cx = track_start + fill_w;
		let thumb_cy = rect.y + rect.height / 2.0;
		if self.accent_thumb
		{
			// Inner pill paints with the track's active fill surface
			// so the thumb's centre always matches the line colour.
			let outer_r    = theme::ACCENT_THUMB_OUTER / 2.0;
			let inner_size = ( theme::ACCENT_THUMB_OUTER - 2.0 * theme::ACCENT_THUMB_BORDER_W ).max( 0.0 );
			let inner_r    = inner_size / 2.0;
			let outer_rect = Rect
			{
				x:      thumb_cx - outer_r,
				y:      thumb_cy - outer_r,
				width:  theme::ACCENT_THUMB_OUTER,
				height: theme::ACCENT_THUMB_OUTER,
			};
			let inner_rect = Rect
			{
				x:      thumb_cx - inner_r,
				y:      thumb_cy - inner_r,
				width:  inner_size,
				height: inner_size,
			};
			canvas.fill_rect( outer_rect, crate::theme::palette().bg, outer_r );
			if let Some( ( surf, _ ) ) = crate::theme::resolve_surface( self.fill_surface )
			{
				canvas.fill_paint_rect( inner_rect, &surf.fill, inner_r );
			}
			else
			{
				canvas.fill_rect( inner_rect, crate::theme::palette().accent, inner_r );
			}
		}
		else
		{
			let thumb_r  = thumb / 2.0;
			let thumb_rect = Rect
			{
				x:      thumb_cx - thumb_r,
				y:      thumb_cy - thumb_r,
				width:  thumb,
				height: thumb,
			};
			canvas.fill_rect( thumb_rect, theme::thumb(), thumb_r );
			canvas.stroke_rect( thumb_rect, theme::thumb_border(), theme::THUMB_BORDER_W, thumb_r );
		}
	}

	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Slider<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		// Wrap the existing Arc<dyn Fn(f32) -> Msg> in a fresh closure
		// that pipes its result through the user's mapper. The original
		// closure is captured by the new one through `Arc::clone` so the
		// original Slider's storage stays valid.
		let on_change = self.on_change.map( |old| -> Arc<dyn Fn( f32 ) -> U>
		{
			let mapper = Arc::clone( f );
			Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
		} );
		let on_release = self.on_release.map( |old| -> Arc<dyn Fn( f32 ) -> U>
		{
			let mapper = Arc::clone( f );
			Arc::new( move |v| ( *mapper )( ( *old )( v ) ) )
		} );
		Slider
		{
			value:         self.value,
			on_change,
			on_release,
			track_surface: self.track_surface,
			fill_surface:  self.fill_surface,
			accent_thumb:  self.accent_thumb,
			track_paint:   self.track_paint,
		}
	}
}

/// Create a [`Slider`] with the given value (clamped to `[0.0, 1.0]`).
pub fn slider<Msg: Clone>( value: f32 ) -> Slider<Msg>
{
	Slider::new( value )
}

fn surface_shadow_margin( surface: &str ) -> f32
{
	crate::theme::resolve_surface( surface )
		.map( |( _, shadows )|
		{
			shadows.iter()
				.map( |s| s.offset[0].abs().max( s.offset[1].abs() ) + s.blur.max( 0.0 ) + s.spread.max( 0.0 ) + 1.0 )
				.fold( 0.0, f32::max )
		} )
		.unwrap_or( 0.0 )
}

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