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

namespace Sunless.Architecture;

[Title( "Pipes & Cables" ), Icon( "plumbing" ), Group( "12" )]
public sealed class ArchPipeSubtool( ArchTool owner ) : ArchSubtool( owner ) {
	public override ArchSurface[] Surfaces => new[]
	{
		ArchSurface.Pipework, ArchSurface.Cabling, ArchSurface.Bracket, ArchSurface.Gutter, ArchSurface.Trim
	};

	public override ArchViewAxis[] Works => new[] { ArchViewAxis.Free, ArchViewAxis.Top, ArchViewAxis.Front, ArchViewAxis.Side };

	enum PipeGesture {
		Route,
		Stack
	}

	readonly ArchDownpipePart stack = new();
	readonly ArchPipePart draft = new();
	readonly List<ArchCurveNode> nodes = new();

	PipeGesture gesture = PipeGesture.Route;

	protected override bool UsesDrag => false;

	protected override bool TakesFreeClick => gesture == PipeGesture.Route;

	protected override bool Adjusts => true;

	protected override ArchKind? DraftKind => gesture == PipeGesture.Route ? ArchKind.Pipe : ArchKind.Downpipe;

	protected override string Title() => "Pipes & Cables";

	protected override string Advice() => gesture switch {
		PipeGesture.Stack => "Click near an eave. The stack taps the nearest gutter and hugs the wall below; a conduit skips the tap and drops on its brackets.",
		_ => "Click each control point of the route. Finish the run to build it; clicking an existing pipe node joins the runs and resolves the fitting there."
	};

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

	protected override void FinishRun() {
		if ( gesture == PipeGesture.Route ) {
			FinishRoute();
		}
	}

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

	ArchCurveNode Node( Vector3 point, Vector3 normal ) {
		return new ArchCurveNode {
			Position = point,
			Normal = normal,
			Mode = ArchTangentMode.Linear
		};
	}

	protected override void DrawHover( Vector2 point ) {
		if ( gesture == PipeGesture.Stack ) {
			DrawStack( point );

			return;
		}
	}

	protected override void DrawFreeHover() {
		if ( !Surface( out var point, out var normal ) ) {
			return;
		}

		var line = new List<ArchCurveNode>( nodes ) { Node( point, normal ) };
		var connected = ArchPipe.Connected( Owner.Plan, line );
		var preview = Previewed( connected );
		var shape = ArchPipeShape.Resolve( Owner.Plan, preview );

		ArchGhost.Path( shape.Path(), false );

		foreach ( var node in connected ) {
			Gizmo.Draw.LineSphere( node.Position, MathF.Max( 3f, draft.Radius ) );
		}

		if ( shape.Stands ) {
			ArchGhost.Note( shape.Path()[^1], $"{draft.Content.ToString().ToLowerInvariant()} · {shape.Length / 12f:0.#} ft{Stepping( shape )}" );
		}
	}

	protected override void OnFreeClick() {
		if ( !Surface( out var point, out var normal ) ) {
			return;
		}

		Owner.EnsureTarget();
		nodes.Add( Node( point, normal ) );
	}

	bool Surface( out Vector3 point, out Vector3 normal ) {
		var ray = Gizmo.CurrentRay;
		var face = Owner.FacesUnder( ray ).FirstOrDefault();

		if ( face is not null ) {
			var facing = Vector3.Dot( face.Normal, ray.Forward );
			var along = Vector3.Dot( face.Centre - ray.Position, face.Normal ) / facing;

			point = ray.Position + ray.Forward * along;
			normal = facing < 0f ? face.Normal : -face.Normal;

			return SeatOnSurface( ref point, normal );
		}

		var trace = Owner.Scene.Trace.Ray( ray.Position, ray.Position + ray.Forward * 32768f ).Run();

		if ( !trace.Hit ) {
			point = default;
			normal = default;

			return false;
		}

		point = trace.EndPosition;
		normal = trace.Normal;

		return SeatOnSurface( ref point, normal );
	}

	bool SeatOnSurface( ref Vector3 point, Vector3 normal ) {
		var snapped = Owner.Grid.CurveControl( point );

		point = snapped - normal * Vector3.Dot( snapped - point, normal );

		return !normal.IsNearZeroLength;
	}

	void DrawStack( Vector2 point ) {
		base.DrawHover( point );

		var sketch = ArchAsks.PlaceDownpipe( Owner.Plan, Owner.ActiveBuilding(), Owner.Kit, point, stack.Profile, stack.Shoe, stack.Run );

		if ( sketch is null ) {
			return;
		}

		var outlet = new Vector3( sketch.Outlet.x, sketch.Outlet.y, sketch.TopHeight );
		var foot = new Vector3( sketch.Wall.x, sketch.Wall.y, sketch.BaseHeight );

		Gizmo.Draw.Line( outlet, new Vector3( sketch.Wall.x, sketch.Wall.y, sketch.TopHeight - 8f ) );
		Gizmo.Draw.Line( new Vector3( sketch.Wall.x, sketch.Wall.y, sketch.TopHeight - 8f ), foot );
		Gizmo.Draw.LineCircle( outlet, Vector3.Up, 4f );
		ArchGhost.Note( outlet + Vector3.Up * 8f, stack.Profile );
	}

