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

//! TimePicker — HH:MM (and optionally :SS / AM-PM) stepper widget.
//!
//! Stateless — the application owns a [`Time`] and updates it from
//! the [`TimePicker::on_change`] callback. Each unit (hour, minute,
//! second) is a small stepper: an up arrow above the digit cell,
//! a down arrow below. Each digit cell is itself an editable
//! [`crate::widget::text_edit::TextEdit`] with `select_on_focus` —
//! click (or Tab) into it and type to set the value with the
//! keyboard, parser-clamped to the unit's valid range.
//!
//! Stepper buttons opt into press-and-hold repeat
//! ([`crate::widget::button::Button::repeating`]) so a held arrow
//! ramps through values at ~8 Hz. The minute / second steppers
//! honour [`TimePicker::minute_step`] (default 1, common values 5 /
//! 15) and **snap to the next or previous multiple of the step**
//! rather than adding the step verbatim — so `:32` with
//! `minute_step( 5 )` jumps to `:35` (next multiple) rather than
//! `:37`. Typed input bypasses the snap so the user can still type
//! any minute they want.
//!
//! ```rust,no_run
//! # use ltk::{ time_picker, Time, TimePicker };
//! # #[ derive( Clone ) ] enum Msg { TimeChanged( Time ) }
//! # struct App { time: Time }
//! # impl App { fn _ex( &self ) -> TimePicker<Msg> {
//! time_picker( self.time )
//!     .minute_step( 5 )
//!     .twelve_hour( true )
//!     .on_change( Msg::TimeChanged )
//! # }}
//! ```

use std::sync::Arc;

use crate::layout::column::column;
use crate::layout::row::row;
use crate::layout::spacer::spacer;
use crate::types::Length;

use super::Element;

mod theme;

#[ cfg( test ) ]
mod tests;

/// A wall-clock time, no date / no timezone. `hour` is 0–23 (24-hour
/// representation regardless of [`TimePicker::twelve_hour`] display
/// mode), `minute` and `second` are 0–59.
#[ derive( Clone, Copy, Debug, PartialEq, Eq, Hash, Default ) ]
pub struct Time
{
	pub hour:   u8,
	pub minute: u8,
	pub second: u8,
}

impl Time
{
	pub const fn new( hour: u8, minute: u8 ) -> Self
	{
		Self { hour, minute, second: 0 }
	}

	pub const fn with_seconds( hour: u8, minute: u8, second: u8 ) -> Self
	{
		Self { hour, minute, second }
	}

	/// Total seconds from 00:00:00. Used internally for arithmetic
	/// that needs to wrap correctly across midnight.
	pub fn as_seconds( self ) -> i32
	{
		self.hour as i32 * 3600 + self.minute as i32 * 60 + self.second as i32
	}

	/// Reconstruct a [`Time`] from a (signed) seconds-of-day count,
	/// wrapping on the 24-hour boundary in either direction.
	pub fn from_seconds( s: i32 ) -> Self
	{
		let total = s.rem_euclid( 24 * 3600 );
		let hour   = ( total / 3600 ) as u8;
		let minute = ( ( total % 3600 ) / 60 ) as u8;
		let second = ( total % 60 ) as u8;
		Self { hour, minute, second }
	}

	/// `true` when the values are in the canonical ranges (h: 0..24,
	/// m / s: 0..60).
	pub fn is_valid( self ) -> bool
	{
		self.hour < 24 && self.minute < 60 && self.second < 60
	}
}

/// Compute the signed delta needed to step `value` to the next
/// (`dir = 1`) or previous (`dir = -1`) multiple of `step`. The
/// returned delta carries the right sign already, so the caller just
/// adds it to its working seconds-of-day count.
///
/// Designed for the minute / second steppers in
/// [`TimePicker`](crate::widget::time_picker::TimePicker) — it ignores
/// the wraparound at 60 (the picker always feeds the result through
/// [`Time::from_seconds`], which handles the `:00` → next-hour
/// rollover via `rem_euclid`).
///
/// Examples (`step = 5`):
/// * `value = 30, dir =  1` → `+5` → next aligned at `35`
/// * `value = 32, dir =  1` → `+3` → next aligned at `35`
/// * `value = 32, dir = -1` → `-2` → previous aligned at `30`
/// * `value = 30, dir = -1` → `-5` → previous aligned at `25`
/// Filter `s` down to its ASCII digit characters, parse them as a
/// decimal integer and clamp the result to `0..=max`. Returns `None`
/// only when the filtered string is empty so the caller can detect
/// "the user erased the field" and skip the on-change message rather
/// than committing a spurious zero. Anything else — leading zeros,
/// punctuation, alpha characters, values past `max` — is forgiven and
/// folded into a valid `u8`.
fn parse_clamped_digits( s: &str, max: u8 ) -> Option<u8>
{
	let digits: String = s.chars().filter( |c| c.is_ascii_digit() ).collect();
	if digits.is_empty() { return None; }
	// `u32` instead of `u8::from_str` so a typed "099" or "100" does
	// not overflow the parse and bail out — we want to clamp, not
	// reject.
	let n: u32 = digits.parse().ok()?;
	Some( n.min( max as u32 ) as u8 )
}

