Editor/Pillar/ArchPillarPart.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public enum PillarPlacement {
	Freestanding,
	Grid,
	Pilaster
}

// APPEND-ONLY: a plan on disk names these by ordinal.
public enum PillarSpan {
	None,
	Beam,
	Arch,
	Haunch
}

public enum PillarFooting {
	None,
	Square,
	Bevelled,
	Stepped
}

public sealed class ArchPillarPart : IArchCollides, IArchPainted, IArchNamed {
	public ArchCollisionMode? Collision { get; set; }

	public int Id { get; set; }
	// Zero = hand-authored; nonzero = owned by an extrude zone etc.
	public int OwnerId { get; set; }
	public string Name { get; set; } = "Pillars";
	public string Type { get; set; } = "";
	public PillarPlacement Placement { get; set; } = PillarPlacement.Grid;

	public Vector2 Origin { get; set; }
	public float Yaw { get; set; }
	public float BaseHeight { get; set; }
	public float Height { get; set; }
	// True = hand-authored seat; false = re-reads terrain/deck on each build
	public bool Founded { get; set; }

	public float Width { get; set; } = 20f;
	public float Depth { get; set; } = 20f;
	// ≤4 = rectangle; >4 = n-gon inscribed in Width×Depth
	public int Sides { get; set; }

	public Vector2 Spacing { get; set; } = new( 192f, 192f );
	public int CountX { get; set; } = 3;
	public int CountY { get; set; } = 2;

	public PillarFooting Footing { get; set; } = PillarFooting.None;
	// Buried below the base so a footing on moving ground doesn't float.
	public float FootingSpread { get; set; } = 8f;
	public float FootingHeight { get; set; } = 10f;
	public float FootingBuried { get; set; } = 4f;

	public bool Plinth { get; set; }
	public float PlinthHeight { get; set; } = 8f;
	public float PlinthOversize { get; set; } = 3f;
	public bool Capital { get; set; }
	public float CapitalHeight { get; set; } = 8f;
	public float CapitalOversize { get; set; } = 3f;
	public PillarFooting CapitalShape { get; set; } = PillarFooting.Square;

	public PillarSpan Span { get; set; } = PillarSpan.None;
	public bool SpanAlongX { get; set; } = true;
	public bool SpanAlongY { get; set; }
	public bool PerimeterOnly { get; set; }
	public float SpanDepth { get; set; } = 14f;
	public float SpanHeight { get; set; } = 18f;
	public float ArchRise { get; set; } = 48f;
	public float ArchRing { get; set; } = 10f;
	public int ArchSegments { get; set; } = 10;

	public int WallId { get; set; }
	public float WallOffset { get; set; }
	public float PilasterWidth { get; set; } = 20f;
	public float PilasterProtrusion { get; set; } = 6f;
	public bool PilasterBothFaces { get; set; }

	public ArchPalette Palette { get; set; } = new();

	public int TotalCount => Placement switch {
		PillarPlacement.Grid => System.Math.Max( 1, CountX ) * System.Math.Max( 1, CountY ),
		_ => 1
	};

	// Measured down from the column top. An arch spends its band on the ring as well; a cove's band IS its radius.
	public float SpanBand => Span switch {
		PillarSpan.Arch => System.MathF.Max( 8f, ArchRise + System.MathF.Max( 2f, ArchRing ) ),
		PillarSpan.Haunch => System.MathF.Max( 2f, ArchRise ),
		_ => System.MathF.Max( 1f, SpanHeight )
	};

	public bool Curved => Span is PillarSpan.Arch or PillarSpan.Haunch;

	// Arch follows pier section minus a shy offset to avoid coplanar flicker
	public float SpanThickness( bool alongX ) {
		var authored = System.MathF.Max( 1f, SpanDepth );
		var across = alongX ? System.MathF.Max( 1f, Depth ) : System.MathF.Max( 1f, Width );

		return Curved ? System.MathF.Max( authored, System.MathF.Max( 1f, across - ArchLap.Shy ) ) : authored;
	}

	public Vector2 Half => new( System.MathF.Max( 1f, Width ) * 0.5f, System.MathF.Max( 1f, Depth ) * 0.5f );

	public float FootingReach => Footing == PillarFooting.None ? 0f : System.MathF.Max( 0.5f, FootingSpread );

	float Dressing => System.MathF.Max( Plinth ? PlinthOversize : 0f, Capital ? CapitalOversize : 0f );

	public float Reach => System.MathF.Max( Half.x, Half.y ) + Dressing;

	public float Seat => System.MathF.Max( Half.x, Half.y ) + System.MathF.Max( Dressing, FootingReach );

	// Per-axis reach including all courses — for flush snap against a face
	public Vector2 Seated => Spanned( Half + Vector2.One * System.MathF.Max( Dressing, FootingReach ) );

	// Top course reach — for flush snap under a soffit
	public Vector2 Heading => Spanned( Half + Vector2.One * (Capital ? System.MathF.Max( 0f, CapitalOversize ) : 0f) );

	Vector2 Spanned( Vector2 reach ) {
		if ( Span == PillarSpan.None || Placement != PillarPlacement.Grid ) {
			return reach;
		}

		return new Vector2(
			System.MathF.Max( reach.x, SpanAlongY ? SpanThickness( false ) * 0.5f : 0f ),
			System.MathF.Max( reach.y, SpanAlongX ? SpanThickness( true ) * 0.5f : 0f ) );
	}

	public ArchPillarPart Section() {
		return new ArchPillarPart {
			Type = Type,
			Placement = Placement,
			Width = Width,
			Depth = Depth,
			Sides = Sides,
			Footing = Footing,
			FootingSpread = FootingSpread,
			FootingHeight = FootingHeight,
			FootingBuried = FootingBuried,
			Plinth = Plinth,
			PlinthHeight = PlinthHeight,
			PlinthOversize = PlinthOversize,
			Capital = Capital,
			CapitalHeight = CapitalHeight,
			CapitalOversize = CapitalOversize,
			CapitalShape = CapitalShape,
			Span = Span,
			SpanAlongX = SpanAlongX,
			SpanAlongY = SpanAlongY,
			PerimeterOnly = PerimeterOnly,
			SpanDepth = SpanDepth,
			SpanHeight = SpanHeight,
			ArchRise = ArchRise,
			ArchRing = ArchRing,
			ArchSegments = ArchSegments,
			Spacing = Spacing,
			PilasterWidth = PilasterWidth,
			PilasterProtrusion = PilasterProtrusion,
			PilasterBothFaces = PilasterBothFaces,
			Palette = Palette
		};
	}
}