Editor/Stair/ArchStairCanvas.cs
using System;
using System.Collections.Generic;
using Editor;

namespace Sunless.Architecture;

public enum ArchStairView {
	Plan,
	Along,
	Across
}

enum ArchStairGrip {
	Head,
	Left,
	Right,
	Lift,
	Body,
	Bend,
	Rail,
	RailFoot,
	RailHead
}

static class ArchStairInk {
	public static readonly Color Walls = new( 0.92f, 0.55f, 0.18f );
	public static readonly Color Floors = new( 0.88f, 0.26f, 0.22f );
	public static readonly Color Flight = new( 0.30f, 0.58f, 0.82f );
	public static readonly Color Platform = new( 0.86f, 0.66f, 0.26f );
	public static readonly Color Balustrade = new( 0.94f, 0.84f, 0.36f );
	public static readonly Color Handrail = new( 0.62f, 0.78f, 0.52f );
	public static readonly Color Lift = new( 0.52f, 0.86f, 0.42f );
	public static readonly Color Reach = new( 0.42f, 0.72f, 0.92f );
	public static readonly Color Head = new( 0.74f, 0.54f, 0.94f );
	public static readonly Color Bend = new( 0.88f, 0.46f, 0.72f );
	public static readonly Color Flow = new( 0.80f, 0.88f, 0.96f );
}

public sealed class ArchStairCanvas : Widget {
	const float Padding = 28f;
	const float GripSize = 9f;
	const float BandHeight = 142f;

	const float Elbow = 20f;

	// The bend canvas ratchets finer than ArchGridService.AngleStep on purpose: a flight turns to meet a landing,
	// not to sit on the plan's angle ladder.
	const float BendStep = 5f;

	const float StationReach = 6f;

	readonly ArchTool tool;
	readonly Func<ArchStairPart> subject;
	readonly Action changed;
	readonly Action committed;

	readonly List<(ArchStairGrip Kind, int Step, int Guard, Vector2 At)> grips = new();

	readonly List<(int Step, Rect At)> walked = new();
	readonly List<(int Step, Vector2 At)> pins = new();

	float scale = 1f;
	Vector2 origin;

	Vector2 bandFoot;
	float bandZoom = 1f;

	int held = -1;
	int heldGuard = -1;
	ArchStairGrip heldKind;
	bool dragging;

	bool profiling;
	int joining = -1;
	Vector2 hover;

	ArchStairPart holding;

	ArchRoom home;

	public ArchStairView View { get; set; } = ArchStairView.Plan;

	public bool Profile { get; set; } = true;

	public bool Surrounds { get; set; } = true;

	public int Picked { get; set; }

	public int PickedGuard { get; set; } = -1;

	public Action<int> Selected { get; set; }

	public ArchStairCanvas( Widget parent, ArchTool tool, Func<ArchStairPart> subject, Action changed, Action committed ) : base( parent ) {
		this.tool = tool;
		this.subject = subject;
		this.changed = changed;
		this.committed = committed;

		MinimumSize = new Vector2( 420, 320 );
		MouseTracking = true;
		FocusMode = FocusMode.Click;
	}

	Vector2 Flat( float along, float across, float lift ) => View switch {
		ArchStairView.Along => new Vector2( along, -lift ),
		ArchStairView.Across => new Vector2( across, -lift ),
		_ => new Vector2( along, across )
	};

	Vector2 Screen( Vector2 flat ) => origin + flat * scale;

	Vector2 Local( Vector2 screen ) => (screen - origin) / MathF.Max( 0.0001f, scale );

	void Frame( ArchStairPart stair, Rect page ) {
		var core = stair.Core;
		var climb = stair.TotalRise;

		var corners = new List<Vector2>
		{
			Flat( 0f, 0f, 0f ),
			Flat( core.Length, core.Width, 0f ),
			Flat( 0f, 0f, climb ),
			Flat( core.Length, core.Width, climb )
		};

		// The room the stair stands in is framed with it, so the drawing opens on the stair AND on what it has to
		// fit into. Only the room - framing the whole plan would shrink the thing being worked on to a smudge.
		if ( Surrounds && View == ArchStairView.Plan ) {
			foreach ( var point in Standing( stair ) ) {
				corners.Add( Flat( point.x, point.y, 0f ) );
			}
		}

		var min = corners[0];
		var max = corners[0];

		foreach ( var corner in corners ) {
			min = new Vector2( MathF.Min( min.x, corner.x ), MathF.Min( min.y, corner.y ) );
			max = new Vector2( MathF.Max( max.x, corner.x ), MathF.Max( max.y, corner.y ) );
		}

		var span = max - min;
		var room = new Vector2( MathF.Max( 1f, page.Width - Padding * 2f ), MathF.Max( 1f, page.Height - Padding * 2f ) );

		scale = MathF.Min( room.x / MathF.Max( 1f, span.x ), room.y / MathF.Max( 1f, span.y ) );
		origin = new Vector2( page.Left + Padding, page.Top + Padding ) - min * scale
			+ (room - span * scale) * 0.5f;
	}


