Editor/Porch/ArchOverhangGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Nothing under it carries it, so it needs a boxed underside of its own.
public static class ArchOverhangGen {
	const float Walkable = 24f;

	public static void Build( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchGround ground ) {
		new ArchOverhangGenerationService( room, building, plan, kit, style, ground ).Create( canvas );
	}

	// How deep the boxed underside hangs below the slab it belongs to.
	public static float Depth( ArchKit kit ) => MathF.Max( 3f, kit.FasciaHeight );

	// THE FACE ANYTHING UNDER AN OVERHANG MEETS FIRST, and it is not the slab's own underside - the box hangs a
	// fascia depth lower than that. A column that measured the slab stops inside the very thing it is propping,
	// and a cursor that measured it lands eight inches above the surface being pointed at.
	public static float Soffit( ArchRoom room, ArchKit kit ) => room.BaseHeight - kit.FloorThickness - Depth( kit );

	// The post a storey props itself on, which is a PILLAR - the same section, footing and dressing the Pillars
	// tool authors, so a prop under an overhang and a column in a colonnade are one thing dressed twice.
	public static ArchPillarPart Section( ArchRoom room ) {
		var section = (room?.OverhangPost ?? new ArchPillarPart()).Section();

		section.Placement = PillarPlacement.Freestanding;
		section.Span = PillarSpan.None;

		return section;
	}

	// Walkable depth needs carrying: posts land on open-air edges, corners first, then evenly. Set IN by their own
	// seat, and the shell they lean on grown by the same, or a post on the corner of the loop stands three
	// quarters of itself out in the air and the run along the building grows a row it never needed.
	internal static void Posts( ArchMesh canvas, List<List<Vector2>> region, ArchOverhangSupportService support, ArchRoom room, ArchKit kit, float top, ArchBrush shaft, ArchBrush cap ) {
		var section = Section( room );
		var seat = MathF.Max( 1f, section.Seat );

		if ( ArchFootprint.Shrink( region, MathF.Max( Walkable, seat * 2f ) * 0.5f ).Count == 0 ) {
			return;
		}

		var stood = ArchFootprint.Shrink( region, seat );
		var leaning = ArchFootprint.Grow( support.Carried, seat );
		var spacing = MathF.Max( 32f, MathF.Max( section.Width, section.Depth ) * 8f );

		foreach ( var loop in ArchFootprint.Outer( stood ) ) {
			var open = ArchRegion.Open( loop, leaning ).Select( run => ArchRunPath.Of( run, top ) );

			foreach ( var at in ArchBays.Posts( ArchBays.Over( open, spacing, MathF.Max( 6f, seat * 2f ) ) ) ) {
				Stand( canvas, section, at.Point, support.GradeAt( at.Point ), top, shaft, cap );
			}
		}
	}

	static void Stand( ArchMesh canvas, ArchPillarPart section, Vector2 at, float grade, float top, ArchBrush shaft, ArchBrush cap ) {
		var post = section.Section();

		post.Placement = PillarPlacement.Freestanding;
		post.Origin = at;
		post.BaseHeight = grade;
		post.Height = MathF.Max( 8f, top - grade );

		ArchPillarGen.Stand( canvas, post, shaft, cap );
	}

	// The line below grows by half a wall first, or the footprint reads unsupported.
	public static List<List<Vector2>> Unsupported( ArchRoom room, ArchBuilding building, ArchKit kit ) {
		return new ArchOverhangSupportService( room, building, null, kit, default ).Resolve();
	}
}

sealed class ArchOverhangGenerationService {
	readonly ArchRoom room;
	readonly ArchBuilding building;
	readonly ArchPlan plan;
	readonly ArchKit kit;
	readonly ArchStyle style;
	readonly ArchGround ground;

	public ArchOverhangGenerationService( ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchGround ground ) {
		this.room = room;
		this.building = building;
		this.plan = plan;
		this.kit = kit;
		this.style = style;
		this.ground = ground;
	}

