// 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 horizontal layout container.
///
/// Children are arranged left-to-right. Use [`Row::align_right`] to
/// push the content block to the right edge of the available width.
///
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use ltk::{ icon_button, row, Element };
/// # #[ derive( Clone ) ] enum Msg { A, B }
/// # fn _ex( a_rgba: Arc<Vec<u8>>, b_rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
/// row()
///     .spacing( 16.0 )
///     .align_right()
///     .push( icon_button( a_rgba, w, h ).on_press( Msg::A ) )
///     .push( icon_button( b_rgba, w, h ).on_press( Msg::B ) )
/// .into()
/// # }
/// ```
///
/// `spacing` and `padding` accept any [`crate::Length`]:
///
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use ltk::{ icon_button, row, Length, Element };
/// # #[ derive( Clone ) ] enum Msg { A, B }
/// # fn _ex( a_rgba: Arc<Vec<u8>>, b_rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> Element<Msg> {
/// row()
///     // 2 % of the viewport's smaller side, never below 8 px.
///     .spacing( Length::vmin( 2.0 ).at_least( 8.0 ) )
///     .push( icon_button( a_rgba, w, h ).on_press( Msg::A ) )
///     .push( icon_button( b_rgba, w, h ).on_press( Msg::B ) )
/// .into()
/// # }
/// ```
pub struct Row<Msg: Clone>
{
	pub( crate ) children:    Vec<Element<Msg>>,
	/// Horizontal gap between children. [`Length`]; default `8.0` px.
	pub( crate ) spacing:     Length,
	/// Padding on all sides. [`Length`]; default `0.0` px.
	pub( crate ) padding:     Length,
	pub( crate ) align_right: bool,
	pub( crate ) align_top:   bool,
	pub( crate ) fill_height: bool,
	/// When false the row keeps every child at its preferred width even if
	/// the cluster overflows. Default true.
	pub( crate ) shrink:      bool,
}

impl<Msg: Clone> Row<Msg>
{
	pub fn new() -> Self
	{
		Self
		{
			children:    Vec::new(),
			spacing:     Length::px( 8.0 ),
			padding:     Length::px( 0.0 ),
			align_right: false,
			align_top:   false,
			fill_height: false,
			shrink:      true,
		}
	}

