Weapons/ToolGun/Modes/Stacker/StackerTool.cs
using System.Numerics;

/// <summary>
/// Direction to stack objects in.
/// </summary>
public enum StackDirection
{
	Up,
	Down,
	Left,
	Right,
	Forward,
	Back
}

/// <summary>
/// How to interpret the stack direction axis.
/// </summary>
public enum StackAlignMode
{
	/// <summary>
	/// Stack along world-space axes regardless of object orientation.
	/// </summary>
	World,

	/// <summary>
	/// Stack along the target object's local axes.
	/// </summary>
	Object
}

[Icon( "📚" )]
[ClassName( "stacker" )]
[Group( "#tool.group.building" )]
[Title( "#tool.name.stacker" )]
public sealed class StackerTool : ToolMode
{
	private const int MaxStackCount = 50;

	public override IEnumerable<string> TraceIgnoreTags => ["player", "constraint", "collision"];

	/// <summary>
	/// Number of copies to create.
	/// </summary>
	[Property, Sync, Range( 1, MaxStackCount ), Step( 1 ), Title( "Count" ), ClientEditable]
	public float StackCount { get; set; } = 2;

	/// <summary>
	/// Which direction to stack in.
	/// </summary>
	[Property, Sync]
	public StackDirection Direction { get; set; } = StackDirection.Up;

	/// <summary>
	/// Whether to align the stack axis to the world or the target object.
	/// </summary>
	[Property, Sync, Title( "Alignment" )]
	public StackAlignMode AlignMode { get; set; } = StackAlignMode.Object;

	/// <summary>
	/// Rotation offset (degrees) around the first perpendicular axis of the stack direction.
	/// For Up/Down stacking this rotates around the right axis.
	/// </summary>
	[Property, Sync, Title( "Angle X" ), Range( -180, 180 ), Step( 1 )]
	public float AngleOffsetX { get; set; } = 0f;

	/// <summary>
	/// Rotation offset (degrees) around the second perpendicular axis of the stack direction.
	/// For Up/Down stacking this rotates around the forward axis.
	/// </summary>
	[Property, Sync, Title( "Angle Y" ), Range( -180, 180 ), Step( 1 )]
	public float AngleOffsetY { get; set; } = 0f;

	/// <summary>
	/// Extra gap (in units) between each stacked copy.
	/// </summary>
	[Property, Sync, Range( 0, 128 ), Title( "Gap" )]
	public float PositionOffset { get; set; } = 0f;

	/// <summary>
	/// When true, stacked copies will be frozen (motion disabled).
	/// </summary>
	[Property, Sync, Title( "Freeze" )]
	public bool FreezeAll { get; set; } = true;

	public override string Description => "#tool.hint.stacker.description";

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

