Editor/PieMenu.cs
using System;

namespace Editor;

/// <summary>
/// Blender-style radial menu with button boxes arranged in a circle.
/// Opens on Ctrl+Right Click.
/// </summary>
public class PieMenu : Widget
{
	public class PieOption
	{
		public string Text { get; set; }
		public string Icon { get; set; }
		public Action Action { get; set; }
		public bool Enabled { get; set; } = true;
	}

	private List<PieOption> _options = new();
	private int _hoveredIndex = -1;
	private Vector2 _centerPosition;
	private float _currentIndicatorAngle = -90f; // Start at top
	private float _targetIndicatorAngle = -90f;

	public float Radius { get; set; } = 180f;
	public float CenterRadius { get; set; } = 30f;
	public float CenterRingThickness { get; set; } = 10f;
	public float ButtonPadding { get; set; } = 20f;
	public float ButtonHeight { get; set; } = 28f;

	// s&box themed colors
	public Color ButtonColor { get; set; } = Theme.ButtonBackground;
	public Color ButtonHoverColor { get; set; } = Theme.Blue;
	public Color CenterRingColor { get; set; } = Theme.ControlBackground.Darken( 0.2f );
	public Color IndicatorColor { get; set; } = Theme.Yellow;

	// Track which button this menu should respond to
	public MouseButtons TriggerButton { get; set; } = MouseButtons.Forward;

	public PieMenu( Widget parent = null ) : base( parent )
	{
		WindowFlags = WindowFlags.Popup | WindowFlags.FramelessWindowHint | WindowFlags.NoDropShadowWindowHint;
		TranslucentBackground = true;
	}

	public PieOption AddOption( string text, string icon = null, Action action = null )
	{
		var option = new PieOption
		{
			Text = text,
			Icon = icon,
			Action = action
		};

		_options.Add( option );
		return option;
	}

	public void Clear()
	{
		_options.Clear();
		_hoveredIndex = -1;
	}

	public void OpenAtCursor()
	{
		OpenAt( Application.CursorPosition );
	}

	public void OpenAt( Vector2 position )
	{
		if ( _options.Count == 0 ) return;

		Paint.SetDefaultFont( 10, 500 );
		float maxButtonWidth = 0;
		foreach ( var option in _options )
		{
			var textSize = Paint.MeasureText( option.Text );
			float buttonWidth = textSize.x + ButtonPadding * 2;
			maxButtonWidth = Math.Max( maxButtonWidth, buttonWidth );
		}

		var size = (Radius + maxButtonWidth + 40) * 2;
		var widgetSize = new Vector2( size );

		Size = widgetSize;
		MinimumSize = widgetSize;
		MaximumSize = widgetSize;
		Position = position - (widgetSize / 2f);

		_centerPosition = widgetSize / 2;
		_hoveredIndex = -1;

		Show();
		Focus();
		MouseTracking = true;

		var localPos = Application.CursorPosition;
		UpdateHoveredOption( localPos );
	}

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

		if ( _options.Count == 0 ) return;

		Paint.Antialiasing = true;

		var center = LocalRect.Center;
		int optionCount = _options.Count;
		float angleStep = 360f / optionCount;
		float startAngle = -90f;

		// Draw button boxes positioned around the circle
		for ( int i = 0; i < optionCount; i++ )
		{
			var option = _options[i];

			float angle = startAngle + (i * angleStep);
			float angleRad = angle * MathF.PI / 180f;

			// Calculate button position
			var buttonCenter = center + new Vector2(
				MathF.Cos( angleRad ) * Radius,
				MathF.Sin( angleRad ) * Radius
			);

			// Measure text width to size button
			Paint.SetDefaultFont( 10, 500 );
			var textSize = Paint.MeasureText( option.Text );

			// Button width = text width + padding
			float buttonWidth = textSize.x + ButtonPadding * 2;

			var buttonRect = new Rect(
				buttonCenter.x - buttonWidth / 2,
				buttonCenter.y - ButtonHeight / 2,
				buttonWidth,
				ButtonHeight
			);

			var buttonColor = i == _hoveredIndex ? ButtonHoverColor : ButtonColor;
			Paint.ClearPen();
			Paint.SetBrush( buttonColor );
			Paint.DrawRect( buttonRect, 3 );

			// Draw text - centered
			Paint.SetPen( Color.White );
			Paint.SetDefaultFont( 10, 500 );

			var textRect = buttonRect.Shrink( 10, 0 );
			Paint.DrawText( textRect, option.Text, TextFlag.Center );
		}

		// Draw center ring (donut shape)
		Paint.ClearPen();
		Paint.SetBrush( CenterRingColor );
		DrawRing( center, CenterRadius - CenterRingThickness, CenterRadius );

