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

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

/// A vertical layout container.
///
/// Children are arranged top-to-bottom with optional spacing and padding.
/// Spacers absorb remaining vertical space, enabling push-to-bottom layouts.
///
/// ```rust,no_run
/// # use ltk::{ button, column, spacer, text, Element };
/// # #[ derive( Clone ) ] enum Msg { Ok }
/// # fn _ex() -> Element<Msg> {
/// column()
///     .padding( 24.0 )
///     .spacing( 12.0 )
///     .push( text( "Title" ) )
///     .push( spacer() )
///     .push( button( "OK" ).on_press( Msg::Ok ) )
/// .into()
/// # }
/// ```
///
/// `padding`, `spacing` and `max_width` all accept any
/// [`crate::Length`], so a responsive layout reads as:
///
/// ```rust,no_run
/// # use ltk::{ button, column, text, Length, Element };
/// # #[ derive( Clone ) ] enum Msg { Ok }
/// # fn _ex() -> Element<Msg> {
/// column()
///     // Padding is 3 % of the viewport's smaller side, clamped to 16..48 px.
///     .padding( Length::vmin( 3.0 ).clamp( 16.0, 48.0 ) )
///     .spacing( Length::vmin( 1.5 ).at_least( 8.0 ) )
///     .max_width( Length::vw( 60.0 ).at_most( 720.0 ) )
///     .push( text( "Responsive" ) )
///     .push( button( "OK" ).on_press( Msg::Ok ) )
/// .into()
/// # }
/// ```
pub struct Column<Msg: Clone>
{
	pub( crate ) children:      Vec<Element<Msg>>,
	/// Vertical gap between children. Stored as [`Length`] so a `Vmin(2.0)`
	/// or `Em(0.5)` gap scales with the viewport instead of freezing at a
	/// px constant.
	pub( crate ) spacing:       Length,
	/// Padding on all sides. Same [`Length`] semantics as `spacing`.
	pub( crate ) padding:       Length,
	pub( crate ) align_center_x: bool,
	pub( crate ) center_y:      bool,
	pub( crate ) max_width:     Option<Length>,
	pub( crate ) fit_content:   bool,
}

impl<Msg: Clone> Column<Msg>
{
	pub fn new() -> Self
	{
		Self
		{
			children:       Vec::new(),
			spacing:        Length::px( 8.0 ),
			padding:        Length::px( 16.0 ),
			align_center_x: true,
			center_y:       false,
			max_width:      None,
			fit_content:    false,
		}
	}

	/// Append a child widget or layout.
	pub fn push( mut self, e: impl Into<Element<Msg>> ) -> Self
	{
		self.children.push( e.into() );
		self
	}

	/// Set the vertical gap between children. Default: `8.0` px. Accepts
	/// any [`Length`] — pass an `f32` for the px case, or a relative
	/// value like `Length::vmin( 2.0 )` to scale with the viewport.
	pub fn spacing( mut self, s: impl Into<Length> ) -> Self
	{
		self.spacing = s.into();
		self
	}

	/// Set the padding (all sides). Default: `16.0` px. Accepts any
	/// [`Length`].
	pub fn padding( mut self, p: impl Into<Length> ) -> Self
	{
		self.padding = p.into();
		self
	}

	/// When `true` (default), children are centered horizontally.
	pub fn align_center_x( mut self, c: bool ) -> Self
	{
		self.align_center_x = c;
		self
	}

	/// When `true`, center the content block vertically (only when no spacers are present).
	pub fn center_y( mut self, c: bool ) -> Self
	{
		self.center_y = c;
		self
	}

	/// Limit the content width. Accepts any [`Length`]. The column still
	/// reports the parent's `max_width` as its preferred width so the
	/// parent allocates the full available rect.
	pub fn max_width( mut self, w: impl Into<Length> ) -> Self
	{
		self.max_width = Some( w.into() );
		self
	}

	#[ inline ]
	fn resolved_spacing( &self, canvas: &Canvas ) -> f32
	{
		canvas.resolve_geom( self.spacing )
	}

