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

namespace Sunless.Architecture;

[Title( "Road" ), Icon( "add_road" ), Group( "02" )]
public sealed class ArchRoadSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	protected override ArchKind? DraftKind => ArchKind.Road;

	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.Road, ArchSurface.Pavement, ArchSurface.Kerb, ArchSurface.RoadLine, ArchSurface.Deck, ArchSurface.Railing
	};

	readonly List<ArchCurveNode> nodes = new();
	readonly ArchRoadPart draft = new();

	bool straight;
	// Nothing else could put a centreline in the air - a deck is the road's own surface.
	float lift;

	protected override bool UsesDrag => false;

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

	protected override string Advice() => "Click to lay the centreline. Click the first node again to close a loop.";

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

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

	protected override ArchRunState Run() => new( nodes.Count > 0, $"{nodes.Count} node{(nodes.Count == 1 ? "" : "s")}" );

	protected override void FinishRun() => Finish( false );

	protected override void CancelRun() => nodes.Clear();

	protected override void OnClick( Vector2 point ) {
		if ( nodes.Count >= 3 && (Flat( nodes[0] ) - point).Length < Owner.Kit.GridSize ) {
			Finish( true );
			return;
		}

		nodes.Add( Node( Grounded( point ) ) );
	}

	// Tangent mode is per node, so a road can turn a corner and then curve.
	ArchCurveNode Node( Vector3 position ) {
		return new ArchCurveNode {
			Position = position,
			Mode = straight ? ArchTangentMode.Linear : ArchTangentMode.Auto
		};
	}

	// Draped on terrain or laid on the level, standing its own section clear of whichever. Neither height lands on
	// the ladder: a rung is coarser than the whole ride and rounds it away, which is a road laid in the ground.
	Vector3 Grounded( Vector2 point ) {
		var lifted = MathF.Max( 0f, lift );

		if ( draft.Conform && ArchTerrain.Sample( Owner.Scene, point, out var sampled ) ) {
			return Owner.Grid.Draped( new Vector3( point.x, point.y, sampled + draft.Seat( Owner.Kit ) + lifted ) );
		}

		// A level's height already carries the ground clearance, so only the camber is still owed.
		return Owner.Grid.Draped( new Vector3( point.x, point.y, Owner.LevelHeight + MathF.Max( 0f, draft.Camber ) + lifted ) );
	}

	protected override void DrawHover( Vector2 point ) {
		base.DrawHover( point );

		if ( nodes.Count == 0 ) {
			ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight ), "click to start the centreline" );

			return;
		}

		var line = new List<ArchCurveNode>( nodes ) { Node( Grounded( point ) ) };
		var curve = ArchCurve.Of( line );

		if ( !curve.IsUsable ) {
			return;
		}

		var frames = ArchRoadGen.Frames( Sketch( line, false ), curve );

		ArchGhost.Curve( curve );
		ArchGhost.Verges( curve, frames, draft.HalfWidth );

		if ( draft.Pavements != RoadSide.None ) {
			ArchGhost.Verges( curve, frames, draft.HalfWidth + draft.PavementWidth );
		}

		Gizmo.Draw.Color = ArchGhost.Accent;
		Gizmo.Draw.LineSphere( nodes[0].Position, Owner.Kit.GridSize * 0.5f );
		ArchGhost.Note( frames[^1].Position, $"{curve.Length / 12f:0.#} ft" );
	}

	ArchRoadPart Sketch( IReadOnlyList<ArchCurveNode> line, bool closed ) {
		var road = draft.Copy();

		road.Nodes = line.Select( node => node.Copy() ).ToList();
		road.Closed = closed;

		return road;
	}

	void Finish( bool closed ) {
		if ( nodes.Count >= 2 ) {
			var road = Sketch( nodes, closed );

			road.Id = Owner.Plan.AllocateId();
			road.Name = $"Road{Owner.Plan.Roads().Count + 1}";

			Owner.Plan.Units.Add( road );
			FinishDraft( road.Id );
			Owner.Commit( "Create Road" );
		}

		nodes.Clear();
	}

	static Vector2 Flat( ArchCurveNode node ) => new( node.Position.x, node.Position.y );

	// One list: the shape of the line, then which sides carry a pavement, a kerb and a barrier - every one of them
	// an exclusive answer, so the sequence runs itself down to the typed rows at the end. A barrier contributes its
	// own rows straight into this sequence rather than opening a second one.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );

		flow.Step( "Centreline", straight ? "Straight" : "Spline", straight ? "timeline" : "gesture", PickCentreline );

		flow.Step( "Pavement", draft.Pavements.ToString(), ArchRoadUi.SideGlyph( draft.Pavements ), step => {
			ArchRoadUi.Pavements( step.Layout, draft, step.Chose, null );
			ArchRoadUi.PavementSizes( step.Layout, draft, null );
		} );

		flow.Step( "Kerb", draft.Kerb.ToString(), ArchRoadUi.KerbGlyph( draft.Kerb ), step => {
			ArchRoadUi.Kerbs( step.Layout, draft, step.Chose, null );
			ArchRoadUi.KerbSizes( step.Layout, draft, null );
		} );

		flow.Step( "Barrier side", draft.Barriers.ToString(), ArchRoadUi.SideGlyph( draft.Barriers ), step =>
			ArchRoadUi.BarrierSide( step.Layout, draft, step.Chose, null ) );

		if ( draft.Barriers != RoadSide.None ) {
			ArchBarrierUi.Steps( flow, draft.Barrier, Refresh, null );
		}

		flow.Step( "Carriageway", $"{draft.Lanes} lanes", "road", step => ArchRoadUi.Carriageway( step.Layout, draft, null ) );

		flow.Step( "Markings", Marked, "linear_scale", step => ArchRoadUi.Markings( step.Layout, draft, Owner.Kit, null ) );

		flow.Step( "Terrain", TerrainName, draft.Conform ? "landscape" : "horizontal_rule", step =>
			ArchRoadUi.Terrain( step.Layout, draft, null ) );

		flow.Step( "Construction", $"{draft.Camber:0.##} camber", "tune", step =>
			ArchRoadUi.Construction( step.Layout, draft, null ) );
	}

	// Deck height rides with the centreline because it is where the NEXT clicks land, not a property of the section.
	void PickCentreline( ArchWorkflowStep step ) {
		using ( var shape = ArchIconGrid.In( step.Layout ) ) {
			shape.Pick( "Spline — eased through every node", "road_spline", "gesture", !straight,
				() => { straight = false; step.Chose(); } );

			shape.Pick( "Straight — a corner at every node", "road_straight", "timeline", straight,
				() => { straight = true; step.Chose(); } );
		}

		var deck = ArchPartUi.Number( "Deck height", lift, 0f, value => lift = value );

		deck.ToolTip = "Raise it before the middle nodes to carry a run over something, then span it with the Bridge tool.";
		step.Layout.Add( deck );
	}

	string Marked => ArchPartUi.Listed(
		(!string.IsNullOrWhiteSpace( draft.CentreLine ), "Centre"),
		(!string.IsNullOrWhiteSpace( draft.LaneLine ), "Lane"),
		(!string.IsNullOrWhiteSpace( draft.EdgeLine ), "Edge") );

	string TerrainName => ArchPartUi.Listed( (draft.Conform, "Follow"), (draft.Stamp, "Stamp") );
}