Editor/Layers/ArchBuildingStamp.cs
using Sandbox;

namespace Sunless.Architecture;

public readonly record struct ArchAssetShape( IReadOnlyList<Vector2> Loop, float Bottom, float Top );

// What a saved building looks like before it exists, worked out once when it is chosen rather than per
// frame: the hover redraws every frame and a footprint derived from every room and deck each time is a
// tool that stutters while you aim it.
public sealed class ArchAssetGhost {
	public List<ArchAssetShape> Shapes { get; init; } = new();
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }
}

// Placing a saved building back into a plan. The copy is exact - every layer and every subtool standing in
// it - so the only things that change are the ids it answers to and where on the map it stands.
public static class ArchBuildingStamp {
	public static ArchBuilding Place( ArchPlan plan, ArchBuildingAsset asset, Vector2 at, float degrees ) {
		if ( plan is null || asset is not { IsUsable: true } ) {
			return null;
		}

		var text = ArchStorage.Serialize( asset );

		if ( !ArchLayerIds.Range( text, out var lowest, out var highest ) ) {
			return null;
		}

		var delta = plan.NextId - lowest;

		if ( ArchStorage.Deserialize<ArchBuildingAsset>( ArchLayerIds.Shifted( text, delta ) ) is not { IsUsable: true } copy ) {
			return null;
		}

		var unit = copy.Unit;

		Bounds( unit, out var min, out var max );
		ArchCarry.Unit( unit, Landing( min, max, at, degrees ) );

		unit.Facing = degrees;

		unit.Name = Unique( plan, string.IsNullOrWhiteSpace( asset.Title ) ? unit.Name : asset.Title );

		plan.Units.Add( unit );
		plan.Layers.AddRange( copy.Layers );
		plan.Parts.AddRange( copy.Parts );
		plan.Links.AddRange( copy.Links );

		plan.NextId = Math.Max( plan.NextId, highest + delta + 1 );

		return unit;
	}

	// The cursor holds the corner of the TURNED shape, not the corner it was authored at: spinning about the
	// authored corner walks the building off to one side of the pointer, further with every turn.
	public static ArchRemap Landing( Vector2 min, Vector2 max, Vector2 at, float degrees ) {
		var size = max - min;

		var turned = ArchFootprint
			.Turned( new[] { Vector2.Zero, size, new Vector2( size.x, 0f ), new Vector2( 0f, size.y ) }, Vector2.Zero, degrees )
			.Aggregate( Vector2.Min );

		return ArchRemap.Spun( min, degrees, at - turned );
	}

	public static ArchRemap Landing( ArchAssetGhost ghost, Vector2 at, float degrees ) => Landing( ghost.Min, ghost.Max, at, degrees );

	// CARRYING holds the centre, where stamping holds a corner. A house already standing is grabbed and taken
	// somewhere, so it has to spin in place under the pointer: held by a corner, every quarter threw it across
	// the screen because the corner the cursor holds is a different corner after the turn.
	public static ArchRemap Carried( Vector2 min, Vector2 max, Vector2 at, float degrees ) {
		return ArchRemap.Spun( (min + max) * 0.5f, degrees, at );
	}

	public static ArchRemap Carried( ArchAssetGhost ghost, Vector2 at, float degrees ) => Carried( ghost.Min, ghost.Max, at, degrees );

	public static ArchAssetGhost Ghost( ArchBuildingAsset asset ) {
		return asset is { IsUsable: true } ? Ghost( asset.Unit ) : new ArchAssetGhost();
	}

	// The same ghost a STANDING building offers, because carrying a house that already exists is the same
	// gesture as stamping one that does not - and two ghosts of one shape is two shapes.
	public static ArchAssetGhost Ghost( ArchBuilding unit ) {
		if ( unit is null ) {
			return new ArchAssetGhost();
		}

		var shapes = new List<ArchAssetShape>();

		foreach ( var room in unit.Rooms ) {
			var loop = ArchFloorGen.Footprint( room );

			if ( loop.Count >= 3 ) {
				shapes.Add( new ArchAssetShape( loop, room.BaseHeight, room.BaseHeight + room.WallHeight ) );
			}
		}

		// The deck reads as a plate rather than a solid - a prism the depth of a roof over the walls under it
		// hides the very footprint the hover is for.
		foreach ( var roof in unit.Roofs ) {
			var loop = roof.Outline();

			if ( loop.Count >= 3 ) {
				shapes.Add( new ArchAssetShape( loop, roof.BaseHeight, roof.BaseHeight + MathF.Max( 1f, roof.Thickness ) ) );
			}
		}

		Bounds( unit, out var min, out var max );

		return new ArchAssetGhost { Shapes = shapes, Min = min, Max = max };
	}

	// Card art off the saved plan itself, so what is browsed is what will stand.
	public static ArchStaged Stage( ArchBuildingAsset asset ) {
		if ( asset is not { IsUsable: true } ||
			ArchStorage.Deserialize<ArchBuilding>( ArchStorage.Serialize( asset.Unit ) ) is not { } unit ) {
			return new ArchStaged( null, Vector3.Zero, 480f );
		}

		var plan = new ArchPlan();

		plan.Units.Add( unit );
		plan.Normalize();

		Bounds( unit, out var min, out var max );

		var size = max - min;
		var centre = (min + max) * 0.5f;
		var height = Standing( unit );

		return new ArchStaged(
			plan,
			new Vector3( centre.x, centre.y, height * 0.55f ),
			MathF.Max( 520f, MathF.Max( MathF.Max( size.x, size.y ), height ) * 1.9f ) );
	}

	public static string Identity( ArchBuildingAsset asset ) => ArchStageKey.Of( "building", asset.Name, asset );

	public static string Describe( ArchBuildingAsset asset ) {
		if ( asset is not { IsUsable: true } ) {
			return "empty";
		}

		Bounds( asset.Unit, out var min, out var max );

		var size = max - min;
		var rooms = asset.Unit.Rooms.Count;

		return $"{size.x:0} x {size.y:0} — {rooms} room{(rooms == 1 ? "" : "s")}";
	}

	static float Standing( ArchBuilding unit ) {
		var height = 128f;

		foreach ( var room in unit.Rooms ) {
			height = MathF.Max( height, room.BaseHeight + room.WallHeight );
		}

		return height;
	}

	static void Bounds( ArchBuilding unit, out Vector2 min, out Vector2 max ) {
		if ( ArchHandles.Bounds( unit, out min, out max ) ) {
			return;
		}

		min = Vector2.Zero;
		max = Vector2.Zero;
	}

	static string Unique( ArchPlan plan, string wanted ) {
		var taken = plan.Units.Select( unit => unit.Name ).ToHashSet( StringComparer.OrdinalIgnoreCase );

		if ( !taken.Contains( wanted ) ) {
			return wanted;
		}

		for ( var suffix = 2; suffix < 1000; suffix++ ) {
			if ( !taken.Contains( $"{wanted} {suffix}" ) ) {
				return $"{wanted} {suffix}";
			}
		}

		return wanted;
	}
}