Editor/Stair/ArchStairHandles.cs
namespace Sunless.Architecture;

// A stair is authored by GRABBING POINTS, the way a pipe run is: a square on the foot, one on every step's head
// and one on each of its flanks, each dragged where you are looking at it, plus a lift arrow for the climb.
//
// There is no box. A box could only ever say how big the whole thing was and nothing about where any flight in it
// went, and it stood over the very surface the next gesture is drawn on - so the shaft is derived from the steps
// standing in it and is never a widget of its own.
public static class ArchStairHandles {
	// A step picked in the stack gets ITS widgets and nobody else's - six flights' worth of squares standing at
	// once is a thicket, and the row you clicked is the one you meant to move. Below zero is the whole stair.
	public static bool Draw( ArchTool tool, ArchStairPart stair, ArchRoom room, ArchBuilding building = null, int only = -1 ) {
		var core = stair.Core;

		if ( core is null ) {
			return false;
		}

		var shape = tool.Resolved( stair.Id, () => ArchStairShape.Resolve( stair, room, tool.Kit, building?.Rooms, building ) );

		if ( shape.Runs.Count == 0 && shape.Pads.Count == 0 ) {
			return false;
		}

		ArchStairGhost.Flight( shape, Soffit( tool, room ) );

		var gesture = ArchGesture.On( ArchKind.Stair, stair.Id );

		// A shaft left with no step authored resolves as one flight filling it. The widgets WRITE, so that
		// flight has to be real before one can be dragged - a handle editing a list the resolve synthesised
		// per call would move nothing and look broken.
		if ( stair.Lanes.Count == 0 ) {
			stair.Lanes.AddRange( ArchStairShape.Standing( stair, core ).Select( lane => lane.Copy() ) );
		}

		var lanes = stair.Lanes;

		if ( only < 0 && Travelled( tool, gesture, stair, core ) ) {
			FollowOpening( stair, room, building, tool.Kit );

			return true;
		}

		if ( only < 0 && Swung( gesture, stair, core ) ) {
			FollowOpening( stair, room, building, tool.Kit );

			return true;
		}

		var changed = only < 0 && Footed( tool, gesture, stair, core );

		changed |= Headed( tool, gesture, stair, core, lanes, shape, only );
		changed |= Flanked( tool, gesture, stair, core, lanes, shape, only );
		changed |= Climbs( tool, gesture, stair, lanes, shape, only );
		changed |= Terrace( tool, gesture, stair, lanes, shape, only );
		changed |= only < 0 && Arrival( tool, gesture, stair, shape );

		if ( changed ) {
			ArchStairLanes.Fit( core, stair.Lanes );
			FollowOpening( stair, room, building, tool.Kit );
		}

		return changed | (only < 0 && Opening( tool, gesture, stair, core, shape ));
	}

	static void FollowOpening( ArchStairPart stair, ArchRoom room, ArchBuilding building, ArchKit kit )
	{
		if ( stair.TopOpening is not { Count: >= 3 } || stair.Core is not { } core )
		{
			return;
		}

		var required = ArchStairShape.Resolve( stair, room, kit, building?.Rooms, building )
			.AutomaticTopOpening
			.SelectMany( loop => loop )
			.Select( point => ArchStairLanes.Local( core, point ) )
			.ToList();

		if ( required.Count < 3 )
		{
			return;
		}

		ArchFootprint.Bounds( stair.TopOpening, out var min, out var max );
		ArchFootprint.Bounds( required, out var requiredMin, out var requiredMax );

		if ( stair.TopOpeningSnap is { Count: >= 3 } )
		{
			ArchFootprint.Bounds( stair.TopOpeningSnap, out var snappedMin, out var snappedMax );

			var opening = stair.TopOpening.ToList();
			ArchRemap.Between( snappedMin, snappedMax, requiredMin, requiredMax ).Loop( opening );

			stair.TopOpening = opening;
			stair.TopOpeningSnap = ArchFootprint.Rect( requiredMin, requiredMax );

			return;
		}

		var stoodMin = min;
		var stoodMax = max;
		var minX = min.x;
		var maxX = max.x;
		var minY = min.y;
		var maxY = max.y;

		Contain( requiredMin.x, requiredMax.x, ref minX, ref maxX );
		Contain( requiredMin.y, requiredMax.y, ref minY, ref maxY );

		stair.TopOpening = ArchFootprint.Rect( new Vector2( minX, minY ), new Vector2( maxX, maxY ) );

		if ( (stoodMin - new Vector2( minX, minY )).Length > 0.01f
			|| (stoodMax - new Vector2( maxX, maxY )).Length > 0.01f )
		{
			stair.TopOpeningSnap = ArchFootprint.Rect( requiredMin, requiredMax );
		}
	}

