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

namespace Sunless.Architecture;

[Title( "Pillars" ), Icon( "view_column" ), Group( "09" )]
public sealed class ArchPillarSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => Bridging ? Spans.MergedDraftKind : ArchKind.Pillar;

	public override ArchSurface[] Surfaces => new[] { ArchSurface.Pillar, ArchSurface.PillarCap };

	enum PillarGesture {
		Single,
		Grid,
		Span
	}

	ArchPillarPart draft = new();
	ArchSpanSubtool spans;

	PillarGesture gesture = PillarGesture.Single;

	float maxSpacing;

	ArchSpanSubtool Spans => spans ??= new ArchSpanSubtool( Owner );

	bool Bridging => gesture == PillarGesture.Span || Owner.Picked?.Item is ArchSpanPart;

	protected override bool UsesDrag => gesture == PillarGesture.Grid;

	// Single uses flush snap; grid keeps tight snap so the drag isn't eaten
	public override ArchSnapReach SnapReach( float authored ) {
		return gesture == PillarGesture.Single ? ArchWallSnap.Flush( authored ) : base.SnapReach( authored );
	}

	protected override bool Adjusts => Bridging ? Spans.MergedAdjusts : Owner.Picked?.Item is ArchPillarPart;

	protected override string Title() => Bridging ? "Spans" : "Pillars";

	protected override ArchRunState Run() => Bridging ? Spans.MergedRun : default;

	protected override void CancelRun() => Spans.MergedCancel();

	protected override string Advice() => gesture switch {
		PillarGesture.Span => Spans.MergedAdvice,
		PillarGesture.Grid => "Drag the extent. Bays divide it evenly, so a shorter drag gives shorter spans, and each column climbs to whatever stands over it.",
		_ => "Click to stand one column. It climbs to whatever stands over it, and the widgets on it move, turn and section it afterwards."
	};

	ArchPillarType Kind => Owner.FindPillarType( draft.Type ) ?? Owner.DefaultPillar();

	void Seeded() {
		if ( draft.Type.Length > 0 || Owner.DefaultPillar() is not { } first ) {
			return;
		}

		draft = first.Standing( default, 0f, 0f );
		maxSpacing = MathF.Max( draft.Spacing.x, draft.Spacing.y );
	}

	void Adopt( ArchPillarType type, ArchWorkflowStep step ) {
		draft = type.Standing( default, 0f, 0f );
		maxSpacing = MathF.Max( draft.Spacing.x, draft.Spacing.y );

		step.Chose();
	}

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

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

	protected override void DrawHover( Vector2 point ) {
		if ( gesture == PillarGesture.Span ) {
			Spans.MergedHover( point );

			return;
		}

		if ( gesture == PillarGesture.Grid ) {
			base.DrawHover( point );

			return;
		}

		Seeded();

		var seated = Seated( point );

		Ghost( seated, point, Owner.RoomOnLevel( seated ), Lone() );
	}

	protected override void DrawPreview() {
		Seeded();
		Ghost( DragStart, DragCurrent, Owner.RoomOnLevel( DragStart ), Bays( DragCurrent - DragStart ) );
	}

	Vector2 Seated( Vector2 point ) {
		return gesture == PillarGesture.Single && Snapped
			? ArchPillarSnap.Seat( Owner.Plan, Owner.Kit, Pointer, draft )
			: point;
	}

	void Ghost( Vector2 origin, Vector2 note, ArchRoom room, Grid bays ) {
		var standing = Standing( origin, bays, room );
		var seated = ArchPillarGen.Seat( standing, Owner.Kit, Owner.Plan );
		var height = ArchPillarGen.Height( standing, room, Owner.Kit, Owner.Plan );
		var lift = Lift( room );
		var half = standing.Seated;

		foreach ( var column in ArchPillarGen.Columns( standing, seated ) ) {
			var at = new Vector2( column.At.x, column.At.y );
			var foot = column.At.z + lift;

			ArchGhost.Volume( at - half, at + half, foot, foot + height );
		}

		ArchGhost.Note( new Vector3( note.x, note.y, seated + lift + height ), Measured( standing ) );
	}

	float Lift( ArchRoom room ) {
		var host = Owner.Plan.OwnerOf( room ) ?? Owner.ActiveBuilding();

		return host is null ? 0f : ArchAsks.Lift( Owner.Plan, host, Owner.Kit );
	}

	string Measured( ArchPillarPart standing ) {
		var title = Kind?.Title ?? "pillars";

		if ( standing.CountX <= 1 && standing.CountY <= 1 ) {
			return $"{title} — one column, {standing.Width:0} x {standing.Depth:0}";
		}

		return $"{title} — {standing.CountX} x {standing.CountY} · {MathF.Abs( standing.Spacing.x ):0} x {MathF.Abs( standing.Spacing.y ):0} bays";
	}

	protected override void OnClick( Vector2 point ) {
		if ( gesture == PillarGesture.Span ) {
			Bridge( point );

			return;
		}

		Place( Seated( point ), Lone() );
	}

	void Bridge( Vector2 point ) {
		if ( Spans.MergedClick( point ) is not { } span ) {
			return;
		}

		Drew( span );
		FinishDraft( span.Id );
		Owner.Commit();
	}

	protected override void OnDrag( Vector2 from, Vector2 to ) {
		Place( from, Bays( to - from ) );
	}

	void Place( Vector2 at, Grid bays ) {
		Seeded();

		var room = Owner.RoomOnLevel( at );

		if ( room is null || Kind is null ) {
			return;
		}

		var part = Standing( at, bays, room );

		part.Id = Owner.Plan.AllocateId();
		part.Name = $"Pillars{room.Pillars.Count + 1}";

		room.Pillars.Add( part );
		FinishDraft( part.Id );
		Owner.Picked = new ArchSelection { Item = part, Room = room };
		Drew( part );
		Owner.Commit();
	}

	ArchPillarPart Standing( Vector2 from, Grid bays, ArchRoom room ) {
		var part = draft.Section();

		part.Type = Kind?.Name ?? draft.Type;
		part.Origin = from;
		part.BaseHeight = Owner.SeatAt( from, room, Beneath(), Upon() );
		part.Placement = PillarPlacement.Grid;
		part.CountX = bays.CountX;
		part.CountY = bays.CountY;
		part.Spacing = bays.Spacing;

		return part;
	}

	float? Beneath() => Pointer.Found && Pointer.Face == ArchCursorFace.Ceiling ? Pointer.Height : null;

	float? Upon() => Pointer.Found && Pointer.Face == ArchCursorFace.Floor ? Pointer.Height : null;

	Grid Lone() => new( 1, 1, draft.Spacing );

	readonly record struct Grid( int CountX, int CountY, Vector2 Spacing );

	Grid Bays( Vector2 reach ) {
		var limit = MathF.Max( 32f, maxSpacing > 1f ? maxSpacing : MathF.Max( draft.Spacing.x, draft.Spacing.y ) );
		var alongX = Owner.Grid.DivideAtMost( Dragged( reach.x ), limit );
		var alongY = Owner.Grid.DivideAtMost( Dragged( reach.y ), limit );

		return new Grid(
			alongX.Count + 1,
			alongY.Count + 1,
			new Vector2( MathF.Sign( reach.x ) * alongX.Step, MathF.Sign( reach.y ) * alongY.Step ) );
	}

	static float Dragged( float reach ) => MathF.Abs( reach ) < 1f ? 0f : MathF.Abs( reach );

	protected override void BuildOptions( ToolSidebarWidget panel ) {
		Seeded();

		if ( Bridging && Spans.PickedName is { } picked ) {
			panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {picked}. What is tuned here is also what the next bridge is drawn from." ) );
		}

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

		flow.Step( "Gesture", GestureName, GestureGlyph, PickGesture );

		if ( Bridging ) {
			Spans.MergedSteps( flow );

			return;
		}

		flow.Step( "Type", Kind?.Title ?? "None", Kind?.Icon ?? "view_column", step => Browse( panel, step ) );

		if ( gesture == PillarGesture.Grid ) {
			flow.Step( "Bays", $"{Reach():0} max", "grid_on", BuildBays );
		}
	}

	float Reach() => maxSpacing > 1f ? maxSpacing : MathF.Max( draft.Spacing.x, draft.Spacing.y );

	string GestureName => Bridging ? "Span" : gesture == PillarGesture.Grid ? "Grid" : "Single";

	string GestureGlyph => Bridging ? "bridge" : gesture == PillarGesture.Grid ? "grid_on" : "view_column";

	void PickGesture( ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Single — click to stand one column", "pillar_single", "view_column",
			!Bridging && gesture == PillarGesture.Single, () => Switch( PillarGesture.Single, step ) );

		grid.Pick( "Grid — drag a colonnade, bays dividing the reach", "pillar_grid", "grid_on",
			!Bridging && gesture == PillarGesture.Grid, () => Switch( PillarGesture.Grid, step ) );

		grid.Pick( "Span — click two piers and it bridges them with a beam, an arch or a haunch", "pillar_span", "bridge",
			Bridging, () => Switch( PillarGesture.Span, step ) );
	}

	void Switch( PillarGesture wanted, ArchWorkflowStep step ) {
		gesture = wanted;

		if ( wanted != PillarGesture.Span ) {
			Spans.MergedCancel();
		}

		step.Chose();
	}

	void BuildBays( ArchWorkflowStep step ) {
		step.Layout.Add( ArchPartUi.Number( "Max bay", maxSpacing, 240f, value => maxSpacing = value ) );
	}

	void Browse( ToolSidebarWidget panel, ArchWorkflowStep step ) {
		var offered = Offered();

		if ( offered.Count == 0 ) {
			step.Layout.Add( ArchSidebarLayout.Advice( $"No pillar types are authored — add one to Assets/{ArchPillarCatalog.Directory}." ) );

			return;
		}

		var shelf = new ArchPresetBrowser<ArchPillarType>( panel, Owner.Kit, Scope( "types" ), "Pillar types" ) {
			Chosen = type => string.Equals( draft.Type, type.Name, StringComparison.OrdinalIgnoreCase ),
			Choose = type => Adopt( type, step ),
			Exports = "pillar",
			Design = () => new ArchPillarDesigner( panel, Owner, draft, Kind, designed => ApplyDesigned( designed, step ) ).Show(),
			Reload = () => {
				Owner.ReloadPillarTypes();
				Refresh();
			}
		};

		shelf.DesignButton.Visible = true;
		shelf.DesignButton.ToolTip = "Design a pillar type";
		shelf.ReloadButton.Visible = true;
		shelf.Set( offered );

		step.Add( shelf );
	}

	List<ArchPresetItem<ArchPillarType>> Offered() {
		var height = Owner.Kit.WallHeight;

		return Owner.OfferedPillars()
			.Select( type => new ArchPresetItem<ArchPillarType> {
				Value = type,
				Name = type.Title,
				Detail = ArchPillarStage.Describe( type ),
				Identity = ArchPillarStage.Identity( type.Standing( default, 0f, 0f ), height ),
				Category = type.Column?.Span switch {
					PillarSpan.Arch => "Arcade",
					PillarSpan.Beam => "Beam",
					_ => "Post"
				},
				Glyph = type.Icon,
				Tags = $"{type.Name} {type.Description}",
				View = ArchPresetPreview.Quarter,
				Focus = ArchPreviewFocus.Whole,
				Recipe = () => ArchPillarStage.Of( type.Standing( default, 0f, 0f ), height )
			} )
			.ToList();
	}

	void ApplyDesigned( ArchPillarPart designed, ArchWorkflowStep step ) {
		draft = designed;
		maxSpacing = MathF.Max( draft.Spacing.x, draft.Spacing.y );

		step.Chose();
	}
}