Code/XGUI/Elements/ScrollPanel.cs
using Sandbox.UI;
using Sandbox.UI.Construct;
using System;

namespace XGUI;

/// <summary>
/// A panel with custom vertical and horizontal scrollbar functionality.
/// </summary>
public class ScrollPanel : Panel
{
	public Panel VerticalScrollbar { get; private set; }
	public Button UpButton { get; private set; }
	public Panel ScrollArea { get; private set; }
	public Panel ScrollThumb { get; private set; }
	public Button DownButton { get; private set; }

	// Horizontal scrollbar elements
	public Panel HorizontalScrollbar { get; private set; }
	public Button LeftButton { get; private set; }
	public Panel HScrollArea { get; private set; }
	public Panel HScrollThumb { get; private set; }
	public Button RightButton { get; private set; }

	// Corner Panel (For when both scrollbars are visible)
	public Panel CornerPanel { get; private set; }

	// Scrollbar dragging state
	private bool _isDraggingThumb = false;
	private float _dragMouseStartY;
	private float _dragThumbStartY;

	private bool _isDraggingHThumb = false;
	private float _dragMouseStartX;
	private float _dragThumbStartX;

	// Scroll settings
	public float ScrollStep { get; set; } = 50f;
	public float PageScrollStepMultiplier { get; set; } = 0.9f;
	public bool DisableScrollBounce { get; set; } = true;

	// For tracking changes
	private Vector2 _lastScrollOffset;
	private Vector2 _lastSize;

	public ScrollPanel()
	{
		AddClass( "scrollpanel" );

		// Main vertical scrollbar container
		VerticalScrollbar = Add.Panel( "scrollbar_vertical scrollbar" );
		VerticalScrollbar.AddEventListener( "onmousedown", OnTrackMouseDown );

		// Up button
		//UpButton = VerticalScrollbar.Add.Button( "", OnUpButtonClick ); Add.Button removed.
		UpButton = VerticalScrollbar.AddChild<Button>( "" );
		UpButton.AddEventListener( "onclick", OnUpButtonClick );
		UpButton.AddClass( "scrollbar_button_up scrollbar_button" );
		UpButton.Icon = "arrow_upward";

		// Scrollable track area
		ScrollArea = VerticalScrollbar.Add.Panel( "scrollbar_track" );

		// Draggable thumb
		ScrollThumb = ScrollArea.Add.Panel( "scrollbar_thumb" );
		ScrollThumb.AddEventListener( "onmousedown", StartThumbDrag );

		// Down button
		//DownButton = VerticalScrollbar.Add.Button( "", OnDownButtonClick );
		DownButton = VerticalScrollbar.AddChild<Button>( "" );
		DownButton.AddEventListener( "onclick", OnDownButtonClick );
		DownButton.AddClass( "scrollbar_button_down scrollbar_button" );
		DownButton.Icon = "arrow_downward";

		// --- Horizontal scrollbar ---
		HorizontalScrollbar = Add.Panel( "scrollbar_horizontal scrollbar" );
		HorizontalScrollbar.AddEventListener( "onmousedown", OnHTrackMouseDown );

		// Left button
		//LeftButton = HorizontalScrollbar.Add.Button( "", OnLeftButtonClick );
		LeftButton = HorizontalScrollbar.AddChild<Button>( "" );
		LeftButton.AddEventListener( "onclick", OnLeftButtonClick );
		LeftButton.AddClass( "scrollbar_button_left scrollbar_button" );
		LeftButton.Icon = "arrow_back";

		// Scrollable horizontal track area
		HScrollArea = HorizontalScrollbar.Add.Panel( "scrollbar_track" );

		// Draggable horizontal thumb
		HScrollThumb = HScrollArea.Add.Panel( "scrollbar_thumb" );
		HScrollThumb.AddEventListener( "onmousedown", StartHThumbDrag );

		// Right button
		//RightButton = HorizontalScrollbar.Add.Button( "", OnRightButtonClick );
		RightButton = HorizontalScrollbar.AddChild<Button>( "" );
		RightButton.AddEventListener( "onclick", OnRightButtonClick );
		RightButton.AddClass( "scrollbar_button_right scrollbar_button" );
		RightButton.Icon = "arrow_forward";

		// --- Corner Panel ---
		CornerPanel = Add.Panel( "scrollbar_corner" );

		// Initialize state tracking
		UpdateScrollbarVisuals();
		UpdateHScrollbarVisuals();
		_lastScrollOffset = ScrollOffset;
		_lastSize = Vector2.Zero;
	}

