Editor/InteriorLayoutBuilder/RoomLayoutTool.CorridorInput.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutTool
{
	private void HandleCorridorMouse( Vector2 cursor )
	{
		if ( drawingCorridor )
		{
			UpdateCorridorDraftPreview( cursor );

			if ( Gizmo.WasLeftMousePressed )
			{
				HandleCorridorDraftClick( cursor );
			}

			return;
		}

		if ( !Gizmo.WasLeftMousePressed )
		{
			return;
		}

		if ( !TryFindNearestDoor( cursor, out var door, document.Settings.GridSize * 0.65f, roomOnly: true ) )
		{
			if ( TryFindNearestCorridor( cursor, out var clickedCorridor ) )
			{
				selectedCorridorId = clickedCorridor.Id;
				selectedRoomId = 0;
				selectedDoorId = 0;
				selectedWindowId = 0;
				selectedFloorCutoutId = 0;
			}

			return;
		}

		StartCorridorDraft( door );
	}

	private void HandleCorridorDraftClick( Vector2 cursor )
	{
		if ( TryFindNearestDoor( cursor, out var door, document.Settings.GridSize * 0.65f, roomOnly: true ) && door.Id != selectedDoorId )
		{
			TryFinishCorridorDraft( door );
			return;
		}

		AddCorridorDraftBend( cursor );
	}

	private void StartCorridorDraft( RoomLayoutDoor door )
	{
		selectedDoorId = door.Id;
		selectedRoomId = door.RoomId;
		selectedCorridorId = 0;
		selectedWindowId = 0;
		selectedFloorCutoutId = 0;
		drawingCorridor = true;
		corridorDraftBendPoints.Clear();
		corridorDraftForcedHorizontal = null;
		TryGetCorridorDraftLastPoint( out corridorDraftCurrent );
	}

	private bool TryFinishCorridorDraft( RoomLayoutDoor door )
	{
		if ( !TryGetDoorAnchor( door.Id, out var endAnchor ) )
		{
			return false;
		}

		var width = CorridorWidthForDoors( selectedDoorId, door.Id );
		if ( !TryGetCorridorDraftLastPoint( width, out var last ) )
		{
			return false;
		}

		var endTarget = CorridorApproachPoint( endAnchor, width );
		var bends = CleanCorridorDraftBends();

		if ( !IsOrthogonalSegment( last, endTarget ) )
		{
			var corner = PreferredFinalCorridorCorner( last, endTarget );
			if ( Vector2.DistanceBetween( last, corner ) < 1.0f ||
				Vector2.DistanceBetween( corner, endTarget ) < 1.0f )
			{
				return false;
			}

			bends.Add( RoomLayoutPoint.FromVector( corner ) );
		}

		var createdCorridor = new RoomLayoutCorridor
		{
			Id = document.AllocateId(),
			Floor = activeFloor,
			StartDoorId = selectedDoorId,
			EndDoorId = door.Id,
			Width = width,
			ManualPath = true,
			BendPoints = CleanCorridorBends( bends )
		};

		PushLayoutUndo();
		document.Corridors.Add( createdCorridor );
		selectedCorridorId = createdCorridor.Id;
		CancelCorridorDraft();
		CommitLayoutChange();
		return true;
	}

	private List<RoomLayoutPoint> CleanCorridorBends( IEnumerable<RoomLayoutPoint> bends )
	{
		var points = bends
			.Select( bend => bend.Vector )
			.ToList();

		SimplifyCorridorPath( points );
		return points.Select( RoomLayoutPoint.FromVector ).ToList();
	}

	private void AddCorridorDraftBend( Vector2 cursor )
	{
		if ( !TryGetCorridorDraftLastPoint( out var last ) )
		{
			return;
		}

		var next = ConstrainCorridorPoint( last, cursor, corridorDraftForcedHorizontal );
		if ( Vector2.DistanceBetween( last, next ) < 1.0f )
		{
			ToggleCorridorDraftDirection();
			UpdateCorridorDraftPreview( cursor );
			return;
		}

		corridorDraftBendPoints.Add( RoomLayoutPoint.FromVector( next ) );
		corridorDraftCurrent = next;
		corridorDraftForcedHorizontal = null;
	}

	private void UpdateCorridorDraftPreview( Vector2 cursor )
	{
		if ( !TryGetCorridorDraftLastPoint( out var last ) )
		{
			corridorDraftCurrent = cursor;
			return;
		}

		corridorDraftCurrent = ConstrainCorridorPoint( last, cursor, corridorDraftForcedHorizontal );
	}

	private bool TryGetCorridorDraftLastPoint( out Vector2 point )
	{
		return TryGetCorridorDraftLastPoint( CorridorDraftWidth(), out point );
	}

	private bool TryGetCorridorDraftLastPoint( float width, out Vector2 point )
	{
		if ( corridorDraftBendPoints.Count > 0 )
		{
			point = corridorDraftBendPoints[^1].Vector;
			return true;
		}

		point = default;
		if ( !TryGetDoorAnchor( selectedDoorId, out var startAnchor ) )
		{
			return false;
		}

		point = CorridorApproachPoint( startAnchor, width );
		return true;
	}

	private float CorridorDraftWidth()
	{
		var selectedDoor = document.FindDoor( selectedDoorId );
		var width = MathF.Max( 1.0f, document.Settings.DefaultCorridorWidth );
		return selectedDoor is null || selectedDoor.Width <= 0.0f
			? width
			: MathF.Min( width, selectedDoor.Width );
	}

	private List<RoomLayoutPoint> CleanCorridorDraftBends()
	{
		return CleanCorridorBends( corridorDraftBendPoints );
	}

	private void CancelCorridorDraft( bool clearSelectedDoor = true )
	{
		drawingCorridor = false;
		corridorDraftBendPoints.Clear();
		corridorDraftCurrent = default;
		corridorDraftForcedHorizontal = null;

		if ( clearSelectedDoor )
		{
			selectedDoorId = 0;
		}
	}

	private void ToggleCorridorDraftDirection()
	{
		corridorDraftForcedHorizontal = corridorDraftForcedHorizontal.HasValue
			? !corridorDraftForcedHorizontal.Value
			: PreferredInstantTurnAxis();
	}

	private bool PreferredInstantTurnAxis()
	{
		if ( TryGetCorridorDraftPreviousPoint( out var previous ) &&
			TryGetCorridorDraftLastPoint( out var last ) &&
			Vector2.DistanceBetween( previous, last ) > 1.0f )
		{
			return !IsHorizontalSegment( previous, last );
		}

		if ( TryGetDoorAnchor( selectedDoorId, out var start ) )
		{
			return MathF.Abs( start.Normal.y ) > MathF.Abs( start.Normal.x );
		}

		return true;
	}

	private bool TryGetCorridorDraftPreviousPoint( out Vector2 point )
	{
		point = default;

		if ( corridorDraftBendPoints.Count > 1 )
		{
			point = corridorDraftBendPoints[^2].Vector;
			return true;
		}

		if ( corridorDraftBendPoints.Count == 1 && TryGetDoorAnchor( selectedDoorId, out var start ) )
		{
			point = CorridorApproachPoint( start, CorridorDraftWidth() );
			return true;
		}

		return false;
	}

	private Vector2 ConstrainCorridorPoint( Vector2 from, Vector2 to, bool? forcedHorizontal = null )
	{
		var dx = MathF.Abs( to.x - from.x );
		var dy = MathF.Abs( to.y - from.y );
		var horizontal = forcedHorizontal ?? PreferredCorridorDraftAxis( from, dx, dy );
		return horizontal
			? new Vector2( to.x, from.y )
			: new Vector2( from.x, to.y );
	}

	private bool PreferredCorridorDraftAxis( Vector2 from, float dx, float dy )
	{
		if ( MathF.Abs( dx - dy ) > 0.001f )
		{
			return dx > dy;
		}

		if ( TryGetCorridorDraftPreviousPoint( out var previous ) &&
			Vector2.DistanceBetween( previous, from ) > 1.0f )
		{
			return !IsHorizontalSegment( previous, from );
		}

		return true;
	}

	private Vector2 PreferredFinalCorridorCorner( Vector2 last, Vector2 endPortal )
	{
		if ( corridorDraftForcedHorizontal.HasValue )
		{
			return corridorDraftForcedHorizontal.Value
				? new Vector2( endPortal.x, last.y )
				: new Vector2( last.x, endPortal.y );
		}

		if ( TryGetCorridorDraftPreviousPoint( out var previous ) &&
			Vector2.DistanceBetween( previous, last ) > 1.0f )
		{
			return IsHorizontalSegment( previous, last )
				? new Vector2( endPortal.x, last.y )
				: new Vector2( last.x, endPortal.y );
		}

		var dx = MathF.Abs( endPortal.x - last.x );
		var dy = MathF.Abs( endPortal.y - last.y );
		return dx >= dy
			? new Vector2( endPortal.x, last.y )
			: new Vector2( last.x, endPortal.y );
	}

	private static bool IsOrthogonalSegment( Vector2 a, Vector2 b )
	{
		return a.x.AlmostEqual( b.x ) || a.y.AlmostEqual( b.y );
	}

	private static bool IsHorizontalSegment( Vector2 a, Vector2 b )
	{
		return MathF.Abs( b.x - a.x ) >= MathF.Abs( b.y - a.y );
	}
}