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

namespace Sunless.Architecture;

// A row of hangers standing in a dragged volume, sized by WHAT IS ACTUALLY IN IT. The volume says where the
// row runs and how often; every other number - how wide the cross-piece is, how far the drops reach, which
// lane each clamp closes on - is read off the runs crossing it, so adding a pipe to a bundle widens its
// hangers and moving the bundle moves them, with nothing to re-author either time.
//
// The same order rule the rest of the stack obeys decides what it holds: a bracket picks up the runs that
// were STANDING when it was made. To catch one added later, drag its row below that run in the stack - which
// is the whole of the non-destructive workflow, and needs no second control anywhere.
public sealed class ArchPipeBracketShape {
	public bool Stands { get; init; }
	public ArchPipeFrame Frame { get; init; }
	public List<float> Stations { get; init; } = new();
	public List<ArchPipeShape> Held { get; init; } = new();
	public ArchPipeHangerSpec Spec { get; init; } = new();
	// What the row falls back to where it holds nothing yet: the box the author dragged, so an empty bracket
	// volume shows the frame it will make rather than silently building nothing.
	public float Depth { get; init; }

	public int Count => Stations.Count;

	public List<ArchPipeHold> Holding( float station ) {
		var holds = ArchPipeHanger.Holding( Frame, station, Held );

		if ( holds.Count > 0 ) {
			return holds;
		}

		return new List<ArchPipeHold>
		{
			new()
			{
				Offset = 0f,
				Lift = MathF.Max( 1f, Depth - Spec.Member ),
				Radius = MathF.Max( 0f, Frame.Span * 0.5f - Spec.Overhang )
			}
		};
	}
}

public static class ArchPipeBracket {
	public static ArchPipeBracketShape Resolve( ArchPlan plan, ArchPipeBracketPart part ) {
		var frame = ArchPipeFrame.Of( part );

		if ( !frame.Stands ) {
			return new ArchPipeBracketShape();
		}

		return new ArchPipeBracketShape {
			Stands = true,
			Frame = frame,
			Stations = ArchDivide.AtMost( frame.Length, MathF.Max( 12f, part.Spacing ) ).Nodes.ToList(),
			Held = Overlapping( plan, part ).Select( run => ArchPipeShape.Resolve( plan, run ) ).ToList(),
			Spec = new ArchPipeHangerSpec {
				Kind = part.Kind,
				Member = MathF.Max( 0.5f, part.MemberWidth ),
				Clearance = MathF.Max( 0f, part.Clearance ),
				Overhang = MathF.Max( 0f, part.Overhang ),
				Detail = part.Detail
			},
			Depth = Depth( part )
		};
	}

	// Standing in the box AND standing before it. Plan-wide, because a volume is world-space and a bracket
	// under a party wall holds the runs of both units.
	static IEnumerable<ArchPipePart> Overlapping( ArchPlan plan, ArchPipeBracketPart part ) {
		if ( plan is null ) {
			return Array.Empty<ArchPipePart>();
		}

		return plan.Buildings
			.SelectMany( building => ArchLayerGate.Enabled( building.Pipes ) )
			.Where( run => ArchLayerOrder.Applies( plan, part.Id, run.Id ) )
			.Where( run => ArchPipeShape.Resolve( plan, run ).Path().Any( point =>
				point.x >= part.Min.x && point.x <= part.Max.x
				&& point.y >= part.Min.y && point.y <= part.Max.y
				&& point.z >= part.BaseHeight && point.z <= part.TopHeight ) );
	}

	// How far the volume reaches along its own dodge axis - down off a ceiling, up off a floor, out of a wall.
	static float Depth( ArchPipeBracketPart part ) {
		if ( part.Mount != PipeMount.Wall ) {
			return part.Rise;
		}

		var size = part.Size;

		return MathF.Abs( part.AlongX ? size.y : size.x );
	}
}