	/// <summary>
	/// Handles clicking on the scrollbar track (not the thumb)
	/// </summary>
	private void OnTrackMouseDown( PanelEvent e )
	{
		// Ignore if clicking on interactive elements
		if ( e.Target == ScrollThumb || e.Target == UpButton || e.Target == DownButton ||
			ScrollThumb.HasHovered || UpButton.HasHovered || DownButton.HasHovered )
			return;

		if ( e is not MousePanelEvent me )
			return;

		// Calculate where the click happened relative to the thumb
		float clickY = me.LocalPosition.y;
		float thumbTop = ScrollThumb.Style.Top?.Value ?? 0f;
		float pageScrollAmount = Box.Rect.Height * PageScrollStepMultiplier;

		// Page up/down based on click position
		if ( clickY < thumbTop )
		{
			ScrollOffset = new Vector2( ScrollOffset.x, ScrollOffset.y - pageScrollAmount );
		}
		else if ( clickY > thumbTop + (ScrollThumb.Style.Height?.Value ?? 0f) )
		{
			ScrollOffset = new Vector2( ScrollOffset.x, ScrollOffset.y + pageScrollAmount );
		}

		e.StopPropagation();
	}

	private void OnUpButtonClick() =>
		ScrollOffset = new Vector2( ScrollOffset.x, ScrollOffset.y - ScrollStep );

	private void OnDownButtonClick() =>
		ScrollOffset = new Vector2( ScrollOffset.x, ScrollOffset.y + ScrollStep );

	private void StartThumbDrag( PanelEvent e )
	{
		if ( e.Target != ScrollThumb ) return;

		_isDraggingThumb = true;
		_dragMouseStartY = MousePosition.y;
		_dragThumbStartY = ScrollThumb.Style.Top?.Value ?? 0;
		ScrollThumb.AddClass( "active" );
		ScrollVelocity = Vector2.Zero;

		e.StopPropagation();

		// Global mouse handlers would improve drag behavior when cursor leaves the panel
		AddEventListener( "onmouseup", StopThumbDrag );
		AddEventListener( "onmousemove", UpdateThumbDrag );
	}

	private void UpdateThumbDrag( PanelEvent e )
	{
		if ( !_isDraggingThumb ) return;

		float currentMouseY = MousePosition.y;
		float mouseDeltaY = currentMouseY - _dragMouseStartY;

		float scrollableTrackHeight = GetScrollableTrackHeight();
		float thumbHeight = ScrollThumb.Style.Height?.Value ?? 20f;
		float maxThumbTop = Math.Max( 0, scrollableTrackHeight - thumbHeight );

		if ( scrollableTrackHeight <= 0 || thumbHeight <= 0 ) return;

		float newThumbTop = Math.Clamp( _dragThumbStartY + mouseDeltaY, -0.05f, maxThumbTop );
		float contentMaxScroll = ScrollSize.y;

		if ( contentMaxScroll <= 0 ) return;

		float thumbTopPercent = maxThumbTop > 0 ? newThumbTop / maxThumbTop : 0f;
		ScrollVelocity = new Vector2(
			0,//ScrollVelocity.x,
			thumbTopPercent * contentMaxScroll - _lastScrollOffset.y
		);
	}

	private void StopThumbDrag( PanelEvent e = null )
	{
		if ( !_isDraggingThumb ) return;

		_isDraggingThumb = false;
		ScrollThumb.RemoveClass( "active" );
		ScrollVelocity = Vector2.Zero;

		// Remove global listeners
		//RemoveEventListener( "onmouseup", StopThumbDrag );
		//RemoveEventListener( "onmousemove", UpdateThumbDrag );
	}

