Elements/Element.cs
using Duccsoft.ImGui.Rendering;
using System.Collections.Generic;
namespace Duccsoft.ImGui.Elements;

public abstract class Element
{
	protected Element( Element parent )
	{
		Parent = parent;
		if ( Parent is null )
		{
			Window = this as Window;
		}
		else
		{
			Window = Parent.Window;
		}

		if ( Id == 0 )
		{
			var typeId = GetType().GetHashCode();
			Id = ImGui.GetID( typeId );
		}
	}

	internal static ImGuiSystem System => ImGuiSystem.Current;

	public int Id { get; init; }

	#region Layout
	public Element Parent { get; init; }
	public Window Window { get; init; }

	public IReadOnlyList<Element> Children => _children;
	protected List<Element> _children = new();
	/// <summary>
	/// Returns the element currently being built as a child of this element, or null if 
	/// no child is currently being built.
	/// </summary>
	internal Element CurrentItem { get; set; }
	internal Element CurrentItemRecursive
	{
		get
		{
			if ( CurrentItem is not null )
				return CurrentItem.CurrentItemRecursive;

			if ( Parent is null )
				return null;

			return IsEnded ? null : this;
		}
	}
	/// <summary>
	/// Returns the last child that has already been built, or null if no child has been built.
	/// </summary>
	internal Element LastItem
	{
		get
		{
			var lastChildIdx = Children.Count - 1;
			if ( lastChildIdx < 0 )
				return null;

			if ( Children[lastChildIdx].IsEnded )
				return Children[lastChildIdx];

			if ( lastChildIdx < 1 )
				return null;

			return Children[lastChildIdx - 1];
		}
	}
	internal Element LastItemRecursive
	{
		get
		{
			// If our last item is still being built...
			if ( CurrentItem is not null )
			{
				var currentLast = CurrentItem.LastItemRecursive;
				// ...and it has built an item of its own, return that item.
				if ( currentLast is not null )
					return currentLast;
			}

			if ( LastItem is not null )
				return LastItem.LastItemRecursive;

			return IsEnded ? this : null;
		}
	}
	internal bool IsEnded { get; private set; }

	internal Element FindAncestor( int id )
	{
		if ( Parent is null )
			return null;

		if ( Parent.Id == id )
			return Parent;

		return Parent.FindAncestor( id );
	}

	internal Element FindDescendant( int id )
	{
		if ( _children.Count < 1 )
			return null;

		foreach( var child in _children )
		{
			if ( child.Id == id )
				return child;
		}
		foreach( var child in _children )
		{
			var found = child.FindDescendant( id );
			if ( found is not null )
				return found;
		}
		return null;
	}

	public bool IsAncestor( Element element )
	{
		if ( element is null || Parent is null || element == this )
			return false;

		if ( Parent == element )
			return true;

		return Parent.IsAncestor( element );
	}

	public bool IsDescendant( Element element )
	{
		if ( element is null || element == this )
			return false;

		return element.IsAncestor( this );
	}
	#endregion

	#region Transform
	public Vector2 Pivot { get; set; }
	public Vector2 Position 
	{ 
		get
		{
			var pos = _position;
			if ( IsDragged )
			{
				pos += MouseState.LeftClickDragTotalDelta;
			}
			return pos;
		}
		set
		{
			var pos = value;
			pos -= Size * Pivot;
			_position = pos;
		}
	}
	private Vector2 _position;
	public virtual Vector2 ScreenPosition
	{
		get
		{
			if ( Parent is null )
				return Position;

			return Parent.ScreenPosition + Position;
		}
	}
	public Vector2 Padding { get; set; }
	public virtual Vector2 Size
	{
		get
		{
			if ( !CustomSize.IsNearZeroLength )
				return CustomSize;

			var padding = ImGui.GetStyle().WindowPadding;
			var size = ContentSize + padding * 2f;
			return size;
		}
		set => CustomSize = value;
	}
	public Vector2 CustomSize { get; set; }
	public Rect ScreenRect => new( ScreenPosition, Size );
	public Vector2 ContentSize { get; set; }
	public Rect ContentScreenRect => new( ScreenRect.Position + Padding, ContentSize );
	#endregion