	#[ inline ]
	fn resolved_padding( &self, canvas: &Canvas ) -> f32
	{
		canvas.resolve_geom( self.padding )
	}

	#[ inline ]
	fn resolved_max_width( &self, canvas: &Canvas ) -> Option<f32>
	{
		self.max_width.map( |l| canvas.resolve_geom( l ) )
	}

	/// Report the intrinsic content width as preferred width instead of filling
	/// the available `max_width`. Use this when the column represents a card
	/// or widget meant to sit side-by-side with other children inside a
	/// [`Row`](crate::layout::row::Row) — without this flag, two columns in a
	/// row each claim the full row width and overflow their siblings.
	///
	/// The preferred width is computed as the max of children's preferred
	/// widths plus padding, capped by the external `max_width` the parent
	/// offers and by any `max_width` setting on the column itself.
	pub fn fit_content( mut self ) -> Self
	{
		self.fit_content = true;
		self
	}

	fn inner_w( &self, available: f32, canvas: &Canvas ) -> f32
	{
		let w = available - self.resolved_padding( canvas ) * 2.0;
		self.resolved_max_width( canvas ).map( |m| w.min( m ) ).unwrap_or( w )
	}

	fn content_h( &self, inner_w: f32, canvas: &Canvas ) -> f32
	{
		// Spacers and flex children contribute 0 to natural height (their
		// real height is leftover distribution, mirroring how a row counts
		// flex width); spacing still applies between all children.
		self.children.iter()
			.map( |c| match c
			{
				Element::Spacer( s ) => s.resolved_height( canvas ).unwrap_or( 0.0 ),
				Element::Flex( _ )   => 0.0,
				other => other.preferred_size( inner_w, canvas ).1,
			} )
			.sum::<f32>()
			+ self.resolved_spacing( canvas ) * ( self.children.len().saturating_sub( 1 ) ) as f32
	}

	/// Return the preferred `(width, height)` given available `max_width`.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		let inner_w = self.inner_w( max_width, canvas );
		let pad     = self.resolved_padding( canvas );
		let total_h = self.content_h( inner_w, canvas ) + pad * 2.0;

		let w = if self.fit_content
		{
			// "Filler" widgets (Spacer, Separator, Scroll, ProgressBar, Slider,
			// Toggle, TextEdit) all report `max_width` as their preferred width:
			// they stretch across whatever rect the parent allocates. Including
			// them when picking the intrinsic content width would claim
			// `max_width` and defeat the flag, so skip them — only content-sized
			// children (Text, Button, Image, nested fit-content Columns/Rows)
			// drive the natural width.
			let content_w = self.children.iter()
				.map( |c| match c
				{
					Element::Spacer( _ )      => 0.0,
					Element::Separator( _ )   => 0.0,
					Element::Scroll( _ )      => 0.0,
					Element::ProgressBar( _ ) => 0.0,
					Element::Slider( _ )      => 0.0,
					// TextEdit defaults to claiming `max_width`, but
					// a field built with `.fixed_width( w )` reports
					// a pinned natural size — let those through so a
					// numeric digit field inside a `fit_content`
					// stepper column can drive the column's width.
					Element::TextEdit( t ) => if t.fixed_width.is_some()
					{
						t.preferred_size( inner_w, canvas ).0
					} else { 0.0 },
					other => other.preferred_size( inner_w, canvas ).0,
				} )
				.fold( 0.0_f32, f32::max );
			( content_w + pad * 2.0 ).min( max_width )
		} else {
			max_width
		};