	protected override void OnPaint() {
		Paint.ClearPen();
		Paint.SetBrush( Theme.ControlBackground.Darken( 0.25f ) );
		Paint.DrawRect( LocalRect );

		if ( subject() is not { Core: not null } stair || stair.Lanes.Count == 0 ) {
			Paint.SetPen( Theme.TextControl.WithAlpha( 0.5f ) );
			Paint.DrawText( LocalRect, "no stair", TextFlag.Center );

			return;
		}

		var band = Strip();

		grips.Clear();
		walked.Clear();
		pins.Clear();

		home = ArchStairSteps.Home( tool?.Plan, stair );

		Frame( stair, new Rect( 0f, 0f, Width, Height - band.Height ) );

		PaintContext( stair );
		PaintShaft( stair );

		// Hoisted, not asked per row: both walk every step, so reaching through one inside the loop is how an
		// honest table becomes an n-squared allocation sixty times a second.
		var climbs = ArchStairLanes.Climbs( stair.Core, stair.Lanes );
		var bases = ArchStairEdges.Bases( stair );

		for ( var index = 0; index < stair.Lanes.Count; index++ ) {
			PaintStep( stair, index, climbs, bases );
		}

		PaintFlow( stair, climbs, bases );
		PaintGrips();
		PaintLegend();
		PaintJoining( stair );

		if ( band.Height > 1f ) {
			PaintProfile( stair, band, climbs, bases );
		}
	}

	Rect Strip() => Profile && Height > BandHeight * 2f
		? new Rect( 0f, Height - BandHeight, Width, BandHeight )
		: new Rect( 0f, Height, Width, 0f );

	void PaintContext( ArchStairPart stair ) {
		if ( tool?.Plan is null || !Surrounds ) {
			return;
		}

		if ( View == ArchStairView.Plan ) {
			PaintRooms( stair );
			PaintWalls( stair );

			return;
		}

		PaintStoreys( stair );
	}

	int Storey => home?.Floor ?? tool?.Level ?? 0;

	IEnumerable<Vector2> Standing( ArchStairPart stair ) {
		if ( home is not { HasFootprint: true } room ) {
			return Array.Empty<Vector2>();
		}

		return room.Footprint.Select( point => ArchStairLanes.Local( stair.Core, point ) );
	}

	void PaintRooms( ArchStairPart stair ) {
		var core = stair.Core;
		var level = Storey;

		foreach ( var room in tool.Plan.AllRooms().Where( candidate => candidate.Floor == level && candidate.HasFootprint ) ) {
			var loop = room.Footprint
				.Select( point => ArchStairLanes.Local( core, point ) )
				.Select( local => Screen( Flat( local.x, local.y, 0f ) ) )
				.ToArray();

			Paint.ClearPen();
			Paint.SetBrush( ArchStairInk.Floors.WithAlpha( 0.07f ) );
			Paint.DrawPolygon( loop );

			Paint.ClearBrush();
			Paint.SetPen( ArchStairInk.Floors.WithAlpha( 0.4f ), 1f );

			for ( var corner = 0; corner < loop.Length; corner++ ) {
				Paint.DrawLine( loop[corner], loop[(corner + 1) % loop.Length] );
			}
		}
	}

	void PaintWalls( ArchStairPart stair ) {
		var core = stair.Core;
		var thickness = MathF.Max( 1f, tool.Kit.WallThickness );

		Paint.ClearPen();
		Paint.SetBrush( ArchStairInk.Walls.WithAlpha( 0.5f ) );

		foreach ( var wall in tool.Plan.WallsOn( Storey ) ) {
			var from = ArchStairLanes.Local( core, wall.Start );
			var to = ArchStairLanes.Local( core, wall.End );
			var span = to - from;

			if ( span.IsNearZeroLength ) {
				continue;
			}

			var half = new Vector2( -span.Normal.y, span.Normal.x ) * (wall.Thickness > 0f ? wall.Thickness : thickness) * 0.5f;

			Paint.DrawPolygon(
				Screen( Flat( from.x - half.x, from.y - half.y, 0f ) ),
				Screen( Flat( to.x - half.x, to.y - half.y, 0f ) ),
				Screen( Flat( to.x + half.x, to.y + half.y, 0f ) ),
				Screen( Flat( from.x + half.x, from.y + half.y, 0f ) ) );
		}
	}

	void PaintStoreys( ArchStairPart stair ) {
		Paint.SetDefaultFont( 7 );

		foreach ( var (level, lift) in Floors( stair ) ) {
			var line = Screen( Flat( 0f, 0f, lift ) ).y;

			Paint.ClearBrush();
			Paint.SetPen( ArchStairInk.Floors.WithAlpha( 0.5f ), 1f );
			Paint.DrawLine( new Vector2( 0f, line ), new Vector2( Width, line ) );

			Paint.SetPen( ArchStairInk.Floors.WithAlpha( 0.85f ) );
			Paint.DrawText( new Rect( 4f, line - 12f, 120f, 12f ), $"floor {level} · {stair.BaseHeight + lift:0}", TextFlag.LeftTop );
		}
	}

