// 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;

/// One option inside a mutually-exclusive group.
///
/// Renders a circle with a centred dot when selected. Unlike
/// [`Checkbox`](super::checkbox::Checkbox), a radio is meaningful only as
/// part of a group: the group is the application's responsibility — define
/// an enum for the choices, store the current value, and build one `Radio`
/// per variant with `selected = current == this_variant`.
///
/// ```rust,no_run
/// # use ltk::{ column, radio, Element };
/// #[ derive( Clone, PartialEq ) ]
/// enum Priority { Low, Medium, High }
/// # #[ derive( Clone ) ] enum Msg { SetPriority( Priority ) }
/// # struct App { priority: Priority }
/// # impl App { fn _ex( &self ) -> Element<Msg> {
/// // In view():
/// column()
///     .push( radio( self.priority == Priority::Low    ).label( "Low"    ).on_select( Msg::SetPriority( Priority::Low    ) ) )
///     .push( radio( self.priority == Priority::Medium ).label( "Medium" ).on_select( Msg::SetPriority( Priority::Medium ) ) )
///     .push( radio( self.priority == Priority::High   ).label( "High"   ).on_select( Msg::SetPriority( Priority::High   ) ) )
/// .into()
/// # }}
/// ```
///
/// `ltk` does not enforce mutual exclusion automatically; the application's
/// `update` decides which variant becomes active when a radio is selected.
pub struct Radio<Msg: Clone>
{
	/// Whether this option is currently selected. Drawn from this field
	/// every frame.
	pub( crate ) selected:  bool,
	/// Message emitted when the user picks this option. `None` leaves the
	/// radio inert.
	pub( crate ) on_select: Option<Msg>,
	/// Optional label drawn to the right of the circle.
	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> Radio<Msg>
{
	/// Create a radio in the given state, with no label and no callback.
	pub fn new( selected: bool ) -> Self
	{
		Self { selected, on_select: None, label: None, id: None, height: None }
	}

	/// Set the message emitted when this option is picked. The
	/// application's `update` is responsible for flipping the group's
	/// current value.
	pub fn on_select( mut self, msg: Msg ) -> Self
	{
		self.on_select = Some( msg );
		self
	}

	/// Set a text label rendered to the right of the circle.
	pub fn label( mut self, label: impl Into<String> ) -> Self
	{
		self.label = Some( label.into() );
		self
	}

	/// Assign a stable identifier for focus management.
	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. The resolved
	/// value is floored at the outer circle size so the ring never
	/// clips; the circle keeps its theme size and stays centred.
	pub fn height( mut self, h: impl Into<Length> ) -> Self
	{
		self.height = Some( h.into() );
		self
	}

	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		let outer = canvas.geom_px( theme::OUTER_SIZE );
		let w = if let Some( ref label ) = self.label
		{
			let text_w = canvas.measure_text( label, canvas.font_px( theme::FONT_SIZE ) );
			( outer + canvas.geom_px( theme::GAP ) + text_w ).min( max_width )
		} else {
			outer.min( max_width )
		};
		let h = match self.height
		{
			Some( l ) => canvas.resolve_geom( l ).max( outer ),
			None      => canvas.geom_px( theme::HEIGHT ),
		};
		( w, h )
	}

	/// Focus ring on the outer circle extends `FOCUS_W + 2 + FOCUS_W/2 ≈ 6.5 px`
	/// beyond the circle (which sits flush with the widget's left edge).
	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 outer_size = canvas.geom_px( theme::OUTER_SIZE );
		let circle_y = rect.y + ( rect.height - outer_size ) / 2.0;
		let outer_r  = outer_size / 2.0;
		let outer_rect = Rect
		{
			x:      rect.x,
			y:      circle_y,
			width:  outer_size,
			height: outer_size,
		};

		if self.selected
		{
			canvas.fill_rect( outer_rect, theme::selected(), outer_r );
			let dot_size = canvas.geom_px( theme::DOT_SIZE );
			let dot_r = dot_size / 2.0;
			let cx    = rect.x + outer_r;
			let cy    = circle_y + outer_r;
			let dot_rect = Rect
			{
				x:      cx - dot_r,
				y:      cy - dot_r,
				width:  dot_size,
				height: dot_size,
			};
			canvas.fill_rect( dot_rect, theme::dot_color(), dot_r );
		} else {
			canvas.stroke_rect( outer_rect, theme::ring_color(), theme::BORDER_W, outer_r );
		}

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

		if let Some( ref label ) = self.label
		{
			let fs     = canvas.font_px( theme::FONT_SIZE );
			let text_x = rect.x + outer_size + canvas.geom_px( theme::GAP );
			let text_y = rect.y + ( rect.height + fs ) / 2.0 - 2.0;
			canvas.draw_text( label, text_x, text_y, fs, theme::label_color() );
		}
	}

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

/// Create a [`Radio`] option in the given state.
///
/// Shorthand for [`Radio::new`]. See the [`Radio`] type-level docs for
/// the full mutual-exclusion pattern across multiple options.
pub fn radio<Msg: Clone>( selected: bool ) -> Radio<Msg>
{
	Radio::new( selected )
}

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