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

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

mod theme;

#[ cfg( test ) ]
mod tests;

/// A two-state on / off switch.
///
/// Renders as a horizontal pill with a circular thumb that slides between the
/// off (left, divider colour) and on (right, accent colour) positions. The
/// widget is stateless: the application owns `value` and rebuilds the toggle
/// from its current state on every frame. Tapping or pressing Enter / Space
/// while focused emits the message configured with [`Self::on_toggle`] — the
/// app's `update` is then expected to flip the bool and re-render.
///
/// ```rust,no_run
/// # use ltk::{ toggle, Toggle };
/// # #[ derive( Clone ) ] enum Msg { ToggleWifi }
/// # struct App { wifi_enabled: bool }
/// # impl App { fn _ex( &self ) -> Toggle<Msg> {
/// // In view():
/// toggle( self.wifi_enabled )
///     .label( "Wi-Fi" )
///     .on_toggle( Msg::ToggleWifi )
/// # }}
/// ```
///
/// See also [`Checkbox`](super::checkbox::Checkbox) for binary opt-in
/// controls (terms acceptance, multi-select form fields) where the
/// pill-style affordance is too prominent.
pub struct Toggle<Msg: Clone>
{
	/// Current on / off state. Drawn from this field every frame; the
	/// runtime never mutates it.
	pub( crate ) value:     bool,
	/// Message emitted on activation. `None` leaves the toggle inert (it
	/// still renders and takes focus, but does nothing on press).
	pub( crate ) on_toggle: Option<Msg>,
	/// Optional label drawn to the left of the track.
	pub( crate ) label:     Option<String>,
	/// Optional stable identifier for focus management.
	pub( crate ) id:        Option<WidgetId>,
	/// Optional override of the theme row height.
	pub( crate ) height:    Option<Length>,
}

impl<Msg: Clone> Toggle<Msg>
{
	/// Create a toggle in the given state, with no label and no callback.
	///
	/// Wire activation through [`Self::on_toggle`] before adding it to a
	/// widget tree, otherwise the toggle is decorative.
	pub fn new( value: bool ) -> Self
	{
		Self { value, on_toggle: None, label: None, id: None, height: None }
	}

	/// Set the message emitted when the toggle is activated (tap, Enter or
	/// Space while focused). The application's `update` is responsible for
	/// flipping `value` in response.
	pub fn on_toggle( mut self, msg: Msg ) -> Self
	{
		self.on_toggle = Some( msg );
		self
	}

	/// Set a text label rendered to the left of the track. The toggle's
	/// preferred width grows to fit `label_width + gap + track_width`,
	/// capped at the parent-supplied `max_width`.
	pub fn label( mut self, label: impl Into<String> ) -> Self
	{
		self.label = Some( label.into() );
		self
	}