	/// <summary>
	/// Handles clicking on the horizontal scrollbar track (not the thumb)
	/// </summary>
	private void OnHTrackMouseDown( PanelEvent e )
	{
		if ( e.Target == HScrollThumb || e.Target == LeftButton || e.Target == RightButton ||
			HScrollThumb.HasHovered || LeftButton.HasHovered || RightButton.HasHovered )
			return;

		if ( e is not MousePanelEvent me )
			return;

		float clickX = me.LocalPosition.x;
		float thumbLeft = HScrollThumb.Style.Left?.Value ?? 0f;
		float pageScrollAmount = Box.Rect.Width * PageScrollStepMultiplier;

		if ( clickX < thumbLeft )
		{
			ScrollOffset = new Vector2( ScrollOffset.x - pageScrollAmount, ScrollOffset.y );
		}
		else if ( clickX > thumbLeft + (HScrollThumb.Style.Width?.Value ?? 0f) )
		{
			ScrollOffset = new Vector2( ScrollOffset.x + pageScrollAmount, ScrollOffset.y );
		}

		e.StopPropagation();
	}

	private void OnLeftButtonClick() =>
		ScrollOffset = new Vector2( ScrollOffset.x - ScrollStep, ScrollOffset.y );

	private void OnRightButtonClick() =>
		ScrollOffset = new Vector2( ScrollOffset.x + ScrollStep, ScrollOffset.y );

	private void StartHThumbDrag( PanelEvent e )
	{
		if ( e.Target != HScrollThumb ) return;

		_isDraggingHThumb = true;
		_dragMouseStartX = MousePosition.x;
		_dragThumbStartX = HScrollThumb.Style.Left?.Value ?? 0;
		HScrollThumb.AddClass( "active" );
		ScrollVelocity = Vector2.Zero;

		e.StopPropagation();

		AddEventListener( "onmouseup", StopHThumbDrag );
		AddEventListener( "onmousemove", UpdateHThumbDrag );
	}

	private void UpdateHThumbDrag( PanelEvent e )
	{
		if ( !_isDraggingHThumb ) return;

		float currentMouseX = MousePosition.x;
		float mouseDeltaX = currentMouseX - _dragMouseStartX;

		float scrollableTrackWidth = GetScrollableHTrackWidth();
		float thumbWidth = HScrollThumb.Style.Width?.Value ?? 20f;
		float maxThumbLeft = Math.Max( 0, scrollableTrackWidth - thumbWidth );

		if ( scrollableTrackWidth <= 0 || thumbWidth <= 0 ) return;

		float newThumbLeft = Math.Clamp( _dragThumbStartX + mouseDeltaX, -0.05f, maxThumbLeft );
		float contentMaxScroll = ScrollSize.x;

		if ( contentMaxScroll <= 0 ) return;

		float thumbLeftPercent = maxThumbLeft > 0 ? newThumbLeft / maxThumbLeft : 0f;
		ScrollVelocity = new Vector2(
			thumbLeftPercent * contentMaxScroll - _lastScrollOffset.x,
			0//ScrollVelocity.y
		);
	}

	private void StopHThumbDrag( PanelEvent e = null )
	{
		if ( !_isDraggingHThumb ) return;

		_isDraggingHThumb = false;
		HScrollThumb.RemoveClass( "active" );
		ScrollVelocity = Vector2.Zero;
	}

	public bool CanScrollHorizontally()
	{
		if ( HorizontalScrollbar == null || Box.Rect.Size == Vector2.Zero )
			return false;
		float contentWidth = Box.RectInner.Width + ScrollSize.x;
		return contentWidth > Box.Rect.Width;
	}

	public bool CanScrollVertically()
	{
		if ( VerticalScrollbar == null || Box.Rect.Size == Vector2.Zero )
			return false;
		float contentHeight = Box.RectInner.Height + ScrollSize.y;
		return contentHeight > Box.Rect.Height;
	}

