Editor/Tool/Subtools/ArchBracketSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

// A row of hangers, dragged as a volume like everything else here. Nothing about it is typed twice: the box
// says where the row runs and how often, and every member is sized off the runs actually crossing it - so a
// pipe added to the bundle widens the cross-piece with no second edit, and moving the bundle moves the
// hangers with it.
[Title( "Brackets" ), Icon( "build" ), Group( "12" )]
public sealed class ArchBracketSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	public override ArchSurface[] Surfaces => new[] { ArchSurface.Bracket, ArchSurface.Pipework, ArchSurface.Cabling };

	readonly ArchPipeBracketPart draft = new();

	PipeMount? aim;
	ArchCursorFace face = ArchCursorFace.Ceiling;
	float depth = ArchPipe.DefaultDepth;

	protected override bool Adjusts => true;

	protected override ArchKind? DraftKind => ArchKind.Bracket;

	protected override string Title() => "Brackets";

	protected override string Advice() => "Drag a box across the runs to hold.";

	public override void OnEnabled() {
		base.OnEnabled();

		ArchWorkflow.Restart( Scope( "flow" ) );
	}

	PipeMount Mount() => ArchPipe.Aimed( aim, face );

	void Aiming() {
		if ( Owner.Cursor( out var cursor ) ) {
			face = cursor.Face;
		}
	}

	ArchPipeDrag Sketch( Vector2 from, Vector2 to ) {
		var floor = Owner.LevelHeight;
		var min = Min( from, to );
		var max = Max( from, to );

		return ArchPipe.Sketch( Owner.Grid, Owner.Plan, from, to, Mount(), floor, Soffit( min, max, floor ), depth );
	}

	float Soffit( Vector2 min, Vector2 max, float floor ) {
		var centre = (min + max) * 0.5f;

		if ( Owner.RoomOnLevel( centre ) is { } room && Owner.Plan.OwnerOf( room ) is { } building ) {
			return ArchFloorGen.Overhead( Owner.Plan, building, room, Owner.Kit, centre );
		}

		return floor + Owner.Kit.WallHeight - ArchFloorGen.CeilingDepth( Owner.Kit );
	}

	protected override void DrawHover( Vector2 point ) {
		Aiming();

		base.DrawHover( point );
	}

	protected override void DrawPreview() {
		Aiming();

		var drag = Sketch( DragStart, DragCurrent );
		var shape = ArchPipeBracket.Resolve( Owner.Plan, Previewed( drag ) );

		ArchGhost.Volume( drag.Min, drag.Max, drag.Bottom, drag.Top );

		foreach ( var station in shape.Stations ) {
			var holds = shape.Holding( station );
			var low = holds.Min( hold => hold.Offset - hold.Radius ) - shape.Spec.Overhang;
			var high = holds.Max( hold => hold.Offset + hold.Radius ) + shape.Spec.Overhang;
			var lift = holds.Max( hold => hold.Lift + hold.Radius ) + shape.Spec.Clearance;

			ArchGhost.Path( new List<Vector3>
			{
				shape.Frame.At( station, low, 0f ),
				shape.Frame.At( station, low, lift ),
				shape.Frame.At( station, high, lift ),
				shape.Frame.At( station, high, 0f )
			}, false );
		}

		ArchGhost.Note( new Vector3( drag.Max.x, drag.Max.y, drag.Top ),
			$"{shape.Count} hangers · holds {shape.Held.Count} runs" );
	}

	// Standing nowhere in the plan yet, so the overlap pass reads every corridor as already standing - which
	// is what a bracket about to be placed at the top of the stack actually is.
	ArchPipeBracketPart Previewed( ArchPipeDrag drag ) {
		return new ArchPipeBracketPart {
			Id = 0,
			Kind = draft.Kind,
			Mount = drag.Mount,
			Min = drag.Min,
			Max = drag.Max,
			BaseHeight = drag.Bottom,
			TopHeight = drag.Top,
			AlongX = drag.AlongX,
			Outward = drag.Outward,
			Spacing = draft.Spacing,
			MemberWidth = draft.MemberWidth,
			Clearance = draft.Clearance,
			Overhang = draft.Overhang,
			Detail = draft.Detail
		};
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		var building = Owner.ActiveBuilding();
		var drag = Sketch( from, to );

		if ( ArchPipe.Hang( Owner.Plan, building, Owner.Level, drag, draft ) is not { } placed ) {
			Log.Info( $"Architecture: that drag is shorter than {ArchPipe.MinSize} inches either way, so there is no row in it." );

			return;
		}

		FinishDraft( placed.Id );

		Owner.Picked = new ArchSelection { Item = placed, Building = building };

		Drew( placed );

		Owner.Commit( "Place Brackets" );
	}

	// One list: what the row hangs from and what kind of hanger it is - both picks, so the sequence runs itself
	// down to the three typed rows that can never answer. The face is asked only while seeding, because a row
	// already standing carries the face it was dragged against.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		var picked = Editing();
		var part = picked?.Item as ArchPipeBracketPart ?? draft;
		var changed = picked is null ? null : (Action)(() => Owner.Touch( $"Edit {picked.Describe()}" ));

		if ( picked is not null ) {
			panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {picked.Describe()}." ) );
		}

		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		if ( picked is null ) {
			flow.Step( "Mounted on", ArchPipeUi.MountName( aim ), ArchPipeUi.MountGlyph( aim ), PickMount );
		}

		flow.Step( "Hangers", part.Kind.ToString(), ArchPipeUi.KindGlyph( part.Kind ), step => PickKind( part, step, changed ) );

		flow.Step( "Row", $"{part.Spacing:0} apart", "space_bar", step => BuildRow( part, picked, step, changed ) );

		flow.Step( "Members", $"{part.MemberWidth:0.##} in", "straighten", step => ArchPipeUi.Members( step.Layout, part, changed ) );

		flow.Step( "Detail", $"{part.Detail.Sides} sides", "tune", step => ArchPipeUi.Detail( step.Layout, part.Detail, Refresh, changed ) );
	}

	void PickMount( ArchWorkflowStep step ) {
		ArchPipeUi.Mount( step.Layout, aim, chosen => { aim = chosen; step.Chose(); } );
	}

	void PickKind( ArchPipeBracketPart part, ArchWorkflowStep step, Action changed ) {
		ArchPipeUi.Kinds( step.Layout, part.Kind, kind => { part.Kind = kind; changed?.Invoke(); step.Chose(); } );
	}

	void BuildRow( ArchPipeBracketPart part, ArchSelection picked, ArchWorkflowStep step, Action changed ) {
		if ( picked is null ) {
			step.Layout.Add( ArchPartUi.Number( "Depth off the surface", depth, ArchPipe.DefaultDepth,
				value => depth = MathF.Max( 2f, value ) ) );
		}

		ArchPipeUi.Spacing( step.Layout, part, changed );
	}

	ArchSelection Editing() {
		return Owner.Picked?.Item is ArchPipeBracketPart ? Owner.Picked : null;
	}
}