Editor/Services/ArchChamfer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly record struct ArchChamferFacet( Vector2 Corner, Vector2 From, Vector2 To ) {
	public float Reach => (From - Corner).Length;

	public bool Stands => (To - From).Length >= 1f;

	public Vector2 Face => Stands ? (To - From).Normal : new Vector2( 1f, 0f );

	public Vector2 Normal {
		get {
			var face = Face;

			return new Vector2( -face.y, face.x );
		}
	}
}

public sealed class ArchChamferReport {
	public List<ArchWall> Facets { get; } = new();
	public int Loops { get; set; }
	public int Ends { get; set; }
	public int Runs { get; set; }
	public int Stranded { get; set; }

	public bool Chamfered => Loops > 0 || Facets.Count > 0;
}

public static class ArchChamfer {
	public static ArchChamferReport Corner( ArchPlan plan, ArchBuilding building, Vector2 corner, float setback ) {
		var report = new ArchChamferReport();

		if ( plan is null || building is null ) {
			return report;
		}

		foreach ( var room in building.Rooms ) {
			Storey( plan, room, corner, setback, report );
		}

		foreach ( var roof in building.Roofs ) {
			Deck( plan, roof, corner, setback, report );
		}

		foreach ( var platform in building.Platforms ) {
			Outline( platform.Outline(), corner, setback, platform.Reshape, report );
		}

		return report;
	}

	public static ArchChamferFacet? Facing( IReadOnlyList<Vector2> loop, Vector2 corner, float setback ) {
		if ( loop is not { Count: >= 3 } ) {
			return null;
		}

		var wound = ArchFootprint.Wind( loop.ToList() );
		var index = Nearest( wound, corner );

		if ( index < 0 ) {
			return null;
		}

		var at = wound[index];
		var back = wound[(index - 1 + wound.Count) % wound.Count] - at;
		var forward = wound[(index + 1) % wound.Count] - at;

		// Setback on the ladder, not the points — snapping them would rotate neighbouring elevations
		var reach = MathF.Min( ArchGridService.Fine( setback ), MathF.Min( back.Length, forward.Length ) * 0.45f );

		if ( reach < 1f ) {
			return null;
		}

		return new ArchChamferFacet( at, at + back.Normal * reach, at + forward.Normal * reach );
	}

	static void Storey( ArchPlan plan, ArchRoom room, Vector2 corner, float setback, ArchChamferReport report ) {
		if ( Facing( ArchFloorGen.Footprint( room ), corner, setback ) is not { Stands: true } facet ) {
			return;
		}

		if ( room.HasFootprint && Split( room.Footprint, facet, true ) is not null ) {
			report.Loops++;
		}

		var faced = Stopped( room.Walls, facet, report );

		if ( faced.Count > 0 ) {
			Seat( room.Walls, Facet( plan, faced, facet ), report );
		}

		// A following run traces its host and re-derives; a hand-drawn one keeps whatever path it was given.
		foreach ( var trim in room.Trims.Where( trim => !trim.Follows ) ) {
			report.Runs += Split( trim, facet ) ? 1 : 0;
		}
	}

	static void Deck( ArchPlan plan, ArchRoofPart roof, Vector2 corner, float setback, ArchChamferReport report ) {
		var outline = roof.Outline();

		if ( Facing( outline, corner, setback ) is not { Stands: true } facet ) {
			return;
		}

		if ( Split( outline, facet, true ) is not null ) {
			roof.Reshape( outline );
			report.Loops++;
		}

		var faced = Stopped( roof.Walls, facet, report );

		if ( faced.Count > 0 ) {
			Seat( roof.Walls, Facet( plan, faced, facet ), report );
		}
	}

	static void Outline( List<Vector2> loop, Vector2 corner, float setback, Action<List<Vector2>> reshape, ArchChamferReport report ) {
		if ( Facing( loop, corner, setback ) is not { Stands: true } facet || Split( loop, facet, true ) is null ) {
			return;
		}

		reshape( loop );
		report.Loops++;
	}

	static List<ArchWall> Stopped( List<ArchWall> walls, ArchChamferFacet facet, ArchChamferReport report ) {
		var faced = new List<ArchWall>();

		foreach ( var wall in walls ) {
			if ( wall.Length < 1f ) {
				continue;
			}

			if ( ArchCarry.Coincident( wall.Start, facet.Corner ) ) {
				var moved = Retracted( wall.Start, wall.Direction, wall.Length, facet );

				wall.Start = moved;
				Restationed( wall, (moved - facet.Corner).Length, report );
			} else if ( ArchCarry.Coincident( wall.End, facet.Corner ) ) {
				wall.End = Retracted( wall.End, -wall.Direction, wall.Length, facet );
				Restationed( wall, 0f, report );
			} else {
				continue;
			}

			faced.Add( wall );
			report.Ends++;
		}

		return faced;
	}

