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

namespace Sunless.Architecture;

// Coordinates are the wall's own: x along, y across, z up.
public readonly struct ArchOpeningGrille {
	public OpeningFurniture Furniture { get; init; }
	public ArchSurface Surface { get; init; }
	public bool Upright { get; init; }
	public float Border { get; init; }
	public float Member { get; init; }
	public float Gap { get; init; }
	public float Face { get; init; }
	public float Depth { get; init; }
	public Vector2 Min { get; init; }
	public Vector2 Max { get; init; }

	public Vector2 InnerMin => new( Min.x + Border, Min.y + Border );

	public Vector2 InnerMax => new( Max.x - Border, Max.y - Border );

	public float Pitch => Member + Gap;

	public bool Stands => Furniture != OpeningFurniture.None
		&& InnerMax.x - InnerMin.x > Member
		&& InnerMax.y - InnerMin.y > Member;

	public int Members => ArchDivide.AtMost( Upright ? InnerMax.x - InnerMin.x : InnerMax.y - InnerMin.y, Pitch ).Count;
}

public static class ArchWallOpenings {
	public static ArchSurface BooleanRevealSurface => ArchSurface.Trim;

	public static List<List<Vector2>> BooleanRevealLoops( IEnumerable<ArchOpening> openings, float wallHeight ) {
		return ArchFootprint.Union( openings
			.Select( opening => {
				var bottom = Math.Clamp( opening.SillHeight, 0f, wallHeight );
				var top = Math.Clamp( opening.Top, 0f, wallHeight );

				return (IReadOnlyList<Vector2>)ArchFootprint.Rect(
					new Vector2( opening.Left, bottom ),
					new Vector2( opening.Right, top ) );
			} )
			.Where( loop => loop[2].x - loop[0].x > 0.01f && loop[2].y - loop[0].y > 0.01f ) ).ToList();
	}

	public static void BooleanReveals( ArchMesh canvas, IEnumerable<ArchOpening> openings, float half, float wallHeight, ArchBrush brush ) {
		foreach ( var loop in BooleanRevealLoops( openings, wallHeight ) ) {
			for ( var index = 0; index < loop.Count; index++ ) {
				var from = loop[index];
				var to = loop[(index + 1) % loop.Count];
				var horizontal = MathF.Abs( from.y - to.y ) < 0.01f;

				if ( horizontal && (MathF.Abs( from.y ) < 0.01f || MathF.Abs( from.y - wallHeight ) < 0.01f) ) {
					continue;
				}

				canvas.Quad(
					new Vector3( from.x, half, from.y ),
					new Vector3( from.x, -half, from.y ),
					new Vector3( to.x, -half, to.y ),
					new Vector3( to.x, half, to.y ),
					brush );
			}
		}
	}

	public static List<(Vector2 Min, Vector2 Max)> BooleanCasingBoards( IEnumerable<ArchOpening> openings, ArchKit kit, float wallHeight ) {
		var boards = new List<(Vector2 Min, Vector2 Max)>();
		var cased = openings.Where( opening => opening.Cased ).ToList();
		var authored = cased.Select( opening => opening.CasingWidth ).FirstOrDefault( value => value > 0.1f );
		var width = authored > 0.1f ? authored : kit.CasingWidth;

		if ( cased.Count == 0 || width <= 0.1f || kit.CasingDepth <= 0.05f ) {
			return boards;
		}

		var loops = BooleanRevealLoops( cased, wallHeight );
		var region = loops.Select( loop => (IReadOnlyList<Vector2>)loop ).ToList();
		var bite = ArchLap.Bite( kit );

		foreach ( var loop in loops ) {
			for ( var index = 0; index < loop.Count; index++ ) {
				if ( Architrave( region, loop[index], loop[(index + 1) % loop.Count], wallHeight, width, bite ) is { } board ) {
					boards.Add( board );
				}
			}
		}

		return boards;
	}

