Editor/Handles/ArchShapeHandles.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public enum ArchHandleMode {
	Move,
	Turn,
	Scale
}

public static class ArchShapeHandles {
	// Persisted across tools and sessions via EditorCookie.
	public static ArchHandleMode Mode {
		get => (ArchHandleMode)EditorCookie.Get( "arch.handles.mode", (int)ArchHandleMode.Scale );
		set => EditorCookie.Set( "arch.handles.mode", (int)value );
	}

	const float RingSize = 34f;

	static readonly Dictionary<string, Vector3> loose = new();
	static readonly Dictionary<string, Vector3> grabbed = new();
	static readonly Dictionary<string, float> carried = new();

	public static bool HandlePressed { get; private set; }

	// Pressed.Any includes child scopes; require mouse down to exclude the release frame.
	public static void BeginFrame() {
		HandlePressed = Gizmo.Pressed.Any && Gizmo.IsLeftMouseDown;

		if ( Gizmo.IsLeftMouseDown ) {
			return;
		}

		loose.Clear();
		grabbed.Clear();
		carried.Clear();
	}

	public static bool CapturesPlacement( bool handlePressed, bool adjusting ) => handlePressed || adjusting;

	// Suppress handles on the shape just drawn so its dragger doesn't capture the next placement.
	public static bool ShowsHandles( object picked, object drawn ) => picked is not null && !ReferenceEquals( picked, drawn );

	public static bool Snapping( bool snapToGrid, bool controlHeld ) => snapToGrid != controlHeld;

	// Steps a delta, not the coordinate — see ArchGridService.Stepped.
	static Vector2 Stepped( ArchTool tool, Vector2 from, Vector2 to ) {
		return Snapping( Gizmo.Settings.SnapToGrid, Gizmo.IsCtrlPressed )
			? ArchGridService.Stepped( from, to, tool.Grid.BaseSize )
			: to;
	}

	static float Stepped( ArchTool tool, float from, float to ) {
		return Snapping( Gizmo.Settings.SnapToGrid, Gizmo.IsCtrlPressed )
			? ArchGridService.Stepped( from, to, tool.Grid.BaseSize )
			: to;
	}

	public static Rotation Facing( ArchViewAxis axis ) => Facing( axis, 0f );

	// Yaw only applies in plan view — in elevation it would tilt the drag plane.
	public static Rotation Facing( ArchViewAxis axis, float yaw ) => axis switch {
		ArchViewAxis.Front => Rotation.LookAt( Vector3.Forward ),
		ArchViewAxis.Side => Rotation.LookAt( Vector3.Left ),
		_ => Rotation.FromYaw( yaw ) * Rotation.LookAt( Vector3.Up )
	};

	// Optional frame rotates into local space so turned shapes don't get axis-aligned.
	public static bool Volume( ArchTool tool, ArchGesture gesture, Vector2 min, Vector2 max, float bottom, float top, out BBox pulled, Rotation? frame = null ) {
		var standing = new BBox( new Vector3( min.x, min.y, bottom ), new Vector3( max.x, max.y, top ) );

		pulled = standing;

		if ( Mode != ArchHandleMode.Scale ) {
			return false;
		}

		using ( Gizmo.Scope( gesture.Key, new Transform( Vector3.Zero, frame ?? Rotation.Identity ) ) ) {
			if ( !Gizmo.Control.BoundingBox( "volume", standing, out var moved ) ) {
				HandlePressed |= Gizmo.Pressed.This;

				return false;
			}

			HandlePressed |= Gizmo.Pressed.This;

			// Step each face relative to where it stood, not absolutely.
			var low = Stepped( tool, new Vector2( standing.Mins.x, standing.Mins.y ), new Vector2( moved.Mins.x, moved.Mins.y ) );
			var high = Stepped( tool, new Vector2( standing.Maxs.x, standing.Maxs.y ), new Vector2( moved.Maxs.x, moved.Maxs.y ) );
			var foot = Stepped( tool, standing.Mins.z, MathF.Min( moved.Mins.z, moved.Maxs.z ) );
			var head = Stepped( tool, standing.Maxs.z, MathF.Max( moved.Mins.z, moved.Maxs.z ) );

			pulled = new BBox(
				new Vector3( MathF.Min( low.x, high.x ), MathF.Min( low.y, high.y ), MathF.Min( foot, head ) ),
				new Vector3( MathF.Max( low.x, high.x ), MathF.Max( low.y, high.y ), MathF.Max( foot, head ) ) );
		}

		return (pulled.Mins - standing.Mins).Length > 0.01f || (pulled.Maxs - standing.Maxs).Length > 0.01f;
	}

