Editor/Services/ArchRemap.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Transforms a shape's old bounds into its new ones; shared by moves, corner drags and edge drags.
public readonly struct ArchRemap {
	readonly Vector2 from;
	readonly Vector2 scale;
	readonly Vector2 to;
	readonly float degrees;

	ArchRemap( Vector2 from, Vector2 scale, Vector2 to, float degrees = 0f ) {
		this.from = from;
		this.scale = scale;
		this.to = to;
		this.degrees = Wrapped( degrees );
	}

	public static ArchRemap Translation( Vector2 shift ) => new( Vector2.Zero, Vector2.One, shift );

	public static ArchRemap Turned( Vector2 pivot, int quarters, Vector2 to ) => Spun( pivot, quarters * 90f, to );

	public static ArchRemap Spun( Vector2 pivot, float degrees ) => Spun( pivot, degrees, pivot );

	public static ArchRemap Spun( Vector2 pivot, float degrees, Vector2 to ) => new( pivot, Vector2.One, to, degrees );

	// A degenerate axis cannot be scaled, so it translates - a flat footprint still follows its drag.
	public static ArchRemap Between( Vector2 fromMin, Vector2 fromMax, Vector2 toMin, Vector2 toMax ) {
		var span = fromMax - fromMin;
		var wanted = toMax - toMin;

		return new ArchRemap( fromMin, new Vector2( Axis( span.x, wanted.x ), Axis( span.y, wanted.y ) ), toMin );
	}

	static float Axis( float span, float wanted ) => MathF.Abs( span ) < 0.01f ? 1f : wanted / span;

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

		return turn < 0f ? turn + 360f : turn;
	}

	public float Degrees => degrees;

	public int Quarters => (int)MathF.Round( degrees / 90f ) % 4;

	// Right-angle maps are exact; non-90 requires trig and cannot preserve axis-aligned bounds.
	public bool KeepsAxes => MathF.Abs( degrees - MathF.Round( degrees / 90f ) * 90f ) < 0.001f;

	public bool SwapsAxes => KeepsAxes && Quarters % 2 == 1;

	// Exact swaps, no trig — a sine rotation drifts grid coordinates off the grid.
	public static Vector2 Spun( Vector2 offset, int quarters ) => (((quarters % 4) + 4) % 4) switch {
		1 => new Vector2( -offset.y, offset.x ),
		2 => new Vector2( -offset.x, -offset.y ),
		3 => new Vector2( offset.y, -offset.x ),
		_ => offset
	};

	public Vector2 Spun( Vector2 direction ) => KeepsAxes
		? Spun( direction, Quarters )
		: ArchFootprint.Turned( direction, Vector2.Zero, degrees );

	public float Spun( float yaw ) => yaw + degrees;

	// Centre travels, size kept (swapped on odd quarters).
	public void Centred( ref Vector2 min, ref Vector2 max ) {
		var size = Swapped( max - min );
		var at = Of( (min + max) * 0.5f );

		min = at - size * 0.5f;
		max = at + size * 0.5f;
	}

	public bool Axis( bool alongX ) {
		if ( KeepsAxes ) {
			return alongX ^ SwapsAxes;
		}

		var along = Spun( alongX ? new Vector2( 1f, 0f ) : new Vector2( 0f, 1f ) );

		return MathF.Abs( along.x ) >= MathF.Abs( along.y );
	}

	public Vector2 Swapped( Vector2 pair ) => SwapsAxes ? new Vector2( pair.y, pair.x ) : pair;

	public Vector2 Of( Vector2 point ) => to + Spun( (point - from) * scale );

	public Vector3 Of( Vector3 point ) {
		var flat = Of( new Vector2( point.x, point.y ) );

		return new Vector3( flat.x, flat.y, point.z );
	}

	// Min/Max pairs must stay ordered: a negative scale would otherwise hand back an inside-out box.
	public void Box( ref Vector2 min, ref Vector2 max ) {
		var first = Of( min );
		var second = Of( max );

		min = Vector2.Min( first, second );
		max = Vector2.Max( first, second );
	}

	public void Loop( List<Vector2> loop ) {
		for ( var index = 0; index < loop.Count; index++ ) {
			loop[index] = Of( loop[index] );
		}
	}

	public void Path( List<Vector3> path ) {
		for ( var index = 0; index < path.Count; index++ ) {
			path[index] = Of( path[index] );
		}
	}

	public void Nodes( List<ArchCurveNode> nodes ) {
		foreach ( var node in nodes ) {
			node.Position = Of( node.Position );
		}
	}
}