	IEnumerable<(int Level, float Lift)> Floors( ArchStairPart stair ) {
		var storey = MathF.Max( 1f, tool.StoreyHeight );
		var clearance = tool.Kit.GroundClearance;
		var lowest = (int)MathF.Floor( (stair.BaseHeight - clearance) / storey );
		var reach = lowest + (int)MathF.Ceiling( stair.TotalRise / storey ) + 1;

		for ( var level = lowest; level <= reach; level++ ) {
			yield return (level, level * storey + clearance - stair.BaseHeight);
		}
	}

	void PaintShaft( ArchStairPart stair ) {
		var core = stair.Core;
		var climb = stair.TotalRise;

		var a = Screen( Flat( 0f, 0f, 0f ) );
		var b = Screen( Flat( core.Length, core.Width, View == ArchStairView.Plan ? 0f : climb ) );

		Paint.ClearPen();
		Paint.SetBrush( Theme.ControlBackground.WithAlpha( 0.35f ) );
		Paint.DrawRect( Rect.FromPoints( a, b ) );

		Paint.ClearBrush();
		Paint.SetPen( Theme.TextControl.WithAlpha( 0.16f ), 1f );
		Paint.DrawRect( Rect.FromPoints( a, b ) );
	}

	void PaintStep( ArchStairPart stair, int index, float[] climbs, float[] bases ) {
		var lane = stair.Lanes[index];
		var seat = bases[index];
		var climb = climbs[index];
		var picked = index == Picked;

		var foot = seat - stair.BaseHeight;
		var head = foot + (lane.Climbs ? climb : 0f);
		var body = Body( lane, foot, head );
		var bounds = Rect.FromPoints( body[0], body[0] );

		foreach ( var corner in body ) {
			bounds.Add( corner );
		}

		Paint.ClearPen();
		Paint.SetBrush( Tint( lane, picked ) );
		Paint.DrawPolygon( body );

		if ( lane.Climbs ) {
			PaintTreads( stair, lane, index, foot, head );
		}

		Paint.ClearBrush();
		Paint.SetPen( picked ? Theme.Primary : Theme.TextControl.WithAlpha( 0.45f ), picked ? 2f : 1f );

		for ( var corner = 0; corner < body.Length; corner++ ) {
			Paint.DrawLine( body[corner], body[(corner + 1) % body.Length] );
		}

		PaintLabel( bounds, lane, index, climb );
		PaintRails( lane, index );
		Offer( stair, index, climbs, bases );
	}

	Vector2[] Body( ArchStairLane lane, float foot, float head ) {
		if ( View == ArchStairView.Plan ) {
			return lane.Corners().Select( corner => Screen( Flat( corner.x, corner.y, 0f ) ) ).ToArray();
		}

		var (from, to) = Reach( lane );

		return new[]
		{
			Screen( Flat( from.x, from.y, foot ) ),
			Screen( Flat( to.x, to.y, foot ) ),
			Screen( Flat( to.x, to.y, head ) ),
			Screen( Flat( from.x, from.y, head ) )
		};
	}

	static (Vector2 From, Vector2 To) Reach( ArchStairLane lane ) {
		var frame = lane.Frame;

		return (frame.Flat( 0f, lane.Width * 0.5f ), frame.Flat( lane.Length, lane.Width * 0.5f ));
	}

	static Color Tint( ArchStairLane lane, bool picked ) {
		return Hue( lane ).WithAlpha( picked ? 0.55f : 0.30f );
	}

	static Color Hue( ArchStairLane lane ) => lane.Climbs ? ArchStairInk.Flight : ArchStairInk.Platform;

	void PaintTreads( ArchStairPart stair, ArchStairLane lane, int index, float foot, float head ) {
		var going = stair.StepGoing > 0.5f ? stair.StepGoing : 12f;
		var riser = stair.StepRise > 0.5f ? stair.StepRise : 8f;
		var steps = ArchStairLanes.Steps( head - foot, riser, lane.Length, going );

		Paint.ClearBrush();
		Paint.SetPen( Theme.TextControl.WithAlpha( 0.22f ), 1f );

		for ( var step = 1; step < steps; step++ ) {
			var part = step / (float)steps;

			var (from, to) = Nosing( lane, part, foot, head );

			Paint.DrawLine( Screen( from ), Screen( to ) );
		}
	}

	(Vector2 From, Vector2 To) Nosing( ArchStairLane lane, float part, float foot, float head ) {
		var lift = View == ArchStairView.Plan ? 0f : foot + (head - foot) * part;
		var frame = lane.Frame;
		var along = lane.Length * part;

		var from = frame.Flat( along, 0f );
		var to = frame.Flat( along, lane.Width );

		return (Flat( from.x, from.y, lift ), Flat( to.x, to.y, lift ));
	}

