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

namespace Sunless.Architecture;

// Which elements a following run is allowed to dress. A run round a shaft usually wants the floor edge and
// the jambs of every wall the shaft went through; one round a plinth wants the deck edge alone.
public enum ArchTrimReach {
	Floor,
	Walls,
	Both
}

// ONE resolution of a following run's path. The generator, the ghost and the report all read this, so the
// run you are shown is the run you get.
//
// A ring on the floor and nothing on the wall it passes through is two mouldings that stop at each other.
// This walks the host's outline and, where an edge crosses a wall the host OPENED, turns the run up that
// opening's jamb, over its head and back down - so what comes out is one continuous path rather than a
// floor ring plus a set of loose uprights.
public static class ArchTrimFollow {
	// Below this the jamb is a nick in the plaster, not a reveal worth a moulding round.
	const float LeastJamb = 4f;

	public static List<List<Vector3>> Runs( ArchPlan plan, ArchKit kit, ArchTrimPart trim ) {
		var runs = new List<List<Vector3>>();

		if ( trim is null ) {
			return runs;
		}

		if ( !trim.Follows ) {
			runs.Add( Closed( trim.Path, trim.Closed ) );

			return runs;
		}

		var loops = ArchLayerShape.Loops( plan, trim.FollowsId );

		if ( loops.Count == 0 ) {
			return runs;
		}

		var loop = loops[Math.Clamp( trim.FollowsLoop, 0, loops.Count - 1 )];
		var height = ArchLayerShape.HeightOf( plan, trim.FollowsId, 0f ) + trim.Lift;
		var jambs = trim.Reach == ArchTrimReach.Floor
			? new List<Jamb>()
			: Jambs( plan, kit, trim.FollowsId ).ToList();

		// Walls only: the uprights on their own, each its own open run - there is no floor edge to join them.
		if ( trim.Reach == ArchTrimReach.Walls ) {
			runs.AddRange( jambs.Select( jamb => Upright( jamb ) ) );

			return runs;
		}

		runs.Add( Walked( loop, height, jambs ) );

		return runs;
	}

	// The outline walked edge by edge, climbing every jamb standing on that edge in the order it is met, so
	// the path never doubles back on itself.
	static List<Vector3> Walked( IReadOnlyList<Vector2> loop, float height, List<Jamb> jambs ) {
		var path = new List<Vector3>();

		for ( var index = 0; index < loop.Count; index++ ) {
			var a = loop[index];
			var b = loop[(index + 1) % loop.Count];

			path.Add( new Vector3( a.x, a.y, height ) );

			foreach ( var jamb in Along( jambs, a, b ) ) {
				path.AddRange( Upright( jamb ) );
			}
		}

		// Closed: the last corner runs back to the first, which is what makes it ONE moulding.
		if ( path.Count >= 3 ) {
			path.Add( path[0] );
		}

		return path;
	}

	// A jamb visited as three points: up its face, over the head and down again, so the run reads as having
	// gone round the reveal rather than jumped it.
	static List<Vector3> Upright( Jamb jamb ) {
		return new List<Vector3>
		{
			new( jamb.Foot.x, jamb.Foot.y, jamb.Bottom ),
			new( jamb.Foot.x, jamb.Foot.y, jamb.Top ),
			new( jamb.Head.x, jamb.Head.y, jamb.Top ),
			new( jamb.Head.x, jamb.Head.y, jamb.Bottom )
		};
	}

	// The jambs lying on this edge, ordered along it - two on the same edge have to be climbed in the order
	// the walk reaches them or the moulding crosses itself.
	static IEnumerable<Jamb> Along( List<Jamb> jambs, Vector2 a, Vector2 b ) {
		var span = b - a;
		var length = span.Length;

		if ( length < 0.5f ) {
			yield break;
		}

		var unit = span / length;

		var found = jambs
			.Select( jamb => (Jamb: jamb, At: Vector2.Dot( (jamb.Foot + jamb.Head) * 0.5f - a, unit )) )
			.Where( entry => entry.At > 0.5f && entry.At < length - 0.5f )
			.Where( entry => MathF.Abs( Across( (entry.Jamb.Foot + entry.Jamb.Head) * 0.5f - a, unit ) ) < LeastJamb )
			.OrderBy( entry => entry.At );

		foreach ( var entry in found ) {
			yield return entry.Jamb;
		}
	}

	static float Across( Vector2 offset, Vector2 unit ) => offset.x * unit.y - offset.y * unit.x;

	// Every opening the host owns, as the reveal it left in its wall. Owned, so a run round a shaft climbs
	// the holes that shaft made and never a door that happened to be beside it.
	static IEnumerable<Jamb> Jambs( ArchPlan plan, ArchKit kit, int hostId ) {
		var kinds = ArchKinds.Load();

		foreach ( var room in plan.AllRooms() ) {
			var wallHeight = ArchFloorGen.WallHeight( room, kit );

			foreach ( var wall in plan.Filed<ArchWall>( room, kinds ) ) {
				foreach ( var opening in wall.Openings.Where( entry => entry.OwnerId == hostId && ArchLayerGate.On( entry ) && ArchLayerGate.Owned( entry.OwnerId ) ) ) {
					var height = MathF.Min( opening.Height, wallHeight - opening.SillHeight );

					if ( height < LeastJamb || opening.Width < LeastJamb ) {
						continue;
					}

					yield return new Jamb {
						Foot = wall.PointAt( opening.Left ),
						Head = wall.PointAt( opening.Right ),
						Bottom = room.BaseHeight + opening.SillHeight,
						Top = room.BaseHeight + opening.SillHeight + height
					};
				}
			}
		}
	}

	static List<Vector3> Closed( IReadOnlyList<Vector3> path, bool close ) {
		var walked = new List<Vector3>( path );

		if ( close && walked.Count >= 3 && (walked[0] - walked[^1]).Length > 0.5f ) {
			walked.Add( walked[0] );
		}

		return walked;
	}

	// One reveal in one wall: where it starts, where it ends and the band it spans.
	sealed class Jamb {
		public Vector2 Foot;
		public Vector2 Head;
		public float Bottom;
		public float Top;
	}
}