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

//! DatePicker — month-grid calendar widget.
//!
//! Stateless: the application owns the selected date and the
//! currently-visible month. Two callbacks bridge widget → app:
//!
//! - [`DatePicker::on_change`] fires when the user taps a day cell;
//!   the message carries the picked [`Date`].
//! - [`DatePicker::on_navigate`] fires when the user taps the
//!   previous / next-month arrows; the message carries the new
//!   `(year, month)` that the calendar should display.
//!
//! `on_navigate` is optional — when not wired, the navigation arrows
//! render disabled. The application typically stores both `date:
//! Date` and a `view: Date` (or `(view_year, view_month)`) in its
//! state and updates `view` from `on_navigate` to let the user scroll
//! through months without changing the selection.
//!
//! Date arithmetic (leap years, day-of-week, month wraparound) is
//! built in — no `chrono` / `time` dependency. Limited to the
//! Gregorian calendar from year 1 onwards (Zeller's congruence).
//!
//! ```rust,no_run
//! # use ltk::{ date_picker, Date, DatePicker };
//! # #[ derive( Clone ) ] enum Msg { DateChanged( Date ), DateView( i32, u8 ) }
//! # struct App { date: Date, view_year: i32, view_month: u8, today: Date }
//! # impl App { fn _ex( &self ) -> DatePicker<Msg> {
//! date_picker( self.date )
//!     .view( self.view_year, self.view_month )
//!     .today( self.today )
//!     .on_change( Msg::DateChanged )
//!     .on_navigate( |y, m| Msg::DateView( y, m ) )
//! # }}
//! ```

use std::sync::Arc;

use crate::layout::column::column;
use crate::layout::spacer::spacer;
use crate::layout::wrap_grid::grid;

use super::Element;

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

/// A calendar date in the proleptic Gregorian calendar. No time
/// component, no timezone. `month` is 1–12, `day` is 1–31 (further
/// constrained by [`days_in_month`]).
#[ derive( Clone, Copy, Debug, PartialEq, Eq, Hash ) ]
pub struct Date
{
	pub year:  i32,
	pub month: u8,
	pub day:   u8,
}

impl Date
{
	/// Construct a [`Date`] without validating bounds. Callers that
	/// might pass user-controlled data should run [`Self::is_valid`]
	/// first.
	pub const fn new( year: i32, month: u8, day: u8 ) -> Self
	{
		Self { year, month, day }
	}

	/// `true` when `month` is in `1..=12` and `day` is a real day of
	/// that month / year (29-Feb in leap years, etc.).
	pub fn is_valid( self ) -> bool
	{
		( 1..=12 ).contains( &self.month )
			&& self.day >= 1
			&& self.day <= days_in_month( self.year, self.month )
	}
}

/// `true` when `year` is a leap year in the proleptic Gregorian
/// calendar.
pub fn is_leap_year( year: i32 ) -> bool
{
	( year % 4 == 0 && year % 100 != 0 ) || year % 400 == 0
}

/// Number of days in `month` of `year` (1-indexed month). Returns 0
/// for invalid month numbers.
pub fn days_in_month( year: i32, month: u8 ) -> u8
{
	match month
	{
		1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
		4 | 6 | 9 | 11              => 30,
		2 => if is_leap_year( year ) { 29 } else { 28 },
		_ => 0,
	}
}

/// Day of the week for `(year, month, day)`. `0 = Sunday`,
/// `1 = Monday`, …, `6 = Saturday`. Uses Zeller's congruence;
/// undefined for years before AD 1.
pub fn day_of_week( year: i32, month: u8, day: u8 ) -> u8
{
	let ( m, y ) = if month < 3 { ( month as i32 + 12, year - 1 ) } else { ( month as i32, year ) };
	let k = y.rem_euclid( 100 );
	let j = y.div_euclid( 100 );
	let h = ( day as i32 + ( 13 * ( m + 1 ) ) / 5 + k + k / 4 + j / 4 + 5 * j ).rem_euclid( 7 );
	// h: 0=Saturday, 1=Sunday, 2=Monday, ..., 6=Friday → re-map to 0=Sun..6=Sat
	( ( h + 6 ).rem_euclid( 7 ) ) as u8
}

/// Add `delta` months to `(year, month)` with wraparound. Negative
/// deltas walk backwards; year crosses are handled.
pub fn add_months( year: i32, month: u8, delta: i32 ) -> ( i32, u8 )
{
	let total = year * 12 + ( month as i32 ) - 1 + delta;
	let new_year  = total.div_euclid( 12 );
	let new_month = ( total.rem_euclid( 12 ) + 1 ) as u8;
	( new_year, new_month )
}

