Editor/Pipe/ArchPipeGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchPipeGen {
	public static void Build( ArchMesh canvas, ArchPlan plan, ArchPipePart part, ArchStyle style, ArchPalette host ) {
		var shape = ArchPipeShape.Resolve( plan, part );

		if ( !shape.Stands ) {
			return;
		}

		var run = Indexed( part, part.RunMaterialIndex, style.Brush( Surface( part ), part.Palette, host ) );
		var fittings = Indexed( part, part.FittingMaterialIndex, style.Brush( Surface( part ), part.Palette, host ) );
		var supports = Indexed( part, part.SupportMaterialIndex, style.Brush( ArchSurface.Bracket, part.Palette, host ) );

		Run( canvas, shape, part, run );
		Fittings( canvas, plan, shape, part, fittings );
		Supports( canvas, shape, part, supports );
	}

	// A pipe and a duct are plumbing; a conduit, a cable and the tray it lies in are electrical. Two roles,
	// so a map can paint a red rainwater run and a grey services run apart without touching either part.
	public static ArchSurface Surface( ArchPipePart part ) => part.Content switch {
		PipeContent.Conduit or PipeContent.Cable or PipeContent.Tray => ArchSurface.Cabling,
		_ => ArchSurface.Pipework
	};

	static ArchBrush Indexed( ArchPipePart part, int index, ArchBrush fallback ) {
		var path = part.MaterialAt( index );

		if ( string.IsNullOrWhiteSpace( path ) ) {
			return fallback;
		}

		return new ArchBrush {
			Material = ArchStyle.Load( path ),
			TexelScale = fallback.TexelScale,
			Shift = fallback.Shift
		};
	}

	// The run's own body stays on the HOST canvas - a generator that put everything into named pieces leaves
	// an empty host, which the cache reads as a part that has vanished and rebuilds the whole scene cold.
	static void Run( ArchMesh canvas, ArchPipeShape shape, ArchPipePart part, ArchBrush brush ) {
		canvas.Extrude( shape.Path(), ArchPipeSection.Of( part ), 1f, Rotation.Identity, brush );
	}

	// Collars down the run and a joint at every turn. Round stock only: a tray and a duct are fabricated in
	// lengths and flanged, not socketed, so a ring round one reads as a mistake.
	static void Fittings( ArchMesh canvas, ArchPlan plan, ArchPipeShape shape, ArchPipePart part, ArchBrush brush ) {
		var stations = Fixings( plan, shape, part );

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

		var section = ArchPipeSection.Of( part );
		var width = MathF.Max( 1f, part.Radius * 0.8f );

		using ( canvas.Part( ArchPieces.Fittings ) ) {
			foreach ( var station in stations ) {
				var ring = new List<Vector3>
				{
					shape.At( station - width * 0.5f ),
					shape.At( station + width * 0.5f )
				};

				canvas.Extrude( ring, section, 1.3f, Rotation.Identity, brush );
			}
		}
	}

	// Where every coupling down a run stands, so the generator and the report count the same fittings.
	public static List<float> Fixings( ArchPlan plan, ArchPipeShape shape, ArchPipePart part ) {
		var stations = new List<float>();
		var length = shape.Length;

		if ( part.Detail.Couplings && part.Detail.CouplingSpacing >= 8f && length >= 16f ) {
			stations.AddRange( ArchDivide.AtMost( length, part.Detail.CouplingSpacing ).Inner );
		}

		if ( part.Detail.BendCollars ) {
			stations.AddRange( shape.Joints );
		}

		foreach ( var fitting in ArchPipeFittings.Resolve( plan, part ) ) {
			var nearest = shape.Curve.Dense.OrderBy( frame => (frame.Position - fitting.Position).Length ).FirstOrDefault();

			stations.Add( nearest.Distance );
		}

		return stations
			.Where( station => station >= 0f && station <= length )
			.OrderBy( station => station )
			.Aggregate( new List<float>(), ( kept, station ) => {
				if ( kept.Count == 0 || MathF.Abs( kept[^1] - station ) > 0.5f ) {
					kept.Add( station );
				}

				return kept;
			} );
	}

	// The run's own hangers go through the one hanger emitter the Brackets tool uses, so a support under a
	// bundle and a bracket dragged across it are the same fabrication measured the same way.
	static void Supports( ArchMesh canvas, ArchPipeShape shape, ArchPipePart part, ArchBrush brush ) {
		if ( shape.Supports.Count == 0 ) {
			return;
		}

		var spec = new ArchPipeHangerSpec {
			Kind = part.SupportKind,
			Member = MathF.Max( 1f, part.Radius * 0.8f ),
			Clearance = 0.5f,
			Overhang = MathF.Max( 1f, part.Radius ),
			Detail = part.Detail
		};

		using ( canvas.Part( ArchPieces.Brackets ) ) {
			foreach ( var station in shape.Supports ) {
				var frame = shape.HangerFrame( part, station );
				var holds = new List<ArchPipeHold>
				{
					new()
					{
						Offset = 0f,
						Lift = part.Radius,
						Radius = part.Radius
					}
				};

				ArchPipeHanger.Emit( canvas, frame, 0f, holds, spec, brush );
			}
		}
	}
}