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

use crate::render::Canvas;
use crate::types::Length;
use crate::widget::Element;

/// A flexible, invisible spacer that expands to fill available space.
///
/// The optional `weight` controls how much of the remaining space this spacer
/// claims relative to other spacers in the same layout. A spacer with `weight = 2`
/// takes twice as much space as one with `weight = 1`.
///
/// Place a `Spacer` between two widgets inside a [`Column`](crate::layout::column::Column)
/// or [`Row`](crate::layout::row::Row) to push them apart:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "Top" ) )
///     .push( spacer() )    // pushes "Bottom" to the bottom
///     .push( text( "Bottom" ) )
/// .into()
/// # }
/// ```
///
/// Use [`.weight(n)`](Spacer::weight) to replace several consecutive spacers:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, Column };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() {
/// // These two are equivalent:
/// let _: Column<Msg> = column().push( spacer().weight( 3 ) );
/// let _: Column<Msg> = column().push( spacer() ).push( spacer() ).push( spacer() );
/// # }
/// ```
///
/// Use [`.height(px)`](Spacer::height) to create a fixed-size vertical spacer:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "Header" ) )
///     .push( spacer().height( 20.0 ) )  // Exactly 20 px gap
///     .push( text( "Content" ) )
/// .into()
/// # }
/// ```
///
/// `.height(...)` and `.width(...)` also accept any [`crate::Length`], so the
/// gap can scale with the surface instead of being frozen at a px constant:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Length, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "Header" ) )
///     // 6 % of the surface's smaller side, never below 16 px or above 64 px.
///     .push( spacer().height( Length::vmin( 6.0 ).clamp( 16.0, 64.0 ) ) )
///     .push( text( "Content" ) )
/// .into()
/// # }
/// ```
pub struct Spacer
{
	/// Relative weight of this spacer (default 1).
	pub( crate ) weight: u32,
	/// Fixed height (overrides flexible behavior in a column). Accepts any
	/// [`Length`] — pass an `f32`/`i32`/`u32` for the px case (kept for
	/// backward compatibility with existing call sites), or
	/// `Length::vmin( … )` etc. for viewport-relative gaps.
	pub( crate ) fixed_height: Option<Length>,
	/// Fixed width (overrides flexible behavior in a row). Same length-type
	/// semantics as [`Self::fixed_height`].
	pub( crate ) fixed_width:  Option<Length>,
}

impl Spacer
{
	/// Set the relative weight of this spacer (default 1).
	pub fn weight( mut self, w: u32 ) -> Self
	{
		self.weight = w;
		self
	}

	/// Set a fixed height for this spacer. Accepts any [`Length`]: a bare
	/// `24.0_f32` is treated as `Length::px( 24.0 )` for source-level
	/// backwards compatibility; a `Length::vmin( 6.0 )` makes the gap
	/// scale with the surface's smaller dimension.
	pub fn height( mut self, h: impl Into<Length> ) -> Self
	{
		self.fixed_height = Some( h.into() );
		self
	}

	/// Set a fixed width for this spacer. Mirrors [`Self::height`] for the
	/// horizontal axis.
	pub fn width( mut self, w: impl Into<Length> ) -> Self
	{
		self.fixed_width = Some( w.into() );
		self
	}

	/// Returns `( fixed_width, fixed_height )` resolved against the
	/// current canvas viewport, falling back to `0.0` on axes that were
	/// not pinned. The parent layout distributes leftover along its main
	/// axis among the still-flexible spacers and `Flex` wrappers,
	/// weighted by `weight`.
	pub fn preferred_size( &self, canvas: &Canvas ) -> ( f32, f32 )
	{
		(
			self.fixed_width .map( |l| canvas.resolve_geom( l ) ).unwrap_or( 0.0 ),
			self.fixed_height.map( |l| canvas.resolve_geom( l ) ).unwrap_or( 0.0 ),
		)
	}

	/// Resolved fixed height in logical pixels, or `None` when the
	/// spacer is flex. Cheaper than [`Self::preferred_size`] when the
	/// layout only needs the main-axis size for one orientation.
	pub fn resolved_height( &self, canvas: &Canvas ) -> Option<f32>
	{
		self.fixed_height.map( |l| canvas.resolve_geom( l ) )
	}

	pub fn resolved_width( &self, canvas: &Canvas ) -> Option<f32>
	{
		self.fixed_width.map( |l| canvas.resolve_geom( l ) )
	}

	/// No-op — spacers are invisible.
	pub fn draw( &self ) {}
}

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

/// Create a flexible spacer with weight 1.
///
/// Call [`.weight(n)`](Spacer::weight) to set a relative weight greater than 1:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, Column };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() {
/// // These two are equivalent:
/// let _: Column<Msg> = column().push( spacer().weight( 3 ) );
/// let _: Column<Msg> = column().push( spacer() ).push( spacer() ).push( spacer() );
/// # }
/// ```
///
/// Call [`.height(px)`](Spacer::height) to create a fixed-size vertical gap:
///
/// ```rust,no_run
/// # use ltk::{ column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// column()
///     .push( text( "First" ) )
///     .push( spacer().height( 24.0 ) )  // Fixed 24px gap
///     .push( text( "Second" ) )
/// .into()
/// # }
/// ```
pub fn spacer() -> Spacer
{
	Spacer { weight: 1, fixed_height: None, fixed_width: None }
}

#[ cfg( test ) ]
mod tests
{
	use super::*;
	use crate::render::Canvas;

	fn make_canvas() -> Canvas { Canvas::new( 800, 600 ) }

	#[ test ]
	fn height_accepts_f32_as_pixels()
	{
		let s = spacer().height( 24.0 );
		let canvas = make_canvas();
		assert_eq!( s.resolved_height( &canvas ), Some( 24.0 ) );
	}

	#[ test ]
	fn height_accepts_length_and_resolves_against_viewport()
	{
		// 10 % of the smaller side (= 600) = 60 px.
		let s = spacer().height( Length::vmin( 10.0 ) );
		let canvas = make_canvas();
		assert_eq!( s.resolved_height( &canvas ), Some( 60.0 ) );
	}

	#[ test ]
	fn width_accepts_length_and_resolves_against_viewport()
	{
		let s = spacer().width( Length::vw( 25.0 ) );
		let canvas = make_canvas();
		// 25 % of 800 = 200.
		assert_eq!( s.resolved_width( &canvas ), Some( 200.0 ) );
	}

	#[ test ]
	fn flex_spacer_has_no_resolved_dimensions()
	{
		let s = spacer().weight( 3 );
		let canvas = make_canvas();
		assert_eq!( s.resolved_height( &canvas ), None );
		assert_eq!( s.resolved_width(  &canvas ), None );
	}

	#[ test ]
	fn builder_stores_weight_and_fixed_dimensions()
	{
		let d = spacer();
		assert_eq!( d.weight, 1 );
		assert!( d.fixed_height.is_none() );
		assert!( d.fixed_width.is_none() );

		let h = spacer().height( 24.0 );
		assert_eq!( h.fixed_height, Some( Length::px( 24.0 ) ) );
		assert!( h.fixed_width.is_none() );

		let w = spacer().width( 12.0 );
		assert_eq!( w.fixed_width, Some( Length::px( 12.0 ) ) );
		assert!( w.fixed_height.is_none() );

		assert_eq!( spacer().weight( 5 ).weight, 5 );
	}
}