		RegisterAction( ToolInput.Primary, () => "#tool.hint.stacker.stack", OnStack );
		RegisterAction( ToolInput.Secondary, () => "#tool.hint.stacker.cycle_alignment", CycleAlignment );
		RegisterAction( ToolInput.Reload, () => "#tool.hint.stacker.cycle_direction", CycleDirection );
	}

	void OnStack()
	{
		var select = TraceSelect();
		if ( !IsValidTarget( select ) ) return;

		SpawnStack( ResolveRoot( select.GameObject ) );
		ShootEffects( select );
	}

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

		var select = TraceSelect();

		IsValidState = IsValidTarget( select );
		if ( !IsValidState )
			return;

		var target = ResolveRoot( select.GameObject );
		var transforms = ComputeStackTransforms( target );

		DrawStackPreview( target, transforms );
	}

	/// <summary>
	/// Resolve to the networked root object so we clone the full prop, not a child collider.
	/// </summary>
	private static GameObject ResolveRoot( GameObject go )
	{
		return go?.Network?.RootGameObject ?? go;
	}

	/// <summary>
	/// Cycle through stack directions on reload.
	/// </summary>
	private void CycleDirection()
	{
		Direction = Direction switch
		{
			StackDirection.Up => StackDirection.Right,
			StackDirection.Right => StackDirection.Down,
			StackDirection.Down => StackDirection.Left,
			StackDirection.Left => StackDirection.Forward,
			StackDirection.Forward => StackDirection.Back,
			StackDirection.Back => StackDirection.Up,
			_ => StackDirection.Up
		};
	}

	/// <summary>
	/// Toggle between world and object alignment.
	/// </summary>
	private void CycleAlignment()
	{
		AlignMode = AlignMode == StackAlignMode.World
			? StackAlignMode.Object
			: StackAlignMode.World;
	}

	/// <summary>
	/// Check if the aimed-at object is a valid stacking target.
	/// </summary>
	private bool IsValidTarget( SelectionPoint select )
	{
		if ( !select.IsValid() ) return false;
		if ( select.IsWorld ) return false;
		if ( select.IsPlayer ) return false;

		var root = ResolveRoot( select.GameObject );
		if ( root.Tags.Contains( "constraint" ) ) return false;

		return true;
	}

	/// <summary>
	/// Get the local-space direction vector for the configured <see cref="Direction"/>.
	/// </summary>
	private static Vector3 GetLocalDirection( StackDirection dir )
	{
		return dir switch
		{
			StackDirection.Up => Vector3.Up,
			StackDirection.Down => Vector3.Down,
			StackDirection.Left => Vector3.Left,
			StackDirection.Right => Vector3.Right,
			StackDirection.Forward => Vector3.Forward,
			StackDirection.Back => Vector3.Backward,
			_ => Vector3.Up
		};
	}

	/// <summary>
	/// Get the two perpendicular axes for a given stack direction.
	/// X and Y angle offsets rotate around these respectively.
	/// </summary>
	private static (Vector3 axisX, Vector3 axisY) GetPerpendicularAxes( StackDirection dir )
	{
		return dir switch
		{
			StackDirection.Up or StackDirection.Down => (Vector3.Right, Vector3.Forward),
			StackDirection.Left or StackDirection.Right => (Vector3.Forward, Vector3.Up),
			StackDirection.Forward or StackDirection.Back => (Vector3.Right, Vector3.Up),
			_ => (Vector3.Right, Vector3.Forward)
		};
	}

	/// <summary>
	/// Build the per-step rotation from the X/Y angle offsets around the
	/// two axes perpendicular to the stack direction.
	/// </summary>
	private Rotation GetStepRotation()
	{
		if ( AngleOffsetX == 0f && AngleOffsetY == 0f )
			return Rotation.Identity;

		var (axisX, axisY) = GetPerpendicularAxes( Direction );
		return Rotation.FromAxis( axisX, AngleOffsetX ) * Rotation.FromAxis( axisY, AngleOffsetY );
	}

	/// <summary>
	/// Compute how far apart copies should be along the stack axis by projecting
	/// the object's oriented bounding box corners onto the actual world-space axis.
	/// </summary>
	private float GetStackExtent( GameObject target, Vector3 worldAxis )
	{
		var renderer = target.GetComponentInChildren<ModelRenderer>();
		BBox localBounds;

		if ( renderer.IsValid() && renderer.Model.IsValid() )
		{
			var mb = renderer.Model.Bounds;
			localBounds = new BBox( mb.Mins * renderer.WorldScale, mb.Maxs * renderer.WorldScale );
		}
		else
		{
			localBounds = new BBox( -Vector3.One * 8f, Vector3.One * 8f );
		}

		// Project all 8 corners of the oriented bounding box onto the axis
		var rot = target.WorldRotation;
		float min = float.MaxValue;
		float max = float.MinValue;

		for ( int i = 0; i < 8; i++ )
		{
			var corner = new Vector3(
				(i & 1) == 0 ? localBounds.Mins.x : localBounds.Maxs.x,
				(i & 2) == 0 ? localBounds.Mins.y : localBounds.Maxs.y,
				(i & 4) == 0 ? localBounds.Mins.z : localBounds.Maxs.z
			);

			var worldCorner = rot * corner;
			var proj = Vector3.Dot( worldCorner, worldAxis );
			min = MathF.Min( min, proj );
			max = MathF.Max( max, proj );
		}

		return (max - min) + PositionOffset;
	}

	/// <summary>
	/// Compute the world transforms for all stacked copies using iterative placement.
	/// Each copy is positioned relative to the previous one, so angle offsets
	/// produce correct arcs and circles rather than spirals.
	/// </summary>
	private Transform[] ComputeStackTransforms( GameObject target )
	{
		var localDir = GetLocalDirection( Direction );
		var basePos = target.WorldPosition;
		var baseRot = target.WorldRotation;

		// The initial world-space axis (before any angle offsets)
		var initialAxis = AlignMode == StackAlignMode.Object
			? baseRot * localDir
			: localDir;

		var extent = GetStackExtent( target, initialAxis );
		var stepAngle = GetStepRotation();

		var count = Math.Clamp( StackCount, 1, MaxStackCount );
		var transforms = new Transform[count.CeilToInt()];

		// Track the current step's rotation and position iteratively
		var prevPos = basePos;
		var prevRot = baseRot;

		for ( int i = 0; i < count; i++ )
		{
			// Apply the per-step angle offset to get this copy's rotation
			Rotation copyRot;
			if ( AlignMode == StackAlignMode.Object )
			{
				// Object mode: angle offset rotates in the object's local frame
				copyRot = prevRot * stepAngle;
			}
			else
			{
				// World mode: angle offset rotates in world space
				copyRot = stepAngle * prevRot;
			}

			// Compute the step direction from the *previous* copy's orientation
			Vector3 stepAxis;
			if ( AlignMode == StackAlignMode.Object )
			{
				stepAxis = prevRot * localDir;
			}
			else
			{
				stepAxis = localDir;
			}

			var copyPos = prevPos + stepAxis * extent;

			transforms[i] = new Transform( copyPos, copyRot, target.WorldScale );

			prevPos = copyPos;
			prevRot = copyRot;
		}

		return transforms;
	}

	/// <summary>
	/// Render ghost previews of each stacked copy.
	/// </summary>
	private void DrawStackPreview( GameObject target, Transform[] transforms )
	{
		foreach ( var tx in transforms )
		{
			DebugOverlay.GameObject( target, transform: tx, color: Color.White.WithAlpha( 0.5f ) );
		}
	}

	/// <summary>
	/// Server-side spawn: recomputes transforms from synced properties to prevent
	/// clients from sending arbitrary placement data.
	/// </summary>
	[Rpc.Host]
	private void SpawnStack( GameObject target )
	{
		if ( !target.IsValid() ) return;

		var root = ResolveRoot( target );
		if ( root.Tags.Contains( "constraint" ) ) return;

		// Recompute transforms server-side from synced properties
		var transforms = ComputeStackTransforms( root );

		// batch these spawns
		using var x = Scene.BatchGroup();

		var undo = Player.Undo.Create();
		undo.Name = "Stack";
		undo.Icon = "📚";

		for ( int i = 0; i < transforms.Length; i++ )
		{
			var tx = transforms[i];

			var clone = root.Clone( new CloneConfig
			{
				Transform = new Transform( tx.Position, tx.Rotation ),
				StartEnabled = true
			} );

			clone.Tags.Add( "removable" );

			if ( FreezeAll )
			{
				var rb = clone.GetComponent<Rigidbody>();
				if ( rb.IsValid() )
				{
					rb.MotionEnabled = false;
				}
			}

			Ownable.Set( clone, Player.Network.Owner );
			clone.NetworkSpawn( true, null );
			undo.Add( clone );
		}
	}
}