Editor/Stair/ArchStairShape.Pierce.cs
using System;
using System.Linq;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;


public sealed partial class ArchStairShape {
	// Holes are keyed to the stair that made them: re-piercing replaces, deleting removes.
	public static void Pierce( ArchPlan plan, ArchBuilding building, ArchRoom room, ArchStairPart stair, ArchKit kit ) {
		if ( building is null ) {
			return;
		}

		// Filing on another storey cuts the shaft through the wrong slab — say why out loud.
		if ( room is not null && MathF.Abs( stair.BaseHeight - room.BaseHeight ) > 1f ) {
			Log.Warning( $"Architecture: stair {stair.Name} starts at {stair.BaseHeight:0.#} but {room.Name}'s floor is at {room.BaseHeight:0.#} - it is filed on the wrong storey." );
		}

		var shape = Resolve( stair, room, kit, building.Rooms, building );

		// A flight drawn out of one building and into the next has to open BOTH: the far building's
		// slabs know nothing about a stair arriving up through them. How far "the next" goes is the
		// group the flight was filed in - outside that scope it has nothing it is allowed to open.
		var reach = ArchLayerGroups.Reach( plan, stair.Id, building.Id );

		foreach ( var host in plan.Buildings.Where( host => host == building || reach is null || reach.Contains( host.Id ) ) ) {
			Open( plan, host, stair, shape, kit, host == building );
		}

		Breach( plan, stair, shape, kit, reach );
	}

	// The loops one building's own slabs lose. A neighbour's storeys need not line up with the flight's,
	// so its level is read back off the well's height rather than carried across.
	static void Open( ArchPlan plan, ArchBuilding host, ArchStairPart stair, ArchStairShape shape, ArchKit kit, bool owner ) {
		// Recycled ids keep an unchanged flight byte-identical, so a rebuild costs no undo entry.
		var recycled = new Queue<int>( host.Cutouts.Where( cutout => cutout.OwnerId == stair.Id ).Select( cutout => cutout.Id ) );

		host.Cutouts.RemoveAll( cutout => cutout.OwnerId == stair.Id );

		foreach ( var well in shape.Levels ) {
			var level = owner ? well.Level : FloorIndex( host, kit, well.Height );

			if ( !owner && MathF.Abs( FloorOf( host, kit, level ) - well.Height ) > 1f ) {
				continue;
			}

			foreach ( var loop in well.Loops ) {
				if ( !owner && !Standing( host, level, loop ) ) {
					continue;
				}

				var cutout = new ArchFloorCutout {
					Id = recycled.Count > 0 ? recycled.Dequeue() : plan.AllocateId(),
					Name = $"Stairwell{stair.Id}",
					Level = level,
					OwnerId = stair.Id
				};

				cutout.Reshape( loop );
				host.Cutouts.Add( cutout );
			}
		}
	}

	// Bounds, not containment: a well that only clips the corner of a slab still takes a bite out of it.
	static bool Standing( ArchBuilding host, int level, IReadOnlyList<Vector2> loop ) {
		ArchFootprint.Bounds( loop, out var min, out var max );

		foreach ( var room in host.Rooms.Where( entry => entry.Floor == level && entry.HasFloor ) ) {
			var footprint = ArchFloorGen.Footprint( room );

			if ( footprint.Count < 3 ) {
				continue;
			}

			ArchFootprint.Bounds( footprint, out var low, out var high );

			if ( low.x <= max.x && high.x >= min.x && low.y <= max.y && high.y >= min.y ) {
				return true;
			}
		}

		return false;
	}

