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

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

#[ cfg( test ) ]
mod tests;

/// Which axes a `Scroll` viewport allows to move along. Determines
/// whether the layout grows the child past the viewport width, the
/// viewport height, or both, and which axis gesture / wheel deltas
/// route to.
///
/// `Vertical` is the default (matches the historic behaviour) so
/// existing call sites do not need a builder call to migrate.
#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
pub enum ScrollAxis
{
	/// Standard list-style scroll: child grows vertically, child width
	/// is clamped to the viewport.
	Vertical,
	/// Strip / timeline scroll: child grows horizontally, child height
	/// is clamped to the viewport.
	Horizontal,
	/// Two-axis pan (large tables, code views, maps). Child may exceed
	/// the viewport on either axis.
	Both,
}

impl ScrollAxis
{
	#[ inline ]
	pub fn allows_x( self ) -> bool { matches!( self, ScrollAxis::Horizontal | ScrollAxis::Both ) }
	#[ inline ]
	pub fn allows_y( self ) -> bool { matches!( self, ScrollAxis::Vertical   | ScrollAxis::Both ) }
}

/// A scrollable viewport that clips its child to its allocated rect.
///
/// The child can be any element — typically a [`Column`](crate::layout::column::Column)
/// for lists or a [`WrapGrid`](crate::layout::wrap_grid::WrapGrid) for icon grids.
///
/// Scroll offset is driven by touch/pointer drag gestures within the viewport.
/// Dragging inside a `Scroll` does **not** trigger the app-level swipe-up gesture.
///
/// ```rust,no_run
/// # use std::sync::Arc;
/// # use ltk::{ column, grid, icon_button, scroll, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex( rgba: Arc<Vec<u8>>, w: u32, h: u32 ) -> ( Element<Msg>, Element<Msg> ) {
/// // Scrollable list
/// let list = scroll( column().spacing( 8.0 ).push( text( "Item 1" ) ).push( text( "Item 2" ) ) );
///
/// // App-drawer style grid
/// let drawer = scroll( grid( 4 ).padding( 16.0 ).spacing( 12.0 ).push( icon_button( rgba, w, h ) ) );
/// # ( list.into(), drawer.into() )
/// # }
/// ```
pub struct Scroll<Msg: Clone>
{
	/// The single child element drawn inside the scrollable viewport.
	pub( crate ) child: Box<Element<Msg>>,
	/// Optional stable identifier — used as scroll state key.
	pub( crate ) id:    Option<WidgetId>,
	/// Which axis (or both) this viewport scrolls along.
	pub( crate ) axis:  ScrollAxis,
}

impl<Msg: Clone> Scroll<Msg>
{
	/// Assign a stable identifier to this scroll widget.
	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = Some( id );
		self
	}

	/// Switch this viewport to horizontal scrolling. The child is laid
	/// out at its preferred width (potentially larger than the
	/// viewport) and clipped on the X axis.
	pub fn horizontal( mut self ) -> Self
	{
		self.axis = ScrollAxis::Horizontal;
		self
	}

	/// Allow both axes. Use for tables / code views / maps.
	pub fn both( mut self ) -> Self
	{
		self.axis = ScrollAxis::Both;
		self
	}

	/// Preferred size — axis-aware.
	///
	/// - **Vertical or both**: returns `(max_width, 0.0)`. The Scroll node
	///   claims all remaining space in the parent layout, exactly like a
	///   [`Spacer`](crate::layout::spacer::Spacer). The actual viewport size
	///   is determined at render time from the rect the parent assigns.
	/// - **Horizontal-only**: claims `max_width` but reports the child's
	///   natural height. A horizontal scroll has an intrinsic height (the
	///   row of items it clips) and must not steal Y space from its siblings
	///   when it sits inside a [`Column`](crate::layout::column::Column).
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		match self.axis
		{
			ScrollAxis::Horizontal =>
			{
				let ( _, h ) = self.child.preferred_size( max_width, canvas );
				( max_width, h )
			}
			ScrollAxis::Vertical | ScrollAxis::Both => ( max_width, 0.0 ),
		}
	}

	/// No-op — rendering is handled entirely by `layout_and_draw` in `draw.rs`.
	pub fn draw( &self ) {}

	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Scroll<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Scroll
		{
			child: Box::new( self.child.map_arc( f ) ),
			id:    self.id,
			axis:  self.axis,
		}
	}
}

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

/// Clamp a raw 1-D scroll offset to `[0, max(content - viewport, 0)]`.
///
/// Extracted as a pure function so it can be unit-tested without a
/// Wayland surface. Used for both axes by passing in the relevant
/// content / viewport dimensions.
pub(crate) fn clamp_offset( offset: f32, content_h: f32, viewport_h: f32 ) -> f32
{
	let max = (content_h - viewport_h).max( 0.0 );
	offset.clamp( 0.0, max )
}

/// Create a scrollable viewport wrapping `child`. Defaults to vertical
/// scrolling; chain `Scroll::horizontal` or `Scroll::both` to
/// switch axes.
///
/// The parent layout controls the viewport size by assigning a rect to
/// this widget. Content that overflows along the allowed axis is
/// scrolled via drag gestures or the scroll wheel.
///
/// ```rust,no_run
/// # use ltk::{ column, scroll, text, Element };
/// # #[ derive( Clone ) ] enum Msg {}
/// # fn _ex() -> Element<Msg> {
/// scroll( column().push( text( "A" ) ).push( text( "B" ) ) )
/// .into()
/// # }
/// ```
pub fn scroll<Msg: Clone>( child: impl Into<Element<Msg>> ) -> Scroll<Msg>
{
	Scroll { child: Box::new( child.into() ), id: None, axis: ScrollAxis::Vertical }
}