	static void Contain( float requiredMin, float requiredMax, ref float min, ref float max )
	{
		var size = max - min;
		var requiredSize = requiredMax - requiredMin;

		if ( size < requiredSize )
		{
			min = requiredMin;
			max = requiredMax;

			return;
		}

		if ( min > requiredMin )
		{
			max += requiredMin - min;
			min = requiredMin;
		}

		if ( max < requiredMax )
		{
			min += requiredMax - max;
			max = requiredMax;
		}
	}

	static bool Opening( ArchTool tool, ArchGesture gesture, ArchStairPart stair, ArchStairCore core, ArchStairShape shape ) {
		var well = shape.Levels.Find( candidate => candidate.Level == shape.TopLevel );

		if ( well is null || well.Loops.Count == 0 ) {
			return false;
		}

		var outline = stair.TopOpening is { Count: >= 3 }
			? stair.TopOpening.Select( point => core.Flat( point.x, point.y ) ).ToList()
			: Boxed( core, well.Loops );
		var height = well.Height + 2f;
		var colour = Color.Magenta;

		Gizmo.Draw.Color = colour.WithAlpha( 0.85f );
		ArchGhost.Ring( outline, height );

		ArchFootprint.Bounds( outline, out var min, out var max );
		var centre = (min + max) * 0.5f;
		var changed = false;

		if ( ArchShapeHandles.Landed( tool, gesture.At( "opening.move" ), new Vector3( centre.x, centre.y, height ), out var moved, colour, core.Yaw ) ) {
			var shift = moved - centre;

			for ( var index = 0; index < outline.Count; index++ ) {
				outline[index] += shift;
			}

			changed = true;
		}

		changed |= ArchShapeHandles.Edges( tool, gesture.At( "opening.resize" ), outline, height, null, colour );

		if ( changed ) {
			stair.TopOpening = outline.Select( point => ArchStairLanes.Local( core, point ) ).ToList();
			stair.TopOpeningSnap = null;
		}

		return changed;
	}

	static List<Vector2> Boxed( ArchStairCore core, IReadOnlyList<List<Vector2>> loops ) {
		var points = loops.SelectMany( loop => loop ).Select( point => ArchStairLanes.Local( core, point ) ).ToList();

		ArchFootprint.Bounds( points, out var min, out var max );

		return core.Axes.Rect( min.x, max.x, min.y, max.y );
	}

	static float Soffit( ArchTool tool, ArchRoom room ) {
		if ( room is null ) {
			return tool.LevelHeight + tool.Kit.WallHeight;
		}

		return room.BaseHeight + (room.WallHeight > 0f ? room.WallHeight : tool.Kit.WallHeight);
	}

	// ---- Where the whole thing stands ----

	static bool Travelled( ArchTool tool, ArchGesture gesture, ArchStairPart stair, ArchStairCore core ) {
		var standing = stair.BaseHeight;
		var foot = core.Origin;

		if ( !ArchShapeHandles.Position( tool, gesture.At( "move" ), new Vector3( foot.x, foot.y, standing ), out var moved ) ) {
			return false;
		}

		var shift = new Vector2( moved.x, moved.y ) - foot;

		ArchCarry.Stair( stair, ArchRemap.Translation( shift ) );

		stair.BaseHeight = moved.z;

		return shift.Length > 0.01f || MathF.Abs( moved.z - standing ) > 0.01f;
	}

