Editor/Wall/ArchWallExtrude.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// One stretch of a wall that an extrude zone covers, in the wall's own stations. The ONE resolve behind every
// answer a zone gives: the wall generator dresses these, the pier affector stands a column on them, and the MCP
// report counts them - so a preview, a build and a report can never disagree about where a zone reaches.
public readonly record struct ArchExtrudeStretch( ArchCutPart Zone, float From, float To, float Bottom, float Top ) {
	public float Along => (From + To) * 0.5f;

	public float Width => To - From;

	public float Rise => Top - Bottom;
}

// The proud work an extrude zone stands on a wall, expressed as ordinary wall modifiers so the generator emits
// them with the same pass it emits an authored quoin with. The zone is the AUTHORING: dragging a box over a
// corner, over the middle of an elevation or over its head is what a station and a width used to have to say.
public static class ArchWallExtrude {
	// How near a wall's own end a zone has to reach before it counts as covering the corner. An inch, because the
	// zone was dragged on the grid and the wall's end is on it too.
	const float Reaching = 1f;

	public static IEnumerable<ArchExtrudeStretch> Stretches(
		ArchPlan plan,
		ArchKit kit,
		ArchBuilding building,
		ArchRoom room,
		ArchWall wall,
		float edgeFrom,
		float edgeTo,
		float height ) {
		foreach ( var (zone, volume) in ArchCut.Extrusions( plan, room.Floor, kit, room.BaseHeight, room.BaseHeight + height, building?.Id ?? 0, ArchCutAffects.Walls, wall.Id ) ) {
			foreach ( var span in ArchFootprint.Inside( volume.Footprint, wall.Start, wall.End ) ) {
				// A zone reaching a wall's own end runs out to the end the JOIN emits, so the blocks off two
				// elevations meet in the corner and read as the single pier they are - which is the whole reason
				// to drag one there.
				var entered = span.From * wall.Length;
				var left = span.To * wall.Length;
				var from = entered <= Reaching ? edgeFrom : entered;
				var to = left >= wall.Length - Reaching ? edgeTo : left;

				if ( to - from < 0.5f ) {
					continue;
				}

				var sample = wall.PointAt( (from + to) * 0.5f );
				var bottom = Math.Clamp( volume.Floor.At( sample ) - room.BaseHeight, 0f, height );
				var top = Math.Clamp( volume.Ceiling.At( sample ) - room.BaseHeight, 0f, height );

				if ( top - bottom < 0.5f ) {
					continue;
				}

				yield return new ArchExtrudeStretch( zone, from, to, bottom, top );
			}
		}
	}

	public static List<ArchWallModShape> Zones(
		ArchPlan plan,
		ArchKit kit,
		ArchBuilding building,
		ArchRoom room,
		ArchWall wall,
		float edgeFrom,
		float edgeTo,
		float height,
		float thickness,
		IReadOnlyList<ArchOpening> openings ) {
		var stood = new List<ArchWallModShape>();

		foreach ( var stretch in Stretches( plan, kit, building, room, wall, edgeFrom, edgeTo, height ) ) {
			if ( !ArchExtrude.Dresses( stretch.Zone.Extrude ) ) {
				continue;
			}

			var half = thickness * 0.5f;
			var depth = ArchExtrude.Reach( stretch.Zone );
			var reach = MathF.Max( half, half + depth - ArchLap.Bite( kit ) );
			var spread = half > 0.01f ? reach / half : 1f;

			// A corner block runs into the NEIGHBOUR's proud face, or the two elevations' blocks leave a notch between them.
			var from = stretch.From <= edgeFrom + 0.01f ? edgeFrom * spread : stretch.From;
			var to = stretch.To >= edgeTo - 0.01f ? wall.Length + (edgeTo - wall.Length) * spread : stretch.To;

			if ( to - from < 0.5f ) {
				continue;
			}

			// Parted around the openings by the walk that already owns that question, so a zone dragged across a
			// whole elevation stands between its windows instead of driving a block through the glass.
			foreach ( var patch in ArchWallGen.Panels( from, to, height, openings ) ) {
				var low = MathF.Max( stretch.Bottom, patch.MinZ );
				var high = MathF.Min( stretch.Top, patch.MaxZ );

				if ( high - low < 0.5f || patch.MaxX - patch.MinX < 0.5f ) {
					continue;
				}

				var shape = ArchWallModShape.Resolve(
					Block( stretch.Zone, patch.MinX, patch.MaxX, low, high - low ),
					patch.MinX, patch.MaxX, height, thickness, kit );

				if ( shape.Stands ) {
					stood.Add( shape );
				}
			}
		}

		return stood;
	}

	static ArchWallModPart Block( ArchCutPart cut, float from, float to, float seat, float rise ) {
		var kind = ArchExtrude.Section( cut.Extrude );

		return new ArchWallModPart {
			Name = $"{cut.Extrude} Zone",
			Kind = kind,
			// A recess bites one elevation whatever the zone was told: on both it would be a hole through the
			// wall, and a hole is Subtract's job.
			Side = cut.ExtrudeBothFaces && !kind.Carves() ? ArchWallSide.Both : ArchWallSide.Outside,
			Along = (from + to) * 0.5f,
			Width = to - from,
			Base = seat,
			Height = rise,
			Depth = ArchExtrude.Reach( cut ),
			Course = cut.ExtrudeCourse,
			Joint = cut.ExtrudeJoint,
			Field = kind.Field(),
			Palette = cut.Palette
		};
	}
}