	string Stepping( ArchPipeShape shape ) => shape.Blocks.Count == 0 ? "" : $" · avoids {shape.Blocks.Count}";

	ArchPipePart Previewed( List<ArchCurveNode> route ) {
		var along = route.Count >= 2 ? route[^1].Position - route[0].Position : Vector3.Forward;

		return new ArchPipePart {
			Content = draft.Content,
			Mount = Mounted( route ),
			Nodes = route,
			Outward = Facing( route, along ),
			Diameter = draft.Diameter,
			Materials = draft.Materials,
			RunMaterialIndex = draft.RunMaterialIndex,
			FittingMaterialIndex = draft.FittingMaterialIndex,
			SupportMaterialIndex = draft.SupportMaterialIndex,
			Dodge = draft.Dodge,
			Clearance = draft.Clearance,
			Supports = false,
			Detail = draft.Detail
		};
	}

	static PipeMount Mounted( IReadOnlyList<ArchCurveNode> route ) {
		var normal = route.Count == 0 ? Vector3.Up : route[0].Normal;

		if ( MathF.Abs( normal.z ) < 0.5f ) {
			return PipeMount.Wall;
		}

		return normal.z < 0f ? PipeMount.Ceiling : PipeMount.Floor;
	}

	Vector2 Facing( IReadOnlyList<ArchCurveNode> route, Vector3 along ) {
		var normal = route.Select( node => node.Normal ).Aggregate( Vector3.Zero, ( total, entry ) => total + entry ).Normal;

		if ( MathF.Abs( normal.z ) < 0.5f ) {
			return new Vector2( normal.x, normal.y ).Normal;
		}

		var centre = route.Count == 0
			? Vector2.Zero
			: route.Select( node => new Vector2( node.Position.x, node.Position.y ) ).Aggregate( Vector2.Zero, ( total, point ) => total + point ) / route.Count;

		return ArchPipe.Facing( Owner.Plan, centre, MathF.Abs( along.x ) >= MathF.Abs( along.y ) );
	}

	protected override void OnClick( Vector2 point ) {
		if ( gesture == PipeGesture.Route ) {
			return;
		}

		var building = Owner.ActiveBuilding();
		var pipe = ArchAsks.PlaceDownpipe( Owner.Plan, building, Owner.Kit, point, stack.Profile, stack.Shoe, stack.Run );

		if ( pipe is null ) {
			Log.Info( "Architecture: no eave near that click — a stack taps a gutter run, so there has to be a roof over it." );

			return;
		}

		building.Downpipes.Add( pipe );
		FinishDraft( pipe.Id );
		Owner.Commit( "Place Downpipe" );
	}

	void FinishRoute() {
		var building = Owner.ActiveBuilding();
		var placed = Previewed( nodes );

		if ( ArchPipe.PlaceRoute( Owner.Plan, building, Owner.Level, nodes, placed ) is not { } route ) {
			nodes.Clear();

			return;
		}

		FinishDraft( route.Id );
		Owner.Picked = new ArchSelection { Item = route, Building = building };
		Drew( route );
		Owner.Commit( "Place Pipe Route" );
		nodes.Clear();
	}

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

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

	// One list: the gesture first, then the questions that gesture has. A route is asked what runs in it, how fat
	// it is, what it does where something crosses and what hangs it - all picks - before the two typed rows that
	// can never answer. A stack is three picks and nothing else.
	protected override void BuildOptions( ToolSidebarWidget panel ) {
		if ( Editing() is { } picked ) {
			panel.Layout.Add( ArchSidebarLayout.Advice( $"Editing {picked.Describe()}. What is tuned here is also what the next run is drawn from." ) );
		}

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

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

		if ( gesture == PipeGesture.Stack ) {
			StackSteps( flow );

			return;
		}

		RouteSteps( panel, flow );
	}

	string GestureName => gesture == PipeGesture.Stack ? "Stack" : "Route";

	string GestureGlyph => gesture == PipeGesture.Stack ? "water_drop" : "timeline";

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

		grid.Pick( "Route — click a centreline through shared nodes", "pipe_run", "timeline",
			gesture == PipeGesture.Route, () => Switch( PipeGesture.Route, step ) );

