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

public sealed class ArchStairHandler : IArchHandler {
	public ArchKind Kind => ArchKind.Stair;

	public bool Draw( ArchTool tool, ArchSelection picked ) {
		return picked.Item is ArchStairPart stair
			&& ArchStairHandles.Draw( tool, stair, picked.Room, picked.Building );
	}
}

// One step of a climb, picked in the stack: its own head, flanks and lift and nothing else. The stair it belongs
// to is looked up, because a step is filed on that stair's Lanes list and nowhere else in the plan.
public sealed class ArchStairStepHandler : IArchHandler {
	public ArchKind Kind => ArchKind.StairStep;

	public bool Draw( ArchTool tool, ArchSelection picked ) {
		if ( picked.Item is not ArchStairLane lane || ArchStairSteps.Owner( tool.Plan, lane ) is not { } stair ) {
			return false;
		}

		return ArchStairHandles.Draw( tool, stair, picked.Room, picked.Building, stair.Lanes.IndexOf( lane ) );
	}
}

public sealed class ArchCarvedStairHandler : IArchHandler {
	public ArchKind Kind => ArchKind.CarvedStair;

	public bool Draw( ArchTool tool, ArchSelection picked ) {
		if ( picked.Item is not ArchStairPart stair
			|| tool.Plan.PlatformCarrying( stair ) is not { } platform
			|| stair.Core is not { } core ) {
			return false;
		}

		var gesture = ArchGesture.On( ArchKind.CarvedStair, stair.Id );
		var top = platform.TopHeight;
		var half = core.Width * 0.5f;
		var start = core.Flat( 0f, half );
		var end = core.Flat( core.Length, half );
		var changed = false;

		if ( ArchShapeHandles.Placed( tool, gesture.At( "foot" ), new Vector3( start.x, start.y, top ), out var foot, Gizmo.Colors.Green ) ) {
			Refit( core, foot, end );
			changed = true;
		}

		if ( ArchShapeHandles.Placed( tool, gesture.At( "head" ), new Vector3( end.x, end.y, top ), out var head, Gizmo.Colors.Green ) ) {
			Refit( core, start, head );
			changed = true;
		}

		var middle = (start + end) * 0.5f;
		var edge = middle + core.Axes.Across * half;

		if ( ArchShapeHandles.Width( tool, gesture.At( "width" ), new Vector3( edge.x, edge.y, top ), middle, core.Axes.Across, 12f, out var widened, Gizmo.Colors.Blue ) ) {
			Widen( core, widened );
			stair.Width = widened;
			changed = true;
		}

		if ( ArchShapeHandles.Lift( tool, gesture.At( "rise" ), middle, stair.HeadHeight, out var risen, Gizmo.Colors.Roll ) ) {
			core.Rise = MathF.Max( 4f, risen - stair.BaseHeight );
			changed = true;
		}

		if ( changed ) {
			Restretch( stair, core );
		}

		return changed;
	}

	// A carve is one straight notch, so its shaft is refitted from the two ends of its centreline.
	static void Refit( ArchStairCore core, Vector2 foot, Vector2 head ) {
		var span = head - foot;

		if ( span.Length < 1f ) {
			return;
		}

		core.Yaw = MathF.Atan2( span.y, span.x ).RadianToDegree();
		core.Length = span.Length;
		core.Origin = foot - core.Axes.Across * (core.Width * 0.5f);
	}

	static void Widen( ArchStairCore core, float width ) {
		var centre = core.Flat( 0f, core.Width * 0.5f );

		core.Width = MathF.Max( 1f, width );
		core.Origin = centre - core.Axes.Across * (core.Width * 0.5f);
	}

	static void Restretch( ArchStairPart stair, ArchStairCore core ) {
		foreach ( var lane in stair.Lanes ) {
			lane.AlongFrom = 0f;
			lane.AlongTo = core.Length;
			lane.AcrossFrom = 0f;
			lane.AcrossTo = core.Width;
		}
	}
}

public sealed class ArchExteriorStairHandler : IArchHandler {
	public ArchKind Kind => ArchKind.ExteriorStair;

	public bool Draw( ArchTool tool, ArchSelection picked ) {
		if ( picked.Item is not ArchExteriorStairPart flight ) {
			return false;
		}

		var gesture = ArchGesture.On( ArchKind.ExteriorStair, flight.Id );
		var changed = false;

		if ( ArchShapeHandles.Move( tool, gesture, flight.Anchor, out var moved ) ) {
			flight.Anchor = moved;
			changed = true;
		}

		if ( ArchShapeHandles.Height( tool, gesture.At( "head" ), flight.Anchor, flight.TopHeight, out var head, Gizmo.Colors.Roll ) ) {
			flight.TopHeight = MathF.Max( flight.BaseHeight + 12f, head );
			changed = true;
		}

		if ( ArchShapeHandles.Height( tool, gesture.At( "foot" ), flight.Anchor, flight.BaseHeight, out var foot, Gizmo.Colors.Pitch ) ) {
			flight.BaseHeight = MathF.Min( flight.TopHeight - 12f, foot );
			changed = true;
		}

		return changed;
	}
}