	void PaintLabel( Rect body, ArchStairLane lane, int index, float climb ) {
		if ( body.Width < 34f || body.Height < 16f ) {
			return;
		}

		Paint.SetPen( Theme.TextControl.WithAlpha( 0.85f ) );
		Paint.SetDefaultFont( 8 );

		var caption = lane.Climbs
			? $"{index + 1}  {lane.Length:0} run · {climb:0} climb"
			: $"{index + 1}  platform {lane.Length:0} deep";

		Paint.DrawText( body, caption, TextFlag.Center );
	}

	void PaintFlow( ArchStairPart stair, float[] climbs, float[] bases ) {
		if ( View != ArchStairView.Plan ) {
			return;
		}

		for ( var index = 0; index < stair.Lanes.Count; index++ ) {
			var lane = stair.Lanes[index];
			var frame = lane.Frame;
			var foot = Screen( Flat( frame.Flat( 0f, lane.Width * 0.5f ).x, frame.Flat( 0f, lane.Width * 0.5f ).y, 0f ) );
			var head = Screen( Flat( frame.Flat( lane.Length, lane.Width * 0.5f ).x, frame.Flat( lane.Length, lane.Width * 0.5f ).y, 0f ) );

			Paint.ClearBrush();
			Paint.SetPen( ArchStairInk.Flow.WithAlpha( index == Picked ? 0.9f : 0.4f ), index == Picked ? 2f : 1.5f );
			Paint.DrawLine( foot, head );

			Arrow( foot, head, index == Picked ? 9f : 7f );

			if ( index + 1 >= stair.Lanes.Count ) {
				continue;
			}

			var next = stair.Lanes[index + 1];
			var stands = next.Frame.Flat( 0f, next.Width * 0.5f );
			var onto = Screen( Flat( stands.x, stands.y, 0f ) );

			// From the JOINT, not from the head: a step joined to a flank leaves from that flank, and an arrow drawn
			// off the head would point at a corner nothing happens at.
			var meets = Nearest( lane, next, stands );
			var joint = Screen( Flat( meets.x, meets.y, 0f ) );
			var apart = (joint - onto).Length > GripSize;

			Paint.SetPen( apart ? ArchStairInk.Floors : ArchStairInk.Flow.WithAlpha( 0.35f ), apart ? 2f : 1f );
			Paint.DrawLine( joint, onto );

			if ( apart ) {
				Arrow( joint, onto, 7f );
			}
		}
	}

	static Vector2 Nearest( ArchStairLane below, ArchStairLane next, Vector2 foot ) {
		return ArchStairSteps.Stations( below, next.Width )
			.OrderBy( station => (station.At - foot).Length )
			.Select( station => station.At )
			.FirstOrDefault();
	}

	static void Arrow( Vector2 from, Vector2 to, float size ) {
		var span = to - from;

		if ( span.Length < size ) {
			return;
		}

		var along = span.Normal;
		var across = new Vector2( -along.y, along.x );

		Paint.DrawLine( to, to - along * size + across * size * 0.5f );
		Paint.DrawLine( to, to - along * size - across * size * 0.5f );
	}

	void PaintJoining( ArchStairPart stair ) {
		if ( joining < 1 || joining >= stair.Lanes.Count ) {
			return;
		}

		var ring = stair.Lanes[joining - 1].Corners()
			.Select( corner => Screen( Flat( corner.x, corner.y, 0f ) ) )
			.ToArray();

		Paint.ClearPen();
		Paint.SetBrush( ArchStairInk.Flow.WithAlpha( 0.15f ) );
		Paint.DrawPolygon( ring );

		Paint.ClearBrush();
		Paint.SetPen( ArchStairInk.Flow, 2f );

		for ( var corner = 0; corner < ring.Length; corner++ ) {
			Paint.DrawLine( ring[corner], ring[(corner + 1) % ring.Length] );
		}

		foreach ( var station in Stations( stair ) ) {
			var hot = Box( station.At ).Grow( StationReach ).IsInside( hover );

			Paint.ClearPen();
			Paint.SetBrush( ArchStairInk.Flow.WithAlpha( hot ? 1f : 0.7f ) );
			Paint.DrawRect( Box( station.At ).Grow( hot ? 3f : 1f ), 2 );
		}

		Paint.SetDefaultFont( 8 );
		Paint.SetPen( ArchStairInk.Flow );
		Paint.DrawText( new Rect( hover.x + 12f, hover.y - 8f, 260f, 16f ),
			$"click a box to join step {joining + 1} there", TextFlag.LeftCenter );
	}

	IEnumerable<(ArchStairStation Station, Vector2 At)> Stations( ArchStairPart stair ) {
		var lane = stair.Lanes[joining];

		return ArchStairSteps.Stations( stair.Lanes[joining - 1], lane.Width )
			.Select( station => (station, Screen( Flat( station.At.x, station.At.y, 0f ) )) );
	}