	// The walls a flight walks THROUGH, inside the scope it was filed in. A wall the flight merely runs
	// alongside is left alone, because that is the wall it is meant to hug. Cutting honours the scope but
	// reaping stays plan-wide, or a regrouped flight would leave holes behind with nobody left to close them.
	public static void Breach( ArchPlan plan, ArchStairPart stair, ArchStairShape shape, ArchKit kit, IReadOnlySet<int> reach = null ) {
		var headroom = MathF.Max( 1f, kit.StairHeadroom );
		var clearance = ArchProbe.Step;
		var wants = new List<ArchWant<ArchWall, ArchOpening>>();

		foreach ( var room in plan.AllRooms() ) {
			if ( reach is not null && plan.OwnerOf( room ) is { } owner && !reach.Contains( owner.Id ) ) {
				continue;
			}

			var wallHeight = ArchFloorGen.WallHeight( room, kit );

			foreach ( var wall in room.Walls ) {
				foreach ( var run in shape.Runs ) {
					if ( !Crossing( wall, run.Axes, run.Length, run.Width, out var from, out var to, out var alongFrom, out var alongTo ) ) {
						continue;
					}

					var under = MathF.Min( run.Rake( alongFrom ), run.Rake( alongTo ) ) - MathF.Max( 1f, stair.TreadThickness );
					var over = MathF.Max( run.Rake( alongFrom ), run.Rake( alongTo ) ) + headroom;
					var sill = MathF.Max( 0f, under - room.BaseHeight );
					var head = MathF.Min( wallHeight, over - room.BaseHeight );

					if ( head - sill < 1f ) {
						continue;
					}

					wants.Add( Archway( wall, (from + to) * 0.5f, to - from + clearance * 2f, sill, head - sill ) );
				}
			}
		}

		// Slotted by the wall and the ordinal on it, so a flight nudged along the same elevation re-dresses
		// the archway it already owns there rather than closing it and allocating another id.
		ArchOwned.Settle( plan, ArchOwned.Openings, stair.Id, wants, () => new ArchOpening() );
	}

	static ArchWant<ArchWall, ArchOpening> Archway( ArchWall wall, float offset, float width, float sill, float height ) {
		return new ArchWant<ArchWall, ArchOpening>( wall, opening => {
			opening.Preset = "archway";
			opening.Kind = OpeningKind.Archway;
			opening.Offset = offset;
			opening.Width = width;
			opening.SillHeight = sill;
			opening.Height = height;
			opening.Cased = false;
			opening.HasSill = false;
			opening.Leaf = false;
			opening.Glazed = false;
			opening.AutoLayout = false;
		} );
	}

	// The wall's centreline clipped to a run's rectangle, as stations along the wall and as the run's own
	// along at each end - the rake is what decides how tall the hole has to be.
	static bool Crossing( ArchWall wall, ArchStairAxes axes, float length, float width, out float from, out float to, out float alongFrom, out float alongTo ) {
		from = 0f;
		to = 0f;
		alongFrom = 0f;
		alongTo = 0f;

		var span = wall.End - wall.Start;
		var reach = span.Length;

		if ( reach < 1f ) {
			return false;
		}

		var direction = span / reach;

		// A wall the flight runs ALONG is the wall it hugs; only one it walks through is opened.
		if ( MathF.Abs( Vector2.Dot( direction, axes.Along ) ) > 0.7f ) {
			return false;
		}

		var low = 0f;
		var high = reach;
		var offset = wall.Start - axes.Origin;

		if ( !Slab( Vector2.Dot( offset, axes.Along ), Vector2.Dot( direction, axes.Along ), 0f, length, ref low, ref high )
			|| !Slab( Vector2.Dot( offset, axes.Across ), Vector2.Dot( direction, axes.Across ), 0f, width, ref low, ref high )
			|| high - low < 1f ) {
			return false;
		}

		from = low;
		to = high;
		alongFrom = Vector2.Dot( offset + direction * low, axes.Along );
		alongTo = Vector2.Dot( offset + direction * high, axes.Along );

		return true;
	}

	// One axis of the box clip: how much of the wall stays inside that axis' bounds.
	static bool Slab( float start, float rate, float min, float max, ref float low, ref float high ) {
		if ( MathF.Abs( rate ) < 0.0001f ) {
			return start >= min && start <= max;
		}

		var first = (min - start) / rate;
		var second = (max - start) / rate;

		low = MathF.Max( low, MathF.Min( first, second ) );
		high = MathF.Min( high, MathF.Max( first, second ) );

		return high > low;
	}
}