	/// Assign a stable identifier so the application can target this
	/// toggle through [`crate::App::take_focus_request`].
	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = Some( id );
		self
	}

	/// Override the preferred height (default: the theme row height,
	/// `theme::HEIGHT` design px). Accepts any [`Length`] so dense
	/// layouts can tie the row height to the viewport. When the resolved
	/// value falls below the theme row height, the pill (track and thumb)
	/// scales down proportionally so it never clips; it never grows
	/// beyond its theme size.
	pub fn height( mut self, h: impl Into<Length> ) -> Self
	{
		self.height = Some( h.into() );
		self
	}

	/// Return the preferred `(width, height)` given available `max_width`.
	///
	/// Width is `track_width` for an unlabelled toggle, or
	/// `label_width + gap + track_width` (clamped to `max_width`) when a
	/// label is set. Height is the theme-defined row height.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		let h = match self.height
		{
			Some( l ) => canvas.resolve_geom( l ),
			None      => canvas.geom_px( theme::HEIGHT ),
		};
		let scale   = ( h / canvas.geom_px( theme::HEIGHT ) ).min( 1.0 );
		let track_w = canvas.geom_px( theme::TRACK_W ) * scale;
		let w = if let Some( ref label ) = self.label
		{
			let text_w = canvas.measure_text( label, canvas.font_px( theme::FONT_SIZE ) );
			( text_w + canvas.geom_px( theme::GAP ) + track_w ).min( max_width )
		} else {
			track_w.min( max_width )
		};
		( w, h )
	}

	/// Bounding box of everything painted at `rect` across all states. The
	/// focus ring is drawn as `track_rect.expand( FOCUS_W + 2 )` with a stroke
	/// of width `FOCUS_W`, so it extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px`
	/// beyond the track. Since the track sits at the right edge of `rect`,
	/// the ring can extend that far outside the widget's layout rect.
	pub fn paint_bounds( &self, rect: Rect ) -> Rect
	{
		rect.expand( theme::FOCUS_W + 2.0 + theme::FOCUS_W * 0.5 + 1.0 )
	}

	pub fn draw( &self, canvas: &mut Canvas, rect: Rect, focused: bool )
	{
		let scale      = ( rect.height / canvas.geom_px( theme::HEIGHT ) ).min( 1.0 );
		let track_w    = canvas.geom_px( theme::TRACK_W ) * scale;
		let track_h    = canvas.geom_px( theme::TRACK_H ) * scale;
		let thumb_size = canvas.geom_px( theme::THUMB_SIZE ) * scale;

		let track_x = if let Some( ref label ) = self.label
		{
			let fs     = canvas.font_px( theme::FONT_SIZE );
			let text_y = rect.y + ( rect.height + fs ) / 2.0 - 2.0;
			canvas.draw_text( label, rect.x, text_y, fs, theme::label_color() );
			rect.x + rect.width - track_w
		} else {
			rect.x + ( rect.width - track_w ) / 2.0
		};

		let track_y = rect.y + ( rect.height - track_h ) / 2.0;
		let track_r = track_h / 2.0;

		let track_rect = Rect
		{
			x:      track_x,
			y:      track_y,
			width:  track_w,
			height: track_h,
		};
		let track_color = if self.value { theme::track_on() } else { theme::track_off() };
		canvas.fill_rect( track_rect, track_color, track_r );

		let thumb_pad = ( track_h - thumb_size ) / 2.0;
		let thumb_cx = if self.value
		{
			track_x + track_w - thumb_pad - thumb_size / 2.0
		} else {
			track_x + thumb_pad + thumb_size / 2.0
		};
		let thumb_cy = track_y + track_h / 2.0;
		let thumb_r  = thumb_size / 2.0;
		let thumb_rect = Rect
		{
			x:      thumb_cx - thumb_r,
			y:      thumb_cy - thumb_r,
			width:  thumb_size,
			height: thumb_size,
		};
		canvas.fill_rect( thumb_rect, theme::thumb(), thumb_r );
		canvas.stroke_rect( thumb_rect, theme::thumb_border(), theme::THUMB_BORDER_W, thumb_r );

		if focused
		{
			let ring = track_rect.expand( theme::FOCUS_W + 2.0 );
			canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, track_r + theme::FOCUS_W + 2.0 );
		}
	}

	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Toggle<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Toggle
		{
			value:     self.value,
			on_toggle: self.on_toggle.map( |m| ( *f )( m ) ),
			label:     self.label,
			id:        self.id,
			height:    self.height,
		}
	}
}

/// Create a [`Toggle`] in the given state.
///
/// Shorthand for [`Toggle::new`]. Wire activation with [`Toggle::on_toggle`]
/// and an optional label with [`Toggle::label`]:
///
/// ```rust,no_run
/// # use ltk::{ toggle, Toggle };
/// # #[ derive( Clone ) ] enum Msg { ToggleWifi }
/// # struct App { wifi_enabled: bool }
/// # impl App { fn _ex( &self ) -> Toggle<Msg> {
/// toggle( self.wifi_enabled )
///     .label( "Wi-Fi" )
///     .on_toggle( Msg::ToggleWifi )
/// # }}
/// ```
pub fn toggle<Msg: Clone>( value: bool ) -> Toggle<Msg>
{
	Toggle::new( value )
}

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