	void Offer( ArchStairPart stair, int index, float[] climbs, float[] bases ) {
		if ( index != Picked ) {
			return;
		}

		var lane = stair.Lanes[index];
		var frame = lane.Frame;

		if ( View == ArchStairView.Plan ) {
			grips.Add( (ArchStairGrip.Head, index, -1, frame.Flat( lane.Length, lane.Width * 0.5f )) );
			grips.Add( (ArchStairGrip.Left, index, -1, frame.Flat( lane.Length * 0.5f, lane.Width )) );
			grips.Add( (ArchStairGrip.Right, index, -1, frame.Flat( lane.Length * 0.5f, 0f )) );

			// EVERY step drags, and the ones around it give way rather than being passed through - so a stair is
			// moved by taking hold of it, and the first step carries the whole climb with it.
			grips.Add( (ArchStairGrip.Body, index, -1, frame.Flat( lane.Length * 0.5f, lane.Width * 0.5f )) );

			// Standing off the head, because it is the angle the step LEAVES at - dragging it round is how a
			// segment of a curve is aimed once the sweep has been cut.
			if ( lane.Climbs ) {
				grips.Add( (ArchStairGrip.Bend, index, -1, frame.Flat( lane.Length + Elbow, lane.Width * 0.5f )) );
			}

			return;
		}

		var top = bases[index] - stair.BaseHeight + (lane.Climbs ? climbs[index] : 0f);
		var middle = frame.Flat( lane.Length * 0.5f, lane.Width * 0.5f );

		grips.Add( (ArchStairGrip.Lift, index, -1, Flat( middle.x, middle.y, top )) );
	}

	void PaintRails( ArchStairLane lane, int index ) {
		if ( View != ArchStairView.Plan ) {
			return;
		}

		for ( var guard = 0; guard < lane.Guards.Count; guard++ ) {
			var rail = lane.Guards[guard];
			var (from, to) = RailLine( lane, rail );
			var picked = index == Picked && guard == PickedGuard;

			Paint.ClearBrush();
			Paint.SetPen( rail.Railing == StairRailing.Handrail
				? ArchStairInk.Handrail
				: ArchStairInk.Balustrade, picked ? 4f : 2.5f );

			Paint.DrawLine( Screen( Flat( from.x, from.y, 0f ) ), Screen( Flat( to.x, to.y, 0f ) ) );

			grips.Add( (ArchStairGrip.RailFoot, index, guard, from) );
			grips.Add( (ArchStairGrip.RailHead, index, guard, to) );
			grips.Add( (ArchStairGrip.Rail, index, guard, (from + to) * 0.5f) );
		}
	}

	static (Vector2 From, Vector2 To) RailLine( ArchStairLane lane, ArchStairGuardPart rail ) {
		var frame = lane.Frame;

		if ( rail.Edge == StairEdge.Head ) {
			return (frame.Flat( lane.Length, 0f ), frame.Flat( lane.Length, lane.Width ));
		}

		var hand = rail.Edge == StairEdge.Left ? lane.Width : 0f;

		return (frame.Flat( lane.Length * rail.Start, hand ), frame.Flat( lane.Length * rail.End, hand ));
	}

	static float Fraction( ArchStairLane lane, Vector2 local ) {
		return Math.Clamp( ArchStairEdges.Along( lane, local ) / MathF.Max( 1f, lane.Length ), 0f, 1f );
	}

	void PaintGrips() {
		foreach ( var grip in grips ) {
			var at = Screen( View == ArchStairView.Plan
				? Flat( grip.At.x, grip.At.y, 0f )
				: grip.At );

			var hot = grip.Step == Picked;

			var hue = grip.Kind switch {
				ArchStairGrip.Head => ArchStairInk.Head,
				ArchStairGrip.Lift => ArchStairInk.Lift,
				ArchStairGrip.Body => ArchStairInk.Platform,
				ArchStairGrip.Bend => ArchStairInk.Bend,
				ArchStairGrip.Rail or ArchStairGrip.RailFoot or ArchStairGrip.RailHead => ArchStairInk.Balustrade,
				_ => ArchStairInk.Reach
			};

			Paint.ClearPen();
			Paint.SetBrush( hue.WithAlpha( hot ? 1f : 0.65f ) );

			Paint.DrawRect( Box( at ), 2 );
		}
	}

	static Rect Box( Vector2 at ) => new( at.x - GripSize * 0.5f, at.y - GripSize * 0.5f, GripSize, GripSize );

	static readonly (Color Hue, string Name)[] Keys =
	{
		(ArchStairInk.Flight, "flight"),
		(ArchStairInk.Platform, "platform"),
		(ArchStairInk.Balustrade, "railing"),
		(ArchStairInk.Walls, "walls"),
		(ArchStairInk.Floors, "floors")
	};

	void PaintLegend() {
		var top = 8f;

		Paint.SetDefaultFont( 7 );

		foreach ( var key in Keys ) {
			Paint.ClearPen();
			Paint.SetBrush( key.Hue.WithAlpha( 0.85f ) );
			Paint.DrawRect( new Rect( Width - 62f, top + 3f, 8f, 8f ), 2 );

			Paint.SetPen( Theme.TextControl.WithAlpha( 0.5f ) );
			Paint.DrawText( new Rect( Width - 50f, top, 44f, 14f ), key.Name, TextFlag.LeftCenter );

			top += 13f;
		}
	}

