Editor/Data/ArchBuild.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Auto lays the ridge along the longer side, so the flat ends land on the short walls.
public enum RidgeRun {
	Auto,
	AlongX,
	AlongY
}

public enum SectionRoof {
	Continue,
	Hip,
	Gable,
	Capped,
	None
}

public sealed class ArchSection {
	public ArchRoom Room { get; init; }
	public ArchRoofPart Roof { get; init; }
	public int SharedWalls { get; init; }
	public bool Merged { get; init; }
}

// Shared by the Building subtool and the arch_* tools - both grow a plan the same way.
public static class ArchBuild {
	// THE STOREY GRID, and the only answer to it. A building's own override first; otherwise the storey it
	// actually STANDS - its ground rooms' real plate plus a floor - never the one the kit would have stood. An
	// archetype gives its shell taller walls than the kit's, and a grid that never learns seats the next floor
	// mid-wall inside the storey below, with every column under it standing straight through the slab.
	public static float StoreyHeight( ArchBuilding building, ArchKit kit ) {
		if ( building is { StoreyHeight: > 1f } ) {
			return building.StoreyHeight;
		}

		return Ground( building, kit ) + kit.FloorThickness;
	}

	public static float FloorOf( ArchBuilding building, ArchKit kit, int level ) {
		return level * StoreyHeight( building, kit ) + kit.GroundClearance;
	}

	// The tallest room on the ground, because a storey is as tall as the walls the floor above lands on. A
	// building with nothing standing yet is the kit's, which is what the first drag needs.
	static float Ground( ArchBuilding building, ArchKit kit ) {
		var standing = 0f;

		foreach ( var room in building?.Rooms ?? Enumerable.Empty<ArchRoom>() ) {
			if ( room.Floor != 0 || room.Spans ) {
				continue;
			}

			standing = MathF.Max( standing, ArchFloorGen.WallHeight( room, kit ) );
		}

		return standing > 1f ? standing : kit.WallHeight;
	}

	public static ArchSection Shell(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		RoofStyle? style,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto ) {
		var placement = new ArchBoundaryPlacement( plan, kit ).Outside( level, min, max );

		if ( !placement.IsUsable ) {
			return null;
		}

		min = placement.Min;
		max = placement.Max;
		var shell = CreateRoom( plan, building, "Shell", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );

		if ( style is null ) {
			return new ArchSection { Room = shell };
		}

		var roof = Roof( plan, kit, level, style.Value, min, max, baseHeight + kit.WallHeight, gutters, false, ridge );
		building.Roofs.Add( roof );

		return new ArchSection { Room = shell, Roof = roof };
	}

	public static ArchSection Partition(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor ) {
		(min, max) = new ArchGridService().Rectangle( min, max );
		var party = new List<(Vector2 From, Vector2 To)>();
		var room = CreateRoom( plan, building, $"Room{building.Rooms.Count + 1}", level, baseHeight, kit.WallHeight, min, max, floor, party, kit.WallThickness );

		return new ArchSection { Room = room, SharedWalls = party.Count };
	}

	// Two buildings on one plot each roof their own storey and the pair clip each other into nonsense.
	public static bool Stacks( ArchBuilding building, ArchKit kit, Vector2 min, Vector2 max ) {
		if ( building is null || !building.HasContent ) {
			return false;
		}

		// UNIONED first: the storeys are separate regions, not the rings of one, and ArchFootprint.Encloses folds
		// a set by PARITY - so two storeys standing on the same footprint cancelled and the building read as
		// covering nothing at all, which turned every third storey into a second building.
		var covered = ArchRegion.Shell( ArchFootprint.Union( ArchRegion.Footprints( building.Rooms ) ), kit.WallThickness );

		return ArchRegion.Covers( covered, ArchFootprint.Rect( min, max ) );
	}

	public static ArchSection Storey(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		RoofStyle? style,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto ) {
		(min, max) = new ArchGridService().Rectangle( min, max );
		var snapped = new ArchWingPlacement( building, kit, level ).Snap( min, max );
		min = snapped.Min;
		max = snapped.Max;

		var room = CreateRoom( plan, building, $"Storey{level}", level, baseHeight, kit.WallHeight, min, max, floor, null, kit.WallThickness );
		var plate = baseHeight + kit.WallHeight;
		var outline = ArchFootprint.Rect( min, max );

		if ( DeckBuiltOverBy( building, outline ) is { } below ) {
			below.Level = level;
			below.BaseHeight = plate;
			below.Reshape( outline );

			return new ArchSection { Room = room, Roof = below, Merged = true };
		}

		if ( style is null ) {
			return new ArchSection { Room = room };
		}

		var roof = Roof( plan, kit, level, style.Value, min, max, plate, gutters, false, ridge );
		building.Roofs.Add( roof );

		return new ArchSection { Room = room, Roof = roof };
	}

