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

namespace Sunless.Architecture;

public static partial class ArchWallGen {
	// Lap the field, don't replace it: panel carries material, profile the shadow.
	static void Clad( ArchMesh canvas, Panel panel, float half, ArchKit kit, WallCladding cladding, ArchBrush brush ) {
		switch ( cladding ) {
			case WallCladding.Weatherboard:
				Courses( canvas, panel, half, kit, brush );
				break;

			case WallCladding.Profiled:
				Ribs( canvas, panel, half, kit, brush );
				break;
		}
	}

	// Ribs index off x = 0, or the sheeting steps at every opening.
	static void Ribs( ArchMesh canvas, Panel panel, float half, ArchKit kit, ArchBrush brush ) {
		var pitch = MathF.Max( 2f, kit.SheetPitch );
		var crest = Math.Clamp( kit.SheetCrest, 0.5f, pitch * 0.6f );
		var proud = MathF.Max( 0.1f, kit.SheetProud );
		var web = (pitch - crest) * 0.3f;
		var valley = pitch - crest - web * 2f;

		if ( panel.MaxZ - panel.MinZ < 0.05f || panel.MaxX - panel.MinX < 0.05f ) {
			return;
		}

		var first = MathF.Floor( panel.MinX / pitch );
		var last = MathF.Ceiling( panel.MaxX / pitch );

		for ( var index = first; index < last; index++ ) {
			var at = index * pitch;
			var flat = -half;
			var high = -half - proud;

			Rib( canvas, panel, at, flat, at + valley, flat, brush );
			Rib( canvas, panel, at + valley, flat, at + valley + web, high, brush );
			Rib( canvas, panel, at + valley + web, high, at + valley + web + crest, high, brush );
			Rib( canvas, panel, at + valley + web + crest, high, at + pitch, flat, brush );
		}
	}

	static void Rib( ArchMesh canvas, Panel panel, float fromX, float fromY, float toX, float toY, ArchBrush brush ) {
		var minX = MathF.Max( panel.MinX, fromX );
		var maxX = MathF.Min( panel.MaxX, toX );

		if ( maxX - minX < 0.01f ) {
			return;
		}

		var span = toX - fromX;

		float Depth( float x ) => span < 0.001f ? fromY : fromY + (x - fromX) / span * (toY - fromY);

		var near = Depth( minX );
		var far = Depth( maxX );

		canvas.Quad(
			new Vector3( minX, near, panel.MinZ ),
			new Vector3( maxX, far, panel.MinZ ),
			new Vector3( maxX, far, panel.MaxZ ),
			new Vector3( minX, near, panel.MaxZ ),
			brush );
	}

	// Courses index off z = 0, or the coursing would step at every opening.
	static void Courses( ArchMesh canvas, Panel panel, float half, ArchKit kit, ArchBrush brush ) {
		var course = MathF.Max( 2f, kit.SidingCourse );
		var proud = MathF.Max( 0.1f, kit.SidingProud );

		if ( panel.MaxX - panel.MinX < 0.05f ) {
			return;
		}

		var first = MathF.Floor( panel.MinZ / course );
		var last = MathF.Ceiling( panel.MaxZ / course );

		for ( var index = first; index < last; index++ ) {
			var boardBottom = index * course;
			var bottom = MathF.Max( panel.MinZ, boardBottom );
			var top = MathF.Min( panel.MaxZ, boardBottom + course );

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

			// The face is a ramp; a clipped course samples it, not the full lap.
			var nearProud = proud * (1f - (bottom - boardBottom) / course);
			var farProud = proud * (1f - (top - boardBottom) / course);

			canvas.Quad(
				new Vector3( panel.MinX, -half - nearProud, bottom ),
				new Vector3( panel.MaxX, -half - nearProud, bottom ),
				new Vector3( panel.MaxX, -half - farProud, top ),
				new Vector3( panel.MinX, -half - farProud, top ),
				brush );

			canvas.Quad(
				new Vector3( panel.MinX, -half, bottom ),
				new Vector3( panel.MaxX, -half, bottom ),
				new Vector3( panel.MaxX, -half - nearProud, bottom ),
				new Vector3( panel.MinX, -half - nearProud, bottom ),
				brush );

			EndCap( canvas, panel.MinX, half, bottom, top, nearProud, farProud, false, brush );
			EndCap( canvas, panel.MaxX, half, bottom, top, nearProud, farProud, true, brush );
		}
	}