	public override void Tick()
	{
		base.Tick();

		// Show scrollbars only if needed
		VerticalScrollbar.Style.Display = CanScrollVertically() ? DisplayMode.Flex : DisplayMode.None;
		HorizontalScrollbar.Style.Display = CanScrollHorizontally() ? DisplayMode.Flex : DisplayMode.None;
		CornerPanel.Style.Display = CanScrollHorizontally() && CanScrollVertically() ? DisplayMode.Flex : DisplayMode.None;

		// Check for scroll or size changes
		if ( ScrollOffset != _lastScrollOffset || ScrollSize != _lastSize )
		{
			UpdateScrollbarVisuals();
			UpdateHScrollbarVisuals();
			UpdateScrollbarPosition();
			UpdateHScrollbarPosition();
			UpdateCornerPosition();
			_lastScrollOffset = ScrollOffset;
			_lastSize = ScrollSize;
		}

		// Prevent scroll bounce if enabled
		if ( DisableScrollBounce && HasScrollY )
		{
			if ( ScrollOffset.y < 0 || ScrollOffset.y > ScrollSize.y )
			{
				ScrollOffset = new Vector2(
					ScrollOffset.x,
					Math.Clamp( ScrollOffset.y, 0, Math.Max( 0, ScrollSize.y ) )
				);
			}
		}
		if ( DisableScrollBounce && HasScrollX )
		{
			if ( ScrollOffset.x < 0 || ScrollOffset.x > ScrollSize.x )
			{
				ScrollOffset = new Vector2(
					Math.Clamp( ScrollOffset.x, 0, Math.Max( 0, ScrollSize.x ) ),
					ScrollOffset.y
				);
			}
		}

		UpdatePadding();
		KeepScrollOffsetInBounds();
	}
	/// <summary>
	/// Gets the size of the visible content area, excluding scrollbars.
	/// </summary>
	public Vector2 ViewportSize
	{
		get
		{
			float width = Box.RectInner.Width;
			float height = Box.RectInner.Height;

			// Subtract vertical scrollbar width if visible
			if ( VerticalScrollbar != null && VerticalScrollbar.Style.Display != DisplayMode.None )
				width -= VerticalScrollbar.Box.Rect.Width;

			// Subtract horizontal scrollbar height if visible
			if ( HorizontalScrollbar != null && HorizontalScrollbar.Style.Display != DisplayMode.None )
				height -= HorizontalScrollbar.Box.Rect.Height;

			// Clamp to non-negative values
			return new Vector2( Math.Max( 0, width ), Math.Max( 0, height ) );
		}
	}
	/// <summary>
	/// Gets the total scrollable content size, excluding scrollbars themselves.
	/// </summary>
	public Vector2 ScrollContentSize
	{
		get
		{
			// If you have a dedicated content panel, use its size:
			// return ContentPanel?.Box.Rect.Size ?? Vector2.Zero;

			// Otherwise, measure all children except scrollbars and corner panel:
			float maxRight = 0, maxBottom = 0;
			foreach ( var child in Children )
			{
				if ( child == VerticalScrollbar || child == HorizontalScrollbar || child == CornerPanel )
					continue;

				var rect = child.Box.Rect;
				maxRight = Math.Max( maxRight, rect.Right );
				maxBottom = Math.Max( maxBottom, rect.Bottom );
			}
			return new Vector2( maxRight, maxBottom );
		}
	}

	private void KeepScrollOffsetInBounds()
	{
		if ( ScrollSize == Vector2.Zero ) return;
		float maxX = Math.Max( ScrollSize.x - Box.Rect.Width + HorizontalScrollbar.Box.Rect.Width, 0 );
		float maxY = Math.Max( ScrollSize.y - Box.Rect.Height + VerticalScrollbar.Box.Rect.Height, 0 );
		ScrollOffset = new Vector2(
			Math.Clamp( ScrollOffset.x, 0, maxX ),
			Math.Clamp( ScrollOffset.y, 0, maxY )
		);
	}

	public Vector2 ClampedScrollOffset
	{
		get
		{
			float maxX = Math.Max( ScrollSize.x - Box.Rect.Width + HorizontalScrollbar.Box.Rect.Width, 0 );
			float maxY = Math.Max( ScrollSize.y - Box.Rect.Height + VerticalScrollbar.Box.Rect.Height, 0 );
			return new Vector2(
				Math.Clamp( ScrollOffset.x, 0, maxX ),
				Math.Clamp( ScrollOffset.y, 0, maxY )
			);
		}
	}

