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

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutTool
{
	private const TextFlag LayoutLabelFlags = TextFlag.Center | TextFlag.DontClip;

	private void DrawLayout()
	{
		using ( Gizmo.Scope( "Interior Layout Builder" ) )
		{
			Gizmo.Draw.IgnoreDepth = true;

			foreach ( var corridor in document.Corridors.Where( IsActiveFloor ) )
			{
				DrawCorridor( corridor );
			}

			if ( drawingCorridor )
			{
				DrawCorridorDraft();
			}

			foreach ( var room in document.Rooms.Where( IsActiveFloor ) )
			{
				DrawRoom( room, room.Id == selectedRoomId );
			}

			foreach ( var cutout in document.FloorCutouts.Where( IsActiveFloor ) )
			{
				DrawFloorCutout( cutout, cutout.Id == selectedFloorCutoutId );
			}

			foreach ( var door in document.Doors.Where( IsActiveFloor ) )
			{
				DrawDoor( door, door.Id == selectedDoorId );
			}

			foreach ( var window in document.Windows.Where( IsActiveFloor ) )
			{
				DrawWindow( window, window.Id == selectedWindowId );
			}

			if ( drawingRoom )
			{
				DrawRect( SnapRect( RoomLayoutRect.FromPoints( dragStart, dragCurrent ) ), Color.Green );
			}

			if ( drawingFloorCutout )
			{
				DrawRect( SnapRect( RoomLayoutRect.FromPoints( dragStart, dragCurrent ) ), Color.Red, 3.0f );
			}
		}
	}

	private void DrawRoom( RoomLayoutRoom room, bool selected )
	{
		DrawRect( room.Bounds, selected ? Color.Cyan : Color.White );
		DrawLayoutLabel( room.Name, room.Bounds.Center, selected ? Color.Cyan : Color.White );
	}

	private void DrawFloorCutout( RoomLayoutFloorCutout cutout, bool selected )
	{
		var color = selected ? Color.Orange : Color.Red;
		DrawRect( cutout.Bounds, color, selected ? 4.0f : 2.5f );
		DrawCutoutCross( cutout.Bounds, color );
		DrawLayoutLabel(
			string.IsNullOrWhiteSpace( cutout.Name ) ? $"Floor Cutout {cutout.Id}" : cutout.Name,
			cutout.Bounds.Center,
			color );
	}

	private void DrawCutoutCross( RoomLayoutRect rect, Color color )
	{
		var z = ActiveFloorZ() + 4.0f;
		Gizmo.Draw.Color = color.WithAlpha( 0.8f );
		Gizmo.Draw.LineThickness = 1.5f;
		Gizmo.Draw.Line(
			new Vector3( rect.X, rect.Y, z ),
			new Vector3( rect.X + rect.Width, rect.Y + rect.Height, z ) );
		Gizmo.Draw.Line(
			new Vector3( rect.X + rect.Width, rect.Y, z ),
			new Vector3( rect.X, rect.Y + rect.Height, z ) );
		Gizmo.Draw.LineThickness = 1.0f;
	}

	private void DrawLayoutLabel( string text, Vector2 position, Color color )
	{
		var textScope = new TextRendering.Scope
		{
			Text = text,
			TextColor = color,
			FontName = "Roboto",
			FontSize = 16.0f,
			Outline = new TextRendering.Outline { Enabled = true, Color = Color.Black, Size = 2.0f }
		};
		Gizmo.Draw.ScreenText( textScope, new Vector3( position.x, position.y, ActiveFloorZ() + 8.0f ), Vector2.Zero, LayoutLabelFlags );
	}

	private void DrawDoor( RoomLayoutDoor door, bool selected )
	{
		if ( !TryGetDoorFootprint( door, out var footprint ) )
		{
			return;
		}

		DrawRect( footprint, selected ? Color.Orange : Color.Yellow, selected ? 4.0f : 2.5f );
	}

	private void DrawWindow( RoomLayoutWindow window, bool selected )
	{
		if ( !TryGetWindowFootprint( window, out var footprint ) )
		{
			return;
		}

		DrawRect( footprint, selected ? Color.Orange : Color.Magenta, selected ? 4.0f : 2.5f );
	}

	private void DrawCorridor( RoomLayoutCorridor corridor )
	{
		if ( !TryGetCorridorPath( corridor, out var points ) )
		{
			return;
		}

		var selected = corridor.Id == selectedCorridorId;
		var width = CorridorClearWidth( corridor );
		var boundsColor = selected ? Color.Orange : Color.Blue.WithAlpha( 0.75f );

		Gizmo.Draw.LineThickness = selected ? 3.0f : 2.0f;
		if ( document.Settings.ThresholdsEnabled )
		{
			DrawCorridorThresholds( corridor, width, boundsColor );
		}

		for ( var i = 0; i < points.Count - 1; i++ )
		{
			DrawCorridorSegmentBounds( points[i], points[i + 1], width, boundsColor );
		}

		DrawCorridorJoinBounds( points, width, boundsColor );

		Gizmo.Draw.Color = selected ? Color.Orange.WithAlpha( 0.9f ) : Color.Blue.WithAlpha( 0.55f );
		Gizmo.Draw.LineThickness = selected ? 4.0f : 2.0f;

		for ( var i = 0; i < points.Count - 1; i++ )
		{
			Gizmo.Draw.Line(
				new Vector3( points[i].x, points[i].y, ActiveFloorZ() + 5.0f ),
				new Vector3( points[i + 1].x, points[i + 1].y, ActiveFloorZ() + 5.0f ) );
		}

		Gizmo.Draw.LineThickness = 1.0f;
		DrawLayoutLabel( $"Corridor {corridor.Id}", CorridorLabelPosition( points ), selected ? Color.Orange : Color.White );
	}

	private static Vector2 CorridorLabelPosition( IReadOnlyList<Vector2> points )
	{
		var bestLength = 0.0f;
		var bestPosition = points.Count > 0 ? points[0] : Vector2.Zero;

		for ( var i = 0; i < points.Count - 1; i++ )
		{
			var a = points[i];
			var b = points[i + 1];
			var length = (b - a).Length;
			if ( length <= bestLength )
			{
				continue;
			}

			bestLength = length;
			bestPosition = (a + b) * 0.5f;
		}

		return bestPosition;
	}

	private void DrawCorridorDraft()
	{
		if ( !TryGetDoorAnchor( selectedDoorId, out var start ) )
		{
			return;
		}

		var width = CorridorDraftWidth();
		var points = new List<Vector2>();
		AddCorridorPoint( points, CorridorPortalPoint( start ) );
		AddCorridorPoint( points, CorridorApproachPoint( start, width ) );

		foreach ( var bend in corridorDraftBendPoints )
		{
			AddCorridorPoint( points, bend.Vector );
		}

		AddCorridorPoint( points, corridorDraftCurrent );
		SimplifyCorridorPath( points );

		var color = Color.Green.WithAlpha( 0.85f );
		Gizmo.Draw.LineThickness = 2.0f;
		if ( document.Settings.ThresholdsEnabled )
		{
			DrawCorridorThreshold( start, width, color );
		}

		for ( var i = 0; i < points.Count - 1; i++ )
		{
			DrawCorridorSegmentBounds( points[i], points[i + 1], width, color );
		}

		DrawCorridorJoinBounds( points, width, color );

		Gizmo.Draw.Color = color;
		Gizmo.Draw.LineThickness = 4.0f;
		for ( var i = 0; i < points.Count - 1; i++ )
		{
			Gizmo.Draw.Line(
				new Vector3( points[i].x, points[i].y, ActiveFloorZ() + 6.0f ),
				new Vector3( points[i + 1].x, points[i + 1].y, ActiveFloorZ() + 6.0f ) );
		}

		Gizmo.Draw.LineThickness = 1.0f;
	}

	private void DrawCorridorThresholds( RoomLayoutCorridor corridor, float width, Color color )
	{
		if ( TryGetDoorAnchor( corridor.StartDoorId, out var start ) )
		{
			DrawCorridorThreshold( start, width, color );
		}

		if ( TryGetDoorAnchor( corridor.EndDoorId, out var end ) )
		{
			DrawCorridorThreshold( end, width, color );
		}
	}

	private void DrawCorridorThreshold( RoomLayoutDoorAnchor anchor, float width, Color color )
	{
		var center = anchor.Point + anchor.Normal * (document.Settings.WallThickness * 0.5f);
		var verticalWall = MathF.Abs( anchor.Normal.y ) > MathF.Abs( anchor.Normal.x );
		var thresholdDepth = Math.Clamp( document.Settings.ThresholdDepth, 0.5f, 512.0f );
		var rect = verticalWall
			? new RoomLayoutRect( center.x - width * 0.5f, center.y - thresholdDepth * 0.5f, width, thresholdDepth )
			: new RoomLayoutRect( center.x - thresholdDepth * 0.5f, center.y - width * 0.5f, thresholdDepth, width );

		DrawRect( rect, color );
	}

	private void DrawCorridorJoinBounds( IReadOnlyList<Vector2> points, float width, Color color )
	{
		for ( var i = 1; i < points.Count - 1; i++ )
		{
			DrawRect( new RoomLayoutRect( points[i].x - width * 0.5f, points[i].y - width * 0.5f, width, width ), color );
		}
	}

	private void DrawCorridorSegmentBounds( Vector2 a, Vector2 b, float width, Color color )
	{
		var delta = b - a;
		if ( delta.Length < 1.0f )
		{
			return;
		}

		var horizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
		if ( horizontal )
		{
			var minX = MathF.Min( a.x, b.x );
			DrawRect( new RoomLayoutRect( minX, a.y - width * 0.5f, MathF.Abs( delta.x ), width ), color );
			return;
		}

		var minY = MathF.Min( a.y, b.y );
		DrawRect( new RoomLayoutRect( a.x - width * 0.5f, minY, width, MathF.Abs( delta.y ) ), color );
	}

	private void DrawSelectedRoomBounds()
	{
		if ( mode is not (RoomLayoutToolMode.Rooms or RoomLayoutToolMode.Select) ||
			drawingRoom ||
			movingRoom ||
			selectedRoomId == 0 ||
			document.FindRoom( selectedRoomId ) is not { } room )
		{
			return;
		}

		var bounds = room.Bounds;
		var box = new BBox(
			new Vector3( bounds.X, bounds.Y, ActiveFloorZ() ),
			new Vector3( bounds.X + bounds.Width, bounds.Y + bounds.Height, ActiveFloorZ() + document.Settings.WallHeight ) );

		using ( Gizmo.Scope( $"Room {room.Id} Resize" ) )
		{
			Gizmo.Hitbox.DepthBias = 0.01f;

			if ( !Gizmo.Pressed.Any )
			{
				roomResizeStartBox = box;
				roomResizeDeltaBox = default;
			}

			if ( Gizmo.Control.BoundingBox( $"Room {room.Id} Bounds", box, out var resized ) )
			{
				BeginLayoutEdit();
				resizingRoom = true;
				roomResizeDeltaBox.Mins += resized.Mins - box.Mins;
				roomResizeDeltaBox.Maxs += resized.Maxs - box.Maxs;

				var snapped = SnapGizmoBox( roomResizeStartBox, roomResizeDeltaBox );
				var next = SnapRect( new RoomLayoutRect( snapped.Mins.x, snapped.Mins.y, snapped.Size.x, snapped.Size.y ) );
				if ( next.Width >= document.Settings.GridSize && next.Height >= document.Settings.GridSize )
				{
					room.Bounds = next;
				}
			}
			else if ( resizingRoom && !Gizmo.IsLeftMouseDown )
			{
				resizingRoom = false;
				CommitLayoutEdit();
			}
		}
	}

	private void DrawSelectedCorridorBounds()
	{
		if ( mode is not (RoomLayoutToolMode.Corridors or RoomLayoutToolMode.Select) ||
			selectedCorridorId == 0 ||
			SelectedCorridor() is not { } corridor )
		{
			return;
		}

		if ( !TryGetCorridorFootprint( corridor, out var bounds, out var horizontal ) )
		{
			return;
		}

		var box = new BBox(
			new Vector3( bounds.X, bounds.Y, ActiveFloorZ() ),
			new Vector3( bounds.X + bounds.Width, bounds.Y + bounds.Height, ActiveFloorZ() + document.Settings.WallHeight ) );

		using ( Gizmo.Scope( $"Corridor {corridor.Id} Width" ) )
		{
			Gizmo.Hitbox.DepthBias = 0.01f;

			if ( !Gizmo.Pressed.Any )
			{
				corridorResizeStartBox = box;
				corridorResizeDeltaBox = default;
			}

			if ( Gizmo.Control.BoundingBox( $"Corridor {corridor.Id} Width Bounds", box, out var resized ) )
			{
				BeginLayoutEdit();
				resizingCorridor = true;
				corridorResizeDeltaBox.Mins += resized.Mins - box.Mins;
				corridorResizeDeltaBox.Maxs += resized.Maxs - box.Maxs;

				var snapped = SnapGizmoBox( corridorResizeStartBox, corridorResizeDeltaBox );
				var resizedWidth = horizontal ? snapped.Size.y : snapped.Size.x;
				SetCorridorWidth( corridor, resizedWidth );
			}
			else if ( resizingCorridor && !Gizmo.IsLeftMouseDown )
			{
				resizingCorridor = false;
				CommitLayoutEdit();
			}
		}
	}

	private void DrawSelectedFloorCutoutBounds()
	{
		if ( mode is not (RoomLayoutToolMode.FloorCutouts or RoomLayoutToolMode.Select) ||
			drawingFloorCutout ||
			movingFloorCutout ||
			selectedFloorCutoutId == 0 ||
			SelectedFloorCutout() is not { } cutout )
		{
			return;
		}

		var bounds = cutout.Bounds;
		var editHeight = MathF.Max( 16.0f, document.Settings.FloorThickness );
		var box = new BBox(
			new Vector3( bounds.X, bounds.Y, ActiveFloorZ() ),
			new Vector3( bounds.X + bounds.Width, bounds.Y + bounds.Height, ActiveFloorZ() + editHeight ) );

		using ( Gizmo.Scope( $"Floor Cutout {cutout.Id} Resize" ) )
		{
			Gizmo.Hitbox.DepthBias = 0.01f;

			if ( !Gizmo.Pressed.Any )
			{
				floorCutoutResizeStartBox = box;
				floorCutoutResizeDeltaBox = default;
			}

			if ( Gizmo.Control.BoundingBox( $"Floor Cutout {cutout.Id} Bounds", box, out var resized ) )
			{
				BeginLayoutEdit();
				resizingFloorCutout = true;
				floorCutoutResizeDeltaBox.Mins += resized.Mins - box.Mins;
				floorCutoutResizeDeltaBox.Maxs += resized.Maxs - box.Maxs;

				var snapped = SnapGizmoBox( floorCutoutResizeStartBox, floorCutoutResizeDeltaBox );
				var next = SnapRect( new RoomLayoutRect( snapped.Mins.x, snapped.Mins.y, snapped.Size.x, snapped.Size.y ) );
				if ( next.Width >= document.Settings.GridSize && next.Height >= document.Settings.GridSize )
				{
					cutout.Bounds = next;
				}
			}
			else if ( resizingFloorCutout && !Gizmo.IsLeftMouseDown )
			{
				resizingFloorCutout = false;
				CommitLayoutEdit();
			}
		}
	}

	private bool TryGetCorridorFootprint( RoomLayoutCorridor corridor, out RoomLayoutRect bounds, out bool horizontal )
	{
		bounds = default;
		horizontal = true;

		if ( !TryGetCorridorPath( corridor, out var points ) || points.Count < 2 )
		{
			return false;
		}

		var width = CorridorClearWidth( corridor );
		var hasBounds = false;
		var longestSegment = 0.0f;

		for ( var i = 0; i < points.Count - 1; i++ )
		{
			var a = points[i];
			var b = points[i + 1];
			var delta = b - a;
			if ( delta.Length < 1.0f )
			{
				continue;
			}

			var segmentHorizontal = MathF.Abs( delta.x ) >= MathF.Abs( delta.y );
			var segmentLength = segmentHorizontal ? MathF.Abs( delta.x ) : MathF.Abs( delta.y );
			var rect = segmentHorizontal
				? new RoomLayoutRect( MathF.Min( a.x, b.x ), a.y - width * 0.5f, segmentLength, width )
				: new RoomLayoutRect( a.x - width * 0.5f, MathF.Min( a.y, b.y ), width, segmentLength );

			bounds = hasBounds ? UnionRect( bounds, rect ) : rect;
			hasBounds = true;

			if ( segmentLength > longestSegment )
			{
				longestSegment = segmentLength;
				horizontal = segmentHorizontal;
			}
		}

		for ( var i = 1; i < points.Count - 1; i++ )
		{
			var join = new RoomLayoutRect( points[i].x - width * 0.5f, points[i].y - width * 0.5f, width, width );
			bounds = hasBounds ? UnionRect( bounds, join ) : join;
			hasBounds = true;
		}

		return hasBounds;
	}

	private static RoomLayoutRect UnionRect( RoomLayoutRect a, RoomLayoutRect b )
	{
		var minX = MathF.Min( a.X, b.X );
		var minY = MathF.Min( a.Y, b.Y );
		var maxX = MathF.Max( a.X + a.Width, b.X + b.Width );
		var maxY = MathF.Max( a.Y + a.Height, b.Y + b.Height );
		return new RoomLayoutRect( minX, minY, maxX - minX, maxY - minY );
	}

	private void DrawRect( RoomLayoutRect rect, Color color, float lineThickness = 2.0f )
	{
		var z = ActiveFloorZ() + 4.0f;
		var a = new Vector3( rect.X, rect.Y, z );
		var b = new Vector3( rect.X + rect.Width, rect.Y, z );
		var c = new Vector3( rect.X + rect.Width, rect.Y + rect.Height, z );
		var d = new Vector3( rect.X, rect.Y + rect.Height, z );

		Gizmo.Draw.Color = color;
		Gizmo.Draw.LineThickness = lineThickness;
		Gizmo.Draw.Line( a, b );
		Gizmo.Draw.Line( b, c );
		Gizmo.Draw.Line( c, d );
		Gizmo.Draw.Line( d, a );
		Gizmo.Draw.LineThickness = 1.0f;
	}
}