		( w, total_h )
	}

	pub fn draw( &self, _canvas: &mut Canvas, _rect: Rect, _focused: bool ) {}

	/// Layout children within rect and return (rect, child_index) pairs.
	pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
	{
		let inner_w = self.inner_w( rect.width, canvas );
		let pad     = self.resolved_padding( canvas );
		let spacing = self.resolved_spacing( canvas );

		let total_weight: u32 = self.children.iter()
			.map( |c| match c
			{
				Element::Spacer( s ) if s.resolved_height( canvas ).is_none() => s.weight,
				Element::Scroll( s ) if s.axis.allows_y() => 1,
				Element::Flex( f )                         => f.weight,
				_                                          => 0,
			} )
			.sum();

		let fixed_h: f32 = self.children.iter()
			.map( |c|
			{
				if matches!( c, Element::Scroll( s ) if s.axis.allows_y() )
				{
					0.0
				} else if matches!( c, Element::Flex( _ ) )
				{
					0.0
				} else if let Element::Spacer( s ) = c {
					s.resolved_height( canvas ).unwrap_or( 0.0 )
				} else {
					c.preferred_size( inner_w, canvas ).1
				}
			} )
			.sum::<f32>()
			+ spacing * ( self.children.len().saturating_sub( 1 ) ) as f32;

		let avail_h      = rect.height - pad * 2.0;
		let avail_spare  = ( avail_h - fixed_h ).max( 0.0 );

		// `center_y` only applies when there are no flexible children
		// (weight-only spacers, y-scrolls, flex wrappers).
		let start_y = if total_weight == 0 && self.center_y
		{
			rect.y + pad + avail_spare / 2.0
		} else {
			rect.y + pad
		};

		let start_x = rect.x + (rect.width - inner_w) / 2.0;

		let mut y = start_y;
		let mut result = Vec::new();
		for ( i, child ) in self.children.iter().enumerate()
		{
			let ( w, h ) = match child
			{
				Element::Spacer( s ) =>
				{
					let h = if let Some( fixed ) = s.resolved_height( canvas )
					{
						fixed
					} else if total_weight > 0
					{
						avail_spare * s.weight as f32 / total_weight as f32
					} else {
						0.0
					};
					( inner_w, h )
				},
				Element::Scroll( s ) if s.axis.allows_y() =>
				{
					let h = if total_weight > 0
					{
						avail_spare / total_weight as f32
					} else {
						0.0
					};
					( inner_w, h )
				},
				Element::Flex( f ) =>
				{
					let h = if total_weight > 0
					{
						avail_spare * f.weight as f32 / total_weight as f32
					} else {
						0.0
					};
					( inner_w, h )
				},
				other => other.preferred_size( inner_w, canvas ),
			};
			let x = if self.align_center_x && !matches!( child, Element::Spacer( _ ) )
			{
				start_x + (inner_w - w) / 2.0
			} else {
				start_x
			};
			result.push( ( Rect { x, y, width: w, height: h }, i ) );
			y += h + spacing;
		}
		result
	}

	pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> Column<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Column
		{
			children:       self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
			spacing:        self.spacing,
			padding:        self.padding,
			align_center_x: self.align_center_x,
			center_y:       self.center_y,
			max_width:      self.max_width,
			fit_content:    self.fit_content,
		}
	}
}

/// Create an empty column layout.
pub fn column<Msg: Clone>() -> Column<Msg>
{
	Column::new()
}