	/// <summary>
	/// Updates the scrollbar position to appear fixed in the viewport
	/// </summary>
	private void UpdateScrollbarPosition()
	{
		if ( VerticalScrollbar == null || Box.Rect.Size == Vector2.Zero ) return;

		VerticalScrollbar.Style.Top = MathF.Round( ClampedScrollOffset.y );
		VerticalScrollbar.Style.Bottom = MathF.Round( -ClampedScrollOffset.y ) + Style.PaddingBottom.GetValueOrDefault().Value;


		HorizontalScrollbar.Style.Bottom = MathF.Round( -ClampedScrollOffset.y );
	}

	private void UpdateCornerPosition()
	{
		if ( CornerPanel == null || Box.Rect.Size == Vector2.Zero ) return;
		// Position the corner panel at the bottom right of the scrollable area
		float rightPad = Style.PaddingRight.GetValueOrDefault().Value;
		float bottomPad = Style.PaddingBottom.GetValueOrDefault().Value;
		CornerPanel.Style.Width = Length.Pixels( rightPad );
		CornerPanel.Style.Height = Length.Pixels( bottomPad );
		CornerPanel.Style.Right = MathF.Round( -ClampedScrollOffset.x );
		CornerPanel.Style.Bottom = MathF.Round( -ClampedScrollOffset.y );
	}

	/// <summary>
	/// Updates the horizontal scrollbar position to appear fixed in the viewport
	/// </summary>
	private void UpdateHScrollbarPosition()
	{
		if ( HorizontalScrollbar == null || Box.Rect.Size == Vector2.Zero ) return;

		HorizontalScrollbar.Style.Left = MathF.Round( ClampedScrollOffset.x );
		HorizontalScrollbar.Style.Right = MathF.Round( -ClampedScrollOffset.x ) + Style.PaddingRight.GetValueOrDefault().Value;


		VerticalScrollbar.Style.Right = MathF.Round( -ClampedScrollOffset.x );
	}

	/// <summary>
	/// Updates the right and bottom padding to accommodate the scrollbars
	/// </summary>
	private void UpdatePadding()
	{
		float rightPad = 0f;
		float bottomPad = 0f;

		if ( HasScrollY && VerticalScrollbar != null && Box.Rect.Size != Vector2.Zero )
		{
			float scrollbarWidth = VerticalScrollbar.Box.Rect.Width;
			rightPad = scrollbarWidth > 0 ? scrollbarWidth : 0f;
		}
		if ( HasScrollX && HorizontalScrollbar != null && Box.Rect.Size != Vector2.Zero )
		{
			float scrollbarHeight = HorizontalScrollbar.Box.Rect.Height;
			bottomPad = scrollbarHeight > 0 ? scrollbarHeight : 0f;
		}

		Style.PaddingRight = Length.Pixels( rightPad );
		Style.PaddingBottom = Length.Pixels( bottomPad );
	}

	public override void FinalLayout( Vector2 offset )
	{
		base.FinalLayout( offset );
		UpdateScrollbarPosition();
		UpdateHScrollbarPosition();
		UpdateCornerPosition();
	}

	protected override void OnAfterTreeRender( bool firstTime )
	{
		base.OnAfterTreeRender( firstTime );
		UpdateScrollbarVisuals();
		UpdateHScrollbarVisuals();
		_lastScrollOffset = ScrollOffset;
	}

	/// <summary>
	/// Calculates the available height for the scrollbar thumb to move
	/// </summary>
	private float GetScrollableTrackHeight()
	{
		float upButtonHeight = UpButton?.Box.Rect.Height ?? 0f;
		float downButtonHeight = DownButton?.Box.Rect.Height ?? 0f;
		float scrollbarHeight = VerticalScrollbar?.Box.RectInner.Height ?? 0f;

		return scrollbarHeight - upButtonHeight - downButtonHeight;
	}

	/// <summary>
	/// Calculates the available width for the horizontal scrollbar thumb to move
	/// </summary>
	private float GetScrollableHTrackWidth()
	{
		float leftButtonWidth = LeftButton?.Box.Rect.Width ?? 0f;
		float rightButtonWidth = RightButton?.Box.Rect.Width ?? 0f;
		float scrollbarWidth = HorizontalScrollbar?.Box.RectInner.Width ?? 0f;

		return scrollbarWidth - leftButtonWidth - rightButtonWidth;
	}