	void PaintProfile( ArchStairPart stair, Rect band, float[] climbs, float[] bases ) {
		Paint.ClearPen();
		Paint.SetBrush( Theme.ControlBackground.Darken( 0.45f ) );
		Paint.DrawRect( band );

		Paint.ClearBrush();
		Paint.SetPen( Theme.TextControl.WithAlpha( 0.14f ), 1f );
		Paint.DrawLine( new Vector2( band.Left, band.Top ), new Vector2( band.Right, band.Top ) );

		var run = 0f;

		foreach ( var lane in stair.Lanes ) {
			run += lane.Length;
		}

		var climb = MathF.Max( 1f, stair.TotalRise );
		var left = band.Left + 58f;
		var seat = band.Bottom - 20f;

		bandZoom = MathF.Min( (band.Right - 18f - left) / MathF.Max( 1f, run ), (seat - band.Top - 22f) / climb );
		bandFoot = new Vector2( left, seat );

		PaintFloorLines( stair, band );
		PaintClimbNote( stair, band, run, PaintWalked( stair, band, climbs, bases ) );
	}

	Vector2 Profiled( float along, float lift ) => new( bandFoot.x + along * bandZoom, bandFoot.y - lift * bandZoom );

	void PaintFloorLines( ArchStairPart stair, Rect band ) {
		Paint.SetDefaultFont( 7 );

		foreach ( var (level, lift) in Floors( stair ) ) {
			var line = Profiled( 0f, lift ).y;

			if ( line < band.Top + 12f || line > band.Bottom - 6f ) {
				continue;
			}

			Paint.ClearBrush();
			Paint.SetPen( ArchStairInk.Floors.WithAlpha( 0.45f ), 1f );
			Paint.DrawLine( new Vector2( band.Left + 54f, line ), new Vector2( band.Right - 8f, line ) );

			Paint.SetPen( ArchStairInk.Floors.WithAlpha( 0.9f ) );
			Paint.DrawText( new Rect( band.Left + 6f, line - 7f, 44f, 14f ),
				$"{level} · {stair.BaseHeight + lift:0}", TextFlag.RightCenter );
		}
	}

	float PaintWalked( ArchStairPart stair, Rect band, float[] climbs, float[] bases ) {
		var along = 0f;
		var carried = Vector2.Zero;
		var arrival = 0f;

		for ( var index = 0; index < stair.Lanes.Count; index++ ) {
			var lane = stair.Lanes[index];
			var foot = bases[index] - stair.BaseHeight;
			var head = foot + (lane.Climbs ? climbs[index] : 0f);
			var picked = index == Picked;
			var start = Profiled( along, foot );

			walked.Add( (index, new Rect( start.x, band.Top, MathF.Max( 2f, lane.Length * bandZoom ), band.Height )) );

			// A pinned platform lifts the climb before it stands, so the walking line jumps. Drawn, because a split
			// level nobody can see is a split level nobody meant.
			if ( index > 0 && MathF.Abs( start.y - carried.y ) > 1f ) {
				Paint.ClearBrush();
				Paint.SetPen( ArchStairInk.Platform.WithAlpha( 0.8f ), 1.5f );
				Paint.DrawLine( carried, start );
			}

			if ( lane.Climbs ) {
				PaintRake( stair, lane, along, foot, head, picked );
			} else {
				PaintPad( lane, along, head, picked );
			}

			along += lane.Length;
			arrival = head;
			carried = Profiled( along, head );

			pins.Add( (index, carried) );

			Paint.ClearPen();
			Paint.SetBrush( ArchStairInk.Lift.WithAlpha( picked ? 1f : 0.6f ) );
			Paint.DrawRect( Box( carried ), 2 );

			if ( !picked ) {
				continue;
			}

			Paint.SetDefaultFont( 7 );
			Paint.SetPen( Theme.Primary );
			Paint.DrawText( new Rect( carried.x - 62f, carried.y - 16f, 60f, 13f ),
				$"{stair.BaseHeight + head:0}", TextFlag.RightCenter );
		}

		return arrival;
	}

	void PaintRake( ArchStairPart stair, ArchStairLane lane, float along, float foot, float head, bool picked ) {
		var going = stair.StepGoing > 0.5f ? stair.StepGoing : 12f;
		var riser = stair.StepRise > 0.5f ? stair.StepRise : 8f;
		var steps = ArchStairLanes.Steps( head - foot, riser, lane.Length, going );
		var run = lane.Length / steps;
		var lift = (head - foot) / steps;

		Paint.ClearPen();
		Paint.SetBrush( ArchStairInk.Flight.WithAlpha( picked ? 0.5f : 0.24f ) );

		for ( var step = 0; step < steps; step++ ) {
			var top = Profiled( along + run * step, foot + lift * (step + 1) );
			var toe = Profiled( along + run * (step + 1), 0f );

			Paint.DrawRect( Rect.FromPoints( top, new Vector2( toe.x, bandFoot.y ) ) );
		}

		Paint.ClearBrush();
		Paint.SetPen( picked ? Theme.Primary : ArchStairInk.Flight, picked ? 2.5f : 1.5f );
		Paint.DrawLine( Profiled( along, foot ), Profiled( along + lane.Length, head ) );
	}