	public static void BooleanCasing( ArchMesh canvas, IReadOnlyList<ArchOpening> openings, ArchKit kit, float half, float wallHeight, ArchBrush brush ) {
		var boards = BooleanCasingBoards( openings, kit, wallHeight );

		foreach ( var face in new[] { half, -half } ) {
			var sign = Math.Sign( face );
			var back = ArchLap.Bury( kit, face, sign );
			var near = MathF.Min( back, face + sign * kit.CasingDepth );
			var far = MathF.Max( back, face + sign * kit.CasingDepth );

			foreach ( var board in boards ) {
				canvas.Box(
					new Vector3( board.Min.x, near, board.Min.y ),
					new Vector3( board.Max.x, far, board.Max.y ),
					brush );
			}
		}
	}

	static (Vector2 Min, Vector2 Max)? Architrave(
		IReadOnlyList<IReadOnlyList<Vector2>> region,
		Vector2 from,
		Vector2 to,
		float wallHeight,
		float width,
		float bite ) {
		var span = to - from;
		var middle = (from + to) * 0.5f;

		if ( span.Length < 0.01f ) {
			return null;
		}

		var outward = new Vector2( span.y, -span.x ).Normal;

		if ( ArchFootprint.Encloses( region, middle + outward * 0.1f ) ) {
			outward = -outward;
		}

		var low = new Vector2( MathF.Min( from.x, to.x ), MathF.Min( from.y, to.y ) );
		var high = new Vector2( MathF.Max( from.x, to.x ), MathF.Max( from.y, to.y ) );

		if ( MathF.Abs( span.x ) < 0.01f ) {
			var jamb = high.x + outward.x * width;
			// Jambs at the floor line extend below it — the well trim stands proud
			var foot = low.y < 0.01f ? -width : low.y - width;

			return (new Vector2( MathF.Min( jamb, high.x - outward.x * bite ), foot ),
				new Vector2( MathF.Max( jamb, high.x - outward.x * bite ), MathF.Min( wallHeight, high.y + width ) ));
		}

		if ( MathF.Abs( high.y ) < 0.01f || MathF.Abs( high.y - wallHeight ) < 0.01f ) {
			return null;
		}

		var rail = high.y + outward.y * width;

		return (new Vector2( low.x - width, MathF.Min( rail, high.y - outward.y * bite ) ),
			new Vector2( high.x + width, MathF.Max( rail, high.y - outward.y * bite ) ));
	}

	public static void Reveals( ArchMesh canvas, ArchOpening opening, float half, float wallHeight, bool sill, ArchBrush brush ) {
		var left = opening.Left;
		var right = opening.Right;
		var bottom = Math.Max( 0f, opening.SillHeight );
		var top = Math.Min( wallHeight, opening.Top );

		if ( top <= bottom ) {
			return;
		}

		canvas.Quad(
			new Vector3( left, -half, bottom ),
			new Vector3( left, half, bottom ),
			new Vector3( left, half, top ),
			new Vector3( left, -half, top ),
			brush );

		canvas.Quad(
			new Vector3( right, half, bottom ),
			new Vector3( right, -half, bottom ),
			new Vector3( right, -half, top ),
			new Vector3( right, half, top ),
			brush );

		canvas.Quad(
			new Vector3( left, half, top ),
			new Vector3( right, half, top ),
			new Vector3( right, -half, top ),
			new Vector3( left, -half, top ),
			brush );

		if ( bottom > 0.01f && !sill ) {
			canvas.Quad(
				new Vector3( left, -half, bottom ),
				new Vector3( right, -half, bottom ),
				new Vector3( right, half, bottom ),
				new Vector3( left, half, bottom ),
				brush );
		}
	}

	public static void Casing( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchBrush brush ) {
		var width = opening.CasingWidth > 0f ? opening.CasingWidth : kit.CasingWidth;
		var depth = kit.CasingDepth;

		if ( width <= 0.1f || depth <= 0.05f ) {
			return;
		}

		var left = opening.Left;
		var right = opening.Right;
		var bottom = Math.Max( 0f, opening.SillHeight );
		var top = Math.Min( wallHeight, opening.Top );
		var foot = Math.Max( 0f, bottom - width );

		var headWidth = CasingHead( opening, kit );
		var headDepth = opening.IsGlazed ? depth * 1.7f : depth;
		var headRun = opening.IsGlazed ? width * 1.6f : width;
		var bite = ArchLap.Bite( kit );

		foreach ( var face in new[] { half, -half } ) {
			var sign = Math.Sign( face );
			var back = ArchLap.Bury( kit, face, sign );
			var near = Math.Min( back, face + sign * depth );
			var far = Math.Max( back, face + sign * depth );
			var headNear = Math.Min( back, face + sign * headDepth );
			var headFar = Math.Max( back, face + sign * headDepth );

			canvas.Box( new Vector3( left - width, near, foot ), new Vector3( left + bite, far, top ), brush );
			canvas.Box( new Vector3( right - bite, near, foot ), new Vector3( right + width, far, top ), brush );
			canvas.Box( new Vector3( left - headRun, headNear, top - bite ), new Vector3( right + headRun, headFar, top + headWidth ), brush );

			if ( bottom > 0.01f ) {
				canvas.Box( new Vector3( left, near, foot ), new Vector3( right, far, bottom + bite ), brush );
			}
		}
	}