		grid.Pick( "Stack — click an eave for a rainwater downpipe", "pipe_stack", "water_drop",
			gesture == PipeGesture.Stack, () => Switch( PipeGesture.Stack, step ) );
	}

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

		nodes.Clear();
		CancelDraft();
		BeginDraft();

		step.Chose();
	}

	// The picked part when it is this gesture's kind, else the seed for the next drag - the same steps either
	// way, so a tuned run and the next one drawn after it cannot say different things.
	ArchPipePart Routed => Owner.Picked?.Item as ArchPipePart ?? draft;

	ArchDownpipePart Stacked => Owner.Picked?.Item as ArchDownpipePart ?? stack;

	void StackSteps( ArchWorkflow flow ) {
		var pipe = Stacked;

		flow.Step( "Run", pipe.Run == PipeRun.Conduit ? "Conduit" : "Rainwater",
			pipe.Run == PipeRun.Conduit ? "power_input" : "water_drop", step => PickRun( pipe, step ) );

		flow.Step( "Profile", pipe.Profile, ArchIcons.ProfileGlyph, step => PickProfile( pipe, step ) );

		flow.Step( "Foot", pipe.Shoe ? "Shoe" : "Square", pipe.Shoe ? "south" : "block", step => PickFoot( pipe, step ) );
	}

	void PickRun( ArchDownpipePart pipe, ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Rainwater — taps the gutter and swans over to the wall", "pipe_rainwater", "water_drop",
			pipe.Run == PipeRun.Rainwater, () => { pipe.Run = PipeRun.Rainwater; Settle( pipe, step ); } );

		grid.Pick( "Conduit — a bracketed service run straight down the elevation", "pipe_conduit", "power_input",
			pipe.Run == PipeRun.Conduit, () => { pipe.Run = PipeRun.Conduit; Settle( pipe, step ); } );
	}

	void PickProfile( ArchDownpipePart pipe, ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		foreach ( var entry in Owner.Kit.Profiles.Where( entry => entry.Name.Contains( "pipe", StringComparison.OrdinalIgnoreCase ) ) ) {
			var name = entry.Name;

			grid.Pick( name, ArchIcons.ProfileSlug( name ), ArchIcons.ProfileGlyph, pipe.Profile == name,
				() => { pipe.Profile = name; Settle( pipe, step ); } );
		}
	}

	// A shoe throws the water clear of the wall; without one the stack ends square. Two states of one answer, so
	// the row advances rather than leaving a lone tick at the end of the list.
	void PickFoot( ArchDownpipePart pipe, ArchWorkflowStep step ) {
		using var grid = ArchIconGrid.In( step.Layout );

		grid.Pick( "Shoe — a swan neck at the bottom throwing the water clear", "opt_shoe", "south",
			pipe.Shoe, () => { pipe.Shoe = true; Settle( pipe, step ); } );

		grid.Pick( "Square — the stack ends flush with the ground", "opt_no_shoe", "block",
			!pipe.Shoe, () => { pipe.Shoe = false; Settle( pipe, step ); } );
	}

	void Settle( ArchDownpipePart pipe, ArchWorkflowStep step ) {
		if ( !ReferenceEquals( pipe, stack ) ) {
			Owner.Touch( "Edit Downpipe" );
		}

		step.Chose();
	}

	void RouteSteps( ToolSidebarWidget panel, ArchWorkflow flow ) {
		var part = Routed;
		var changed = ReferenceEquals( part, draft ) ? null : (Action)(() => Owner.Touch( "Edit Pipe" ));

		flow.Step( "Content", part.Content.ToString(), ContentGlyph( part.Content ), step =>
			ArchPipeUi.Content( step.Layout, part, step.Chose, changed ) );

		flow.Step( "Section", $"{part.Diameter:0.##} in", "radio_button_unchecked", step =>
			ArchPipeUi.Section( step.Layout, part, step.Chose, changed ) );

		flow.Step( "Crossings", CrossingName( part.Dodge ), CrossingGlyph( part.Dodge ), step =>
			ArchPipeUi.Crossings( step.Layout, part, step.Chose, changed ) );

		flow.Step( "Supports", part.Supports ? part.SupportKind.ToString() : "None", part.Supports ? "table_rows" : "block", step =>
			ArchPipeUi.Supports( step.Layout, part, step.Chose, changed ) );

		// The material rows open asset pickers, so they are parented to the SIDEBAR - a dialog hung off a step
		// dies with the step on the next refresh.
		flow.Step( "Materials", $"{part.Materials.Count} slot{(part.Materials.Count == 1 ? "" : "s")}", "image", step =>
			ArchPipeUi.Materials( panel, step.Layout, part, Refresh, changed ) );

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

	static string ContentGlyph( PipeContent content ) => content switch {
		PipeContent.Conduit => "power_input",
		PipeContent.Cable => "cable",
		PipeContent.Tray => "view_stream",
		PipeContent.Duct => "crop_square",
		_ => "water_damage"
	};

	static string CrossingName( PipeDodge dodge ) => dodge switch {
		PipeDodge.Away => "Over",
		PipeDodge.Toward => "Under",
		PipeDodge.Side => "Around",
		_ => "Straight through"
	};

	static string CrossingGlyph( PipeDodge dodge ) => dodge switch {
		PipeDodge.Away => "trending_up",
		PipeDodge.Toward => "trending_down",
		PipeDodge.Side => "swap_horiz",
		_ => "close"
	};

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