	/// 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 horizontal gap between children. Default: `8.0` px. Accepts
	/// any [`Length`] so the gap can 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: `0.0` px. Accepts any
	/// [`Length`].
	pub fn padding( mut self, p: impl Into<Length> ) -> Self
	{
		self.padding = p.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 )
	}

	/// Keep every child at its preferred width even when the cluster does not
	/// fit, letting it overflow the row's rect.
	///
	/// The default distributes the shortfall instead, which is what a row of
	/// labelled buttons wants. Opt out for strips that are deliberately wider
	/// than their viewport — a carousel rail, a tray meant to be scrolled —
	/// where shrinking to fit would defeat the point.
	pub fn no_shrink( mut self ) -> Self
	{
		self.shrink = false;
		self
	}

	/// Push the content block to the right edge of the available width.
	pub fn align_right( mut self ) -> Self
	{
		self.align_right = true;
		self
	}

	/// Pin children to the top edge instead of the default vertical
	/// centering, so siblings of slightly different heights (e.g. two
	/// cards whose text metrics differ) share the same top line.
	pub fn align_top( mut self ) -> Self
	{
		self.align_top = true;
		self
	}

	/// Stretch every child to the row's inner height, so siblings share
	/// both the top and the bottom line regardless of their natural
	/// heights. The row's own height still comes from the tallest
	/// child's preferred size; children whose content should track the
	/// stretch need internal flexible spacers.
	pub fn fill_height( mut self ) -> Self
	{
		self.fill_height = true;
		self
	}

	/// Return the preferred `(width, height)` given available `max_width`.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		let pad     = self.resolved_padding( canvas );
		let spacing = self.resolved_spacing( canvas );
		// Width contribution of every fixed (non-flex, non-flex-spacer) child.
		// Used to compute the residual width that wrap-style children will
		// actually render in, so their reported height matches the layout.
		let inner_w = ( max_width - pad * 2.0 ).max( 0.0 );
		let gaps    = spacing * self.children.len().saturating_sub( 1 ) as f32;
		let fixed_w: f32 = self.children.iter()
			.filter( |c| match c
			{
				Element::Flex( _ )   => false,
				Element::Spacer( s ) => s.resolved_width( canvas ).is_some(),
				_                    => true,
			} )
			.map( |c| c.preferred_size( max_width, canvas ).0 )
			.sum();
		let residual = ( inner_w - fixed_w - gaps ).max( 0.0 );

		let max_h: f32 = self.children.iter()
			.map( |c| match c
			{
				Element::Flex( _ )   => c.preferred_size( residual, canvas ).1,
				Element::Spacer( _ ) => c.preferred_size( max_width, canvas ).1,
				_                    => c.preferred_size( max_width, canvas ).1,
			} )
			.fold( 0.0_f32, f32::max );

		// `align_right` and any flex / weight-only spacer child make the row
		// claim the full `max_width`: in those cases the row's rendered width
		// comes from leftover distribution (or the right-edge anchor), not
		// from the sum of children's preferred widths. Reporting only the
		// natural sum would leave the parent allocating a too-narrow rect and
		// the flex children would collapse to 0.
		let has_flex = self.children.iter().any( |c| match c
		{
			Element::Flex( _ )   => true,
			Element::Spacer( s ) => s.resolved_width( canvas ).is_none(),
			_                    => false,
		} );

		let w = if self.align_right || has_flex
		{
			max_width
		} else {
			let total_w: f32 = self.children.iter()
				.map( |c| c.preferred_size( max_width, canvas ).0 )
				.sum::<f32>()
				+ gaps
				+ pad * 2.0;
			total_w.min( max_width )
		};

		( w, max_h + pad * 2.0 )
	}

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

	/// Layout children within rect and return `(rect, child_index)` pairs.
	///
	/// Flexible [`Spacer`](crate::layout::spacer::Spacer) children claim the
	/// leftover horizontal space: non-spacer widgets keep their preferred
	/// width, the remaining width (after subtracting spacing + padding) is
	/// distributed between spacers in proportion to their `weight`. When no
	/// spacers are present the cluster is centered (or right-aligned via
	/// [`Row::align_right`]).
	pub fn layout( &self, rect: Rect, canvas: &Canvas ) -> Vec<(Rect, usize)>
	{
		let pad     = self.resolved_padding( canvas );
		let spacing = self.resolved_spacing( canvas );
		let inner_h = rect.height - pad * 2.0;

		// Spacers and `Flex` wrappers report 0 width here; their real width
		// comes from the flex distribution below.
		let sizes: Vec<(f32, f32)> = self.children.iter()
			.map( |c| c.preferred_size( rect.width, canvas ) )
			.collect();

		let gaps = spacing * self.children.len().saturating_sub( 1 ) as f32;
		let fixed_w: f32 = self.children.iter().zip( sizes.iter() )
			.filter( |( c, _ )| match c
			{
				// Pure flex children (`Flex` and weight-only `Spacer`) take
				// width from the leftover pool; everything else, including
				// `Spacer::width(...)`-pinned spacers, contributes to the
				// fixed-width tally.
				Element::Flex( _ )   => false,
				Element::Spacer( s ) => s.resolved_width( canvas ).is_some(),
				_                    => true,
			} )
			.map( |( _, ( w, _ ) )| *w )
			.sum();

		let total_weight: u32 = self.children.iter()
			.filter_map( |c| match c {
				Element::Spacer( s ) if s.resolved_width( canvas ).is_none() => Some( s.weight ),
				Element::Flex( f )   => Some( f.weight ),
				_                    => None,
			} )
			.sum();

		let inner_w = ( rect.width - pad * 2.0 ).max( 0.0 );

		// Deficit distribution: cap the widest children first, so a long
		// label gives up its width before a neighbouring icon does. A uniform
		// scale-down would thin a header's back arrow along with its title.
		// Spacers keep the width they were pinned to — an explicit gap is a
		// decision, not slack.
		let mut flexible: Vec<f32> = self.children.iter().zip( sizes.iter() )
			.filter( |( c, _ )| !matches!( c, Element::Flex( _ ) | Element::Spacer( _ ) ) )
			.map( |( _, ( w, _ ) )| *w )
			.collect();
		let flexible_w: f32 = flexible.iter().sum();
		let rigid_w = ( fixed_w - flexible_w ).max( 0.0 );
		let cap = if self.shrink
		{
			width_cap( &mut flexible, ( inner_w - gaps - rigid_w ).max( 0.0 ) )
		} else {
			f32::INFINITY
		};

		let laid_w      = rigid_w + flexible.iter().map( |w| w.min( cap ) ).sum::<f32>();
		let leftover    = ( inner_w - laid_w - gaps ).max( 0.0 );
		let has_spacers = total_weight > 0;

		let ( start_x, flex_unit ) = if has_spacers
		{
			// Spacers and `Flex` wrappers claim the leftover; the cluster
			// sits flush to the left edge of the inner rect.
			( rect.x + pad, leftover / total_weight as f32 )
		}
		else if self.align_right
		{
			( rect.x + rect.width - ( laid_w + gaps ) - pad, 0.0 )
		}
		else
		{
			( rect.x + ( rect.width - laid_w - gaps ) / 2.0, 0.0 )
		};

		let mut x = start_x;
		let mut result = Vec::with_capacity( self.children.len() );
		for ( i, ( (w, h), child) ) in sizes.into_iter().zip( self.children.iter() ).enumerate()
		{
			let width = match child
			{
				Element::Spacer( s ) => match s.resolved_width( canvas )
				{
					Some( fw ) => fw,
					None       => flex_unit * s.weight as f32,
				},
				Element::Flex( f )   => flex_unit * f.weight as f32,
				_                    => w.min( cap ),
			};
			let ( y, height ) = if self.fill_height && !matches!( child, Element::Spacer( _ ) )
			{
				( rect.y + pad, inner_h )
			} else if self.align_top {
				( rect.y + pad, h )
			} else {
				( rect.y + pad + ( inner_h - h ) / 2.0, h )
			};
			result.push( ( Rect { x, y, width, height }, i ) );
			x += width + spacing;
		}
		result
	}

	pub( crate ) fn map_msg<U>( self, f: &crate::widget::MapFn<Msg, U> ) -> Row<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Row
		{
			children:    self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
			spacing:     self.spacing,
			padding:     self.padding,
			align_right: self.align_right,
			align_top:   self.align_top,
			fill_height: self.fill_height,
			shrink:      self.shrink,
		}
	}
}