public sealed class ArchStairPicker : IArchPicker {
	public ArchKind Kind => ArchKind.Stair;

	public void Under( ArchPicking picking ) {
		if ( picking.Node.Payload is not ArchStairPart stair ) {
			return;
		}

		if ( picking.Node.Room is { } room && room.Stairs.Contains( stair ) ) {
			if ( ArchStairShape.Resolve( stair, room, picking.Kit ).Wells().Any( loop => ArchFootprint.Contains( loop, picking.Point ) ) ) {
				picking.Hit( ArchHitChannel.Volume, 0f, true );
			}

			return;
		}

		ArchCarvedStairPicker.Pick( picking, stair );
	}
}

public sealed class ArchCarvedStairPicker : IArchPicker {
	public ArchKind Kind => ArchKind.CarvedStair;

	public void Under( ArchPicking picking ) {
		if ( picking.Node.Payload is ArchStairPart stair ) {
			Pick( picking, stair );
		}
	}

	internal static void Pick( ArchPicking picking, ArchStairPart stair ) {
		if ( stair.Core is { } core && ArchFootprint.Contains( core.Outline(), picking.Point ) ) {
			picking.Hit( ArchHitChannel.Volume, 0f, true );
		}
	}
}

public sealed class ArchCarvedStairPreview : IArchPreview {
	public ArchKind Kind => ArchKind.CarvedStair;

	public void Draw( ArchPreviewContext context ) {
		if ( context.Payload is not ArchStairPart stair
			|| context.Tool?.Plan.PlatformCarrying( stair ) is not { } platform
			|| stair.Core is not { } core ) {
			return;
		}

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

		ArchGhost.Prism( core.Outline(), platform.GradeHeight, platform.TopHeight );
		ArchGhost.Note( new Vector3( head.x, head.y, platform.TopHeight ),
			$"carve — {core.Length:0} run, {core.Width:0} wide, {stair.TotalRise:0} rise" );
	}
}

public sealed class ArchStairPreview : IArchPreview {
	public ArchKind Kind => ArchKind.Stair;

	public void Draw( ArchPreviewContext context ) {
		if ( !context.Selected || context.Payload is not ArchStairPart stair || context.Room is null || context.Tool is null ) {
			return;
		}

		ArchStairGhost.Blueprint( stair );

		var shape = ArchStairShape.Resolve( stair, context.Room, context.Tool.Kit, context.Building?.Rooms, context.Building );

		foreach ( var well in shape.Levels ) {
			Gizmo.Draw.Color = (well.Level == shape.TopLevel ? Color.Magenta : Color.Yellow).WithAlpha( 0.5f );

			foreach ( var outline in well.Loops ) {
				ArchGhost.Ring( outline, well.Height );
			}
		}
	}
}

public sealed class ArchStairExtentProvider : IArchExtentProvider {
	public ArchKind Kind => ArchKind.Stair;

	public ArchExtent Measure( object payload, ArchExtentContext context ) {
		var extent = new ArchExtent();

		if ( payload is not ArchStairPart stair || context.Room is null ) {
			return extent;
		}

		var shape = ArchStairShape.Resolve( stair, context.Room, context.Kit );
		var top = shape.TopHeight + MathF.Max( 12f, context.Kit.HandrailHeight ) + context.Kit.NewelRise;

		foreach ( var well in shape.Wells() ) {
			extent.Add( well, shape.BaseHeight - context.Kit.StringerDepth - 4f, top );
		}

		return extent;
	}
}

public sealed class ArchStairEffects : IArchAffector {
	public ArchKind Kind => ArchKind.Stair;

	public int Order => 20;

	public int Resolve( ArchPlan plan, ArchKit kit ) => ArchStairAffector.Resolve( plan, kit );

	public bool Closes( object payload ) => payload is ArchStairPart;

	public void Close( ArchPlan plan, object payload ) {
		if ( payload is ArchStairPart stair ) {
			ArchStairAffector.Remove( plan, stair );
		}
	}
}

public sealed class ArchExteriorStairEffects : IArchAffector {
	public ArchKind Kind => ArchKind.ExteriorStair;

	public int Order => 60;

	public int Resolve( ArchPlan plan, ArchKit kit ) => ArchExteriorStairAffector.Resolve( plan, kit );

	public bool Closes( object payload ) => payload is ArchExteriorStairPart;

	public void Close( ArchPlan plan, object payload ) {
		if ( payload is ArchExteriorStairPart flight ) {
			ArchExteriorStair.Remove( plan, flight );
		}
	}
}