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

namespace Sunless.Architecture;

public readonly record struct ArchPillarRun( int FromX, int FromY, int ToX, int ToY, bool AlongX );

public readonly record struct ArchPillarColumn( Vector3 At, int X, int Y );

public static class ArchPillarGen {
	const float Adrift = 1f;

	// Resolves the column's base height onto the nearest slab unless Founded
	public static float Seat( ArchPillarPart part, ArchKit kit, ArchPlan plan ) {
		if ( part.Founded || plan is null ) {
			return part.BaseHeight;
		}

		// Only seat on slabs below the column's own bury line
		var poured = ArchPillarSeat.Poured( plan, kit, part.Origin, part.BaseHeight + ArchPillarSeat.Sunk( kit, part ) );

		return poured is { } standing && MathF.Abs( standing - part.BaseHeight ) > Adrift ? standing : part.BaseHeight;
	}

	// Clamped to the soffit — level, not lapped, so the contact pass doesn't bury a face
	public static float Height( ArchPillarPart part, ArchRoom room, ArchKit kit, ArchPlan plan = null ) {
		var floor = Seat( part, kit, plan );
		var slab = ArchPillarSoffit.Slab( plan, kit, part, floor );

		if ( part.Height > 0f ) {
			return slab < float.MaxValue ? MathF.Max( 8f, MathF.Min( part.Height, slab - floor ) ) : part.Height;
		}

		if ( room is null ) {
			return slab < float.MaxValue ? MathF.Max( 8f, slab - floor ) : kit.WallHeight;
		}

		return MathF.Max( 8f, ArchPillarSoffit.Over( plan, kit, room, part, floor ) - floor );
	}

	public static List<ArchPillarRun> Runs( ArchPillarPart part ) {
		var runs = new List<ArchPillarRun>();

		if ( part.Span == PillarSpan.None || part.Placement != PillarPlacement.Grid ) {
			return runs;
		}

		var countX = Math.Max( 1, part.CountX );
		var countY = Math.Max( 1, part.CountY );
		var edge = part.PerimeterOnly;

		if ( part.SpanAlongX ) {
			for ( var iy = 0; iy < countY; iy++ ) {
				if ( edge && iy > 0 && iy < countY - 1 ) {
					continue;
				}

				for ( var ix = 0; ix < countX - 1; ix++ ) {
					runs.Add( new ArchPillarRun( ix, iy, ix + 1, iy, true ) );
				}
			}
		}

		if ( !part.SpanAlongY ) {
			return runs;
		}

		for ( var ix = 0; ix < countX; ix++ ) {
			if ( edge && ix > 0 && ix < countX - 1 ) {
				continue;
			}

			for ( var iy = 0; iy < countY - 1; iy++ ) {
				runs.Add( new ArchPillarRun( ix, iy, ix, iy + 1, false ) );
			}
		}

		return runs;
	}

	public static HashSet<(int X, int Y)> Carried( ArchPillarPart part ) {
		var carried = new HashSet<(int X, int Y)>();

		foreach ( var run in Runs( part ) ) {
			carried.Add( (run.FromX, run.FromY) );
			carried.Add( (run.ToX, run.ToY) );
		}

		return carried;
	}

	public static void Build(
		ArchMesh canvas,
		ArchPillarPart part,
		ArchRoom room,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		ArchStyle style ) {
		var chain = new[] { part.Palette, room.Palette, building.Palette };
		var shaft = style.Brush( ArchSurface.Pillar, chain );
		var cap = style.Brush( ArchSurface.PillarCap, chain );

		var seat = Seat( part, kit, plan );
		var height = Height( part, room, kit, plan );

		if ( part.Placement == PillarPlacement.Pilaster ) {
			Pilaster( canvas, part, room, height, shaft, cap, kit );
			return;
		}

		var spanning = part.Span != PillarSpan.None && part.Placement == PillarPlacement.Grid;
		var shaftHeight = spanning ? MathF.Max( 8f, height - part.SpanBand ) : height;
		var carried = spanning ? Carried( part ) : new HashSet<(int X, int Y)>();

		foreach ( var column in Columns( part, seat ) ) {
			var columnHeight = carried.Contains( (column.X, column.Y) ) ? shaftHeight : height;

			Column( canvas, part, column.At, columnHeight, shaft, cap,
				Bites( plan, kit, part, room, building, column.At, columnHeight ) );

			ArchDamage.PillarCorners( canvas, plan, kit, building, room, part, column.At, columnHeight, style );
		}

		if ( spanning ) {
			using ( canvas.Part( ArchPieces.Span ) ) {
				Spans( canvas, part, kit, seat, height, cap );
			}
		}
	}

