Editor/Roof/ArchRoofRuns.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Single source for both the gutter builder and the downpipe placer - what you see is what a pipe taps.
public static partial class ArchRoofGen {
	public static List<ArchRunPath> GutterRuns( ArchRoofPart roof, ArchBuilding building, ArchKit kit, ArchPlan plan ) {
		var runs = new List<ArchRunPath>();

		foreach ( var region in new ArchRoofRegionService( building, roof ).Resolve() ) {
			runs.AddRange( GutterRuns( roof, building, kit, plan, region ) );
		}

		return runs;
	}

	static List<ArchRunPath> GutterRuns( ArchRoofPart roof, ArchBuilding building, ArchKit kit, ArchPlan plan, ArchRoofRegion region ) {
		var runs = new List<ArchRunPath>();
		var outline = region.Outer;

		if ( !roof.Gutters || roof.Parapet ) {
			return runs;
		}

		var profile = kit.FindProfile( "gutter" );
		var bite = ArchLap.Bite( kit );
		// Bitten on both axes, or the channel back and rim z-fight the fascia.
		var height = roof.BaseHeight - (profile?.Max.y ?? 0f) - bite;
		var stand = (roof.Fascia ? ArchProfiles.FasciaDepth( kit ) : 0f) - bite;
		var kinds = ArchKinds.Load();
		var junctions = ArchCrossGableService.Resolve( building, kit, kinds );
		var fittingAbutments = FittingAbutments( roof, building, plan, kit, junctions, kinds );

		bool Abuts( Vector2 point, Vector2 outward ) => Buried( fittingAbutments, point + outward * ArchProbe.Step );

		// Hip and flat shed off every edge, so the wrap includes the wing's inside corner.
		if ( roof.Style is RoofStyle.Flat or RoofStyle.Hip ) {
			foreach ( var loop in ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, roof.Overhang + stand ), fittingAbutments ) ) {
				runs.AddRange( Runs( loop, height, Abuts ) );
			}

			return runs;
		}

		if ( roof.Style == RoofStyle.Gable ) {
			foreach ( var loop in ArchFootprint.Subtract( ArchFootprint.Grow( region.Loops, roof.Overhang + stand ), fittingAbutments ) ) {
				for ( var index = 0; index < loop.Count; index++ ) {
					var from = loop[index];
					var to = loop[(index + 1) % loop.Count];
					var outward = ArchRegion.Outward( from, to );

					if ( Sheds( roof, RoofStyle.Gable, outward ) ) {
						runs.Add( Edge( from, to, height ) );
					}
				}
			}

			return runs;
		}

		ArchFootprint.Bounds( outline, out var lo, out var hi );

		var overhang = new Vector2( roof.Overhang, roof.Overhang );
		var reach = new Vector2( roof.Overhang + stand, roof.Overhang + stand );

		EaveRuns( roof, lo - overhang, hi + overhang, lo - reach, hi + reach, height, runs );

		return runs.Select( run => Seamed( run, roof, outline ) ).ToList();
	}

	// Seam where the run crosses wall lines so the buried stretch can drop; points are collinear, mitres untouched.
	static ArchRunPath Seamed( ArchRunPath run, ArchRoofPart roof, IReadOnlyList<Vector2> outline ) {
		if ( run.Points.Count < 2 ) {
			return run;
		}

		ArchFootprint.Bounds( outline, out var outlineMin, out var outlineMax );
		var walls = new[]
		{
			(Sideways: true, At: outlineMin.x),
			(Sideways: true, At: outlineMax.x),
			(Sideways: false, At: outlineMin.y),
			(Sideways: false, At: outlineMax.y)
		};
		var seamed = new List<Vector2>();

		for ( var edge = 0; edge < run.Edges; edge++ ) {
			var from = run.At( edge );
			var to = run.At( edge + 1 );

			seamed.Add( from );

			var crossings = new List<float>();

			foreach ( var wall in walls ) {
				var start = wall.Sideways ? from.x : from.y;
				var end = wall.Sideways ? to.x : to.y;
				var span = end - start;

				if ( MathF.Abs( span ) < 0.01f ) {
					continue;
				}

				var along = (wall.At - start) / span;

				if ( along > 0.01f && along < 0.99f ) {
					crossings.Add( along );
				}
			}

			foreach ( var along in crossings.OrderBy( value => value ) ) {
				var seam = Vector2.Lerp( from, to, along );

				if ( (seam - seamed[^1]).Length > 0.05f ) {
					seamed.Add( seam );
				}
			}
		}

		if ( !run.Closed ) {
			seamed.Add( run.Points[^1] );
		}

		return new ArchRunPath { Closed = run.Closed, Points = seamed, Height = run.Height };
	}

	// Broken at abutments so the channel stops there; whole loop = mitred corners.
	static IEnumerable<ArchRunPath> Runs( IReadOnlyList<Vector2> loop, float height, Func<Vector2, Vector2, bool> abuts ) {
		return ArchBrokenRun.Of( loop, abuts ).Level( height ).Resolve();
	}

	// Low edges only; runs sit on the fascia's outer face, wound so +x faces the roof.
	static void EaveRuns( ArchRoofPart roof, Vector2 min, Vector2 max, Vector2 low, Vector2 high, float height, List<ArchRunPath> runs ) {
		switch ( roof.Style ) {
			case RoofStyle.Shed when roof.RidgeAlongX:
				runs.Add( roof.Reversed
					? Edge( new Vector2( high.x, high.y ), new Vector2( low.x, high.y ), height )
					: Edge( new Vector2( low.x, low.y ), new Vector2( high.x, low.y ), height ) );
				break;

			case RoofStyle.Shed:
				runs.Add( roof.Reversed
					? Edge( new Vector2( high.x, low.y ), new Vector2( high.x, high.y ), height )
					: Edge( new Vector2( low.x, high.y ), new Vector2( low.x, low.y ), height ) );
				break;

			case RoofStyle.Sawtooth:
				SawtoothValleys( roof, min, max, low, high, height, runs );
				break;
		}
	}

	// Outermost bay is a real eave; the rest are valley gutters against the previous bay's glazing.
	static void SawtoothValleys( ArchRoofPart roof, Vector2 min, Vector2 max, Vector2 low, Vector2 high, float height, List<ArchRunPath> runs ) {
		var bays = Math.Max( 1, roof.SawtoothBays );
		var alongX = !roof.RidgeAlongX;
		var step = (alongX ? max.x - min.x : max.y - min.y) / bays;

		for ( var bay = 0; bay < bays; bay++ ) {
			if ( alongX ) {
				var x = bay == 0 ? low.x : min.x + bay * step;
				runs.Add( Edge( new Vector2( x, high.y ), new Vector2( x, low.y ), height ) );
				continue;
			}

			var y = bay == 0 ? low.y : min.y + bay * step;
			runs.Add( Edge( new Vector2( low.x, y ), new Vector2( high.x, y ), height ) );
		}
	}

	static ArchRunPath Edge( Vector2 from, Vector2 to, float height ) => ArchRunPath.Between( from, to, height );
}