impl<Msg: Clone> Default for Column<Msg>
{
	fn default() -> Self
	{
		Self::new()
	}
}

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

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

	#[ test ]
	fn preferred_size_width_equals_max_width()
	{
		let canvas = make_canvas();
		let col    = column::<()>().padding( 10.0 );
		let ( w, _ ) = col.preferred_size( 200.0, &canvas );
		assert_eq!( w, 200.0 );
	}

	#[ test ]
	fn empty_column_height_is_two_paddings()
	{
		let canvas = make_canvas();
		let col    = column::<()>().padding( 10.0 );
		let ( _, h ) = col.preferred_size( 200.0, &canvas );
		assert_eq!( h, 20.0 );
	}

	#[ test ]
	fn max_width_caps_inner_w_not_preferred_w()
	{
		let canvas = make_canvas();
		// preferred_size always returns available max_width; max_width caps inner layout only
		let col    = column::<()>().padding( 0.0 ).max_width( 100.0 );
		let ( w, _ ) = col.preferred_size( 200.0, &canvas );
		assert_eq!( w, 200.0 );
	}

	#[ test ]
	fn inner_w_respects_padding_and_max_width()
	{
		let canvas = make_canvas();
		let col = column::<()>().padding( 20.0 ).max_width( 100.0 );
		// available = 200, minus padding*2 = 160, capped at max_width = 100
		assert_eq!( col.inner_w( 200.0, &canvas ), 100.0 );
	}

	#[ test ]
	fn inner_w_without_max_width_subtracts_padding()
	{
		let canvas = make_canvas();
		let col = column::<()>().padding( 10.0 );
		assert_eq!( col.inner_w( 200.0, &canvas ), 180.0 );
	}

	#[ test ]
	fn spacing_between_children_accumulates()
	{
		let canvas = make_canvas();
		// Three zero-height spacers, two 8 px gaps between them = 16.
		let col = column::<()>()
			.padding( 0.0 )
			.spacing( 8.0 )
			.push( crate::spacer() )
			.push( crate::spacer() )
			.push( crate::spacer() );
		let ( _, h ) = col.preferred_size( 100.0, &canvas );
		assert_eq!( h, 16.0 );
	}

	#[ test ]
	fn vmin_spacing_resolves_against_canvas_viewport()
	{
		// Canvas is 800x600 → vmin = 600. 5 % of 600 = 30 px per gap.
		// Three zero-height spacers → two gaps → 60 px total.
		let canvas = make_canvas();
		let col = column::<()>()
			.padding( 0.0 )
			.spacing( Length::vmin( 5.0 ) )
			.push( crate::spacer() )
			.push( crate::spacer() )
			.push( crate::spacer() );
		let ( _, h ) = col.preferred_size( 100.0, &canvas );
		assert_eq!( h, 60.0 );
	}

	#[ test ]
	fn vmin_padding_doubles_around_content()
	{
		// 4 % of 600 = 24 px padding on each side → 48 px on an empty column.
		let canvas = make_canvas();
		let col = column::<()>().padding( Length::vmin( 4.0 ) );
		let ( _, h ) = col.preferred_size( 100.0, &canvas );
		assert_eq!( h, 48.0 );
	}

	#[ test ]
	fn vmin_max_width_caps_inner_w()
	{
		// 20 % of 600 = 120 px max-width.
		let canvas = make_canvas();
		let col = column::<()>().padding( 0.0 ).max_width( Length::vmin( 20.0 ) );
		assert_eq!( col.inner_w( 200.0, &canvas ), 120.0 );
	}

	#[ test ]
	fn flex_child_takes_leftover_height()
	{
		// A 30 px fixed spacer and one flex child in a 100 px rect:
		// the flex gets the remaining 70 px at full inner width.
		let canvas = make_canvas();
		let col = column::<()>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.push( crate::spacer().height( 30.0 ) )
			.push( crate::flex( crate::spacer() ) );
		let rect  = crate::types::Rect { x: 0.0, y: 0.0, width: 200.0, height: 100.0 };
		let rects = col.layout( rect, &canvas );
		assert_eq!( rects.len(), 2 );
		assert!( ( rects[1].0.height - 70.0 ).abs() < 0.01 );
		assert!( ( rects[1].0.width - 200.0 ).abs() < 0.01 );
	}

	#[ test ]
	fn flex_children_split_leftover_by_weight()
	{
		// Two flex children weighted 1:3 share 100 px as 25 / 75.
		let canvas = make_canvas();
		let col = column::<()>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.push( crate::flex( crate::spacer() ) )
			.push( crate::flex( crate::spacer() ).weight( 3 ) );
		let rect  = crate::types::Rect { x: 0.0, y: 0.0, width: 200.0, height: 100.0 };
		let rects = col.layout( rect, &canvas );
		assert!( ( rects[0].0.height - 25.0 ).abs() < 0.01 );
		assert!( ( rects[1].0.height - 75.0 ).abs() < 0.01 );
	}

	#[ test ]
	fn flex_contributes_zero_to_natural_height()
	{
		// Natural height counts only the fixed spacer, like row width math.
		let canvas = make_canvas();
		let col = column::<()>()
			.padding( 0.0 )
			.spacing( 0.0 )
			.push( crate::spacer().height( 30.0 ) )
			.push( crate::flex( crate::spacer() ) );
		let ( _, h ) = col.preferred_size( 200.0, &canvas );
		assert_eq!( h, 30.0 );
	}
}
