Editor/StateItem.cs
using System;
using System.Linq;
using Editor;
using Editor.NodeEditor;
using Facepunch.ActionGraphs;
using static System.Net.Mime.MediaTypeNames;
using static Editor.Label;

namespace Sandbox.States.Editor;

public sealed class StateItem : GraphicsItem, IContextMenuSource, IDeletable
{
	public static Color PrimaryColor { get; } = Color.Parse( "#5C79DB" )!.Value;
	public static Color InitialColor { get; } = Color.Parse( "#BCA5DB" )!.Value;

	public StateMachineView View { get; }
	public State State { get; }

	public float Radius => 64f;

	public event Action? PositionChanged;

	private bool _rightMousePressed;

	private int _lastHash;

	private readonly StateLabel _enterLabel;
	private readonly StateLabel _updateLabel;
	private readonly StateLabel _leaveLabel;

	internal bool HasMoved { get; set; }

	public StateItem( StateMachineView view, State state )
	{
		View = view;
		State = state;

		Size = new Vector2( Radius * 2f, Radius * 2f );
		Position = state.EditorPosition;

		Movable = true;
		Selectable = true;
		HoverEvents = true;

		Cursor = CursorShape.Finger;

		_enterLabel = new StateLabel( this, new StateEnterAction( this ) );
		_updateLabel = new StateLabel( this, new StateUpdateAction( this ) );
		_leaveLabel = new StateLabel( this, new StateLeaveAction( this ) );

		UpdateTooltip();
		AlignLabels();
	}

	public override Rect BoundingRect => base.BoundingRect.Grow( 16f );

	public override bool Contains( Vector2 localPos )
	{
		return (LocalRect.Center - localPos).LengthSquared < Radius * Radius;
	}

	protected override void OnPaint()
	{
		var borderColor = Selected 
			? Color.Yellow : Hovered
			? Color.White : Color.White.Darken( 0.125f );

		var fillColor = State.StateMachine?.InitialState == State
			? InitialColor
			: PrimaryColor;

		fillColor = fillColor
			.Lighten( Selected ? 0.5f : Hovered ? 0.25f : 0f )
			.Desaturate( Selected ? 0.5f : Hovered ? 0.25f : 0f );

		Paint.SetBrushRadial( LocalRect.Center - LocalRect.Size * 0.125f, Radius * 1.5f, fillColor.Lighten( 0.5f ), fillColor.Darken( 0.75f ) );
		Paint.DrawCircle( Size * 0.5f, Size );

		Paint.SetPen( borderColor, Selected || Hovered ? 3f : 2f );
		Paint.SetBrushRadial( LocalRect.Center, Radius, 0.75f, Color.Black.WithAlpha( 0f ), 1f, Color.Black.WithAlpha( 0.25f ) );
		Paint.DrawCircle( Size * 0.5f, Size );

		if ( State.StateMachine?.CurrentState == State )
		{
			Paint.ClearBrush();
			Paint.DrawCircle( Size * 0.5f, Size + 8f );
		}

		var titleRect = (State.OnEnterState ?? State.OnUpdateState ?? State.OnLeaveState) is not null
			? new Rect( 0f, Size.y * 0.35f - 12f, Size.x, 24f )
			: new Rect( 0f, Size.y * 0.5f - 12f, Size.x, 24f );

		Paint.ClearBrush();

		if ( IsEmoji )
		{
			Paint.SetFont( "roboto", Size.y * 0.5f, 600 );
			Paint.SetPen( Color.White );
			Paint.DrawText( new Rect( 0f, -4f, Size.x, Size.y ), State.Name );
		}
		else
		{
			Paint.SetFont( "roboto", 12f, 600 );
			Paint.SetPen( Color.Black.WithAlpha( 0.5f ) );
			Paint.DrawText( new Rect( titleRect.Position + 2f, titleRect.Size ), State.Name );

			Paint.SetPen( borderColor );
			Paint.DrawText( titleRect, State.Name );
		}
	}

	public bool IsEmoji => State.Name.Length == 2 && State.Name[0] >= 0x8000 && char.ConvertToUtf32( State.Name, 0 ) != -1;