/// Locale settings for the date picker. Month names and day-of-week
/// labels are pulled from the i18n registry via
/// [`rust_i18n::t!`] so changing the active locale (e.g. `set_locale("es")`)
/// flips the calendar without recreating the widget. The remaining piece is
/// the **first day of the week**, which varies independently of language
/// (US starts on Sunday, ES / FR / DE on Monday) — that one stays as a
/// builder field.
#[ derive( Clone, Copy, Debug ) ]
pub struct Locale
{
	/// First day of the week (0 = Sunday, 1 = Monday). Default 1
	/// (Monday) — the convention used across most of Europe.
	pub( crate ) first_dow: u8,
}

impl Locale
{
	/// Locale starting the week on Monday. The default.
	pub const MONDAY_FIRST: Self = Self { first_dow: 1 };
	/// Locale starting the week on Sunday — common in US English.
	pub const SUNDAY_FIRST: Self = Self { first_dow: 0 };
}

impl Default for Locale
{
	fn default() -> Self { Self::MONDAY_FIRST }
}

/// Translated month name (1-indexed: `month = 1` → January, …,
/// `month = 12` → December). Resolves through the i18n registry, so
/// `set_locale("es")` returns `"Enero"`, etc.
fn month_name( month: u8 ) -> String
{
	match month
	{
		1  => rust_i18n::t!( "date_picker.month_1"  ).to_string(),
		2  => rust_i18n::t!( "date_picker.month_2"  ).to_string(),
		3  => rust_i18n::t!( "date_picker.month_3"  ).to_string(),
		4  => rust_i18n::t!( "date_picker.month_4"  ).to_string(),
		5  => rust_i18n::t!( "date_picker.month_5"  ).to_string(),
		6  => rust_i18n::t!( "date_picker.month_6"  ).to_string(),
		7  => rust_i18n::t!( "date_picker.month_7"  ).to_string(),
		8  => rust_i18n::t!( "date_picker.month_8"  ).to_string(),
		9  => rust_i18n::t!( "date_picker.month_9"  ).to_string(),
		10 => rust_i18n::t!( "date_picker.month_10" ).to_string(),
		11 => rust_i18n::t!( "date_picker.month_11" ).to_string(),
		12 => rust_i18n::t!( "date_picker.month_12" ).to_string(),
		_  => "?".to_string(),
	}
}

/// Translated single-letter day-of-week label keyed by `dow` where
/// `0 = Sunday`, …, `6 = Saturday`. The display order in the calendar
/// header is rotated by [`Locale::first_dow`].
fn dow_short( dow: u8 ) -> String
{
	match dow
	{
		0 => rust_i18n::t!( "date_picker.dow_short_0" ).to_string(),
		1 => rust_i18n::t!( "date_picker.dow_short_1" ).to_string(),
		2 => rust_i18n::t!( "date_picker.dow_short_2" ).to_string(),
		3 => rust_i18n::t!( "date_picker.dow_short_3" ).to_string(),
		4 => rust_i18n::t!( "date_picker.dow_short_4" ).to_string(),
		5 => rust_i18n::t!( "date_picker.dow_short_5" ).to_string(),
		6 => rust_i18n::t!( "date_picker.dow_short_6" ).to_string(),
		_ => "?".to_string(),
	}
}

/// Calendar date selector.
pub struct DatePicker<Msg: Clone>
{
	pub( crate ) value:        Date,
	pub( crate ) view_year:    i32,
	pub( crate ) view_month:   u8,
	pub( crate ) today:        Option<Date>,
	pub( crate ) on_change:    Option<Arc<dyn Fn( Date ) -> Msg>>,
	pub( crate ) on_navigate:  Option<Arc<dyn Fn( i32, u8 ) -> Msg>>,
	pub( crate ) locale:       Locale,
	pub( crate ) width:        Option<f32>,
}

impl<Msg: Clone + 'static> DatePicker<Msg>
{
	/// Create a date picker with the given selected date. The view
	/// month defaults to the same month as `value`; override with
	/// [`Self::view`] when the user is browsing without selecting.
	pub fn new( value: Date ) -> Self
	{
		Self
		{
			value,
			view_year:   value.year,
			view_month:  value.month,
			today:       None,
			on_change:   None,
			on_navigate: None,
			locale:      Locale::default(),
			width:       None,
		}
	}

	/// Override the visible month. Call this from your view function
	/// with whatever `(year, month)` your application state stores
	/// for "the calendar's current page".
	pub fn view( mut self, year: i32, month: u8 ) -> Self
	{
		self.view_year  = year;
		self.view_month = month;
		self
	}