	// Only a FULLY covered deck is rebuilt - a partial one is a real stepped building.
	static ArchRoofPart DeckBuiltOverBy( ArchBuilding building, List<Vector2> outline ) {
		return building.Roofs.FirstOrDefault( roof => ArchRegion.Covers( new[] { outline }, roof.Outline() ) );
	}

	// Continuing folds the wing into the section; otherwise its own stepped roof.
	public static ArchSection Extend(
		ArchPlan plan,
		ArchBuilding building,
		ArchKit kit,
		int level,
		float baseHeight,
		Vector2 min,
		Vector2 max,
		SectionRoof choice,
		float drop,
		bool floor,
		bool gutters,
		RidgeRun ridge = RidgeRun.Auto ) {
		(min, max) = new ArchGridService().Rectangle( min, max );

		return new ArchWingGeneration( plan, building, kit )
			.On( level, baseHeight, min, max )
			.WithRoof( choice, drop, gutters, ridge )
			.WithFloor( floor )
			.Create();
	}

	public static ArchRoofPart Roof(
		ArchPlan plan,
		ArchKit kit,
		int level,
		RoofStyle style,
		Vector2 min,
		Vector2 max,
		float baseHeight,
		bool gutters,
		bool parapet,
		RidgeRun ridge = RidgeRun.Auto ) {
		return new ArchRoofPart {
			Id = plan.AllocateId(),
			Name = "Roof",
			Level = level,
			Style = style,
			Min = min,
			Max = max,
			Footprint = ArchFootprint.Rect( min, max ),
			BaseHeight = baseHeight,
			Pitch = kit.RoofPitch,
			RidgeAlongX = Ridged( ridge, min, max ),
			// A parapet stands on the wall line - an overhang would leave the coping floating.
			Overhang = parapet ? 0f : kit.RoofOverhang,
			Thickness = kit.RoofThickness,
			Gutters = gutters && !parapet,
			Fascia = !parapet,
			Soffit = !parapet,
			Parapet = parapet,
			Ceiling = Ceiling( style )
		};
	}

	// Shared with the ghost, so what is drawn is what gets built.
	public static bool Ridged( RidgeRun ridge, Vector2 min, Vector2 max ) => ridge switch {
		RidgeRun.AlongX => true,
		RidgeRun.AlongY => false,
		_ => max.x - min.x >= max.y - min.y
	};

	// Hip/gable leave a void, so they get a ceiling; shed/sawtooth are meant to be seen from underneath.
	static bool Ceiling( RoofStyle style ) {
		return style is RoofStyle.Hip or RoofStyle.Gable;
	}

	// The one mapping for a wing's roof choice - the ghost, the generator and the re-dress must agree.
	public static RoofStyle Winged( SectionRoof choice ) {
		return choice switch {
			SectionRoof.Gable => RoofStyle.Gable,
			SectionRoof.Capped => RoofStyle.Flat,
			_ => RoofStyle.Hip
		};
	}

	internal static ArchRoom CreateRoom(
		ArchPlan plan,
		ArchBuilding building,
		string name,
		int level,
		float baseHeight,
		float wallHeight,
		Vector2 min,
		Vector2 max,
		bool floor,
		List<(Vector2 From, Vector2 To)> shared,
		float thickness ) {
		var room = new ArchRoom {
			Id = plan.AllocateId(),
			Name = name,
			Floor = level,
			BaseHeight = baseHeight,
			WallHeight = wallHeight,
			HasFloor = floor,
			Footprint = ArchFootprint.Rect( min, max )
		};

		for ( var index = 0; index < room.Footprint.Count; index++ ) {
			var start = room.Footprint[index];
			var end = room.Footprint[(index + 1) % room.Footprint.Count];

			room.Walls.Add( new ArchWall {
				Id = plan.AllocateId(),
				Start = start,
				End = end,
				Exterior = true,
				Cap = true
			} );

			if ( shared is not null && ArchWallJoins.PartyWallExists( building, room.BaseHeight, start, end, thickness ) ) {
				shared.Add( (start, end) );
			}
		}

		building.Rooms.Add( room );

		return room;
	}

}