	public void Create( ArchMesh canvas ) {
		var support = new ArchOverhangSupportService( room, building, plan, kit, ground );
		var region = support.Resolve();

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

		var chain = new[] { room.Palette, building.Palette };
		var soffit = style.Brush( ArchSurface.Soffit, chain );
		var depth = ArchOverhangGen.Depth( kit );
		var top = room.BaseHeight - kit.FloorThickness;

		foreach ( var loop in ArchFootprint.Outer( region ) ) {
			ArchFloorGen.Slab( canvas, loop, null, top - depth, top, soffit );
		}

		// The slab overhead is poured to the wall CENTRELINE, so half a thickness of it is never there. Over a
		// storey below, that storey's own wall fills it; over open air nothing does, and the slot runs right
		// round the overhang with the wall's bare foot showing through it.
		foreach ( var loop in ArchFootprint.Outer( support.Lip( region ) ) ) {
			ArchFloorGen.Slab( canvas, loop, null, top, room.BaseHeight, soffit );
		}

		if ( !room.OverhangPosts ) {
			return;
		}

		var posts = new[] { room.OverhangPost?.Palette ?? new ArchPalette(), room.Palette, building.Palette };

		using ( canvas.Part( ArchPieces.Posts ) ) {
			ArchOverhangGen.Posts( canvas, region, support, room, kit, top - depth,
				style.Brush( ArchSurface.Pillar, posts ), style.Brush( ArchSurface.PillarCap, posts ) );
		}
	}
}

sealed class ArchOverhangSupportService {
	readonly ArchRoom room;
	readonly ArchBuilding building;
	readonly ArchPlan plan;
	readonly ArchKit kit;
	readonly ArchGround ground;
	readonly List<List<Vector2>> carried;
	readonly List<List<Vector2>> standing;

	public ArchOverhangSupportService( ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchGround ground ) {
		this.room = room;
		this.building = building;
		this.plan = plan;
		this.kit = kit;
		this.ground = ground;
		carried = room is null || building is null || room.Floor <= 0
			? new List<List<Vector2>>()
			: ArchRegion.Shell( ArchRegion.Storey( building, room.Floor - 1 ), kit.WallThickness );
		standing = Standing();
	}

	// Out to the OUTER wall face, not the centreline: the underside has to finish where the elevation does.
	public List<List<Vector2>> Resolve() {
		return standing.Count == 0
			? new List<List<Vector2>>()
			: ArchFootprint.Subtract( ArchRegion.Shell( standing, kit.WallThickness ), carried );
	}

	// The half thickness the slab above never reaches, wherever nothing below stands in for it.
	public List<List<Vector2>> Lip( IEnumerable<IReadOnlyList<Vector2>> resolved ) {
		return standing.Count == 0 ? new List<List<Vector2>>() : ArchFootprint.Subtract( resolved, standing );
	}

	List<List<Vector2>> Standing() {
		var none = new List<List<Vector2>>();

		if ( room is null || building is null || room.Floor <= 0 || room.Spans || carried.Count == 0 ) {
			return none;
		}

		var footprint = ArchFloorGen.Footprint( room );

		if ( footprint.Count < 3 || !ArchFootprint.Bearing( footprint, out _ ) ) {
			return none;
		}

		return new List<List<Vector2>> { footprint };
	}

	// The same question a hand-placed column asks, so it goes to the same place: the storey below carries the post
	// where that storey is really under it, and over open air it seeks the ground.
	public float GradeAt( Vector2 point ) {
		var datum = building.Rooms
			.Where( other => other.Floor == room.Floor - 1 )
			.Select( other => other.BaseHeight )
			.DefaultIfEmpty( kit.GroundClearance )
			.Min();

		var covered = ArchPillarSoffit.Covering( plan, kit, point, datum );

		return ArchPillarSeat.Under( plan, building, kit, ground, point, datum,
			covered < float.MaxValue ? MathF.Max( datum + ArchPillarSeat.Clear, covered ) : datum + ArchPillarSeat.Clear );
	}

	public IReadOnlyList<List<Vector2>> Carried => carried;
}