	/// Mark a specific date as "today" — drawn with a subtle accent
	/// ring even if it is not selected.
	pub fn today( mut self, today: Date ) -> Self
	{
		self.today = Some( today );
		self
	}

	/// Day-tap callback. Required for the picker to be interactive.
	pub fn on_change( mut self, f: impl Fn( Date ) -> Msg + 'static ) -> Self
	{
		self.on_change = Some( Arc::new( f ) );
		self
	}

	/// Arrow-tap callback. The runtime calls `f(new_year, new_month)`
	/// when the user taps prev / next. Wire to your view-month state.
	pub fn on_navigate( mut self, f: impl Fn( i32, u8 ) -> Msg + 'static ) -> Self
	{
		self.on_navigate = Some( Arc::new( f ) );
		self
	}

	/// Override the locale (month / day-of-week names + week start).
	pub fn locale( mut self, l: Locale ) -> Self
	{
		self.locale = l;
		self
	}

	/// Outer width the picker will be laid out at. Used to shrink the
	/// header / day-of-week / day-cell font sizes so the widest labels
	/// ("30", "September 2026", "Mié") still fit without ellipsis. When
	/// unset, the picker uses the design defaults and may truncate
	/// inside very narrow rects.
	pub fn width( mut self, w: f32 ) -> Self
	{
		self.width = Some( w );
		self
	}

	/// Build the `Element` tree representing this date picker.
	pub fn build( self ) -> Element<Msg>
	{
		use super::{ button, container, icon_button, text };
		use super::pressable::pressable;
		use super::button::ButtonVariant;
		use crate::layout::stack::{ stack, HAlign, VAlign };

		let view_y   = self.view_year;
		let view_m   = self.view_month.clamp( 1, 12 );
		let today    = self.today;
		let value    = self.value;
		let on_chg   = self.on_change.clone();
		let on_nav   = self.on_navigate.clone();
		let first    = self.locale.first_dow;

		let ( header_fs, dow_fs, day_fs ) = if let Some( w ) = self.width
		{
			let inner_w = ( w - 2.0 * theme::PADDING ).max( 8.0 );
			let slot_w  = ( ( inner_w - 6.0 * theme::SPACING ) / 7.0 ).max( 4.0 );
			let day     = ( ( slot_w - 8.0 ) / 1.6 ).clamp( 8.0, theme::DAY_FS );
			let dow     = ( slot_w / 2.0 ).clamp( 8.0, theme::DOW_FS );
			let head    = ( ( inner_w - 52.0 ) / ( 14.0 * 0.7 ) ).clamp( 10.0, theme::HEADER_FS );
			( head, dow, day )
		} else {
			( theme::HEADER_FS, theme::DOW_FS, theme::DAY_FS )
		};

		// Header chevrons load from the active theme as SVG icons
		// (`icons/catalogue/filled/multimedia/{previous,next}.svg`),
		// tinted to the primary text colour so they read against
		// either light or dark surfaces. Sized small so the buttons
		// sit visually inside the grid's first / last column.
		// Falls back to the matching Unicode glyph if the icon is
		// missing from the theme.
		const CHEVRON_PX: u32 = 18;
		let nav_button = | name: &str, fallback: &str | -> super::button::Button<Msg>
		{
			match crate::theme::icon_rgba( name, CHEVRON_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( CHEVRON_PX as f32 )
				}
				None => button::<Msg>( fallback ).variant( ButtonVariant::Tertiary ),
			}
		};

		let title = format!( "{} {}", month_name( view_m ), view_y );
		let mut prev = nav_button( "multimedia/previous", "‹" );
		let mut next = nav_button( "multimedia/next",     "›" );
		if let Some( ref nav ) = on_nav
		{
			let ( py, pm ) = add_months( view_y, view_m, -1 );
			let ( ny, nm ) = add_months( view_y, view_m,  1 );
			let nav_p = nav.clone();
			let nav_n = nav.clone();
			prev = prev.on_press( nav_p( py, pm ) );
			next = next.on_press( nav_n( ny, nm ) );
		}

		// Header: title centred horizontally; prev pinned to the left
		// edge, next pinned to the right edge — both at the same
		// padding offset as the calendar grid below, so they align
		// vertically with the first and last day columns. Built as a
		// `Stack` (the only layout that lets independent children
		// sit at left / centre / right of the same rect) wrapped in a
		// container that fixes the row height.
		let header_height = ( CHEVRON_PX as f32 + 16.0 ).max( header_fs + 16.0 );
		let header_inner: Element<Msg> = stack::<Msg>()
			.push_aligned( prev, HAlign::Start, VAlign::Center )
			.push_aligned(
				text( title ).size( header_fs ).color( theme::text() ),
				HAlign::Center, VAlign::Center,
			)
			.push_aligned( next, HAlign::End, VAlign::Center )
			.into();
		let header: Element<Msg> = container::<Msg>( header_inner )
			.padding_v( ( header_height - CHEVRON_PX as f32 ) * 0.5 )
			.padding_h( 0.0 )
			.into();

		// Day-of-week row in the locale's display order. Built as a
		// 7-column `wrap_grid` so the columns share the same
		// equal-width slots that the day-cell grid below will use,
		// guaranteeing letter-and-number alignment frame to frame
		// regardless of glyph width (`W` is wider than `M`, etc.).
		let mut dow_row = grid::<Msg>( 7 ).spacing( theme::SPACING );
		for slot in 0..7u8
		{
			// Convert the column index (display order) back to a
			// weekday number (0 = Sunday) honouring `first_dow`.
			let dow = ( ( slot + first ) % 7 ) as u8;
			let cell: Element<Msg> = container::<Msg>(
				text( dow_short( dow ) )
					.size( dow_fs )
					.color( theme::text_muted() )
					.align_center()
					.no_truncate(),
			)
			.padding_h( 0.0 )
			.padding_v( 4.0 )
			.into();
			dow_row = dow_row.push( cell );
		}

		// Day grid: 7 columns × 6 rows of equal-width cells. Off-month
		// slots stay empty so the calendar always reserves the same
		// height; the `wrap_grid` lays out each cell in its own slot
		// so vertical alignment with the DOW row above is automatic.
		let dim       = days_in_month( view_y, view_m );
		let first_dow = day_of_week( view_y, view_m, 1 );
		let leading_blanks = ( ( first_dow as i32 - first as i32 ).rem_euclid( 7 ) ) as u8;
		let total_cells    = 6 * 7;

		let mut day_grid = grid::<Msg>( 7 ).spacing( theme::SPACING );
		for slot in 0..total_cells
		{
			let day_num = slot as i32 - leading_blanks as i32 + 1;
			let cell: Element<Msg> = if day_num < 1 || day_num > dim as i32
			{
				container::<Msg>( spacer() ).padding( 0.0 ).into()
			} else {
				let day  = day_num as u8;
				let date = Date::new( view_y, view_m, day );
				let is_selected = date == value;
				let is_today    = today == Some( date );

				let label_color = if is_selected { theme::surface() } else { theme::text() };
				let bg = if is_selected
				{
					Some( theme::accent() )
				} else if is_today {
					Some( theme::surface_alt() )
				} else {
					None
				};

				let mut card = container::<Msg>(
					text( format!( "{}", day ) )
						.size( day_fs )
						.color( label_color )
						.align_center()
						.no_truncate(),
				)
				.padding_h( 4.0 )
				.padding_v( 8.0 )
				.radius( theme::CELL_SIZE * 0.5 );
				if let Some( c ) = bg { card = card.background( c ); }

				if let Some( ref cb ) = on_chg
				{
					let m = cb( date );
					pressable( card ).on_press( m ).into()
				} else {
					card.into()
				}
			};
			day_grid = day_grid.push( cell );
		}

		container::<Msg>(
			column::<Msg>()
				.spacing( 0.0 )
				.push( header )
				// Extra breathing space between the month title and
				// the day-of-week header so the row separation reads
				// cleanly. Tuned by feedback — the previous shared
				// `SPACING * 2.0` was too tight.
				.push( spacer().height( theme::SPACING * 4.0 ) )
				.push( dow_row )
				.push( spacer().height( theme::SPACING * 1.5 ) )
				.push( day_grid ),
		)
		.background( theme::surface_alt() )
		.padding( theme::PADDING )
		.radius( theme::RADIUS )
		.into()
	}
}

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

/// Create a [`DatePicker`] with the given selected date.
///
/// ```rust,no_run
/// # use ltk::{ date_picker, Date, DatePicker };
/// # #[ derive( Clone ) ] enum Msg { DateChanged( Date ), DateView( i32, u8 ) }
/// # struct App { date: Date, today: Date }
/// # impl App { fn _ex( &self ) -> DatePicker<Msg> {
/// date_picker( self.date )
///     .today( self.today )
///     .on_change( Msg::DateChanged )
///     .on_navigate( Msg::DateView )
/// # }}
/// ```
pub fn date_picker<Msg: Clone + 'static>( value: Date ) -> DatePicker<Msg>
{
	DatePicker::new( value )
}
