Editor/InteriorLayoutBuilder/RoomLayoutTool.Input.cs
using System;
using System.Linq;
using Editor;
using Sandbox;

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutTool
{
	[Shortcut( "editor.undo", "CTRL+Z", typeof( SceneViewWidget ) )]
	public static void UndoActiveLayoutTool()
	{
		if ( LiveTool?.CancelLayoutInteraction( restoreActiveEdit: true ) == true )
		{
			return;
		}

		global::Editor.EditorShortcuts.PassShortcut = true;
	}

	[Shortcut( "editor.redo", "CTRL+Y", typeof( SceneViewWidget ) )]
	public static void RedoActiveLayoutTool()
	{
		global::Editor.EditorShortcuts.PassShortcut = true;
	}

	[Shortcut( "editor.cancel", "ESC", typeof( SceneViewWidget ) )]
	public static void CancelActiveLayoutToolInteraction()
	{
		if ( LiveTool?.CancelLayoutInteraction( restoreActiveEdit: true ) == true )
		{
			return;
		}

		global::Editor.EditorShortcuts.PassShortcut = true;
	}

	[Shortcut( "editor.delete", "DEL", typeof( SceneViewWidget ) )]
	public static void DeleteActiveLayoutToolSelection()
	{
		if ( LiveTool?.TryDeleteSelectedLayout() == true )
		{
			return;
		}

		global::Editor.EditorShortcuts.PassShortcut = true;
	}

	public void DeleteSelectedLayout()
	{
		TryDeleteSelectedLayout();
	}

	private bool TryDeleteSelectedLayout()
	{
		if ( drawingCorridor )
		{
			CancelCorridorDraft();
			return true;
		}

		if ( selectedFloorCutoutId != 0 )
		{
			PushLayoutUndo();
			document.FloorCutouts.RemoveAll( cutout => cutout.Id == selectedFloorCutoutId );
			selectedFloorCutoutId = 0;
			CommitLayoutChange( !HasLayoutContent() );
			return true;
		}

		if ( selectedDoorId != 0 )
		{
			PushLayoutUndo();
			document.Doors.RemoveAll( door => door.Id == selectedDoorId );
			document.Corridors.RemoveAll( corridor => corridor.StartDoorId == selectedDoorId || corridor.EndDoorId == selectedDoorId );
			selectedRoomId = 0;
			selectedDoorId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange( !HasLayoutContent() );
			return true;
		}

		if ( selectedWindowId != 0 )
		{
			PushLayoutUndo();
			document.Windows.RemoveAll( window => window.Id == selectedWindowId );
			selectedRoomId = 0;
			selectedWindowId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange( !HasLayoutContent() );
			return true;
		}

		if ( selectedCorridorId != 0 )
		{
			PushLayoutUndo();
			document.Doors.RemoveAll( door => door.CorridorId == selectedCorridorId );
			document.Windows.RemoveAll( window => window.CorridorId == selectedCorridorId );
			document.Corridors.RemoveAll( corridor => corridor.Id == selectedCorridorId );
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange( !HasLayoutContent() );
			return true;
		}

		if ( selectedRoomId != 0 )
		{
			PushLayoutUndo();
			document.RemoveRoom( selectedRoomId );
			selectedRoomId = 0;
			selectedWindowId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange( !HasLayoutContent() );
			return true;
		}

		return false;
	}

	private void HandleMouse()
	{
		if ( !TryGetCursorPoint( out var cursor, out var hitGeneratedLayout, out var hitGameObject ) )
		{
			return;
		}

		if ( Gizmo.Pressed.Any && !hitGeneratedLayout )
		{
			if ( mode is RoomLayoutToolMode.Rooms or RoomLayoutToolMode.FloorCutouts || resizingCorridor )
			{
				return;
			}
		}

		if ( Gizmo.WasLeftMousePressed &&
			(mode is RoomLayoutToolMode.Rooms or RoomLayoutToolMode.FloorCutouts or RoomLayoutToolMode.Select) &&
			IsCursorOverExternalSceneObject() )
		{
			return;
		}

		var selectedLayoutAtCursor = false;
		if ( Gizmo.WasLeftMousePressed && !(mode == RoomLayoutToolMode.Corridors && drawingCorridor) )
		{
			selectedLayoutAtCursor = mode == RoomLayoutToolMode.Select &&
				hitGeneratedLayout &&
				TrySelectGeneratedLayoutObject( hitGameObject );
			if ( !selectedLayoutAtCursor )
			{
				selectedLayoutAtCursor = SelectLayoutAtCursor( cursor );
			}
		}

		switch ( mode )
		{
			case RoomLayoutToolMode.Select:
				HandleSelectMouse( cursor, selectedLayoutAtCursor );
				break;
			case RoomLayoutToolMode.Rooms:
				HandleRoomMouse( cursor, selectedLayoutAtCursor );
				break;
			case RoomLayoutToolMode.Doors:
				HandleDoorMouse( cursor );
				break;
			case RoomLayoutToolMode.Windows:
				HandleWindowMouse( cursor );
				break;
			case RoomLayoutToolMode.Corridors:
				HandleCorridorMouse( cursor );
				break;
			case RoomLayoutToolMode.FloorCutouts:
				HandleFloorCutoutMouse( cursor, selectedLayoutAtCursor );
				break;
		}
	}

	private void HandleSelectMouse( Vector2 cursor, bool selectedLayoutAtCursor )
	{
		if ( movingDoor )
		{
			MoveSelectedDoorByDrag( cursor );
			if ( Gizmo.WasLeftMouseReleased )
			{
				movingDoor = false;
				CommitLayoutEdit();
			}
			return;
		}

		if ( movingWindow )
		{
			MoveSelectedWindowByDrag( cursor );
			if ( Gizmo.WasLeftMouseReleased )
			{
				movingWindow = false;
				CommitLayoutEdit();
			}
			return;
		}

		if ( movingRoom )
		{
			if ( document.FindRoom( selectedRoomId ) is { } room )
			{
				var delta = cursor - dragStart;
				room.Bounds = SnapRect( moveStartBounds.MovedBy( delta ) );
			}

			if ( Gizmo.WasLeftMouseReleased )
			{
				movingRoom = false;
				CommitLayoutEdit();
			}
			return;
		}

		if ( movingFloorCutout )
		{
			MoveSelectedFloorCutoutByDrag( cursor );
			if ( Gizmo.WasLeftMouseReleased )
			{
				movingFloorCutout = false;
				CommitLayoutEdit();
			}
			return;
		}

		if ( selectMovePending )
		{
			if ( Gizmo.WasLeftMouseReleased || !Gizmo.IsLeftMouseDown )
			{
				selectMovePending = false;
				return;
			}

			if ( Vector2.DistanceBetween( cursor, dragStart ) < SelectDragThreshold )
			{
				return;
			}

			StartPendingSelectMove();
			selectMovePending = false;

			if ( movingDoor )
			{
				MoveSelectedDoorByDrag( cursor );
			}
			else if ( movingWindow )
			{
				MoveSelectedWindowByDrag( cursor );
			}
			else if ( movingRoom && document.FindRoom( selectedRoomId ) is { } room )
			{
				var delta = cursor - dragStart;
				room.Bounds = SnapRect( moveStartBounds.MovedBy( delta ) );
			}
			else if ( movingFloorCutout )
			{
				MoveSelectedFloorCutoutByDrag( cursor );
			}

			return;
		}

		if ( !Gizmo.WasLeftMousePressed || !selectedLayoutAtCursor )
		{
			return;
		}

		if ( selectedDoorId != 0 && document.FindDoor( selectedDoorId ) is { } door )
		{
			BeginPendingSelectMove( RoomLayoutSelectionKind.Door, cursor, door.Offset );
			return;
		}

		if ( selectedWindowId != 0 && document.FindWindow( selectedWindowId ) is { } window )
		{
			BeginPendingSelectMove( RoomLayoutSelectionKind.Window, cursor, window.Offset );
			return;
		}

		if ( selectedRoomId != 0 && document.FindRoom( selectedRoomId ) is { } selectedRoom )
		{
			BeginPendingSelectMove( RoomLayoutSelectionKind.Room, cursor, 0.0f );
			moveStartBounds = selectedRoom.Bounds;
			return;
		}

		if ( selectedFloorCutoutId != 0 && SelectedFloorCutout() is { } selectedCutout )
		{
			BeginPendingSelectMove( RoomLayoutSelectionKind.FloorCutout, cursor, 0.0f );
			moveStartBounds = selectedCutout.Bounds;
		}
	}

	private void BeginPendingSelectMove( RoomLayoutSelectionKind kind, Vector2 cursor, float offset )
	{
		selectMovePending = true;
		selectMovePendingKind = kind;
		dragStart = cursor;
		moveStartOffset = offset;
	}

	private void StartPendingSelectMove()
	{
		switch ( selectMovePendingKind )
		{
			case RoomLayoutSelectionKind.Room when document.FindRoom( selectedRoomId ) is { } room:
				moveStartBounds = room.Bounds;
				movingRoom = true;
				BeginLayoutEdit();
				break;
			case RoomLayoutSelectionKind.Door when document.FindDoor( selectedDoorId ) is not null:
				movingDoor = true;
				BeginLayoutEdit();
				break;
			case RoomLayoutSelectionKind.Window when document.FindWindow( selectedWindowId ) is not null:
				movingWindow = true;
				BeginLayoutEdit();
				break;
			case RoomLayoutSelectionKind.FloorCutout when SelectedFloorCutout() is { } cutout:
				moveStartBounds = cutout.Bounds;
				movingFloorCutout = true;
				BeginLayoutEdit();
				break;
		}
	}

	private void HandleRoomMouse( Vector2 cursor, bool selectedLayoutAtCursor )
	{
		if ( Gizmo.WasLeftMousePressed )
		{
			if ( TryFindRoom( cursor, out var room ) )
			{
				selectedRoomId = room.Id;
				selectedDoorId = 0;
				selectedWindowId = 0;
				selectedCorridorId = 0;
				selectedFloorCutoutId = 0;
				movingRoom = true;
				BeginLayoutEdit();
				dragStart = cursor;
				moveStartBounds = room.Bounds;
				return;
			}

			if ( selectedLayoutAtCursor )
			{
				return;
			}

			selectedRoomId = 0;
			selectedFloorCutoutId = 0;
			drawingRoom = true;
			dragStart = cursor;
			dragCurrent = cursor;
		}

		if ( drawingRoom )
		{
			dragCurrent = cursor;
			if ( Gizmo.WasLeftMouseReleased )
			{
				CommitDrawnRoom();
			}
			return;
		}

		if ( movingRoom )
		{
			if ( document.FindRoom( selectedRoomId ) is { } room )
			{
				var delta = cursor - dragStart;
				room.Bounds = SnapRect( moveStartBounds.MovedBy( delta ) );
			}

			if ( Gizmo.WasLeftMouseReleased )
			{
				movingRoom = false;
				CommitLayoutEdit();
			}
		}
	}

	private bool SelectLayoutAtCursor( Vector2 cursor )
	{
		if ( mode == RoomLayoutToolMode.Select )
		{
			if ( TryFindFloorCutout( cursor, out var selectCutout ) )
			{
				SelectFloorCutout( selectCutout );
				return true;
			}

			if ( TryFindNearestDoor( cursor, out var selectDoor, OpeningHitDistance() ) )
			{
				SelectDoor( selectDoor );
				return true;
			}

			if ( TryFindWindowUnderCursor( out var selectWindow ) ||
				TryFindNearestWindow( cursor, out selectWindow, WindowHitDistance() ) )
			{
				SelectWindow( selectWindow );
				return true;
			}

			if ( TryFindNearestCorridor( cursor, out var selectCorridor ) )
			{
				SelectCorridor( selectCorridor );
				return true;
			}
		}

		if ( mode == RoomLayoutToolMode.Doors && TryFindNearestDoor( cursor, out var door, OpeningHitDistance() ) )
		{
			SelectDoor( door );
			return true;
		}

		if ( mode == RoomLayoutToolMode.Windows &&
			(TryFindWindowUnderCursor( out var window ) ||
				TryFindNearestWindow( cursor, out window, WindowHitDistance() )) )
		{
			SelectWindow( window );
			return true;
		}

		if ( mode == RoomLayoutToolMode.Corridors && TryFindNearestCorridor( cursor, out var modeCorridor ) )
		{
			SelectCorridor( modeCorridor );
			return true;
		}

		if ( mode == RoomLayoutToolMode.FloorCutouts && TryFindFloorCutout( cursor, out var cutout ) )
		{
			SelectFloorCutout( cutout );
			return true;
		}

		if ( mode == RoomLayoutToolMode.FloorCutouts )
		{
			return false;
		}

		if ( TryFindRoom( cursor, out var room ) || TryFindRoomWall( cursor, out room ) )
		{
			selectedRoomId = room.Id;
			selectedDoorId = 0;
			selectedWindowId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			return true;
		}

		if ( TryFindNearestCorridor( cursor, out var corridor ) )
		{
			SelectCorridor( corridor );
			return true;
		}

		selectedRoomId = 0;
		selectedDoorId = 0;
		selectedWindowId = 0;
		selectedCorridorId = 0;
		selectedFloorCutoutId = 0;
		return false;
	}

	private void SelectDoor( RoomLayoutDoor door )
	{
		selectedRoomId = door.RoomId;
		selectedDoorId = door.Id;
		selectedWindowId = 0;
		selectedCorridorId = 0;
		selectedFloorCutoutId = 0;
		SetActiveFloorFromSelection();
	}

	private void SelectWindow( RoomLayoutWindow window )
	{
		selectedRoomId = window.RoomId;
		selectedDoorId = 0;
		selectedWindowId = window.Id;
		selectedCorridorId = 0;
		selectedFloorCutoutId = 0;
		SetActiveFloorFromSelection();
	}

	private void SelectCorridor( RoomLayoutCorridor corridor )
	{
		selectedRoomId = 0;
		selectedDoorId = 0;
		selectedWindowId = 0;
		selectedCorridorId = corridor.Id;
		selectedFloorCutoutId = 0;
		SetActiveFloorFromSelection();
	}

	private float OpeningHitDistance()
	{
		return MathF.Max( 8.0f, document.Settings.GridSize * 0.25f );
	}

	private float WindowHitDistance()
	{
		return MathF.Max( 16.0f, document.Settings.GridSize * 0.45f );
	}

	private void HandleDoorMouse( Vector2 cursor )
	{
		if ( movingDoor )
		{
			MoveSelectedDoorTo( cursor );
			if ( Gizmo.WasLeftMouseReleased )
			{
				movingDoor = false;
				CommitLayoutEdit();
			}
			return;
		}

		if ( !Gizmo.WasLeftMousePressed )
		{
			return;
		}

		if ( TryFindNearestDoor( cursor, out var door, OpeningHitDistance() ) )
		{
			SelectDoor( door );
			movingDoor = true;
			BeginLayoutEdit();
			MoveSelectedDoorTo( cursor );
			return;
		}

		if ( TryFindNearestWall( cursor, out var room, out var side, out var offset ) )
		{
			PushLayoutUndo();
			var created = AddDoor( room, side, offset );
			selectedDoorId = created.Id;
			selectedRoomId = room.Id;
			selectedWindowId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange();
			return;
		}

		if ( TryFindNearestCorridorWall( cursor, out var corridor, out var segmentIndex, out var corridorSide, out var corridorOffset, out var corridorLength ) )
		{
			PushLayoutUndo();
			var created = AddCorridorDoor( corridor, segmentIndex, corridorSide, corridorOffset, corridorLength );
			selectedDoorId = created.Id;
			selectedRoomId = 0;
			selectedWindowId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange();
		}
	}

	private void HandleWindowMouse( Vector2 cursor )
	{
		if ( movingWindow )
		{
			MoveSelectedWindowTo( cursor );
			if ( Gizmo.WasLeftMouseReleased )
			{
				movingWindow = false;
				CommitLayoutEdit();
			}
			return;
		}

		if ( !Gizmo.WasLeftMousePressed )
		{
			return;
		}

		if ( TryFindWindowUnderCursor( out var window ) ||
			TryFindNearestWindow( cursor, out window, WindowHitDistance() ) )
		{
			SelectWindow( window );
			movingWindow = true;
			BeginLayoutEdit();
			MoveSelectedWindowTo( cursor );
			return;
		}

		if ( TryFindNearestWall( cursor, out var room, out var side, out var offset ) )
		{
			PushLayoutUndo();
			var created = AddWindow( room, side, offset );
			selectedWindowId = created.Id;
			selectedRoomId = room.Id;
			selectedDoorId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange();
			return;
		}

		if ( TryFindNearestCorridorWall( cursor, out var corridor, out var segmentIndex, out var corridorSide, out var corridorOffset, out var corridorLength ) )
		{
			PushLayoutUndo();
			var created = AddCorridorWindow( corridor, segmentIndex, corridorSide, corridorOffset, corridorLength );
			selectedWindowId = created.Id;
			selectedRoomId = 0;
			selectedDoorId = 0;
			selectedCorridorId = 0;
			selectedFloorCutoutId = 0;
			CommitLayoutChange();
		}
	}

	private void CommitDrawnRoom()
	{
		var bounds = SnapRect( RoomLayoutRect.FromPoints( dragStart, dragCurrent ) );
		drawingRoom = false;

		if ( bounds.Width < document.Settings.GridSize || bounds.Height < document.Settings.GridSize )
		{
			return;
		}

		var room = new RoomLayoutRoom
		{
			Id = document.AllocateId(),
			Floor = activeFloor,
			Name = $"Room {document.Rooms.Count + 1}",
			Bounds = bounds
		};

		PushLayoutUndo();
		document.Rooms.Add( room );
		selectedRoomId = room.Id;
		selectedDoorId = 0;
		selectedWindowId = 0;
		selectedCorridorId = 0;
		selectedFloorCutoutId = 0;
		CommitLayoutChange();
	}

	private RoomLayoutDoor AddDoor( RoomLayoutRoom room, RoomLayoutWallSide side, float offset )
	{
		var wallLength = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? room.Bounds.Width
			: room.Bounds.Height;

		var width = MathF.Min( document.Settings.DoorWidth, MathF.Max( 1.0f, wallLength ) );
		var halfWidth = width * 0.5f;
		var snappedOffset = Snap( offset );

		var door = new RoomLayoutDoor
		{
			Id = document.AllocateId(),
			RoomId = room.Id,
			Side = side,
			Offset = Math.Clamp( snappedOffset, halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) ),
			Width = width,
			Height = document.Settings.DoorHeight
		};

		document.Doors.Add( door );
		return door;
	}

	private RoomLayoutDoor AddCorridorDoor( RoomLayoutCorridor corridor, int segmentIndex, int side, float offset, float wallLength )
	{
		var width = MathF.Min( document.Settings.DoorWidth, MathF.Max( 1.0f, wallLength ) );
		var halfWidth = width * 0.5f;
		var snappedOffset = SnapHalfGrid( offset );

		var door = new RoomLayoutDoor
		{
			Id = document.AllocateId(),
			CorridorId = corridor.Id,
			CorridorSegmentIndex = segmentIndex,
			CorridorSide = side,
			Offset = Math.Clamp( snappedOffset, halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) ),
			Width = width,
			Height = document.Settings.DoorHeight
		};

		document.Doors.Add( door );
		return door;
	}

	private RoomLayoutWindow AddWindow( RoomLayoutRoom room, RoomLayoutWallSide side, float offset )
	{
		var wallLength = side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? room.Bounds.Width
			: room.Bounds.Height;

		var width = MathF.Min( document.Settings.WindowWidth, MathF.Max( 1.0f, wallLength ) );
		var halfWidth = width * 0.5f;
		var snappedOffset = Snap( offset );

		var window = new RoomLayoutWindow
		{
			Id = document.AllocateId(),
			RoomId = room.Id,
			Side = side,
			Offset = Math.Clamp( snappedOffset, halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) ),
			Width = width,
			Height = document.Settings.WindowHeight,
			SillHeight = document.Settings.WindowSillHeight
		};

		document.Windows.Add( window );
		return window;
	}

	private RoomLayoutWindow AddCorridorWindow( RoomLayoutCorridor corridor, int segmentIndex, int side, float offset, float wallLength )
	{
		var width = MathF.Min( document.Settings.WindowWidth, MathF.Max( 1.0f, wallLength ) );
		var halfWidth = width * 0.5f;
		var snappedOffset = SnapHalfGrid( offset );

		var window = new RoomLayoutWindow
		{
			Id = document.AllocateId(),
			CorridorId = corridor.Id,
			CorridorSegmentIndex = segmentIndex,
			CorridorSide = side,
			Offset = Math.Clamp( snappedOffset, halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) ),
			Width = width,
			Height = document.Settings.WindowHeight,
			SillHeight = document.Settings.WindowSillHeight
		};

		document.Windows.Add( window );
		return window;
	}

	private void MoveSelectedDoorTo( Vector2 cursor )
	{
		if ( document.FindDoor( selectedDoorId ) is not { } door )
		{
			movingDoor = false;
			return;
		}

		if ( door.CorridorId != 0 )
		{
			MoveCorridorOpeningTo( door.CorridorId, door.CorridorSegmentIndex, door.Width, cursor, value => door.Offset = value );
			return;
		}

		if ( document.FindRoom( door.RoomId ) is not { } room )
		{
			movingDoor = false;
			return;
		}

		var wallLength = WallLength( room, door.Side );
		var halfWidth = MathF.Max( 0.5f, DoorOpeningWidth( door ) * 0.5f );
		var offset = OffsetAlongRoomWall( room, door.Side, cursor );
		door.Offset = Math.Clamp( Snap( offset ), halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) );
	}

	private void MoveSelectedDoorByDrag( Vector2 cursor )
	{
		if ( document.FindDoor( selectedDoorId ) is not { } door )
		{
			movingDoor = false;
			return;
		}

		if ( door.CorridorId != 0 )
		{
			MoveCorridorOpeningByDrag( door.CorridorId, door.CorridorSegmentIndex, door.Width, cursor, value => door.Offset = value );
			return;
		}

		if ( document.FindRoom( door.RoomId ) is not { } room )
		{
			movingDoor = false;
			return;
		}

		var wallLength = WallLength( room, door.Side );
		var halfWidth = MathF.Max( 0.5f, DoorOpeningWidth( door ) * 0.5f );
		var offset = moveStartOffset + DeltaAlongRoomWall( door.Side, cursor - dragStart );
		door.Offset = Math.Clamp( Snap( offset ), halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) );
	}

	private void MoveSelectedWindowTo( Vector2 cursor )
	{
		if ( document.FindWindow( selectedWindowId ) is not { } window )
		{
			movingWindow = false;
			return;
		}

		if ( window.CorridorId != 0 )
		{
			MoveCorridorOpeningTo( window.CorridorId, window.CorridorSegmentIndex, window.Width, cursor, value => window.Offset = value );
			return;
		}

		if ( document.FindRoom( window.RoomId ) is not { } room )
		{
			movingWindow = false;
			return;
		}

		var wallLength = WallLength( room, window.Side );
		var halfWidth = MathF.Max( 0.5f, window.Width * 0.5f );
		var offset = OffsetAlongRoomWall( room, window.Side, cursor );
		window.Offset = Math.Clamp( Snap( offset ), halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) );
	}

	private void MoveSelectedWindowByDrag( Vector2 cursor )
	{
		if ( document.FindWindow( selectedWindowId ) is not { } window )
		{
			movingWindow = false;
			return;
		}

		if ( window.CorridorId != 0 )
		{
			MoveCorridorOpeningByDrag( window.CorridorId, window.CorridorSegmentIndex, window.Width, cursor, value => window.Offset = value );
			return;
		}

		if ( document.FindRoom( window.RoomId ) is not { } room )
		{
			movingWindow = false;
			return;
		}

		var wallLength = WallLength( room, window.Side );
		var halfWidth = MathF.Max( 0.5f, window.Width * 0.5f );
		var offset = moveStartOffset + DeltaAlongRoomWall( window.Side, cursor - dragStart );
		window.Offset = Math.Clamp( Snap( offset ), halfWidth, MathF.Max( halfWidth, wallLength - halfWidth ) );
	}

	private void MoveCorridorOpeningTo( int corridorId, int segmentIndex, float openingWidth, Vector2 cursor, Action<float> setOffset )
	{
		var corridor = document.Corridors.FirstOrDefault( candidate => candidate.Id == corridorId );
		if ( corridor is null || !TryGetCorridorPath( corridor, out var points ) || segmentIndex < 0 || segmentIndex >= points.Count - 1 )
		{
			movingDoor = false;
			movingWindow = false;
			return;
		}

		var a = points[segmentIndex];
		var b = points[segmentIndex + 1];
		var delta = b - a;
		var length = delta.Length;
		if ( length < 1.0f )
		{
			return;
		}

		var t = (((cursor.x - a.x) * delta.x) + ((cursor.y - a.y) * delta.y)) / (length * length);
		var halfWidth = openingWidth * 0.5f;
		var maxOffset = MathF.Max( halfWidth, length - halfWidth );
		var offset = Math.Clamp( t * length, halfWidth, maxOffset );
		setOffset( Math.Clamp( SnapHalfGrid( offset ), halfWidth, maxOffset ) );
	}

	private void MoveCorridorOpeningByDrag( int corridorId, int segmentIndex, float openingWidth, Vector2 cursor, Action<float> setOffset )
	{
		var corridor = document.Corridors.FirstOrDefault( candidate => candidate.Id == corridorId );
		if ( corridor is null || !TryGetCorridorPath( corridor, out var points ) || segmentIndex < 0 || segmentIndex >= points.Count - 1 )
		{
			movingDoor = false;
			movingWindow = false;
			return;
		}

		var a = points[segmentIndex];
		var b = points[segmentIndex + 1];
		var delta = b - a;
		var length = delta.Length;
		if ( length < 1.0f )
		{
			return;
		}

		var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
		var deltaAlong = horizontal ? cursor.x - dragStart.x : cursor.y - dragStart.y;
		var halfWidth = openingWidth * 0.5f;
		var maxOffset = MathF.Max( halfWidth, length - halfWidth );
		var offset = Math.Clamp( moveStartOffset + deltaAlong, halfWidth, maxOffset );
		setOffset( Math.Clamp( SnapHalfGrid( offset ), halfWidth, maxOffset ) );
	}

	private static float WallLength( RoomLayoutRoom room, RoomLayoutWallSide side )
	{
		return side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? room.Bounds.Width
			: room.Bounds.Height;
	}

	private static float DeltaAlongRoomWall( RoomLayoutWallSide side, Vector2 delta )
	{
		return side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? delta.x
			: delta.y;
	}

	private static float OffsetAlongRoomWall( RoomLayoutRoom room, RoomLayoutWallSide side, Vector2 cursor )
	{
		return side is RoomLayoutWallSide.North or RoomLayoutWallSide.South
			? cursor.x - room.Bounds.X
			: cursor.y - room.Bounds.Y;
	}

	private bool IsCursorOverExternalSceneObject()
	{
		var trace = Trace.UseRenderMeshes( true ).UsePhysicsWorld( false ).Run();
		return trace.Hit && trace.GameObject.IsValid() && !IsGeneratedLayoutObject( trace.GameObject );
	}

	private static bool IsGeneratedLayoutObject( GameObject gameObject )
	{
		for ( var current = gameObject; current.IsValid(); current = current.Parent )
		{
			if ( current.Name == RoomLayoutGeometryBuilder.GeneratedRootName )
			{
				return true;
			}
		}

		return false;
	}

}