	// Accumulates raw travel; callers use the public wrappers that snap to the appropriate ladder.
	static bool Square( ArchTool tool, ArchGesture gesture, Vector3 at, out Vector3 moved, Color? tint = null, float yaw = 0f ) {
		moved = at;

		var size = MathF.Max( 3f, tool.Kit.GridSize * 0.25f );
		var movement = Vector3.Zero;
		var held = false;
		var key = gesture.Key;

		using ( Gizmo.Scope( key, new Transform( at ) ) ) {
			Gizmo.Draw.Color = tint ?? Gizmo.Colors.Active;

			held = Gizmo.Control.DragSquare( "drag", new Vector2( size, size ), Facing( tool.Axis, yaw ), out movement );
			HandlePressed |= Gizmo.Pressed.This;
		}

		if ( !held ) {
			return false;
		}

		var travelled = (loose.TryGetValue( key, out var carried ) ? carried : at) + movement;

		loose[key] = travelled;
		moved = travelled;

		return true;
	}

	public static bool Landed( ArchTool tool, ArchGesture gesture, Vector3 at, out Vector2 moved, Color? tint = null, float yaw = 0f ) {
		moved = new Vector2( at.x, at.y );

		if ( !Square( tool, gesture, at, out var dragged, tint, yaw ) ) {
			return false;
		}

		moved = Stepped( tool, moved, new Vector2( dragged.x, dragged.y ) );

		return true;
	}

	// Snaps to grid absolutely, unlike Landed which preserves off-grid offset.
	public static bool Placed( ArchTool tool, ArchGesture gesture, Vector3 at, out Vector2 moved, Color? tint = null ) {
		moved = new Vector2( at.x, at.y );

		if ( !Square( tool, gesture, at, out var dragged, tint ) ) {
			return false;
		}

		moved = tool.Snap( new Vector2( dragged.x, dragged.y ) );

		return true;
	}

	public static bool Control( ArchTool tool, ArchGesture gesture, Vector3 at, out Vector3 moved, Color? tint = null ) {
		moved = at;

		if ( !Square( tool, gesture, at, out var dragged, tint ) ) {
			return false;
		}

		var free = new Vector2( dragged.x, dragged.y );

		// Snap to wall surface if nearby, otherwise step normally.
		var flat = tool.Walled( free, out var wall ) ? wall : Stepped( tool, new Vector2( at.x, at.y ), free );

		moved = new Vector3( flat.x, flat.y, Stepped( tool, at.z, dragged.z ) );

		return true;
	}

	public static bool Reach( ArchTool tool, ArchGesture gesture, Vector3 at, Vector2 origin, Vector2 direction, out float distance, Color? tint = null ) {
		distance = 0f;

		if ( !Square( tool, gesture, at, out var dragged, tint ) ) {
			return false;
		}

		distance = ArchGridService.Fine( Vector2.Dot( new Vector2( dragged.x, dragged.y ) - origin, direction ) );

		return true;
	}

	public static bool Width( ArchTool tool, ArchGesture gesture, Vector3 at, Vector2 middle, Vector2 across, float smallest, out float width, Color? tint = null ) {
		width = smallest;

		if ( !Square( tool, gesture, at, out var dragged, tint ) ) {
			return false;
		}

		var reach = Vector2.Dot( new Vector2( dragged.x, dragged.y ) - middle, across );

		width = ArchGridService.Least( MathF.Abs( reach ) * 2f, smallest );

		return true;
	}

	public static bool Push( ArchTool tool, ArchGesture gesture, Vector3 at, Vector2 station, Vector2 axis, out float push, Color? tint = null, float yaw = 0f ) {
		push = 0f;

		if ( !Square( tool, gesture, at, out var dragged, tint, yaw ) ) {
			return false;
		}

		push = Stepped( tool, 0f, Vector2.Dot( new Vector2( dragged.x, dragged.y ) - station, axis ) );

		return true;
	}

	public static bool Station( ArchTool tool, ArchGesture gesture, ArchCurve curve, float at, out float distance, Color? tint = null ) {
		distance = at;

		if ( !curve.Sample( at, out var seat ) || !Square( tool, gesture, seat.Position, out var dragged, tint ) ) {
			return false;
		}

		if ( !curve.Nearest( new Vector2( dragged.x, dragged.y ), out var landed, out _ ) ) {
			return false;
		}

		distance = landed.Distance;

		return true;
	}