	#region Input
	public bool IsActive => System.ClickedElementId == Id;
	public bool IsFocused 
	{ 
		get
		{
			return System.FocusedWindowId == Id;
		}
		set
		{
			System.Focus( value ? this : null );
		}
	}
	public bool IsHovered { get; set; }
	public bool IsDragged { get; set; }
	public bool IsReleased { get; set; }
	#endregion

	#region History
	public ElementFlags PreviousInputState { get; set; }
	public bool IsAppearing { get; set; }
	public bool IsVisible { get; set; }
	#endregion

	#region Style
	public ImGuiStyle Style => ImGui.GetStyle();
	public Color32 TextColor => ImGui.GetColorU32( ImGuiCol.Text );
	public Color32 FrameColor
	{
		get
		{
			if ( IsActive )
			{
				return ImGui.GetColorU32( ImGuiCol.FrameBgActive );
			}
			else if ( IsHovered )
			{
				return ImGui.GetColorU32( ImGuiCol.FrameBgHovered );
			}
			else
			{
				return ImGui.GetColorU32( ImGuiCol.FrameBg );
			}
		}
	}
	#endregion

	/// <summary>
	/// Add this element to its Parent, updates its input data, and calls ImGui.NewLine.
	/// <br/><br/>
	/// For any subclass of Element, OnBegin should be called within its constructor, or
	/// immediately after the element is constructed.
	/// </summary>
	public virtual void OnBegin()
	{

		Parent?.AddChild( this );
		PreviousInputState = System.PreviousBoundsList.GetElementFlags( Id );
		IsAppearing = !System.PreviousBoundsList.HasId( Id );
		IsVisible = PreviousInputState.IsVisible();
		IsReleased = PreviousInputState.IsHovered() && PreviousInputState.HasFlag( ElementFlags.IsActive ) && MouseState.LeftClickReleased;
	}

	/// <summary>
	/// For any subclass of Element that contains no children, OnEnd should be called
	/// immediately after OnBegin.
	/// </summary>
	public virtual void OnEnd()
	{
		// Only after all children are added will we know what this item's bounds are.
		OnUpdateInput();
		if ( Parent is not null )
		{
			Parent.CurrentItem = null;
		}
		IsEnded = true;
		ImGui.NewLine();
	}

	// TODO: Replace this with AddToParent
	public virtual void AddChild( Element child )
	{
		if ( child is null || child.Parent != this )
			return;

		CurrentItem = child;
		child.Position = ImGui.GetCursorScreenPos() - ScreenPosition;
		_children.Add( child );
		var spacing = ImGui.GetStyle().ItemSpacing.y;
		var maxs = Window.CursorPosition + child.Size + new Vector2( 0f, spacing );
		ContentSize = ContentSize.ComponentMax( maxs );
	}

	public virtual void OnUpdateInput()
	{
		IsHovered = false;

		if ( Window is not null && System.PreviousHoveredWindowId != Window.Id )
			return;

		if ( !IsVisible )
			return;

		if ( !ScreenRect.IsInside( MouseState.Position ) )
			return;

		IsHovered = true;
		if ( MouseState.LeftClickPressed )
		{
			OnClick( MouseState.Position );
		}
	}

	public virtual void OnClick( Vector2 screenPos )
	{
		if ( System.ClickedElementId.HasValue )
		{
			// Items won't be indexed in the BoundsList until the containing window is built.
			var clickedDescendant = FindDescendant( System.ClickedElementId.Value );
			// Within a frame, clicks should prioritize descendants over ancestors.
			if ( clickedDescendant is not null )
				return;
		}

		System.ClickedElementId = Id;
		System.Focus( this );
	}

	protected virtual void OnDrawSelf( ImDrawList drawList ) { }

	public void Draw( ImDrawList drawList )
	{
		OnDrawSelf( drawList );
		foreach( var child in Children )
		{
			child.Draw( drawList );
		}
	}

	public override string ToString()
	{
		return $"({GetType().Name} # {Id})";
	}
}