	/// <summary>
	/// Updates the thumb size and position based on content and scroll position
	/// </summary>
	private void UpdateScrollbarVisuals()
	{
		if ( VerticalScrollbar == null || ScrollThumb == null || Box.Rect.Size == Vector2.Zero ) return;

		float viewportHeight = Box.Rect.Height;
		float contentHeight = Box.RectInner.Height + ScrollSize.y;

		// Hide scrollbar if no scrolling needed
		if ( contentHeight <= viewportHeight )
		{
			VerticalScrollbar.SetClass( "hidden", true );
			return;
		}

		VerticalScrollbar.SetClass( "hidden", false );

		float scrollableTrackHeight = GetScrollableTrackHeight();
		float thumbMinHeight = 8f;

		// Ensure we have space for the thumb
		if ( scrollableTrackHeight <= thumbMinHeight )
		{
			ScrollThumb.Style.Height = Length.Pixels( Math.Max( 0, scrollableTrackHeight ) );
			ScrollThumb.Style.Top = 0f;
			ScrollThumb.Style.Display = DisplayMode.None;
			ScrollThumb.Style.Dirty();
			return;
		}

		ScrollThumb.Style.Display = DisplayMode.Flex;

		// Calculate thumb size proportional to visible content
		float thumbHeightRatio = viewportHeight / contentHeight;
		float thumbHeight = MathF.Min(
			MathF.Max( scrollableTrackHeight * thumbHeightRatio, thumbMinHeight ),
			scrollableTrackHeight
		);
		ScrollThumb.Style.Height = Length.Pixels( thumbHeight );

		// Position the thumb based on scroll position
		float maxScroll = contentHeight - viewportHeight;
		float scrollRatio = maxScroll > 0 ? Math.Clamp( ScrollOffset.y / maxScroll, 0f, 1f ) : 0f;
		float thumbTravel = Math.Max( 0, scrollableTrackHeight - thumbHeight );
		float thumbPosition = scrollRatio * thumbTravel;

		ScrollThumb.Style.Top = Length.Pixels( thumbPosition );
		ScrollThumb.Style.Dirty();
	}

	/// <summary>
	/// Updates the horizontal thumb size and position based on content and scroll position
	/// </summary>
	private void UpdateHScrollbarVisuals()
	{
		if ( HorizontalScrollbar == null || HScrollThumb == null || Box.Rect.Size == Vector2.Zero ) return;

		float viewportWidth = Box.Rect.Width;
		float contentWidth = Box.RectInner.Width + ScrollSize.x;

		// Hide scrollbar if no scrolling needed
		if ( contentWidth <= viewportWidth )
		{
			HorizontalScrollbar.SetClass( "hidden", true );
			return;
		}

		HorizontalScrollbar.SetClass( "hidden", false );

		float scrollableTrackWidth = GetScrollableHTrackWidth();
		float thumbMinWidth = 8f;

		if ( scrollableTrackWidth <= thumbMinWidth )
		{
			HScrollThumb.Style.Width = Length.Pixels( Math.Max( 0, scrollableTrackWidth ) );
			HScrollThumb.Style.Left = 0f;
			HScrollThumb.Style.Display = DisplayMode.None;
			HScrollThumb.Style.Dirty();
			return;
		}

		HScrollThumb.Style.Display = DisplayMode.Flex;

		float thumbWidthRatio = viewportWidth / contentWidth;
		float thumbWidth = MathF.Min(
			MathF.Max( scrollableTrackWidth * thumbWidthRatio, thumbMinWidth ),
			scrollableTrackWidth
		);
		HScrollThumb.Style.Width = Length.Pixels( thumbWidth );

		float maxScroll = contentWidth - viewportWidth;
		float scrollRatio = maxScroll > 0 ? Math.Clamp( ScrollOffset.x / maxScroll, 0f, 1f ) : 0f;
		float thumbTravel = Math.Max( 0, scrollableTrackWidth - thumbWidth );
		float thumbPosition = scrollRatio * thumbTravel;

		HScrollThumb.Style.Left = Length.Pixels( thumbPosition );
		HScrollThumb.Style.Dirty();
	}
}