	public static void Stand( ArchMesh canvas, ArchPillarPart part, ArchBrush shaft, ArchBrush cap ) {
		foreach ( var column in Columns( part ) ) {
			Column( canvas, part, column.At, MathF.Max( 1f, part.Height ), shaft, cap );
		}
	}

	public static List<Vector3> Positions( ArchPillarPart part ) => Columns( part ).Select( column => column.At ).ToList();

	public static List<ArchPillarColumn> Columns( ArchPillarPart part ) => Columns( part, part.BaseHeight );

	public static List<ArchPillarColumn> Columns( ArchPillarPart part, float seat ) {
		var columns = new List<ArchPillarColumn>();
		var rotation = Rotation.FromYaw( part.Yaw );

		if ( part.Placement != PillarPlacement.Grid ) {
			columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x, part.Origin.y, seat ), 0, 0 ) );
			return columns;
		}

		var countX = Math.Max( 1, part.CountX );
		var countY = Math.Max( 1, part.CountY );

		for ( var ix = 0; ix < countX; ix++ ) {
			for ( var iy = 0; iy < countY; iy++ ) {
				var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
				var world = rotation * local;

				columns.Add( new ArchPillarColumn( new Vector3( part.Origin.x + world.x, part.Origin.y + world.y, seat ), ix, iy ) );
			}
		}

		return columns;
	}

	readonly record struct Course( float Bottom, float Top, Vector2 Lower, Vector2 Upper, bool Dressed );

	public static List<ArchCarveVolume> Bites( ArchPlan plan, ArchKit kit, ArchPillarPart part, ArchRoom room, ArchBuilding building, Vector3 basePoint, float height ) {
		var bites = new List<ArchCarveVolume>();

		if ( plan is null || room is null ) {
			return bites;
		}

		var widest = part.Reach;
		var flat = new Vector2( basePoint.x, basePoint.y );
		var footprint = ArchFootprint.Rect( flat - new Vector2( widest, widest ), flat + new Vector2( widest, widest ) );
		var foot = basePoint.z - MathF.Max( 0f, part.FootingBuried );
		var head = basePoint.z + height;

		foreach ( var cut in ArchCut.Over( plan, room.Floor, footprint, building?.Id ?? 0, ArchCutAffects.Pillars, part.Id ) ) {
			bites.AddRange( ArchCut.Resolve( cut, kit ).Where( volume => ArchCut.Reaches( volume, foot, head ) ) );
		}

		return bites;
	}

	static void Column( ArchMesh canvas, ArchPillarPart part, Vector3 basePoint, float height, ArchBrush shaft, ArchBrush cap, List<ArchCarveVolume> bites = null ) {
		var courses = Courses( part, basePoint.z, height );

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

		var flat = new Vector2( basePoint.x, basePoint.y );

		if ( bites is { Count: > 0 } ) {
			Carved( canvas, part, flat, courses, bites, shaft, cap );

			return;
		}

		Hulled( canvas, part, flat, courses, shaft, cap );
	}

	// One welded hull per column — keeps colonnades walkable
	static void Hulled( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, ArchBrush shaft, ArchBrush cap ) {
		var sides = part.Sides;

		using var welding = canvas.Welding();

		using var solid = canvas.Solid( ArchSolid.Hull( Rings( courses, flat, sides, part.Yaw ) ) );

		canvas.Polygon( Ring( flat, courses[0].Lower, courses[0].Bottom, sides, part.Yaw ), courses[0].Dressed ? cap : shaft, true );

		for ( var index = 0; index < courses.Count; index++ ) {
			var course = courses[index];
			var brush = course.Dressed ? cap : shaft;

			Faces( canvas, flat, course, sides, part.Yaw, brush );

			if ( index == courses.Count - 1 ) {
				continue;
			}

			Step( canvas, flat, course, courses[index + 1], sides, part.Yaw, brush );
		}

		var last = courses[^1];

		canvas.Polygon( Ring( flat, last.Upper, last.Top, sides, part.Yaw ), last.Dressed ? cap : shaft );
	}

	// Prism carve per course — no taper (only footings taper, and those are buried)
	static void Carved( ArchMesh canvas, ArchPillarPart part, Vector2 flat, List<Course> courses, List<ArchCarveVolume> bites, ArchBrush shaft, ArchBrush cap ) {
		using var welding = canvas.Welding();

		foreach ( var course in courses ) {
			var ring = Ring( flat, course.Lower, 0f, part.Sides, part.Yaw )
				.Select( point => new Vector2( point.x, point.y ) )
				.ToList();

			var carve = ArchCarve.Prism( ring, course.Bottom, course.Top ).In( part.Yaw );

			foreach ( var bite in bites ) {
				carve.Less( bite );
			}

			foreach ( var face in carve.Resolve().Faces ) {
				canvas.Polygon( face.Points, course.Dressed ? cap : shaft );
			}
		}
	}

	static List<Vector3> Rings( List<Course> courses, Vector2 flat, int sides, float yaw ) {
		var points = new List<Vector3>();

		foreach ( var course in courses ) {
			points.AddRange( Ring( flat, course.Lower, course.Bottom, sides, yaw ) );
			points.AddRange( Ring( flat, course.Upper, course.Top, sides, yaw ) );
		}

		return points;
	}

	static List<Course> Courses( ArchPillarPart part, float baseHeight, float height ) {
		var courses = new List<Course>();
		var section = part.Half;

		var plinth = part.Plinth ? MathF.Max( 0f, part.PlinthHeight ) : 0f;
		var capital = part.Capital ? MathF.Max( 0f, part.CapitalHeight ) : 0f;

		var top = baseHeight + height;
		var shaftBottom = baseHeight + plinth;
		var shaftTop = top - capital;

		Footing( part, courses, section, baseHeight );

		if ( plinth > 0.05f ) {
			var spread = section + new Vector2( part.PlinthOversize, part.PlinthOversize );

			courses.Add( new Course( baseHeight, shaftBottom, spread, spread, true ) );
		}

		if ( shaftTop - shaftBottom > 0.05f ) {
			courses.Add( new Course( shaftBottom, shaftTop, section, section, false ) );
		}

		if ( capital > 0.05f ) {
			Head( part, courses, section, shaftTop, top );
		}

		return courses;
	}

	static void Footing( ArchPillarPart part, List<Course> courses, Vector2 section, float baseHeight ) {
		if ( part.Footing == PillarFooting.None ) {
			return;
		}

		var foot = baseHeight - MathF.Max( 0f, part.FootingBuried );

		courses.AddRange( Pad( part.Footing, section, part.FootingReach,
			foot, foot + MathF.Max( 1f, part.FootingHeight ) ) );
	}

	// Mirrors the footing pad — level with soffit, not lapped
	static void Head( ArchPillarPart part, List<Course> courses, Vector2 section, float from, float top ) {
		var mirrored = new List<Course>();

		foreach ( var course in Pad( part.CapitalShape, section, MathF.Max( 0f, part.CapitalOversize ), from, top ) ) {
			mirrored.Add( new Course( from + top - course.Top, from + top - course.Bottom,
				course.Upper, course.Lower, course.Dressed ) );
		}

		mirrored.Reverse();
		courses.AddRange( mirrored );
	}

	static IEnumerable<Course> Pad( PillarFooting kind, Vector2 section, float spread, float foot, float head ) {
		var wide = section + new Vector2( spread, spread );
		var depth = MathF.Max( 1f, head - foot );

		switch ( kind ) {
			case PillarFooting.Bevelled:
				var shelf = foot + depth * 0.34f;

				yield return new Course( foot, shelf, wide, wide, true );
				yield return new Course( shelf, foot + depth, wide, section, true );
				break;

			case PillarFooting.Stepped:
				var middle = foot + depth * 0.5f;
				var upper = section + new Vector2( spread * 0.5f, spread * 0.5f );

				yield return new Course( foot, middle, wide, wide, true );
				yield return new Course( middle, foot + depth, upper, upper, true );
				break;

			default:
				yield return new Course( foot, foot + depth, wide, wide, true );
				break;
		}
	}

	static void Faces( ArchMesh canvas, Vector2 centre, Course course, int sides, float yaw, ArchBrush brush ) {
		var lower = Ring( centre, course.Lower, course.Bottom, sides, yaw );
		var upper = Ring( centre, course.Upper, course.Top, sides, yaw );

		for ( var index = 0; index < lower.Length; index++ ) {
			var next = (index + 1) % lower.Length;

			canvas.Quad( lower[index], lower[next], upper[next], upper[index], brush );
		}
	}

	static void Step( ArchMesh canvas, Vector2 centre, Course below, Course above, int sides, float yaw, ArchBrush brush ) {
		if ( MathF.Abs( below.Upper.x - above.Lower.x ) < 0.01f && MathF.Abs( below.Upper.y - above.Lower.y ) < 0.01f ) {
			return;
		}

		var stepsIn = below.Upper.x >= above.Lower.x;

		var wide = Ring( centre, stepsIn ? below.Upper : above.Lower, below.Top, sides, yaw );
		var tight = Ring( centre, stepsIn ? above.Lower : below.Upper, below.Top, sides, yaw );

		for ( var index = 0; index < wide.Length; index++ ) {
			var next = (index + 1) % wide.Length;

			if ( stepsIn ) {
				canvas.Quad( wide[index], wide[next], tight[next], tight[index], brush );
				continue;
			}

			canvas.Quad( tight[index], tight[next], wide[next], wide[index], brush );
		}
	}

	static Vector3[] Ring( Vector2 centre, Vector2 half, float height, int sides, float yaw ) {
		var corners = sides < 5
			? new[]
			{
				new Vector2( centre.x - half.x, centre.y - half.y ),
				new Vector2( centre.x + half.x, centre.y - half.y ),
				new Vector2( centre.x + half.x, centre.y + half.y ),
				new Vector2( centre.x - half.x, centre.y + half.y )
			}
			: ArchFootprint.Ellipse( centre - half, centre + half, sides ).ToArray();

		var turned = MathF.Abs( yaw % 360f ) < 0.01f ? corners : ArchFootprint.Turned( corners, centre, yaw ).ToArray();

		return turned.Select( point => new Vector3( point.x, point.y, height ) ).ToArray();
	}

	static void Spans( ArchMesh canvas, ArchPillarPart part, ArchKit kit, float seat, float height, ArchBrush brush ) {
		var rotation = Rotation.FromYaw( part.Yaw );
		var top = seat + height;
		var springing = MathF.Max( seat + 8f, top - part.SpanBand );
		var half = part.Half;
		var lap = ArchLap.Bite( kit );

		foreach ( var run in Runs( part ) ) {
			var inset = (run.AlongX ? half.x : half.y) - lap;

			Connect( canvas, part,
				Point( part, rotation, run.FromX, run.FromY ), Point( part, rotation, run.ToX, run.ToY ),
				run.AlongX, springing, top, inset, brush );
		}

		foreach ( var (ix, iy) in Carried( part ) ) {
			var centre = Point( part, rotation, ix, iy );

			canvas.Box(
				new Vector3( centre.x - half.x, centre.y - half.y, ArchLap.Bury( kit, springing, 1f ) ),
				new Vector3( centre.x + half.x, centre.y + half.y, top ),
				brush );
		}
	}

	static Vector2 Point( ArchPillarPart part, Rotation rotation, int ix, int iy ) {
		var local = new Vector3( ix * part.Spacing.x, iy * part.Spacing.y, 0f );
		var world = rotation * local;

		return new Vector2( part.Origin.x + world.x, part.Origin.y + world.y );
	}

	static void Connect( ArchMesh canvas, ArchPillarPart part, Vector2 from, Vector2 to, bool alongX, float springing, float top, float inset, ArchBrush brush ) {
		ArchSpanGen.Build( canvas, new ArchSpanRun {
			From = from,
			To = to,
			Springing = springing,
			Top = top,
			Thickness = part.SpanThickness( alongX ),
			InsetFrom = inset,
			InsetTo = inset,
			Segments = part.ArchSegments,
			Ring = part.ArchRing,
			Form = part.Span
		}, brush );
	}

	static void Pilaster( ArchMesh canvas, ArchPillarPart part, ArchRoom room, float height, ArchBrush shaft, ArchBrush cap, ArchKit kit ) {
		var wall = room.Walls.Find( candidate => candidate.Id == part.WallId ) ?? room.Walls.FirstOrDefault();

		if ( wall is null ) {
			return;
		}

		var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
		var half = thickness * 0.5f;
		var halfWidth = part.PilasterWidth * 0.5f;
		var offset = Math.Clamp( part.WallOffset, halfWidth, Math.Max( halfWidth, wall.Length - halfWidth ) );

		var direction = wall.Direction;
		var normal = wall.Normal;
		var centre = wall.PointAt( offset );

		Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, false );

		if ( part.PilasterBothFaces ) {
			Face( canvas, centre, direction, normal, halfWidth, half, part, height, shaft, cap, true );
		}
	}

	static void Face(
		ArchMesh canvas,
		Vector2 centre,
		Vector2 direction,
		Vector2 normal,
		float halfWidth,
		float wallHalf,
		ArchPillarPart part,
		float height,
		ArchBrush shaft,
		ArchBrush cap,
		bool inner ) {
		// Exterior is -normal; flip both normal and run direction together to preserve winding
		var sign = inner ? 1f : -1f;
		var run = direction * sign;
		var near = wallHalf * sign;
		var far = (wallHalf + part.PilasterProtrusion) * sign;

		var bottom = part.BaseHeight;
		var plinth = part.Plinth ? Math.Max( 0f, part.PlinthHeight ) : 0f;
		var capital = part.Capital ? Math.Max( 0f, part.CapitalHeight ) : 0f;

		Slab( canvas, centre, run, normal, halfWidth, near, far, bottom + plinth, bottom + height - capital, shaft );

		if ( plinth > 0.05f ) {
			Slab( canvas, centre, run, normal, halfWidth + part.PlinthOversize, near, far + part.PlinthOversize * sign, bottom, bottom + plinth, cap );
		}

		if ( capital > 0.05f ) {
			Slab( canvas, centre, run, normal, halfWidth + part.CapitalOversize, near, far + part.CapitalOversize * sign, bottom + height - capital, bottom + height, cap );
		}
	}

	static void Slab(
		ArchMesh canvas,
		Vector2 centre,
		Vector2 direction,
		Vector2 normal,
		float halfWidth,
		float near,
		float far,
		float bottom,
		float top,
		ArchBrush brush ) {
		if ( top - bottom < 0.05f ) {
			return;
		}

		var a = centre - direction * halfWidth + normal * near;
		var b = centre + direction * halfWidth + normal * near;
		var c = centre + direction * halfWidth + normal * far;
		var d = centre - direction * halfWidth + normal * far;

		var lower = new List<Vector3>
		{
			new( a.x, a.y, bottom ),
			new( b.x, b.y, bottom ),
			new( c.x, c.y, bottom ),
			new( d.x, d.y, bottom )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower ) {
			upper.Add( point.WithZ( top ) );
		}

		canvas.Prism( lower, upper, brush );
	}
}