	void PaintPad( ArchStairLane lane, float along, float level, bool picked ) {
		var top = Profiled( along, level );
		var end = Profiled( along + lane.Length, level );

		Paint.ClearPen();
		Paint.SetBrush( ArchStairInk.Platform.WithAlpha( picked ? 0.5f : 0.24f ) );
		Paint.DrawRect( Rect.FromPoints( top, new Vector2( end.x, bandFoot.y ) ) );

		Paint.ClearBrush();
		Paint.SetPen( picked ? Theme.Primary : ArchStairInk.Platform, picked ? 2.5f : 1.5f );
		Paint.DrawLine( top, end );
	}

	void PaintClimbNote( ArchStairPart stair, Rect band, float run, float arrival ) {
		Paint.SetDefaultFont( 7 );
		Paint.SetPen( Theme.TextControl.WithAlpha( 0.55f ) );
		Paint.DrawText( new Rect( band.Left + 58f, band.Top + 3f, band.Width - 70f, 14f ),
			$"profile · {run:0} walked · {arrival:0} climb · arrives {stair.BaseHeight + arrival:0}", TextFlag.LeftCenter );

		var head = Profiled( run, arrival );

		// The drop to the foot only - the arrival is the last step's own head pin, and a second mark standing on it
		// is one mark you can grab and one you cannot.
		Paint.ClearBrush();
		Paint.SetPen( ArchStairInk.Lift.WithAlpha( 0.7f ), 1f );
		Paint.DrawLine( head, new Vector2( head.x, bandFoot.y ) );
	}

	protected override void OnContextMenu( ContextMenuEvent e ) {
		if ( subject() is not { Core: not null } stair || StepUnder( stair, hover ) is var step && step < 0 ) {
			return;
		}

		Picked = step;
		Selected?.Invoke( Picked );

		var menu = new ContextMenu( this );

		var join = menu.AddOption( "Join to the step below", "link", () => {
			joining = step;
			Update();
		} );

		join.Enabled = step > 0;

		menu.OpenAtCursor();

		e.Accepted = true;
	}

	protected override void OnMousePress( MouseEvent e ) {
		base.OnMousePress( e );

		held = -1;
		dragging = false;

		if ( subject() is not { Core: not null } stair ) {
			return;
		}

		// A join waiting for its point takes the click outright - nothing else in the drawing is being asked for
		// until it has one.
		if ( joining >= 1 && joining < stair.Lanes.Count ) {
			Joined( stair, e.LocalPosition );

			return;
		}

		// The head pins in the profile come first: they stand ON a step's own stride, and a pin you can see and
		// cannot grab is the drawing lying about what it offers.
		foreach ( var pin in pins ) {
			if ( !Box( pin.At ).Grow( 4f ).IsInside( e.LocalPosition ) ) {
				continue;
			}

			held = pin.Step;
			heldKind = ArchStairGrip.Lift;
			heldGuard = -1;
			holding = stair;
			dragging = true;
			profiling = true;
			Picked = pin.Step;
			Selected?.Invoke( Picked );
			Update();

			return;
		}

		// The profile is a drawing of the same steps, so a step is picked in it the way it is picked in the plan.
		foreach ( var stride in walked ) {
			if ( !stride.At.IsInside( e.LocalPosition ) ) {
				continue;
			}

			Picked = stride.Step;
			Selected?.Invoke( Picked );
			Update();

			return;
		}

		for ( var index = 0; index < grips.Count; index++ ) {
			var grip = grips[index];
			var at = Screen( View == ArchStairView.Plan ? Flat( grip.At.x, grip.At.y, 0f ) : grip.At );

			if ( !Box( at ).Grow( 3f ).IsInside( e.LocalPosition ) ) {
				continue;
			}

			held = grip.Step;
			heldKind = grip.Kind;
			heldGuard = grip.Guard;
			holding = stair;
			dragging = true;
			Picked = grip.Step;
			PickedGuard = grip.Guard;
			Selected?.Invoke( Picked );
			Update();

			return;
		}

		// A press that caught no grip still picks the step under it, so the list and the drawing agree about
		// which row is being worked on.
		if ( StepUnder( stair, e.LocalPosition ) is var step && step >= 0 ) {
			Picked = step;
			Selected?.Invoke( Picked );
			Update();
		}
	}