	// Snapped delta along curve — both ends of a span shift by the same amount.
	public static bool Slid( ArchTool tool, ArchGesture gesture, ArchCurve curve, float from, out float delta, Color? tint = null ) {
		delta = 0f;

		if ( !Station( tool, gesture, curve, from, out var distance, tint ) ) {
			return false;
		}

		delta = ArchGridService.Snap( distance - from, tool.Kit.GridSize );

		return true;
	}

	public static bool Corners( ArchTool tool, ArchGesture gesture, List<Vector2> outline, float height, Func<Vector2, Vector2, bool> carried = null ) {
		var changed = false;

		ArchFootprint.Bearing( outline, out var yaw );

		for ( var index = 0; index < outline.Count; index++ ) {
			var corner = outline[index];

			if ( !Landed( tool, gesture.At( "corner", index ), new Vector3( corner.x, corner.y, height ), out var landed, null, yaw ) ) {
				continue;
			}

			if ( carried is not null && !carried( corner, landed ) ) {
				continue;
			}

			outline[index] = landed;
			changed = true;
		}

		return changed;
	}

	public static bool Edges( ArchTool tool, ArchGesture gesture, List<Vector2> outline, float height, Func<Vector2, Vector2, Vector2, bool> carried = null, Color? tint = null ) {
		var changed = false;

		for ( var index = 0; index < outline.Count; index++ ) {
			var next = (index + 1) % outline.Count;
			var from = outline[index];
			var to = outline[next];
			var span = to - from;

			if ( span.Length < 1f ) {
				continue;
			}

			var middle = (from + to) * 0.5f;
			var normal = new Vector2( -span.y, span.x ).Normal;

			var yaw = MathF.Atan2( span.y, span.x ).RadianToDegree();

			if ( !Push( tool, gesture.At( "edge", index ), new Vector3( middle.x, middle.y, height ), middle, normal, out var push, tint ?? Gizmo.Colors.Green, yaw ) ) {
				continue;
			}

			var reach = normal * push;

			if ( carried is not null && !carried( from, to, reach ) ) {
				continue;
			}

			outline[index] = from + reach;
			outline[next] = to + reach;
			changed = true;
		}

		return changed;
	}

	public static bool Nodes( ArchTool tool, ArchGesture gesture, List<ArchCurveNode> nodes ) {
		var changed = false;

		for ( var index = 0; index < nodes.Count; index++ ) {
			var node = nodes[index];

			if ( !Control( tool, gesture.At( "node", index ), node.Position, out var moved ) ) {
				continue;
			}

			node.Position = moved;
			changed = true;
		}

		return changed;
	}

	// Z-axis arrow — works from any camera angle (auto-culls when looking straight down).
	public static bool Lift( ArchTool tool, ArchGesture gesture, Vector2 at, float height, out float moved, Color? tint = null ) {
		moved = height;

		var key = gesture.Key;
		var delta = 0f;
		var held = false;

		using ( Gizmo.Scope( key, new Transform( new Vector3( at.x, at.y, height ) ) ) ) {
			Gizmo.Draw.Color = tint ?? Gizmo.Colors.Up;

			held = Gizmo.Control.Arrow( "lift", Vector3.Up, out delta );
			HandlePressed |= Gizmo.Pressed.This;
		}

		if ( !held ) {
			return false;
		}

		var travelled = (loose.TryGetValue( key, out var carried ) ? carried.z : height) + delta;

		loose[key] = new Vector3( 0f, 0f, travelled );
		moved = Stepped( tool, height, travelled );

		return true;
	}

	public static bool Arrow( ArchTool tool, ArchGesture gesture, Vector3 at, Vector2 direction, out Vector2 moved, Color? tint = null )
	{
		moved = new Vector2( at.x, at.y );

		if ( direction.IsNearZeroLength )
		{
			return false;
		}

		var axis = direction.Normal;
		var key = gesture.Key;
		var delta = 0f;
		var held = false;
		var colour = tint ?? Color.Red;
		var flatAxis = new Vector3( axis.x, axis.y, 0f );

		using ( Gizmo.Scope( key, new Transform( at ) ) )
		{
			Gizmo.Draw.Color = colour;

			held = Gizmo.Control.Arrow( "arrow", flatAxis, out delta, 46f, 14f, 7f );

			Gizmo.Draw.Color = colour;
			Gizmo.Draw.SolidBox( BBox.FromPositionAndSize( flatAxis * 14f, 14f ) );

			HandlePressed |= Gizmo.Pressed.This;
		}

		if ( !held )
		{
			return false;
		}

		var standing = new Vector2( at.x, at.y );
		var travelled = (loose.TryGetValue( key, out var carried ) ? new Vector2( carried.x, carried.y ) : standing) + axis * delta;

		loose[key] = new Vector3( travelled.x, travelled.y, at.z );
		moved = Stepped( tool, standing, travelled );

		return true;
	}

