Editor/Tool/ArchEditGhost.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Editing is placing. A widget drag re-runs the same resolution the placement drag ran, so it has
// to preview like one too - the wireframe of what is already standing says nothing about the shape
// you are pulling it into.
//
// Every height here is a WORLD height, through ArchSelection.Lift, the same answer the overlay and the
// handles take. Drawn at the plan's own numbers instead, the ghost stood a grade lift below the geometry
// it was previewing, so grabbing any widget on a building with a plinth looked like the shape dropping.
public static class ArchEditGhost {
	public static void Draw( ArchTool tool, ArchSelection picked ) {
		var lift = picked?.Lift( tool.Plan, tool.Kit ) ?? 0f;
		var kind = tool.LayerTree.Find( picked?.Item )?.Kind
			?? ArchKinds.Load().Claiming( picked?.Item )?.Kind
			?? default;

		if ( ArchPreviews.Load().Draw( kind,
			new ArchPreviewContext( picked?.Item, picked?.Room, picked?.Building, lift, true, tool ) ) ) {
			return;
		}

		switch ( picked?.Item ) {
			case ArchRoom room:
				Room( tool, room, lift );
				break;

			case ArchBuilding building:
				Blueprint( building, lift );
				break;

			case ArchSiteAssembly group:
				Group( tool, group );
				break;

			case ArchPlatformPart platform:
				Solid( platform.Outline(), platform.GradeHeight, ArchRamp.Deck( platform ),
					$"{(platform.Ramp ? "ramp" : "platform")} — {platform.Name}" );
				break;

			case ArchRoofPart roof:
				Solid( roof.Outline(), roof.BaseHeight + lift, roof.BaseHeight + lift + 4f, $"roof — {roof.Name}" );
				break;

			// Every leg, because a chain winds and the one being pulled is not always the one you drew.
			case ArchCutPart cut:
				foreach ( var leg in cut.Legs() ) {
					var band = ArchCut.WorldBand( leg, lift );
					var label = cut.Mode switch {
						ArchZoneMode.Damage => $"damage zone — {cut.Name}",
						ArchZoneMode.Extrude => $"extrude zone — {cut.Name}",
						_ => $"{(cut.Ramp ? "ramp cut" : "cut")} — {cut.Name}"
					};

					Void( leg.Outline(), ArchCut.Floor( cut, leg ).Raised( lift ), band, label );
				}
				break;

		}
	}

	static void Room( ArchTool tool, ArchRoom room, float lift ) {
		var loop = ArchFloorGen.Footprint( room );

		if ( loop.Count < 3 ) {
			return;
		}

		var floor = room.BaseHeight + lift;
		var height = room.WallHeight > 0f ? room.WallHeight : tool.Kit.WallHeight;

		ArchGhost.Prism( loop, floor, floor + height );
		ArchGhost.Plate( Low( loop ), High( loop ), floor, 8 );
		Measure( loop, floor + height, $"room — {room.Name}" );
	}

	// The ghost stands on the same shell ring the widgets edit: a box around an L previews a rectangle
	// growing while the commit reshapes the L.
	static void Blueprint( ArchBuilding building, float lift ) {
		if ( !ArchHandles.Bounds( building, out var min, out var max ) ) {
			return;
		}

		var top = building.Rooms.Select( room => room.BaseHeight + room.WallHeight ).DefaultIfEmpty( 0f ).Max() + lift;
		var head = MathF.Max( top, lift + 8f );

		var shell = ArchHandles.Shell( building );

		if ( shell is { Count: >= 3 } ) {
			ArchGhost.Prism( shell, lift, head );
		} else {
			ArchGhost.Volume( min, max, lift, head );
		}

		// The ticks follow the ring the widgets edit, or a turned shell previews inside a world-axis box it does
		// not touch - a second set of brackets standing off the building beside the one the overlay already drew.
		ArchGhost.Brackets( shell is { Count: >= 3 } ? shell : ArchFootprint.Rect( min, max ), head );
		ArchGhost.Note( new Vector3( max.x, max.y, head ), $"{building.Name} — {max.x - min.x:0} x {max.y - min.y:0}" );
	}

	// A group previews as the box it holds, so moving one reads as moving the whole scope.
	static void Group( ArchTool tool, ArchSiteAssembly group ) {
		var loops = tool.Plan.Buildings
			.Where( building => group.Children.Contains( building.Id ) )
			.SelectMany( building => building.Rooms )
			.Concat( tool.Plan.AllRooms().Where( room => group.Children.Contains( room.Id ) ) )
			.Select( ArchFloorGen.Footprint )
			.Where( loop => loop.Count >= 3 )
			.ToList();

		if ( loops.Count == 0 ) {
			return;
		}

		var min = loops.Select( loop => Low( loop ) ).Aggregate( Vector2.Min );
		var max = loops.Select( loop => High( loop ) ).Aggregate( Vector2.Max );

		ArchGhost.Brackets( min, max, tool.LevelHeight );
		ArchGhost.Plate( min, max, tool.LevelHeight, 16 );
		ArchGhost.Note( new Vector3( max.x, max.y, tool.LevelHeight ), $"{group.Name} — {group.Children.Count} layers" );
	}

	static void Solid( List<Vector2> loop, float bottom, float top, string note ) {
		Solid( loop, bottom, ArchCarvePlane.Level( top ), note );
	}

	// A hole is drawn by the FLOOR it leaves, so a ramped bite previews as the wedge of material it takes rather
	// than the box its band describes - and the note still reads at the head, where the widget is.
	static void Void( List<Vector2> loop, ArchCarvePlane floor, Vector2 band, string note ) {
		if ( loop.Count < 3 ) {
			return;
		}

		ArchGhost.Solid( loop, floor, ArchCarvePlane.Level( band.y ) );
		Measure( loop, band.y, note );
	}

	// The top as a PLANE, so a ramp being dragged shows the wedge its generator carves rather than the box its
	// bounds describe - the frame still measures at the head, which is where the note can be read.
	static void Solid( List<Vector2> loop, float bottom, ArchCarvePlane top, string note ) {
		if ( loop.Count < 3 ) {
			return;
		}

		ArchGhost.Solid( loop, bottom, top );
		Measure( loop, loop.Max( corner => top.At( corner ) ), note );
	}

	static void Measure( IReadOnlyList<Vector2> loop, float height, string note ) {
		ArchFootprint.Bounds( loop, out var min, out var max );

		ArchGhost.Brackets( min, max, height );
		ArchGhost.Note( new Vector3( max.x, max.y, height ), $"{note} — {max.x - min.x:0} x {max.y - min.y:0}" );
	}

	static Vector2 Low( IReadOnlyList<Vector2> loop ) {
		ArchFootprint.Bounds( loop, out var min, out _ );

		return min;
	}

	static Vector2 High( IReadOnlyList<Vector2> loop ) {
		ArchFootprint.Bounds( loop, out _, out var max );

		return max;
	}
}