/// Largest per-child width `c` for which `sum( min( w, c ) ) <= available`.
///
/// Water filling: the widest children give up width first and the narrow ones
/// are left alone until the wide ones have been levelled down to them. In a
/// header that means the title absorbs the whole shortfall while the back
/// arrow beside it keeps its size. Sorts `widths` in place; returns infinity
/// when everything already fits.
fn width_cap( widths: &mut [f32], available: f32 ) -> f32
{
	if widths.iter().sum::<f32>() <= available
	{
		return f32::INFINITY;
	}
	widths.sort_by( |a, b| a.partial_cmp( b ).unwrap_or( std::cmp::Ordering::Equal ) );

	let mut prefix = 0.0_f32;
	for ( i, w ) in widths.iter().enumerate()
	{
		let remaining = ( widths.len() - i ) as f32;
		let cap = ( available - prefix ) / remaining;
		if cap <= *w
		{
			return cap.max( 0.0 );
		}
		prefix += *w;
	}
	0.0
}

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

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

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

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

	/// Children whose preferred widths overflow the rect are scaled down to
	/// fit, rather than spilling symmetrically out of it.
	#[ test ]
	fn overflowing_children_shrink_to_fit()
	{
		let canvas = make_canvas();
		let build  = || row::<()>()
			.spacing( 0.0 )
			.padding( 0.0 )
			.push( crate::toggle( false ) )
			.push( crate::toggle( false ) );

		let natural = build().preferred_size( 10_000.0, &canvas ).0;
		assert!( natural > 0.0, "toggles must report a width for this to test anything" );
		let rect = Rect { x: 0.0, y: 0.0, width: natural / 2.0, height: 48.0 };

		let laid: f32 = build().layout( rect, &canvas )
			.iter().map( |( r, _ )| r.width ).sum();
		assert!( laid <= rect.width + 0.5, "laid out {laid} into {}", rect.width );

		let kept: f32 = build().no_shrink().layout( rect, &canvas )
			.iter().map( |( r, _ )| r.width ).sum();
		assert!( kept > rect.width, "no_shrink kept {kept}, expected wider than {}", rect.width );
	}

	/// The shortfall comes off the widest child first: a narrow neighbour —
	/// a header's back arrow next to its title — keeps its size.
	#[ test ]
	fn narrow_child_survives_a_wide_neighbour()
	{
		let mut w = vec![ 20.0_f32, 300.0 ];
		let cap   = width_cap( &mut w, 200.0 );
		assert_eq!( 20.0_f32.min( cap ), 20.0 );
		assert_eq!( 300.0_f32.min( cap ), 180.0 );
	}

	/// Equal children share the shortfall equally, which is what a segmented
	/// control wants.
	#[ test ]
	fn equal_children_shrink_equally()
	{
		let mut w = vec![ 100.0_f32, 100.0, 100.0 ];
		let cap   = width_cap( &mut w, 150.0 );
		assert_eq!( cap, 50.0 );
	}

	#[ test ]
	fn nothing_shrinks_when_it_all_fits()
	{
		let mut w = vec![ 10.0_f32, 20.0 ];
		assert_eq!( width_cap( &mut w, 100.0 ), f32::INFINITY );
	}

	/// Pinned spacers are an explicit gap, not slack, so the shrink pass
	/// leaves them alone.
	#[ test ]
	fn pinned_spacers_keep_their_width_when_shrinking()
	{
		let canvas = make_canvas();
		let r = row::<()>()
			.spacing( 0.0 )
			.padding( 0.0 )
			.push( crate::spacer().width( 60.0 ).height( 10.0 ) )
			.push( crate::toggle( false ) );
		let rect  = Rect { x: 0.0, y: 0.0, width: 70.0, height: 48.0 };
		let cells = r.layout( rect, &canvas );
		assert_eq!( cells[ 0 ].0.width, 60.0 );
	}

	#[ test ]
	fn align_right_returns_full_max_width()
	{
		let canvas = make_canvas();
		let r      = row::<()>().align_right();
		let ( w, _ ) = r.preferred_size( 500.0, &canvas );
		assert_eq!( w, 500.0 );
	}

	#[ test ]
	fn align_right_true_regardless_of_children()
	{
		let canvas = make_canvas();
		let r = row::<()>().align_right().spacing( 999.0 );
		let ( w, _ ) = r.preferred_size( 300.0, &canvas );
		assert_eq!( w, 300.0 );
	}

	#[ test ]
	fn centered_empty_row_returns_zero_width()
	{
		let canvas = make_canvas();
		let r      = row::<()>();
		let ( w, _ ) = r.preferred_size( 500.0, &canvas );
		assert_eq!( w, 0.0 );
	}

	#[ test ]
	fn centered_empty_row_returns_zero_height()
	{
		let canvas = make_canvas();
		let r      = row::<()>().padding( 0.0 );
		let ( _, h ) = r.preferred_size( 500.0, &canvas );
		assert_eq!( h, 0.0 );
	}

	#[ test ]
	fn padding_adds_to_height()
	{
		let canvas = make_canvas();
		let r      = row::<()>().padding( 8.0 );
		let ( _, h ) = r.preferred_size( 500.0, &canvas );
		assert_eq!( h, 16.0 );
	}

	#[ test ]
	fn layout_of_empty_row_is_empty()
	{
		let canvas = make_canvas();
		let r      = row::<()>().align_right();
		let rect   = Rect { x: 0., y: 0., width: 400., height: 48. };
		assert!( r.layout( rect, &canvas ).is_empty() );
	}

	#[ test ]
	fn vmin_padding_doubles_around_content()
	{
		// 800x600 canvas → vmin = 600. 4 % = 24 px each side → 48 px height.
		let canvas = make_canvas();
		let r      = row::<()>().padding( Length::vmin( 4.0 ) );
		let ( _, h ) = r.preferred_size( 500.0, &canvas );
		assert_eq!( h, 48.0 );
	}

	#[ test ]
	fn vmin_spacing_pins_visible_layout_gap()
	{
		// Two fixed-width spacers separated by a vmin spacing of 5 %.
		// Canvas vmin = 600 → 30 px between the inner edges. With both
		// spacers 10 px wide, the second one's `x` minus the first one's
		// `x + width` must equal the gap regardless of where the row chose
		// to anchor the cluster (centered, since there are no flex spacers).
		let canvas = make_canvas();
		let r = row::<()>()
			.padding( 0.0 )
			.spacing( Length::vmin( 5.0 ) )
			.push( crate::spacer().width( 10.0 ) )
			.push( crate::spacer().width( 10.0 ) );
		let rect = Rect { x: 0., y: 0., width: 200., height: 48. };
		let placed = r.layout( rect, &canvas );
		assert_eq!( placed.len(), 2 );
		let ( first_rect, _ )  = placed[ 0 ];
		let ( second_rect, _ ) = placed[ 1 ];
		let gap = second_rect.x - ( first_rect.x + first_rect.width );
		assert!( ( gap - 30.0 ).abs() < 1e-3, "expected ~30 px gap, got {gap}" );
	}
}
