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

//! Horizontal carousel: each child occupies `focused_width_frac` of the
//! viewport width, neighbours peek out to the sides. The carousel itself
//! is a pure layout primitive — the caller owns the scroll offset and
//! drives drag / inertia / snap externally. This keeps the widget side
//! stateless and lets the host compositor reuse its existing touch
//! pipeline.
//!
//! ```rust,no_run
//! # use ltk::{ carousel, container, spacer, Element };
//! # #[ derive( Clone ) ] enum Msg { Select( usize ) }
//! # fn _ex( offset: f32 ) -> Element<Msg> {
//! carousel()
//!     .focused_width_frac( 0.8 )
//!     .gap( 16.0 )
//!     .offset( offset )
//!     .push( container( spacer() ) )
//!     .push( container( spacer() ) )
//! .into()
//! # }
//! ```

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

#[ cfg( test ) ]
mod tests;

pub struct Carousel<Msg: Clone>
{
	/// Child widgets in display order, left-to-right.
	pub( crate ) children:           Vec<Element<Msg>>,
	/// Optional stable identifier — used by the host as the key for its
	/// drag / inertia / snap state.
	pub( crate ) id:                 Option<WidgetId>,
	/// Each child's width as a fraction of the viewport width. 0.8 leaves
	/// 10% on each side for the neighbours to peek out.
	pub( crate ) focused_width_frac: f32,
	/// Horizontal gap between adjacent children, in logical pixels.
	pub( crate ) gap:                f32,
	/// Logical-pixel offset applied to every child. Positive values shift
	/// children to the right (revealing later tiles). The host clamps,
	/// snaps and animates this value.
	pub( crate ) offset:             f32,
}

impl<Msg: Clone> Carousel<Msg>
{
	pub fn push( mut self, child: impl Into<Element<Msg>> ) -> Self
	{
		self.children.push( child.into() );
		self
	}

	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = Some( id );
		self
	}

	pub fn focused_width_frac( mut self, f: f32 ) -> Self
	{
		self.focused_width_frac = f.clamp( 0.05, 1.0 );
		self
	}

	pub fn gap( mut self, g: f32 ) -> Self
	{
		self.gap = g.max( 0.0 );
		self
	}

	pub fn offset( mut self, o: f32 ) -> Self
	{
		self.offset = o;
		self
	}

	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
	{
		if self.children.is_empty() { return ( max_width, 0.0 ); }
		let child_w = ( max_width * self.focused_width_frac ).max( 1.0 );
		let max_h = self.children.iter()
			.map( |c| c.preferred_size( child_w, canvas ).1 )
			.fold( 0.0_f32, f32::max );
		( max_width, max_h )
	}

	/// Snap-target offset that centres `idx` in the viewport. Positive
	/// values shift the carousel content right — i.e. the negation of
	/// the natural "scroll x" so callers can pass it straight back into
	/// [`offset()`](Self::offset).
	pub fn snap_offset( &self, viewport_w: f32, idx: usize ) -> f32
	{
		if self.children.is_empty() { return 0.0; }
		let child_w = ( viewport_w * self.focused_width_frac ).max( 1.0 );
		let stride  = child_w + self.gap;
		-( idx as f32 ) * stride
	}

	/// Index of the child whose centre is closest to the viewport centre,
	/// given the current `offset`. Used by the host to decide which tile a
	/// release should snap to and to compute the keyboard-navigation target.
	pub fn focused_index( &self, viewport_w: f32 ) -> usize
	{
		if self.children.is_empty() { return 0; }
		let child_w = ( viewport_w * self.focused_width_frac ).max( 1.0 );
		let stride  = child_w + self.gap;
		let raw     = -self.offset / stride;
		raw.round().clamp( 0.0, ( self.children.len() - 1 ) as f32 ) as usize
	}

	pub fn layout( &self, rect: Rect, _canvas: &Canvas ) -> Vec<( Rect, usize )>
	{
		if self.children.is_empty() { return Vec::new(); }
		let child_w = ( rect.width * self.focused_width_frac ).max( 1.0 );
		let base_x  = rect.x + ( rect.width - child_w ) / 2.0 + self.offset;
		let stride  = child_w + self.gap;

		self.children.iter().enumerate().map( |( i, _ )|
		{
			let x = base_x + ( i as f32 ) * stride;
			( Rect { x, y: rect.y, width: child_w, height: rect.height }, i )
		}).collect()
	}

	pub fn draw( &self ) {}

	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Carousel<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Carousel
		{
			children:           self.children.into_iter().map( |c| c.map_arc( f ) ).collect(),
			id:                 self.id,
			focused_width_frac: self.focused_width_frac,
			gap:                self.gap,
			offset:             self.offset,
		}
	}
}

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

pub fn carousel<Msg: Clone>() -> Carousel<Msg>
{
	Carousel
	{
		children:           Vec::new(),
		id:                 None,
		focused_width_frac: 0.8,
		gap:                16.0,
		offset:             0.0,
	}
}