	int StepUnder( ArchStairPart stair, Vector2 screen ) {
		if ( View != ArchStairView.Plan ) {
			return -1;
		}

		var local = Local( screen );

		for ( var index = 0; index < stair.Lanes.Count; index++ ) {
			if ( ArchFootprint.Contains( stair.Lanes[index].Corners(), local ) ) {
				return index;
			}
		}

		return -1;
	}

	void Joined( ArchStairPart stair, Vector2 screen ) {
		var picked = Stations( stair ).FirstOrDefault( station => Box( station.At ).Grow( StationReach ).IsInside( screen ) );

		if ( picked.At == default ) {
			Update();

			return;
		}

		var lane = stair.Lanes[joining];

		joining = -1;

		ArchStairSteps.JoinTo( stair, lane, picked.Station );
		ArchStairLanes.Reseat( stair.Core, stair.Lanes );

		committed?.Invoke();
	}

	protected override void OnMouseMove( MouseEvent e ) {
		base.OnMouseMove( e );

		hover = e.LocalPosition;

		if ( !dragging || held < 0 || holding is not { Core: not null } stair || held >= stair.Lanes.Count ) {
			if ( joining >= 0 ) {
				Update();
			}

			return;
		}

		var lane = stair.Lanes[held];
		var local = Local( e.LocalPosition );

		if ( heldKind == ArchStairGrip.Lift ) {
			var bases = ArchStairEdges.Bases( stair );
			var foot = bases[held] - stair.BaseHeight;
			var lift = profiling
				? (bandFoot.y - e.LocalPosition.y) / MathF.Max( 0.0001f, bandZoom )
				: -local.y;

			ArchStairSteps.Lift( stair, lane, ArchGridService.Fine( lift - foot ) );
		} else if ( heldKind is ArchStairGrip.Rail or ArchStairGrip.RailFoot or ArchStairGrip.RailHead ) {
			if ( heldGuard < 0 || heldGuard >= lane.Guards.Count ) {
				return;
			}

			var rail = lane.Guards[heldGuard];
			var at = Fraction( lane, local );

			if ( heldKind == ArchStairGrip.RailFoot ) {
				rail.From = MathF.Min( at, rail.End - 0.02f );
			} else if ( heldKind == ArchStairGrip.RailHead ) {
				rail.To = MathF.Max( at, rail.Start + 0.02f );
			} else {
				// The whole railing slid along its edge, keeping the stretch it covers.
				var reach = rail.End - rail.Start;
				var start = Math.Clamp( at - reach * 0.5f, 0f, 1f - reach );

				rail.From = start;
				rail.To = start + reach;
			}
		} else if ( heldKind == ArchStairGrip.Body ) {
			ArchStairSteps.Move( stair, lane, Rested( stair, local ) );
		} else if ( heldKind == ArchStairGrip.Bend ) {
			ArchStairSteps.Bend( stair, lane, Aimed( stair, lane, local ) );
		} else {
			var point = Rested( stair, local );

			if ( heldKind == ArchStairGrip.Head ) {
				ArchStairEdges.Head( lane, point );
			} else {
				ArchStairEdges.Flank( lane, point, heldKind == ArchStairGrip.Left );
			}

			ArchStairSteps.Settle( stair, lane );
		}

		ArchStairLanes.Fit( stair.Core, stair.Lanes );

		changed?.Invoke();
		Update();
	}

	// Plan view snaps to walls; elevations have no world-space snap.
	Vector2 Rested( ArchStairPart stair, Vector2 local ) {
		if ( View == ArchStairView.Plan && tool is not null
			&& tool.Walled( stair.Core.Flat( local.x, local.y ), out var wall, Storey ) ) {
			return ArchStairLanes.Local( stair.Core, wall );
		}

		return ArchGridService.Fine( local );
	}

	// Snaps to BendStep increments and to a neighbour's heading when close.
	static float Aimed( ArchStairPart stair, ArchStairLane lane, Vector2 local ) {
		var reach = local - lane.Seat;

		if ( reach.IsNearZeroLength ) {
			return lane.Bearing;
		}

		var aimed = ArchGridService.Snap( MathF.Atan2( reach.y, reach.x ).RadianToDegree() - lane.Cardinal, BendStep );
		var index = stair.Lanes.IndexOf( lane );

		foreach ( var neighbour in new[] { index - 1, index + 1 } ) {
			if ( neighbour < 0 || neighbour >= stair.Lanes.Count
				|| MathF.Abs( aimed - stair.Lanes[neighbour].Bearing ) >= BendStep ) {
				continue;
			}

			return stair.Lanes[neighbour].Bearing;
		}

		return aimed;
	}

	protected override void OnMouseReleased( MouseEvent e ) {
		base.OnMouseReleased( e );

		if ( dragging ) {
			// The shaft laid back onto the steps standing in it, once - during the drag it only ever grows, because
			// a box that re-seats under the cursor moves the very frame the drag is being read in.
			if ( holding is { Core: not null } stair ) {
				ArchStairLanes.Reseat( stair.Core, stair.Lanes );
			}

			committed?.Invoke();
		}

		dragging = false;
		profiling = false;
		held = -1;
		holding = null;
	}
}