	public static bool Height( ArchTool tool, ArchGesture gesture, Vector2 at, float height, out float moved, Color? tint = null ) {
		moved = height;

		if ( !tool.InElevation || !Square( tool, gesture, new Vector3( at.x, at.y, height ), out var dragged, tint ) ) {
			return false;
		}

		moved = Stepped( tool, height, dragged.z );

		return true;
	}

	public static bool Position( ArchTool tool, ArchGesture gesture, Vector3 at, out Vector3 moved ) {
		moved = at;

		if ( Mode != ArchHandleMode.Move ) {
			return false;
		}

		var key = gesture.Key;

		// Zero planeSize disables the multi-axis quads that drag through Z in non-top views.
		using ( Gizmo.Scope( key, new Transform( at ) ) ) {
			var held = Gizmo.Control.Position( "translate", Vector3.Zero, out var dragged, null, 0f );

			HandlePressed |= Gizmo.Pressed.This;

			if ( !held ) {
				return false;
			}

			var from = grabbed.TryGetValue( key, out var origin ) ? origin : at;
			var travelled = (loose.TryGetValue( key, out var carried ) ? carried : at) + dragged;

			grabbed[key] = from;
			loose[key] = travelled;

			moved = Stepped( tool, from, travelled );
		}

		return (moved - at).Length > 0.01f;
	}

	static Vector3 Stepped( ArchTool tool, Vector3 from, Vector3 travelled ) {
		var flat = Stepped( tool, new Vector2( from.x, from.y ), new Vector2( travelled.x, travelled.y ) );
		var lifted = Stepped( tool, from.z, travelled.z );

		return new Vector3(
			Carried( travelled.x, from.x ) ? flat.x : from.x,
			Carried( travelled.y, from.y ) ? flat.y : from.y,
			Carried( travelled.z, from.z ) ? lifted : from.z );
	}

	static bool Carried( float travelled, float from ) => MathF.Abs( travelled - from ) > ArchGridService.FinestSize;

	// Yaw-only ring. Returns a per-frame delta (stepped if step > 0), accumulated from grab.
	public static bool Turned( ArchGesture gesture, Vector3 at, out float degrees, float step = 0f ) {
		degrees = 0f;

		if ( Mode != ArchHandleMode.Turn ) {
			return false;
		}

		var key = gesture.Key;
		var reported = carried.TryGetValue( key, out var since ) ? since : 0f;

		using ( Gizmo.Scope( key, new Transform( at ) ) )
		using ( Gizmo.GizmoControls.PushFixedScale() )
		using ( Gizmo.Scope( "yaw", Vector3.Zero, Rotation.LookAt( Vector3.Up ) ) ) {
			var moved = Gizmo.Control.RotateSingle( "turn", Gizmo.Colors.Yaw, out var spun, RingSize, false );

			HandlePressed |= Gizmo.Pressed.This;

			// Skip the release frame — the ring reports accumulated total, which would double-apply.
			if ( !moved || !Gizmo.IsLeftMouseDown ) {
				return false;
			}

			var reached = step > 0.01f ? ArchGridService.Snap( spun, step ) : spun;

			degrees = Wrapped( reached - reported );

			if ( MathF.Abs( degrees ) <= 0.001f ) {
				return false;
			}

			carried[key] = reached;
		}

		return true;
	}

	public static float Facing( float degrees ) => ArchGridService.Angle( degrees );

	static float Wrapped( float degrees ) {
		var turn = degrees % 360f;

		if ( turn > 180f ) {
			return turn - 360f;
		}

		return turn < -180f ? turn + 360f : turn;
	}

	public static bool Move( ArchTool tool, ArchGesture gesture, Vector2 at, out Vector2 moved ) {
		return Landed( tool, gesture, new Vector3( at.x, at.y, tool.LevelHeight ), out moved, Gizmo.Colors.Blue );
	}
}