	public static float CasingHead( ArchOpening opening, ArchKit kit ) {
		var width = opening.CasingWidth > 0f ? opening.CasingWidth : kit.CasingWidth;

		if ( !opening.Cased || width <= 0.1f || kit.CasingDepth <= 0.05f ) {
			return 0f;
		}

		return opening.IsGlazed ? width * 1.7f : width;
	}

	public static float Springing( ArchOpening opening, ArchKit kit, float wallHeight ) {
		return MathF.Min( wallHeight, opening.Top ) + CasingHead( opening, kit );
	}

	public static void Hood( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchBrush brush ) {
		var hood = opening.Hooded;

		if ( hood == OpeningHood.None ) {
			return;
		}

		var seat = Springing( opening, kit, wallHeight );
		var rise = MathF.Max( 1f, kit.HoodHeight );
		var overhang = MathF.Max( 0f, kit.HoodOverhang );
		var projection = MathF.Max( 1f, kit.HoodProjection );
		var bite = ArchLap.Bite( kit );
		var back = ArchLap.Bury( kit, -half, -1 );

		if ( seat + rise > wallHeight ) {
			return;
		}

		Corbels( canvas, opening, kit, half, seat, projection, brush );

		canvas.Box(
			new Vector3( opening.Left - overhang * 0.55f, -half - projection * 0.55f, seat - bite ),
			new Vector3( opening.Right + overhang * 0.55f, back, seat + rise * 0.45f ),
			brush );

		canvas.Box(
			new Vector3( opening.Left - overhang, -half - projection, seat + rise * 0.45f ),
			new Vector3( opening.Right + overhang, back, seat + rise ),
			brush );

		if ( hood == OpeningHood.Pediment ) {
			Pediment( canvas, opening, kit, half, wallHeight, seat + rise, back, brush );
		}
	}

	static void Corbels( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float seat, float projection, ArchBrush brush ) {
		var width = MathF.Max( 0f, kit.HoodCorbel );
		var drop = MathF.Max( 0f, kit.HoodCorbelDrop );
		var back = ArchLap.Bury( kit, -half, -1 );
		var foot = seat - drop;

		if ( width < 0.1f || drop < 0.1f || foot <= MathF.Max( 0f, opening.SillHeight ) ) {
			return;
		}

		foreach ( var station in new[] { opening.Left, opening.Right - width } ) {
			canvas.Box(
				new Vector3( station, -half - projection * 0.55f, foot ),
				new Vector3( station + width, back, seat ),
				brush );
		}
	}

	static void Pediment( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, float seat, float back, ArchBrush brush ) {
		var rise = MathF.Max( 1f, kit.PedimentRise );
		var overhang = MathF.Max( 0f, kit.HoodOverhang );
		var left = opening.Left - overhang;
		var right = opening.Right + overhang;
		var apex = (left + right) * 0.5f;
		var near = -half - MathF.Max( 1f, kit.HoodProjection ) * 0.7f;

		if ( seat + rise > wallHeight || right - left < 2f ) {
			return;
		}

		canvas.Rake( new Vector2( left, 0f ), new Vector2( apex, 0f ), near, back, seat, seat, seat, seat + rise, brush );
		canvas.Rake( new Vector2( apex, 0f ), new Vector2( right, 0f ), near, back, seat, seat, seat + rise, seat, brush );
	}