	protected override void OnMousePressed( GraphicsMouseEvent e )
	{
		base.OnMousePressed( e );

		if ( e.RightMouseButton )
		{
			_rightMousePressed = true;

			e.Accepted = true;
		}
	}

	protected override void OnMouseReleased( GraphicsMouseEvent e )
	{
		base.OnMouseReleased( e );

		if ( e.RightMouseButton && _rightMousePressed )
		{
			_rightMousePressed = false;

			e.Accepted = true;
		}
	}

	protected override void OnMouseMove( GraphicsMouseEvent e )
	{
		if ( _rightMousePressed && !Contains( e.LocalPosition ) )
		{
			_rightMousePressed = false;

			View.StartCreatingTransition( this );
		}

		base.OnMouseMove( e );
	}

	public void OnContextMenu( ContextMenuEvent e )
	{
		e.Accepted = true;
		Selected = true;

		var menu = new global::Editor.Menu { DeleteOnClose = true };

		menu.AddHeading( "State" );

		menu.AddMenu( "Rename", "edit" ).AddLineEdit( "Rename", State.Name, onSubmit: value =>
		{
			View.LogEdit( "State Renamed" );

			State.Name = value ?? "Unnamed";
			Update();
		}, autoFocus: true );

		if ( State.StateMachine.InitialState != State )
		{
			menu.AddOption( "Make Initial", "start", action: () =>
			{
				View.LogEdit( "Initial State Assigned" );

				State.StateMachine.InitialState = State;
				Update();
			} );
		}

		menu.AddSeparator();

		foreach ( var label in Children.OfType<StateLabel>() )
		{
			label.Source.BuildAddContextMenu( menu );
		}

		menu.AddSeparator();
		menu.AddOption( "Delete", "delete", action: Delete );

		menu.OpenAtCursor( true );
	}

	protected override void OnMoved()
	{
		HasMoved = true;

		State.EditorPosition = Position.SnapToGrid( View.GridSize );

		UpdatePosition();
	}

	public void UpdatePosition()
	{
		Position = State.EditorPosition;

		PositionChanged?.Invoke();
	}

	protected override void OnDestroy()
	{
		base.OnDestroy();

		var transitions = View.Items.OfType<TransitionItem>()
			.Where( x => x.Source == this || x.Target == this )
			.ToArray();

		foreach ( var transition in transitions )
		{
			transition.Destroy();
		}
	}

	public void Delete()
	{
		View.LogEdit( "State Removed" );

		if ( State.StateMachine.InitialState == State )
		{
			State.StateMachine.InitialState = null;
		}

		var transitions = View.Items.OfType<TransitionItem>()
			.Where( x => x.Source == this || x.Target == this )
			.ToArray();

		foreach ( var transition in transitions )
		{
			transition.Delete();
		}

		State.Remove();
		Destroy();
	}

	private void UpdateTooltip()
	{
		ToolTip = State.StateMachine.InitialState == State ? $"State <b>{State.Name}</b> <i>(initial)</i>" : $"State <b>{State.Name}</b>";
	}

	private void AlignLabels()
	{
		var labels = Children.OfType<StateLabel>()
			.ToArray();

		foreach ( var label in labels )
		{
			label.Layout();
		}

		var size = new Vector2( 32f, 32f );
		var totalWidth = labels.Sum( x => x.Width );

		var origin = Size * 0.5f - new Vector2( totalWidth * 0.5f, size.y * 0.5f );

		if ( !IsEmoji )
		{
			origin.y += Radius / 6f;
		}

		foreach ( var label in labels )
		{
			label.Position = origin;
			label.Update();

			origin.x += label.Width;
		}
	}

	public void ForceUpdate()
	{
		if ( !IsValid ) return;

		AlignLabels();
		Update();
	}

	public void Frame()
	{
		var hash = HashCode.Combine( State.StateMachine.InitialState == State, State.StateMachine.CurrentState == State );
		if ( hash == _lastHash ) return;

		_lastHash = hash;

		UpdateTooltip();
		AlignLabels();
		Update();
	}
}