	// Turning the shaft turns every step in it, because a lane is stated in the core's own frame and has never
	// held a world coordinate to swing.
	static bool Swung( ArchGesture gesture, ArchStairPart stair, ArchStairCore core ) {
		var foot = core.Origin;

		if ( !ArchShapeHandles.Turned( gesture.At( "turn" ), new Vector3( foot.x, foot.y, stair.BaseHeight ), out var degrees, ArchGridService.AngleStep ) ) {
			return false;
		}

		core.Yaw += degrees;

		return true;
	}

	// The bottom step, as a point you drag rather than a mode you switch into. It carries the whole stair, so it
	// is the one handle that answers "where does this go" without asking the author to find the mover first.
	static bool Footed( ArchTool tool, ArchGesture gesture, ArchStairPart stair, ArchStairCore core ) {
		// The engine's mover already stands on this exact point in Move mode, and two widgets in one place is
		// one widget - whichever hit-tests first takes the press and the other can be seen and never grabbed.
		if ( ArchShapeHandles.Mode == ArchHandleMode.Move ) {
			return false;
		}

		var foot = core.Origin;

		if ( !ArchShapeHandles.Control( tool, gesture.At( "foot" ), new Vector3( foot.x, foot.y, stair.BaseHeight ), out var moved, Gizmo.Colors.Pitch ) ) {
			return false;
		}

		core.Origin = new Vector2( moved.x, moved.y );
		stair.BaseHeight = moved.z;

		return true;
	}

	// ---- Each flight's own head and flanks ----

	// The point a flight ARRIVES at. Dragged in plan it lengthens or shortens that flight - which is where the
	// landing after it begins - and dragged in an elevation its height PINS the flight's climb, exactly as the
	// lift arrow does. One point, both questions, standing on the geometry it moves.
	static bool Headed( ArchTool tool, ArchGesture gesture, ArchStairPart stair, ArchStairCore core, IReadOnlyList<ArchStairLane> lanes, ArchStairShape shape, int only ) {
		foreach ( var run in shape.Runs ) {
			if ( run.Index >= lanes.Count || (only >= 0 && run.Index != only) ) {
				continue;
			}

			var lane = lanes[run.Index];
			var at = run.Axes.Point( run.Length, run.Width * 0.5f, run.TopHeight );

			if ( !ArchShapeHandles.Control( tool, gesture.At( "head", run.Index ), at, out var moved, Gizmo.Colors.Roll ) ) {
				continue;
			}

			ArchStairEdges.Head( lane, ArchStairLanes.Local( core, new Vector2( moved.x, moved.y ) ) );

			if ( MathF.Abs( moved.z - run.TopHeight ) > 0.01f ) {
				lane.Rise = MathF.Max( MathF.Max( 1f, stair.StepRise ), moved.z - run.BaseHeight );
			}

			ArchStairSteps.Settle( stair, lane );

			return true;
		}

		return false;
	}

	// Both flanks of every flight, so a width is DRAGGED rather than typed - and each flight keeps its own, which
	// is what lets a grand lower flight arrive at a narrower upper one.
	static bool Flanked( ArchTool tool, ArchGesture gesture, ArchStairPart stair, ArchStairCore core, IReadOnlyList<ArchStairLane> lanes, ArchStairShape shape, int only ) {
		foreach ( var run in shape.Runs ) {
			if ( run.Index >= lanes.Count || (only >= 0 && run.Index != only) ) {
				continue;
			}

			var lane = lanes[run.Index];
			var middle = run.Length * 0.5f;

			foreach ( var side in new[] { 0f, run.Width } ) {
				var at = run.Axes.Point( middle, side, run.Rake( middle ) );
				var key = side > 0f ? "left" : "right";
				var outward = run.Axes.Across * (side > 0f ? 1f : -1f);

				if ( !ArchShapeHandles.Arrow( tool, gesture.At( key, run.Index ), at, outward, out var moved, Color.Red ) ) {
					continue;
				}

				ArchStairEdges.Flank( lane, ArchStairLanes.Local( core, moved ), side > 0f );

				if ( lanes.Count( entry => entry.Climbs ) == 1 ) {
					stair.Width = MathF.Max( ArchStairLanes.MinLane, lane.Width );
				}

				ArchStairSteps.Settle( stair, lane );

				return true;
			}
		}

		return false;
	}

