Editor/Roof/ArchGutterGen.cs
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchGutterGen {
	public static void Run( ArchMesh canvas, ArchRunPath run, ArchKit kit, ArchBrush brush ) {
		var profile = kit.FindProfile( "gutter" );

		if ( profile is null || !profile.IsUsable || run.Points.Count < 2 ) {
			return;
		}

		canvas.Extrude( run.Raised(), profile, 1f, Rotation.Identity, brush, run.Closed );
		Brackets( canvas, run, profile, kit, brush );
	}

	// Extruded, not boxed, so it mitres its corners on a run that isn't axis-aligned.
	static void Brackets( ArchMesh canvas, ArchRunPath run, ArchProfile gutter, ArchKit kit, ArchBrush brush ) {
		var strap = kit.FindProfile( "gutter_bracket" );
		var spacing = kit.GutterBracketSpacing;

		if ( strap is null || !strap.IsUsable || spacing < 8f ) {
			return;
		}

		// Bitten into the channel - short of it reads as a floating tab, exact touch is coplanar.
		var bite = MathF.Max( strap.Max.x, strap.Max.y ) - 0.3f;
		var front = gutter.Min.x - bite;
		var under = gutter.Min.y - bite;

		for ( var edge = 0; edge < run.Edges; edge++ ) {
			var start = run.At( edge );
			var end = run.At( edge + 1 );
			var length = (end - start).Length;

			if ( length < 16f ) {
				continue;
			}

			var along = (end - start) / length;
			var forward = new Vector3( along.x, along.y, 0f );
			var right = Vector3.Cross( Vector3.Up, forward ).Normal;
			var orientation = Rotation.LookAt( Vector3.Up, forward );
			var origin = new Vector3( start.x, start.y, run.Height );

			var inset = MathF.Min( 8f, length * 0.25f );

			foreach ( var offset in ArchDivide.AtMost( length - inset * 2f, spacing ).Nodes ) {
				var at = origin + forward * (inset + offset);

				var path = new List<Vector3>
				{
					at + Vector3.Up * under,
					at + right * front + Vector3.Up * under,
					at + right * front + Vector3.Up * -0.5f
				};

				canvas.Extrude( path, strap, 1f, orientation, brush );
			}
		}
	}

	// Snaps to the nearest run, so a click on a gable end lands on a real eave.
	public static ArchDownpipePart Place( ArchPlan plan, ArchBuilding building, ArchKit kit, Vector2 point, string profileName, bool shoe, PipeRun run = PipeRun.Rainwater ) {
		var gutter = kit.FindProfile( "gutter" );
		var pipe = kit.FindProfile( profileName );

		if ( building is null || gutter is null || pipe is null || !pipe.IsUsable ) {
			return null;
		}

		var kinds = ArchKinds.Load();
		var best = float.MaxValue;
		var tap = Vector3.Zero;
		var outward = Vector2.Zero;

		foreach ( var roof in plan.Filed<ArchRoofPart>( building, kinds ) ) {
			foreach ( var eaveRun in ArchRoofGen.GutterRuns( roof, building, kit, plan ) ) {
				if ( !eaveRun.Nearest( point, out var station, out var gap ) || gap >= best ) {
					continue;
				}

				best = gap;
				tap = station.Raised;
				outward = station.Outward;
			}
		}

		if ( best == float.MaxValue ) {
			return null;
		}

		var eave = new Vector2( tap.x, tap.y );
		var radius = MathF.Max( pipe.Max.x, -pipe.Min.x );
		var face = WallFace( building, kit, eave, outward, out var normal );
		var anchor = face + normal * (radius + kit.DownpipeWallGap);
		var conduit = run == PipeRun.Conduit;

		return new ArchDownpipePart {
			Id = plan.AllocateId(),
			Name = $"{(conduit ? "Conduit" : "Downpipe")}{plan.CountFiled<ArchDownpipePart>( building, kinds ) + 1}",
			// A conduit taps nothing, so it stands off the elevation the whole way and never swans over to it.
			Outlet = conduit ? anchor : eave + outward * (MathF.Abs( gutter.Min.x ) * 0.5f),
			Wall = conduit ? face : anchor,
			TopHeight = tap.z + gutter.Min.y + 1.5f,
			BaseHeight = Ground( building ),
			Profile = profileName,
			Shoe = shoe && !conduit,
			Run = run
		};
	}

	// The elevation's own face, plus the way out of it: a collar hangs a radius clear of it, a strap spans to it.
	static Vector2 WallFace( ArchBuilding building, ArchKit kit, Vector2 eave, Vector2 outward, out Vector2 normal ) {
		normal = outward;

		var best = ArchTool.NearestWall( building.Rooms, eave, out _, out var along, out _ );

		if ( best is null ) {
			return eave;
		}

		var closest = best.PointAt( along );

		normal = best.Normal;

		if ( Vector2.Dot( normal, outward ) < 0f ) {
			normal = -normal;
		}

		var thickness = best.Thickness > 0f ? best.Thickness : kit.WallThickness;

		return closest + normal * (thickness * 0.5f);
	}

	static float Ground( ArchBuilding building ) {
		var lowest = float.MaxValue;

		foreach ( var room in building.Rooms ) {
			lowest = MathF.Min( lowest, room.BaseHeight );
		}

		return lowest == float.MaxValue ? 0f : lowest;
	}

	const float FloorClear = 2f;
	const float ShoeRise = 7f;
	const float StubDrop = 3f;
	const float ShortestNeck = 12f;

	// One mitred polyline, so the extruder makes the elbows.
	public static void Downpipe( ArchMesh canvas, ArchDownpipePart pipe, ArchKit kit, ArchBrush brush ) {
		var profile = kit.FindProfile( pipe.Profile );

		if ( profile is null || !profile.IsUsable ) {
			return;
		}

		var reach = pipe.Outlet - pipe.Wall;
		var stand = reach.Length;
		var outward = stand < 0.01f ? Vector2.Zero : reach / stand;

		var floor = pipe.BaseHeight + FloorClear;
		var bottom = Foot( pipe );

		var stub = Head( pipe );
		var neck = stub - stand;
		var swans = !pipe.IsConduit && stand > 0.5f && neck > bottom + ShortestNeck;

		// Too little wall to swan-neck into, so it drops where it taps.
		var foot = swans ? pipe.Wall : pipe.Outlet;

		var path = new List<Vector3> { At( pipe.Outlet, pipe.TopHeight ) };

		if ( swans ) {
			path.Add( At( pipe.Outlet, stub ) );
			path.Add( At( pipe.Wall, neck ) );
		}

		path.Add( At( foot, bottom ) );

		if ( pipe.Shoe ) {
			path.Add( At( foot + outward * 5f, floor ) );
		}

		canvas.Extrude( path, profile, 1f, Rotation.Identity, brush );

		if ( !pipe.IsConduit ) {
			Outlet( canvas, pipe, profile, brush );
		}

		Fixings( canvas, pipe, foot, profile, kit, brush );
	}

	// Where every collar or bracket down a run stands, so the generator and the report count the same fixings.
	public static List<float> Fixings( ArchDownpipePart pipe, ArchKit kit ) {
		var from = Foot( pipe );
		var run = Head( pipe ) - from;
		var pitch = pipe.IsConduit ? kit.ConduitBracketSpacing : kit.DownpipeBracketSpacing;
		var stations = new List<float>();

		if ( pitch < 8f || run < 16f ) {
			return stations;
		}

		foreach ( var offset in ArchDivide.AtMost( run, pitch ).Inner ) {
			stations.Add( from + offset );
		}

		return stations;
	}

	static float Foot( ArchDownpipePart pipe ) => pipe.BaseHeight + FloorClear + (pipe.Shoe ? ShoeRise : 0f);

	static float Head( ArchDownpipePart pipe ) => pipe.TopHeight - StubDrop;

	static void Outlet( ArchMesh canvas, ArchDownpipePart pipe, ArchProfile profile, ArchBrush brush ) {
		var path = new List<Vector3>
		{
			At( pipe.Outlet, pipe.TopHeight + 1f ),
			At( pipe.Outlet, pipe.TopHeight - StubDrop )
		};

		canvas.Extrude( path, profile, 1.35f, Rotation.Identity, brush );
	}

	// A rainwater pipe wears collars round itself; a conduit is strapped off the wall face it runs down.
	static void Fixings( ArchMesh canvas, ArchDownpipePart pipe, Vector2 foot, ArchProfile profile, ArchKit kit, ArchBrush brush ) {
		var stations = Fixings( pipe, kit );

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

		if ( !pipe.IsConduit ) {
			foreach ( var height in stations ) {
				canvas.Extrude( new List<Vector3> { At( foot, height - 1.2f ), At( foot, height + 1.2f ) },
					profile, 1.3f, Rotation.Identity, brush );
			}

			return;
		}

		var strap = kit.FindProfile( "gutter_bracket" );

		if ( strap is null || !strap.IsUsable || (pipe.Wall - foot).Length < 0.5f ) {
			return;
		}

		using ( canvas.Part( ArchPieces.Brackets ) ) {
			foreach ( var height in stations ) {
				canvas.Extrude( new List<Vector3> { At( foot, height ), At( pipe.Wall, height ) },
					strap, 1f, Rotation.Identity, brush );
			}
		}
	}

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