	// Retracting the start slides openings/modifiers; anything now off the run is removed
	static void Restationed( ArchWall wall, float slid, ArchChamferReport report ) {
		foreach ( var opening in wall.Openings ) {
			opening.Offset -= slid;
		}

		foreach ( var block in wall.Modifiers ) {
			block.Along -= slid;
		}

		var length = wall.Length;

		report.Stranded += wall.Openings.RemoveAll( opening => !Within( opening.Offset, opening.Width, length ) );
		report.Stranded += wall.Modifiers.RemoveAll( block => !Within( block.Along, block.Width, length ) );
	}

	static bool Within( float centre, float width, float length ) {
		var reach = MathF.Max( 0f, width ) * 0.5f;

		return centre - reach > -0.05f && centre + reach < length + 0.05f;
	}

	static Vector2 Retracted( Vector2 end, Vector2 away, float length, ArchChamferFacet facet ) {
		var facing = Vector2.Dot( facet.Normal, away );
		var travel = MathF.Abs( facing ) < 0.001f
			? facet.Reach
			: Vector2.Dot( facet.Normal, facet.From - end ) / facing;

		return end + away * Math.Clamp( travel, 0f, length * 0.45f );
	}

	// Dressed from the adjacent run so the facet matches its neighbours
	static ArchWall Facet( ArchPlan plan, IReadOnlyList<ArchWall> faced, ArchChamferFacet facet ) {
		var wall = new ArchWall {
			Id = plan.AllocateId(),
			Start = facet.From,
			End = facet.To
		};

		var source = faced.FirstOrDefault( run => run.Exterior ) ?? faced.FirstOrDefault();

		if ( source is not null ) {
			ArchWallTreatments.Dress( source, wall );
		}

		return wall;
	}

	static void Seat( List<ArchWall> walls, ArchWall facing, ArchChamferReport report ) {
		walls.Insert( Ring( walls, facing ), facing );
		report.Facets.Add( facing );
	}

	// Seated in the ring so a derived-footprint walk still closes
	static int Ring( List<ArchWall> walls, ArchWall facing ) {
		for ( var index = 0; index < walls.Count; index++ ) {
			if ( ArchCarry.Coincident( walls[index].End, facing.Start ) ) {
				return index + 1;
			}

			if ( ArchCarry.Coincident( walls[index].Start, facing.End ) ) {
				return index;
			}
		}

		return walls.Count;
	}

	readonly record struct ArchChamferSplit( int At, int Count );

	// Ordered by the stored loop's winding, not the wound copy's — open runs take one point, not two
	static ArchChamferSplit? Split( List<Vector2> points, ArchChamferFacet facet, bool closed ) {
		var index = Nearest( points, facet.Corner );

		if ( index < 0 ) {
			return null;
		}

		var count = points.Count;
		var leading = closed || index > 0 ? (index - 1 + count) % count : -1;
		var trailing = closed || index < count - 1 ? (index + 1) % count : -1;

		if ( leading < 0 || trailing < 0 ) {
			points[index] = Nearer( facet, points[leading < 0 ? trailing : leading] );

			return new ArchChamferSplit( index, 1 );
		}

		var forward = (points[leading] - facet.From).Length <= (points[leading] - facet.To).Length;

		points.RemoveAt( index );
		points.InsertRange( index, forward ? new[] { facet.From, facet.To } : new[] { facet.To, facet.From } );

		return new ArchChamferSplit( index, 2 );
	}

	static bool Split( ArchTrimPart trim, ArchChamferFacet facet ) {
		var flats = trim.Path.Select( Flat ).ToList();
		var index = Nearest( flats, facet.Corner );

		if ( index < 0 ) {
			return false;
		}

		var height = trim.Path[index].z;

		if ( Split( flats, facet, trim.Closed ) is not { } split ) {
			return false;
		}

		trim.Path.RemoveAt( split.At );
		trim.Path.InsertRange( split.At, Enumerable.Range( 0, split.Count )
			.Select( offset => Raised( flats[split.At + offset], height ) ) );

		return true;
	}

	static int Nearest( IReadOnlyList<Vector2> points, Vector2 corner ) {
		var found = -1;
		var best = ArchCarry.CornerReach;

		for ( var index = 0; index < points.Count; index++ ) {
			var away = (points[index] - corner).Length;

			if ( away >= best ) {
				continue;
			}

			best = away;
			found = index;
		}

		return found;
	}

	static Vector2 Nearer( ArchChamferFacet facet, Vector2 to ) {
		return (facet.From - to).Length <= (facet.To - to).Length ? facet.From : facet.To;
	}

	static Vector2 Flat( Vector3 point ) => new( point.x, point.y );

	static Vector3 Raised( Vector2 point, float height ) => new( point.x, point.y, height );
}