fn snap_step_delta( value: i32, step: i32, dir: i32 ) -> i32
{
	let step = step.max( 1 );
	if dir >= 0
	{
		// Up: smallest multiple of `step` strictly greater than
		// `value`. `value % step == 0` ⇒ jump a full step;
		// otherwise round up to the next multiple.
		let rem = value.rem_euclid( step );
		if rem == 0 { step } else { step - rem }
	}
	else
	{
		// Down: largest multiple of `step` strictly less than
		// `value`. Symmetric to the up case.
		let rem = value.rem_euclid( step );
		if rem == 0 { -step } else { -rem }
	}
}

/// Time-of-day selector with up / down steppers per unit.
pub struct TimePicker<Msg: Clone>
{
	pub( crate ) value:        Time,
	pub( crate ) on_change:    Option<Arc<dyn Fn( Time ) -> Msg>>,
	pub( crate ) minute_step:  u8,
	pub( crate ) second_step:  u8,
	pub( crate ) seconds:      bool,
	pub( crate ) twelve_hour:  bool,
}

impl<Msg: Clone + 'static> TimePicker<Msg>
{
	/// Create a time picker with the given current time.
	pub fn new( value: Time ) -> Self
	{
		Self
		{
			value,
			on_change:   None,
			minute_step: 1,
			second_step: 1,
			seconds:     false,
			twelve_hour: false,
		}
	}

