Editor/Wall/ArchWallModShape.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// One solid of a modifier. A quoin is a stack of them; everything else is a single block. Foot and
// head depth differ only where the kind tapers, and that is what turns a beam into a prism.
public readonly record struct ArchWallModBlock( float From, float To, float Bottom, float Top, float Foot, float Head );

// The ONE resolution behind a wall modifier: where it sits on the run, how far it reaches, and the
// blocks it is made of. The generator emits these, the ghost draws these, the handles pull these.
public sealed class ArchWallModShape {
	public ArchWallModPart Part { get; init; }
	public bool Carves { get; init; }
	public bool Sealed { get; init; }
	// Which elevations carry it. Both is a pier reading on either face; the two are not exclusive.
	public bool Outside { get; init; }
	public bool Inside { get; init; }
	public float From { get; init; }
	public float To { get; init; }
	public float Bottom { get; init; }
	public float Top { get; init; }
	public float Depth { get; init; }
	public IReadOnlyList<ArchWallModBlock> Blocks { get; init; } = Array.Empty<ArchWallModBlock>();

	public bool Stands => To - From > 0.5f && Top - Bottom > 0.5f && Depth > 0.05f && (Outside || Inside);

	// A shape standing to the storey head carries a course straddling it: the clamp is the STOREY's, not the pier's.
	public bool Spans( float bottom, float top, float wallHeight ) {
		if ( !Stands || top <= Bottom + 0.05f ) {
			return false;
		}

		var reach = wallHeight > 0.05f && Top >= wallHeight - 0.05f ? MathF.Max( Top, top ) : Top;

		return bottom < reach - 0.05f;
	}

	// The faces this block is emitted on, as the sign that turns the wall's half-thickness into the
	// plane it stands on: -1 is the exterior skin's side, +1 the room's.
	public IEnumerable<float> Faces() {
		if ( Outside ) {
			yield return -1f;
		}

		if ( Inside ) {
			yield return 1f;
		}
	}

	// Every modifier on one wall, resolved against the run its join actually emits. The generator,
	// the ghost and the handles all come through here, so a drag can never show a block that the
	// rebuild will not build.
	public static List<ArchWallModShape> For( ArchWall wall, ArchWallJoin join, float wallHeight, float thickness, ArchKit kit ) {
		return ArchLayerGate.Enabled( wall.Modifiers )
			.Select( part => Resolve( part, join.StartExtend, join.EndExtend, wallHeight, thickness, kit ) )
			.Where( shape => shape.Stands )
			.ToList();
	}

	public static ArchWallModShape Resolve( ArchWallModPart part, ArchWall wall, ArchKit kit, float wallHeight ) {
		return Resolve( part, 0f, wall.Length, wallHeight, ArchWallSection.Thickness( wall, kit ), kit );
	}

	public static ArchWallModShape Resolve( ArchWallModPart part, float from, float to, float wallHeight, float thickness, ArchKit kit ) {
		// The STATION is clamped to the run, not the block: one standing on the end straddles the
		// corner, so the piers off two elevations meet and read as the single pier they are.
		var runFrom = MathF.Min( from, to );
		var runTo = MathF.Max( from, to );
		var centre = Math.Clamp( part.Along, runFrom, runTo );
		var reachAlong = MathF.Max( ArchGridService.FinestSize, part.Width ) * 0.5f;
		var left = centre - reachAlong;
		var right = centre + reachAlong;
		var bottom = part.Carves ? MathF.Max( 0f, part.Base ) : part.Base;

		// An authored height is honoured past the wall head, so one pier can climb several storeys as a
		// single layer rather than a stack of them with a slab-thick gap at every floor line. A carved
		// recess is the exception: past the head it would be cutting a field that is not there.
		var climbed = bottom + part.Height;
		var top = part.Height > 0.5f
			? part.Carves ? MathF.Min( climbed, wallHeight ) : climbed
			: wallHeight;

		// Retract, never overextend: a recess landing on the far face is a coplanar pair, and one
		// past it is a hole through a wall nobody asked to open.
		var reach = MathF.Max( 0.05f, part.Kind.Reach( part.Depth ) );
		var depth = part.Carves ? MathF.Min( reach, thickness - ArchLap.Bite( kit ) ) : reach;

		depth = MathF.Max( 0f, depth );

		// OnSurface is answered by the drag that authors the part; nothing downstream can guess a face,
		// so it reads as the street elevation until placement has replaced it.
		var both = part.Side == ArchWallSide.Both;

		return new ArchWallModShape {
			Part = part,
			Carves = part.Carves,
			Outside = both || part.Side != ArchWallSide.Inside,
			Inside = both || part.Side == ArchWallSide.Inside,
			From = left,
			To = right,
			Bottom = bottom,
			Top = top,
			Depth = depth,
			Blocks = Stack( part, left, right, bottom, top, depth ).ToList()
		};
	}

	// Every modifier resolves to the same vocabulary of blocks, so one emitter serves all of them.
	static IEnumerable<ArchWallModBlock> Stack( ArchWallModPart part, float from, float to, float bottom, float top, float depth ) {
		var kind = part.Kind;

		if ( to - from < 0.5f || top - bottom < 0.5f || depth < 0.05f ) {
			yield break;
		}

		if ( kind.Tapers() ) {
			yield return new ArchWallModBlock( from, to, bottom, top, depth, depth * 0.4f );

			yield break;
		}

		// Deep at the foot, dying into the elevation at the head: the same prism a buttress stands, fallen
		// the other way, which is a canopy sloping down over the pavement.
		if ( kind.Falls() ) {
			yield return new ArchWallModBlock( from, to, bottom, top, depth, depth * 0.12f );

			yield break;
		}

		if ( !kind.Coursed() && !kind.Banded() ) {
			yield return new ArchWallModBlock( from, to, bottom, top, depth, depth );

			yield break;
		}

		var width = to - from;
		var pitch = part.Course > 0.5f
			? part.Course
			: kind.Banded() ? ArchWallModKinds.CourseHeight : MathF.Max( ArchGridService.FinestSize, width * 0.75f );
		var courses = ArchDivide.AtMost( top - bottom, pitch );

		// Half a joint off each end of every course, so the recess between two of them is the whole joint and the
		// stack still finishes exactly on the band it was given.
		var joint = (part.Joint > 0.01f ? part.Joint : MathF.Min( 1.5f, pitch * 0.12f )) * 0.5f;
		var centre = (from + to) * 0.5f;

		for ( var index = 0; index < courses.Count; index++ ) {
			// Alternately long and short about the same centre - that is what reads as rustication. A band does
			// not alternate: every course takes the whole width and the joint is the only thing that reads.
			var half = kind.Banded() ? width * 0.5f : width * (index % 2 == 0 ? 0.5f : 0.28f);
			var bay = courses.Bay( index );
			var low = bottom + bay.From + joint;
			var high = bottom + bay.To - joint;

			if ( high - low < 0.05f ) {
				continue;
			}

			yield return new ArchWallModBlock( centre - half, centre + half, low, high, depth, depth );
		}
	}
}