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

//! Spinner — indeterminate progress indicator.
//!
//! A rotating arc that the renderer draws by stroking a fraction of a
//! circle and offsetting its starting angle by [`Spinner::phase`]. The
//! widget is *stateless*: the application owns the phase variable and
//! advances it from a clock or animation tick. Pair with
//! [`App::is_animating`](crate::App::is_animating) so the run loop keeps
//! requesting redraws while the spinner is on screen.
//!
//! ```rust,no_run
//! # use ltk::{ row, spinner, text, Element };
//! # #[ derive( Clone ) ] enum Msg {}
//! # struct App { tick: f32 }
//! # impl App { fn _ex( &self ) -> Element<Msg> {
//! // In view():
//! row()
//!     .push( spinner().phase( self.tick ) )
//!     .push( text( "Loading…" ) )
//! .into()
//! # }}
//! ```

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

mod theme;

/// An indeterminate progress spinner.
///
/// Renders a rotating arc inside a square layout rect. The application
/// drives the rotation by passing a monotonically-increasing `phase`
/// value (any units — only the fractional part of `phase` is used).
pub struct Spinner
{
	/// Rotation phase. Only the fractional part is consumed, so any
	/// monotonically increasing source works (`elapsed.as_secs_f32()`,
	/// frame count divided by FPS, etc.).
	pub( crate ) phase:    f32,
	/// Arc / ring colour. Defaults to the theme's `accent` palette slot.
	pub( crate ) color:    Color,
	/// Square diameter in logical pixels. Both width and height of the
	/// laid-out rect target this size. The `0.0` default follows the
	/// process [`crate::WidgetScaling`] mode; any positive value pins it.
	pub( crate ) size:     f32,
	/// Stroke width of the arc and the dim guide ring.
	pub( crate ) stroke_w: f32,
}

impl Spinner
{
	/// Create a spinner with the theme's accent colour and default size.
	pub fn new() -> Self
	{
		Self
		{
			phase:    0.0,
			color:    theme::fill(),
			size:     0.0,
			stroke_w: theme::STROKE_W,
		}
	}

	/// Resolve the square diameter against the widget-scaling mode: a
	/// positive [`Self::size`] pins it, the `0.0` default follows the mode.
	fn resolved_size( &self, canvas: &Canvas ) -> f32
	{
		if self.size > 0.0 { self.size } else { canvas.geom_px( theme::SIZE ) }
	}

	/// Set the rotation phase. The widget consumes only the fractional
	/// part, so callers can pass an unbounded monotonic clock value.
	pub fn phase( mut self, p: f32 ) -> Self
	{
		self.phase = p;
		self
	}

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

	/// Set the square diameter in logical pixels.
	pub fn size( mut self, s: f32 ) -> Self
	{
		self.size = s;
		self
	}

	/// Set the arc / ring stroke width in logical pixels.
	pub fn stroke_width( mut self, w: f32 ) -> Self
	{
		self.stroke_w = w;
		self
	}

	/// Return the preferred `(width, height)` — the spinner is square.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
	{
		let s = self.resolved_size( canvas ).min( max_width );
		( s, s )
	}

	/// Draw the spinner into `canvas` at `rect`. The arc is centred on
	/// `rect`'s minor diameter so the widget renders correctly even when
	/// laid out into a non-square cell.
	pub fn draw( &self, canvas: &mut Canvas, rect: Rect )
	{
		let cx = rect.x + rect.width  * 0.5;
		let cy = rect.y + rect.height * 0.5;
		let r  = ( rect.width.min( rect.height ) - self.stroke_w ) * 0.5;
		if r <= 0.0 { return; }

		let track = theme::track();
		let n     = theme::SEGMENTS as i32;
		let two_pi = std::f32::consts::TAU;
		let dim   = two_pi / n as f32;

		// Dim guide ring (full circle as polyline).
		for i in 0..n
		{
			let a0 = i as f32 * dim;
			let a1 = ( i + 1 ) as f32 * dim;
			let x0 = cx + r * a0.cos();
			let y0 = cy + r * a0.sin();
			let x1 = cx + r * a1.cos();
			let y1 = cy + r * a1.sin();
			canvas.draw_line( x0, y0, x1, y1, track, self.stroke_w );
		}

		// Moving arc. `phase` is consumed modulo 1.0.
		let frac      = self.phase - self.phase.floor();
		let start_ang = frac * two_pi;
		let end_ang   = start_ang + theme::ARC_FRAC * two_pi;
		let arc_segs  = ( ( theme::ARC_FRAC * n as f32 ).round() as i32 ).max( 1 );
		for i in 0..arc_segs
		{
			let a0 = start_ang + i as f32 * dim;
			let a1 = ( start_ang + ( i + 1 ) as f32 * dim ).min( end_ang );
			let x0 = cx + r * a0.cos();
			let y0 = cy + r * a0.sin();
			let x1 = cx + r * a1.cos();
			let y1 = cy + r * a1.sin();
			canvas.draw_line( x0, y0, x1, y1, self.color, self.stroke_w );
		}
	}
}

impl Default for Spinner
{
	fn default() -> Self { Self::new() }
}

/// Create a [`Spinner`] with default size and theme colour.
pub fn spinner() -> Spinner
{
	Spinner::new()
}

impl<Msg: Clone> From<Spinner> for Element<Msg>
{
	fn from( s: Spinner ) -> Self
	{
		Element::Spinner( s )
	}
}

#[ cfg( test ) ]
mod tests;