	public static void Sill( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, ArchBrush brush ) {
		var depth = kit.SillDepth;
		var thickness = kit.SillThickness;
		var overshoot = kit.CasingWidth;
		var outward = opening.IsGlazed ? depth * 2.2f : depth;

		canvas.Box(
			new Vector3( opening.Left - overshoot, -half - outward, opening.SillHeight - thickness ),
			new Vector3( opening.Right + overshoot, half + depth, opening.SillHeight ),
			brush );

		if ( !opening.IsGlazed ) {
			return;
		}

		canvas.Box(
			new Vector3( opening.Left - overshoot, -half - outward, opening.SillHeight - thickness - 1f ),
			new Vector3( opening.Right + overshoot, -half - outward + 1.3f, opening.SillHeight - thickness ),
			brush );
	}

	public static ArchOpeningGrille Grille( ArchOpening opening, ArchKit kit, float half, float wallHeight ) {
		var fitted = opening.Fitted == OpeningFurniture.Leaves ? OpeningFurniture.None : opening.Fitted;
		var boarded = fitted == OpeningFurniture.Boarded;
		var bite = ArchLap.Bite( kit );
		var proud = MathF.Max( bite, kit.GrilleProud );
		var bottom = MathF.Max( 0f, opening.SillHeight );
		var top = MathF.Min( wallHeight, opening.Top );
		var span = ArchLap.Lapped( kit, opening.Left, opening.Right );
		var rise = ArchLap.Lapped( kit, bottom, top );

		return new ArchOpeningGrille {
			Furniture = fitted,
			Surface = fitted == OpeningFurniture.Shutter ? ArchSurface.Shutter : ArchSurface.Grille,
			Upright = fitted is OpeningFurniture.Bars or OpeningFurniture.Gate,
			Border = boarded ? 0f : MathF.Max( 0f, kit.GrilleFrame ),
			Member = MathF.Max( 1f, boarded ? kit.FloorBoardWidth : kit.GrilleBar ),
			Gap = MathF.Max( 0f, boarded ? kit.FloorBoardGap : kit.GrilleGap ),
			Face = fitted == OpeningFurniture.Gate ? -half + bite : -half - proud,
			Depth = proud + bite,
			Min = boarded ? new Vector2( span.From, rise.From ) : new Vector2( opening.Left + bite, bottom + bite ),
			Max = boarded ? new Vector2( span.To, rise.To ) : new Vector2( opening.Right - bite, top - bite )
		};
	}

	public static IEnumerable<ArchOpeningGrille> Leaves( ArchOpening opening, ArchKit kit, float half, float wallHeight ) {
		if ( opening.Fitted != OpeningFurniture.Leaves ) {
			yield break;
		}

		var bite = ArchLap.Bite( kit );
		var proud = MathF.Max( bite, kit.LeafProud );
		var bottom = MathF.Max( 0f, opening.SillHeight );
		var top = MathF.Min( wallHeight, opening.Top );
		var leaf = opening.Width * 0.5f;

		foreach ( var outer in new[] { opening.Left - leaf, opening.Right } ) {
			yield return new ArchOpeningGrille {
				Furniture = OpeningFurniture.Leaves,
				Surface = ArchSurface.Shutter,
				Upright = false,
				Border = MathF.Max( 0f, kit.GrilleFrame ),
				Member = MathF.Max( 0.4f, kit.LeafSlat ),
				Gap = MathF.Max( 0f, kit.LeafGap ),
				Face = -half - proud,
				Depth = MathF.Max( 0.4f, kit.LeafThickness ),
				Min = new Vector2( outer, bottom ),
				Max = new Vector2( outer + leaf, top )
			};
		}
	}

	public static void Furniture( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float wallHeight, ArchStyle style, ArchPalette[] chain ) {
		var standing = Leaves( opening, kit, half, wallHeight )
			.Append( Grille( opening, kit, half, wallHeight ) )
			.Where( fitting => fitting.Stands )
			.ToList();

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

		using ( canvas.Part( ArchPieces.Bars ) ) {
			foreach ( var fitting in standing ) {
				var brush = style.Brush( fitting.Surface, chain );

				GrilleFrame( canvas, fitting, kit, brush );
				GrilleMembers( canvas, fitting, brush );
			}
		}
	}