	// The flush top edge collapses the section; a degenerate quad breaks the normal.
	static void EndCap( ArchMesh canvas, float x, float half, float bottom, float top, float nearProud, float farProud, bool flip, ArchBrush brush ) {
		var points = new List<Vector3>
		{
			new( x, -half, bottom ),
			new( x, -half - nearProud, bottom ),
			new( x, -half - farProud, top ),
			new( x, -half, top )
		};

		for ( var index = points.Count - 1; index > 0; index-- ) {
			if ( (points[index] - points[index - 1]).Length < 0.01f ) {
				points.RemoveAt( index );
			}
		}

		if ( (points[^1] - points[0]).Length < 0.01f ) {
			points.RemoveAt( points.Count - 1 );
		}

		canvas.Polygon( points, brush, flip );
	}

	static void Wainscot( ArchMesh canvas, float from, float to, float half, ArchKit kit, IReadOnlyList<ArchOpening> openings, ArchBrush panelling, ArchBrush rail, float finish ) {
		var top = kit.WainscotHeight + finish;
		var skirt = MathF.Max( 0f, kit.BaseboardHeight ) + finish;
		var cap = MathF.Max( 0.5f, kit.ChairRailHeight );
		var proud = MathF.Max( 0.2f, kit.WainscotProud );
		var member = MathF.Max( 1f, kit.WainscotStile );
		var field = top - cap;

		if ( field - skirt < member * 2f + 2f ) {
			return;
		}

		var blocked = openings
			.Where( opening => opening.SillHeight < field )
			.Select( opening => Cased( opening, kit ) )
			.OrderBy( gap => gap.Left )
			.ToList();

		var cursor = from;

		foreach ( var gap in blocked ) {
			if ( gap.Left - cursor > 6f ) {
				Dado( canvas, cursor, gap.Left, half, skirt, field, top, proud, member, kit.WainscotBay, kit.ChairRailDepth, panelling, rail );
			}

			cursor = MathF.Max( cursor, gap.Right );
		}

		if ( to - cursor > 6f ) {
			Dado( canvas, cursor, to, half, skirt, field, top, proud, member, kit.WainscotBay, kit.ChairRailDepth, panelling, rail );
		}
	}

	static void Dado(
		ArchMesh canvas,
		float from,
		float to,
		float half,
		float skirt,
		float field,
		float top,
		float proud,
		float member,
		float bay,
		float capDepth,
		ArchBrush panelling,
		ArchBrush rail ) {
		var recess = proud * 0.4f;

		canvas.Box( new Vector3( from, half, skirt ), new Vector3( to, half + recess, field ), panelling, BoxFaces.All & ~BoxFaces.Front );
		canvas.Box( new Vector3( from, half, skirt ), new Vector3( to, half + proud, skirt + member ), panelling );
		canvas.Box( new Vector3( from, half, field - member ), new Vector3( to, half + proud, field ), panelling );

		foreach ( var offset in ArchDivide.AtMost( to - from, MathF.Max( 8f, bay ) ).Nodes ) {
			var left = Math.Clamp( from + offset - member * 0.5f, from, to - member );

			canvas.Box( new Vector3( left, half, skirt ), new Vector3( left + member, half + proud, field ), panelling );
		}

		canvas.Box( new Vector3( from, half, field ), new Vector3( to, half + capDepth, top ), rail );
	}
}