	// ---- The climb ----

	// The up-axis arrow, which is the one widget that works from ANY camera - a head square in a plan view can
	// only be dragged across the ground. A stair of one step has no arrow of its own: its climb is the shaft's.
	static bool Climbs( ArchTool tool, ArchGesture gesture, ArchStairPart stair, IReadOnlyList<ArchStairLane> lanes, ArchStairShape shape, int only ) {
		if ( lanes.Count < 2 ) {
			return false;
		}

		foreach ( var run in shape.Runs ) {
			if ( run.Index >= lanes.Count || (only >= 0 && run.Index != only) ) {
				continue;
			}

			var head = run.Axes.Flat( run.Length, run.Width * 0.5f );

			if ( !ArchShapeHandles.Lift( tool, gesture.At( "climb", run.Index ), head, run.TopHeight, out var lifted ) ) {
				continue;
			}

			lanes[run.Index].Rise = MathF.Max( MathF.Max( 1f, stair.StepRise ), lifted - run.BaseHeight );

			return true;
		}

		return false;
	}

	// A landing's own two gestures, both on the pad it stands on. Its DEPTH is pushed on the far edge, and its
	// HEIGHT is the same lift arrow a flight gets - a landing lifted off the flight below it is a split level,
	// which is the one thing a derived pad could never be.
	static bool Terrace( ArchTool tool, ArchGesture gesture, ArchStairPart stair, IReadOnlyList<ArchStairLane> lanes, ArchStairShape shape, int only ) {
		foreach ( var pad in shape.Pads ) {
			if ( pad.Index < 0 || pad.Index >= lanes.Count || (only >= 0 && pad.Index != only) ) {
				continue;
			}

			var lane = lanes[pad.Index];
			var across = (pad.AcrossFrom + pad.AcrossTo) * 0.5f;
			var middle = pad.Axes.Flat( (pad.AlongFrom + pad.AlongTo) * 0.5f, across );

			if ( ArchShapeHandles.Lift( tool, gesture.At( "pad", pad.Index ), middle, pad.Height, out var lifted ) ) {
				lane.Rise = MathF.Max( 0f, lifted - (pad.Height - MathF.Max( 0f, lane.Rise )) );

				return true;
			}

			var far = pad.Axes.Point( pad.AlongTo, across, pad.Height );

			if ( ArchShapeHandles.Push( tool, gesture.At( "terrace", pad.Index ), far,
				pad.Axes.Flat( pad.AlongTo, across ), pad.Axes.Along, out var push, Gizmo.Colors.Blue ) ) {
				ArchStairEdges.Grow( lane, push );

				ArchStairSteps.Settle( stair, lane );

				return true;
			}
		}

		return false;
	}

	// The one landing whose depth is authored on the stair rather than in the list, because nothing follows it
	// to bound it - and it only exists where the walk did not end on a platform of its own.
	static bool Arrival( ArchTool tool, ArchGesture gesture, ArchStairPart stair, ArchStairShape shape ) {
		if ( !stair.TopLanding ) {
			return false;
		}

		var pad = shape.Pads.Find( candidate => candidate.Arrival && candidate.Index < 0 );

		if ( pad is null ) {
			return false;
		}

		var middle = (pad.AcrossFrom + pad.AcrossTo) * 0.5f;
		var head = pad.Axes.Point( pad.AlongTo, middle, pad.Height );

		if ( !ArchShapeHandles.Push( tool, gesture.At( "landing" ), head,
			pad.Axes.Flat( pad.AlongTo, middle ), pad.Axes.Along, out var push, Gizmo.Colors.Blue ) ) {
			return false;
		}

		stair.TopLandingDepth = MathF.Max( shape.Going, pad.AlongTo - pad.AlongFrom + push );

		return true;
	}
}