	static void GrilleFrame( ArchMesh canvas, ArchOpeningGrille grille, ArchKit kit, ArchBrush brush ) {
		if ( grille.Border < 0.05f ) {
			return;
		}

		var bite = ArchLap.Bite( kit );
		var inner = grille.InnerMin;
		var outer = grille.InnerMax;
		var near = grille.Face;
		var far = grille.Face + grille.Depth;

		canvas.Box( new Vector3( grille.Min.x, near, grille.Min.y ), new Vector3( inner.x, far, grille.Max.y ), brush );
		canvas.Box( new Vector3( outer.x, near, grille.Min.y ), new Vector3( grille.Max.x, far, grille.Max.y ), brush );
		canvas.Box( new Vector3( inner.x - bite, near, grille.Min.y ), new Vector3( outer.x + bite, far, inner.y ), brush );
		canvas.Box( new Vector3( inner.x - bite, near, outer.y ), new Vector3( outer.x + bite, far, grille.Max.y ), brush );
	}

	static void GrilleMembers( ArchMesh canvas, ArchOpeningGrille grille, ArchBrush brush ) {
		var inner = grille.InnerMin;
		var outer = grille.InnerMax;
		var region = new[] { (IReadOnlyList<Vector2>)ArchFootprint.Rect( inner, outer ) };

		var spec = new ArchPlankSpec {
			Width = grille.Member,
			Gap = grille.Gap,
			Thickness = grille.Depth,
			// Four times the diagonal, so no stagger joint can land inside the hole: a bar is one length of steel
			// and a shutter slat runs the whole width of the curtain.
			Length = (outer - inner).Length * 4f,
			Top = grille.Face + grille.Depth,
			Yaw = grille.Upright ? 90f : 0f,
			Soffit = true
		};

		ArchPlanks.Fill( canvas, region, null, spec, brush, ArchPlankPlane.Upright );
	}

	public static float Threshold( ArchMesh canvas, ArchOpening opening, ArchKit kit, float half, float floor, ArchBrush brush ) {
		var proud = MathF.Max( 0f, kit.ThresholdProud );

		if ( !Saddled( opening ) || proud < 0.05f ) {
			return 0f;
		}

		var lap = MathF.Max( 0f, kit.ThresholdLap );
		var top = floor + proud;
		var span = ArchLap.Lapped( kit, opening.Left, opening.Right );

		canvas.Box(
			new Vector3( span.From, -half - lap, ArchLap.Bury( kit, 0f, 1f ) ),
			new Vector3( span.To, half + lap, top ),
			brush );

		return top;
	}

	static bool Saddled( ArchOpening opening ) {
		return opening.Cased && opening.SillHeight < 0.5f && opening.Kind != OpeningKind.Garage;
	}

	public static void Leaves( List<ArchDoorRequest> doors, ArchOpening opening, ArchWall wall, ArchKit kit, float saddle, ArchBrush brush ) {
		if ( doors is null ) {
			return;
		}

		var clearance = saddle + MathF.Max( 0f, kit.DoorUndercut );
		var foot = opening.SillHeight + clearance;
		var height = opening.Height - clearance;
		var thickness = kit.DoorLeafThickness;

		if ( height < 1f ) {
			return;
		}

		if ( opening.Kind == OpeningKind.DoubleDoor ) {
			var leaf = opening.Width * 0.5f;

			doors.Add( Request( opening, wall, new Vector3( opening.Left, 0f, foot ), leaf, height, thickness, false, brush ) );
			doors.Add( Request( opening, wall, new Vector3( opening.Right, 0f, foot ), leaf, height, thickness, true, brush ) );
			return;
		}

		var hingeX = opening.FlipHinge ? opening.Right : opening.Left;
		doors.Add( Request( opening, wall, new Vector3( hingeX, 0f, foot ), opening.Width, height, thickness, opening.FlipHinge, brush ) );
	}

	static ArchDoorRequest Request( ArchOpening opening, ArchWall wall, Vector3 hinge, float width, float height, float thickness, bool mirrored, ArchBrush brush ) {
		return new ArchDoorRequest {
			Opening = opening,
			Wall = wall,
			HingeLocal = hinge,
			LeafWidth = width,
			LeafHeight = height,
			LeafThickness = thickness,
			Mirrored = mirrored,
			Brush = brush,
			Art = opening.LeafArt,
			MaterialGroup = opening.LeafMaterialGroup
		};
	}
}