	pub fn on_change( mut self, f: impl Fn( Time ) -> Msg + 'static ) -> Self
	{
		self.on_change = Some( Arc::new( f ) );
		self
	}

	/// Step size for the minute up / down buttons. Common values: 1
	/// (default), 5, 15. Clamped to `1..=30`.
	pub fn minute_step( mut self, step: u8 ) -> Self
	{
		self.minute_step = step.clamp( 1, 30 );
		self
	}

	/// Step size for the second up / down buttons. Only meaningful
	/// when [`Self::seconds`] is on. Clamped to `1..=30`.
	pub fn second_step( mut self, step: u8 ) -> Self
	{
		self.second_step = step.clamp( 1, 30 );
		self
	}

	/// Show / hide the seconds stepper. Default `false`.
	pub fn seconds( mut self, on: bool ) -> Self
	{
		self.seconds = on;
		self
	}

	/// Display the hour as 12-hour with an AM / PM toggle. Internal
	/// storage stays 24-hour. Default `false`.
	pub fn twelve_hour( mut self, on: bool ) -> Self
	{
		self.twelve_hour = on;
		self
	}

	/// Build the `Element` tree representing this time picker.
	pub fn build( self ) -> Element<Msg>
	{
		use super::{ button, container, icon_button, text, text_edit };
		use super::button::ButtonVariant;
		use super::text::TextAlign;

		let on_chg = self.on_change.clone();
		let value  = self.value;
		let twelve = self.twelve_hour;

		// Up / down arrows load from the active theme as SVG icons
		// (`icons/catalogue/filled/general/{up,down}-simple.svg`)
		// tinted with `palette.text_primary` so they read against
		// either light or dark surfaces. Falls back to the matching
		// Unicode glyph when the icon is missing — the literal
		// `▲` / `▼` characters are not present in every system
		// font, so without the SVG path the buttons would render
		// as missing-glyph boxes (see issue with stock fonts that
		// lack U+25B2 / U+25BC).
		const ARROW_PX: u32 = 16;
		let arrow = | name: &str, fallback: &str | -> super::button::Button<Msg>
		{
			let btn = match crate::theme::icon_rgba( name, ARROW_PX )
			{
				Some( ( rgba, w, h ) ) =>
				{
					let tinted = std::sync::Arc::new(
						crate::theme::tint_symbolic( &rgba, theme::text() ),
					);
					icon_button::<Msg>( tinted, w, h ).icon_size( ARROW_PX as f32 )
				}
				None => button::<Msg>( fallback ).variant( ButtonVariant::Tertiary ),
			};
			// Press-and-hold steps through values continuously, like
			// holding an arrow key. The runtime fires `on_press`
			// immediately on press and then re-fires at the same
			// cadence as keyboard repeat while the button is held.
			btn.repeating( true )
		};

		// Build a stepper column for one unit. The middle element is
		// supplied by the caller (a static `text` for read-only mode,
		// or an editable `text_edit` with the appropriate `on_change`
		// parser when a callback is wired) so each unit can carry its
		// own typing semantics.
		//
		// `fit_content()` + `padding(0.0)` are critical: without them
		// each column would claim the full row width as its preferred
		// size (the `Column` default), pushing the colon and the
		// minute / second columns off-screen to the right.
		let stepper = | middle: Element<Msg>, up_secs: i32, down_secs: i32 | -> crate::layout::column::Column<Msg>
		{
			let mut col = column::<Msg>()
				.spacing( 4.0 )
				.align_center_x( true )
				.padding( 0.0 )
				.fit_content();

			let mut up = arrow( "general/up-simple",   "▲" );
			let mut dn = arrow( "general/down-simple", "▼" );
			if let Some( ref cb ) = on_chg
			{
				let cb_up = cb.clone();
				let cb_dn = cb.clone();
				let new_up = Time::from_seconds( value.as_seconds() + up_secs );
				let new_dn = Time::from_seconds( value.as_seconds() + down_secs );
				up = up.on_press( cb_up( new_up ) );
				dn = dn.on_press( cb_dn( new_dn ) );
			}
			col = col.push( up );
			col = col.push( middle );
			col = col.push( dn );
			col
		};

		// Build the editable digit field for one unit. When no
		// `on_change` is wired, falls back to a static `text` so the
		// digits still render but the picker is read-only. With
		// `on_change` wired, returns a borderless centred
		// `text_edit` with `select_on_focus` so the user can click
		// (or Tab) to focus and immediately retype the value.
		// `parser` translates the typed string into a fresh `Time`,
		// returning `None` when the input is invalid (empty / non-
		// numeric) so the existing value is preserved.
		let digit_field = | display: String, parser: Box<dyn Fn( &str ) -> Option<Time> > | -> Element<Msg>
		{
			match on_chg.as_ref()
			{
				None =>
				{
					text( display )
						.size( Length::widget( theme::VAL_FS ) )
						.color( theme::text() )
						.align_center()
						.into()
				}
				Some( cb ) =>
				{
					let cb = cb.clone();
					let snapshot = value;
					text_edit::<Msg>( "", display )
						.borderless( true )
						.fixed_width( Length::fluid( 72.0 ) )
						.font_size_fluid( theme::VAL_FS )
						.align( TextAlign::Center )
						.select_on_focus( true )
						.on_change( move |s: String|
						{
							let new = parser( &s ).unwrap_or( snapshot );
							cb( new )
						} )
						.into()
				}
			}
		};

		// Hour: respect 12-hour toggle for *display only*; storage
		// stays 0–23. 12-hour displays 12 instead of 0.
		let display_hour = if twelve
		{
			let h12 = value.hour % 12;
			if h12 == 0 { 12 } else { h12 }
		} else {
			value.hour
		};
		let hour_field = digit_field(
			format!( "{:02}", display_hour ),
			Box::new( move |s: &str| -> Option<Time>
			{
				let typed = parse_clamped_digits( s, if twelve { 12 } else { 23 } )?;
				let hour24 = if twelve
				{
					// 12-hour input: 12 with PM stays 12, 12 with AM
					// becomes 0; 1–11 keep the AM / PM the user
					// already had so retyping a value during a PM
					// hour does not silently roll back to AM.
					let is_pm = value.hour >= 12;
					match ( typed, is_pm )
					{
						( 12, false ) => 0,
						( 12, true  ) => 12,
						( h,  false ) => h,
						( h,  true  ) => h.saturating_add( 12 ).min( 23 ),
					}
				} else { typed };
				Some( Time::with_seconds( hour24, value.minute, value.second ) )
			} ),
		);
		let hour_col = stepper( hour_field, 3600, -3600 );

		// Snap minutes / seconds to the next (or previous) multiple
		// of the configured step instead of adding or subtracting the
		// step verbatim. With `minute_step( 5 )` and a starting value
		// of :32, the up arrow takes the user to :35 and the down
		// arrow to :30 rather than perpetuating the off-step :37 /
		// :27 — so the picker can never produce a value that is not a
		// multiple of the step. Same logic for seconds. Typed input
		// goes straight into the field — only the steppers enforce
		// the snap, the user can still type any valid minute.
		let minute_step = self.minute_step as i32;
		let minute_up   = snap_step_delta( value.minute as i32, minute_step,  1 ) * 60;
		let minute_dn   = snap_step_delta( value.minute as i32, minute_step, -1 ) * 60;
		let minute_field = digit_field(
			format!( "{:02}", value.minute ),
			Box::new( move |s: &str| -> Option<Time>
			{
				let m = parse_clamped_digits( s, 59 )?;
				Some( Time::with_seconds( value.hour, m, value.second ) )
			} ),
		);
		let minute_col  = stepper( minute_field, minute_up, minute_dn );

		let mut units = row::<Msg>().spacing( Length::widget( theme::SPACING ) )
			.push( hour_col )
			.push( text( ":" ).size( Length::widget( theme::SEP_FS ) ).color( theme::text_muted() ) )
			.push( minute_col );

		if self.seconds
		{
			let second_step = self.second_step as i32;
			let second_up   = snap_step_delta( value.second as i32, second_step,  1 );
			let second_dn   = snap_step_delta( value.second as i32, second_step, -1 );
			let second_field = digit_field(
				format!( "{:02}", value.second ),
				Box::new( move |s: &str| -> Option<Time>
				{
					let sec = parse_clamped_digits( s, 59 )?;
					Some( Time::with_seconds( value.hour, value.minute, sec ) )
				} ),
			);
			let second_col  = stepper( second_field, second_up, second_dn );
			units = units
				.push( text( ":" ).size( Length::widget( theme::SEP_FS ) ).color( theme::text_muted() ) )
				.push( second_col );
		}

		if twelve
		{
			let is_pm = value.hour >= 12;
			let label = if is_pm { "PM" } else { "AM" };
			let mut ampm = button::<Msg>( label ).variant( ButtonVariant::Secondary );
			if let Some( ref cb ) = on_chg
			{
				// Toggle PM / AM = ±12 hours, modulo 24.
				let toggled = Time::from_seconds( value.as_seconds() + 12 * 3600 );
				ampm = ampm.on_press( cb( toggled ) );
			}
			// Fixed-width gap before AM/PM so the toggle sits next to
			// the digits as a single cluster instead of being pushed
			// to the right edge — Row's auto-centring then balances
			// the whole hh:mm AM block inside the container. A flex
			// spacer here would defeat the auto-centre and pin AM/PM
			// to the right.
			units = units.push( spacer().width( Length::widget( theme::SPACING * 2.0 ) ) ).push( ampm );
		}

		// `Row::layout` centres a cluster horizontally when there are
		// no flex spacers inside (see `layout/row.rs` — `start_x =
		// rect.x + (rect.width - fixed_w - gaps) / 2.0`). All the
		// `units` rows we build here only contain fixed-width
		// children (steppers, separator text, fixed-width spacer for
		// AM/PM), so they centre automatically inside the container.
		container::<Msg>( units )
			.background( theme::surface_alt() )
			.padding( Length::widget( theme::PADDING ) )
			.radius( theme::RADIUS )
			.into()
	}
}

impl<Msg: Clone + 'static> From<TimePicker<Msg>> for Element<Msg>
{
	fn from( t: TimePicker<Msg> ) -> Self { t.build() }
}

/// Create a [`TimePicker`] with the given current time.
///
/// ```rust,no_run
/// # use ltk::{ time_picker, Time, TimePicker };
/// # #[ derive( Clone ) ] enum Msg { TimeChanged( Time ) }
/// # struct App { time: Time }
/// # impl App { fn _ex( &self ) -> TimePicker<Msg> {
/// time_picker( self.time )
///     .minute_step( 5 )
///     .on_change( Msg::TimeChanged )
/// # }}
/// ```
pub fn time_picker<Msg: Clone + 'static>( value: Time ) -> TimePicker<Msg>
{
	TimePicker::new( value )
}