		if ( _hoveredIndex >= 0 && _hoveredIndex < _options.Count )
		{
			// Much faster interpolation for snappier feel
			float angleDiff = _targetIndicatorAngle - _currentIndicatorAngle;

			// Handle wraparound (shortest path)
			if ( angleDiff > 180f ) angleDiff -= 360f;
			if ( angleDiff < -180f ) angleDiff += 360f;

			_currentIndicatorAngle += angleDiff * 0.6f; // Increased from 0.3f for faster response

			// Normalize angle
			if ( _currentIndicatorAngle < 0 ) _currentIndicatorAngle += 360f;
			if ( _currentIndicatorAngle >= 360f ) _currentIndicatorAngle -= 360f;

			// Draw a colored arc segment on the ring
			float arcSpan = 40f;
			Paint.SetBrush( IndicatorColor );
			DrawArcSegment( center, CenterRadius - CenterRingThickness, CenterRadius, _currentIndicatorAngle - arcSpan / 2, _currentIndicatorAngle + arcSpan / 2 );
		}
	}

	/// <summary>
	/// Draw a ring (donut shape)
	/// </summary>
	private void DrawRing( Vector2 center, float innerRadius, float outerRadius )
	{
		const int segments = 64;
		var points = new List<Vector2>();

		// Outer circle
		for ( int i = 0; i <= segments; i++ )
		{
			float angle = (i / (float)segments) * 360f;
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * outerRadius );
		}

		// Inner circle (reverse)
		for ( int i = segments; i >= 0; i-- )
		{
			float angle = (i / (float)segments) * 360f;
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * innerRadius );
		}

		Paint.DrawPolygon( points.ToArray() );
	}

	/// <summary>
	/// Draw an arc segment on a ring
	/// </summary>
	private void DrawArcSegment( Vector2 center, float innerRadius, float outerRadius, float startAngle, float endAngle )
	{
		const int segments = 20;
		float angleRange = endAngle - startAngle;
		int segmentCount = Math.Max( 2, (int)(segments * angleRange / 360f) );

		var points = new List<Vector2>();

		// Inner arc
		for ( int i = 0; i <= segmentCount; i++ )
		{
			float angle = startAngle + (angleRange * i / segmentCount);
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * innerRadius );
		}

		// Outer arc (reverse)
		for ( int i = segmentCount; i >= 0; i-- )
		{
			float angle = startAngle + (angleRange * i / segmentCount);
			float rad = angle * MathF.PI / 180f;
			points.Add( center + new Vector2( MathF.Cos( rad ), MathF.Sin( rad ) ) * outerRadius );
		}

		Paint.DrawPolygon( points.ToArray() );
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		UpdateHoveredOption( e.LocalPosition );
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		e.Accepted = true;
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		// Check if this is the button that opened this menu
		if ( e.Button == TriggerButton )
		{
			ExecuteHoveredOptionAndClose();
			e.Accepted = true;
			return;
		}

		base.OnMouseReleased( e );
	}

	/// <summary>
	/// Execute the currently hovered option and close the menu
	/// </summary>
	public void ExecuteHoveredOptionAndClose()
	{
		if ( _hoveredIndex >= 0 && _hoveredIndex < _options.Count )
		{
			var option = _options[_hoveredIndex];
			if ( option.Enabled )
			{
				option.Action?.Invoke();
			}
		}

		Close();
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		base.OnKeyPress( e );

		if ( e.Key == KeyCode.Escape )
		{
			Close();
			e.Accepted = true;
		}
	}

	private void UpdateHoveredOption( Vector2 mousePos )
	{
		var center = LocalRect.Center;
		var offset = mousePos - center;
		float distance = offset.Length;

		// More forgiving selection area
		if ( distance < 10f || distance > Radius + 250 )
		{
			if ( _hoveredIndex != -1 )
			{
				_hoveredIndex = -1;
				Update();
			}
			return;
		}

		// Calculate angle from center to mouse
		float mouseAngle = MathF.Atan2( offset.y, offset.x ) * 180f / MathF.PI;

		// Find the closest button by angular distance
		int optionCount = _options.Count;
		float angleStep = 360f / optionCount;
		float startAngle = -90f;

		int closestIndex = -1;
		float closestDistance = float.MaxValue;

		for ( int i = 0; i < optionCount; i++ )
		{
			// Calculate the angle of this button's center
			float buttonAngle = startAngle + (i * angleStep);

			// Normalize both angles to 0-360
			float normMouseAngle = mouseAngle;
			if ( normMouseAngle < 0 ) normMouseAngle += 360f;
			float normButtonAngle = buttonAngle;
			if ( normButtonAngle < 0 ) normButtonAngle += 360f;

			// Calculate angular distance (shortest path around the circle)
			float angularDist = Math.Abs( normMouseAngle - normButtonAngle );
			if ( angularDist > 180f ) angularDist = 360f - angularDist;

			if ( angularDist < closestDistance )
			{
				closestDistance = angularDist;
				closestIndex = i;
			}
		}

		// Set target angle to the selected button's angle for smooth snapping
		if ( closestIndex != -1 )
		{
			_targetIndicatorAngle = startAngle + (closestIndex * angleStep);
		}

		_hoveredIndex = closestIndex;
		Update();
	}

	[Shortcut( "editor.paste.color", "CTRL+V", typeof( SceneViewWidget ) )]
	public static void PasteFromClipboard()
	{
		var clipboard = EditorUtility.Clipboard.Paste();

		if ( string.IsNullOrWhiteSpace( clipboard ) )
		{
			// Try GameObject paste if clipboard text is empty
			PasteGameObject();
			return;
		}

		// Try to parse as hex color first
		if ( clipboard.StartsWith( "#" ) )
		{
			if ( TryParseHexColor( clipboard, out Color color ) )
			{
				PasteColor( color );
				return;
			}
			else
			{
				Log.Warning( $"Failed to parse color from clipboard: {clipboard}" );
				return;
			}
		}

		// Try to handle GameObject paste
		PasteGameObject();
	}

	private static void PasteColor( Color color )
	{
		using var scope = SceneEditorSession.Scope();

		var selection = SceneEditorSession.Active.Selection;

		// Get all MeshComponents and ModelRenderers from selected GameObjects
		var meshComponents = selection.OfType<GameObject>()
			.Select( x => x.GetComponent<MeshComponent>() )
			.Where( x => x.IsValid() )
			.ToList();

		var modelRenderers = selection.OfType<GameObject>()
			.Select( x => x.GetComponent<ModelRenderer>() )
			.Where( x => x.IsValid() )
			.ToList();

		var totalCount = meshComponents.Count + modelRenderers.Count;

		if ( totalCount == 0 )
		{
			Log.Info( "No mesh or model renderer components selected" );
			return;
		}

		// Combine both lists for undo tracking
		var allComponents = meshComponents.Cast<Component>()
			.Concat( modelRenderers.Cast<Component>() )
			.ToList();

		using ( SceneEditorSession.Active.UndoScope( "Paste Color" )
			.WithComponentChanges( allComponents )
			.Push() )
		{
			// Apply color to MeshComponents
			foreach ( var component in meshComponents )
			{
				component.Color = color;
			}

			// Apply color to ModelRenderers
			foreach ( var component in modelRenderers )
			{
				component.Tint = color;
			}
		}

		Log.Info( $"Applied color to {meshComponents.Count} mesh component(s) and {modelRenderers.Count} model renderer(s)" );
	}

	private static void PasteGameObject()
	{
		var session = SceneEditorSession.Active;
		if ( session == null ) return;

		// Get the active scene viewport
		var sceneView = SceneViewWidget.Current;
		if ( sceneView?.LastSelectedViewportWidget == null ) return;

		var viewport = sceneView.LastSelectedViewportWidget;

		// First, paste the standard way
		EditorScene.Paste();

		// Get the pasted objects
		var pastedObjects = session.Selection.OfType<GameObject>().ToList();
		if ( pastedObjects.Count == 0 ) return;

		// Compute the average point of all pasted objects
		Vector3 middlePoint = Vector3.Zero;
		foreach ( var go in pastedObjects )
			middlePoint += go.WorldPosition;

		middlePoint /= pastedObjects.Count;

		// Get mouse position in viewport and trace
		var mousePosition = SceneViewportWidget.MousePosition;
		var camera = viewport.Renderer.Camera;

		if ( !camera.IsValid() ) return;

		// Create ray from mouse position
		var ray = camera.ScreenPixelToRay( mousePosition );

		// Trace to find world position
		var trace = session.Scene.Trace
			.Ray( ray, 10000f )
			.UseRenderMeshes( true )
			.UsePhysicsWorld( false )
			.Run();

		if ( trace.Hit )
		{
			using ( session.UndoScope( "Paste at Mouse" ).Push() )
			{
				// Reposition all game objects relative to new center
				foreach ( var go in pastedObjects )
				{
					Vector3 offset = go.WorldPosition - middlePoint;
					go.WorldPosition = trace.HitPosition + offset;
				}

				Log.Info( $"Pasted {pastedObjects.Count} GameObject(s) at mouse position" );
			}
		}
	}

	static bool TryParseHexColor( string hex, out Color color )
	{
		color = default;

		if ( string.IsNullOrEmpty( hex ) )
			return false;

		hex = hex.TrimStart( '#' );

		if ( hex.Length != 6 && hex.Length != 8 )
			return false;

		try
		{
			int r = Convert.ToInt32( hex.Substring( 0, 2 ), 16 );
			int g = Convert.ToInt32( hex.Substring( 2, 2 ), 16 );
			int b = Convert.ToInt32( hex.Substring( 4, 2 ), 16 );
			int a = hex.Length == 8 ? Convert.ToInt32( hex.Substring( 6, 2 ), 16 ) : 255;

			color = new Color( r / 255f, g / 255f, b / 255f, a / 255f );
			return true;
		}
		catch
		{
			return false;
		}
	}

	public IReadOnlyList<PieOption> Options => _options.AsReadOnly();
}