Editor/InteriorLayoutBuilder/RoomLayoutTool.cs
using System;
using System.Collections.Generic;
using System.Text.Json;
using Editor;
using Sandbox;

namespace ReusableRoomLayout;

[EditorTool]
[Title( "Interior Layout Builder" )]
[Icon( "dashboard_customize" )]
[Alias( "roomlayout" )]
[Group( "Scene" )]
public sealed partial class RoomLayoutTool : EditorTool
{
	private const string DefaultLayoutDirectory = "room-layouts";
	private const string LegacyLayoutFileName = "default.roomlayout.json";
	private const string LegacyLayoutPath = DefaultLayoutDirectory + "/" + LegacyLayoutFileName;
	private const string SceneLayoutDirectory = DefaultLayoutDirectory + "/by-scene";
	private const string LayoutFileExtension = ".roomlayout.json";
	private const float SelectDragThreshold = 4.0f;
	private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };

	private readonly RoomLayoutGeometryBuilder builder = new();
	private RoomLayoutDocument document = new();
	private RoomLayoutToolMode mode = RoomLayoutToolMode.Select;
	private int activeFloor;
	private int selectedRoomId;
	private int selectedDoorId;
	private int selectedWindowId;
	private int selectedFloorCutoutId;
	private Vector2 dragStart;
	private Vector2 dragCurrent;
	private RoomLayoutRect moveStartBounds;
	private float moveStartOffset;
	private bool selectMovePending;
	private RoomLayoutSelectionKind selectMovePendingKind;
	private bool drawingRoom;
	private bool movingRoom;
	private bool movingDoor;
	private bool movingWindow;
	private bool drawingFloorCutout;
	private bool movingFloorCutout;
	private bool resizingRoom;
	private BBox roomResizeStartBox;
	private BBox roomResizeDeltaBox;
	private bool resizingFloorCutout;
	private BBox floorCutoutResizeStartBox;
	private BBox floorCutoutResizeDeltaBox;
	private int selectedCorridorId;
	private bool resizingCorridor;
	private BBox corridorResizeStartBox;
	private BBox corridorResizeDeltaBox;
	private bool drawingCorridor;
	private readonly List<RoomLayoutPoint> corridorDraftBendPoints = new();
	private Vector2 corridorDraftCurrent;
	private bool? corridorDraftForcedHorizontal;
	private string activeLayoutEditSnapshot;
	private string pendingLayoutUndoSnapshot;
	private bool applyingLayoutHistory;
	private bool layoutLoadAttempted;
	private string loadedLayoutPath;

	public static RoomLayoutTool ActiveTool { get; private set; }
	public static RoomLayoutTool LiveTool => ActiveTool is { IsLive: true } tool ? tool : null;
	public bool IsLive => Manager?.CurrentTool == this;
	public string Summary => $"{document.Rooms.Count} rooms, {document.Doors.Count} doors, {document.Windows.Count} windows, {document.Corridors.Count} corridors, {document.FloorCutouts.Count} cutouts, {document.UsedFloors().Count} floors";
	public string ModeName => drawingCorridor
		? "Create Corridor (drawing)"
		: drawingFloorCutout
			? "Create Floor Cutout (drawing)"
		: mode switch
		{
			RoomLayoutToolMode.Select => "Select",
			RoomLayoutToolMode.Rooms => "Create Room",
			RoomLayoutToolMode.Doors => "Create Door",
			RoomLayoutToolMode.Windows => "Create Window",
			RoomLayoutToolMode.Corridors => "Create Corridor",
			RoomLayoutToolMode.FloorCutouts => "Create Floor Cutout",
			_ => mode.ToString()
		};

	public override void OnEnabled()
	{
		ActiveTool = this;
		AllowGameObjectSelection = true;
		layoutLoadAttempted = false;
		LoadLayoutQuiet();
	}

	public override void OnDisabled()
	{
		CancelLayoutInteraction( restoreActiveEdit: true );

		if ( ActiveTool == this )
		{
			ActiveTool = null;
		}
	}

	public override void OnUpdate()
	{
		EnsureCurrentSceneLayoutLoaded();

		var acceptsInput = AcceptsLayoutInput();
		if ( !acceptsInput )
		{
			CancelLayoutInteraction( restoreActiveEdit: true );
		}

		DrawLayout();

		if ( acceptsInput )
		{
			DrawSelectedRoomBounds();
			DrawSelectedCorridorBounds();
			DrawSelectedFloorCutoutBounds();
			HandleMouse();
		}
	}

	public void SetMode( RoomLayoutToolMode nextMode )
	{
		CancelLayoutInteraction( restoreActiveEdit: true );
		mode = nextMode;
	}

	public void ClearLayout()
	{
		CancelLayoutInteraction( restoreActiveEdit: true );
		PushLayoutUndo();
		document = new RoomLayoutDocument();
		selectedRoomId = 0;
		selectedDoorId = 0;
		selectedWindowId = 0;
		selectedFloorCutoutId = 0;
		selectedCorridorId = 0;
		drawingCorridor = false;
		drawingFloorCutout = false;
		movingDoor = false;
		movingWindow = false;
		movingFloorCutout = false;
		resizingFloorCutout = false;
		selectMovePending = false;
		corridorDraftBendPoints.Clear();
		corridorDraftForcedHorizontal = null;
		CommitLayoutChange( true );
	}

	public void RebuildGeneratedGeometry()
	{
		document.GeneratedGeometryEnabled = HasLayoutContent();
		if ( !PersistLayoutQuiet() )
		{
			return;
		}

		var existing = RoomLayoutGeometryBuilder.FindGeneratedRoots( Scene );
		RebuildGeneratedGeometry( existing, true, true );
	}

	public void ClearGeneratedGeometry()
	{
		document.GeneratedGeometryEnabled = false;
		if ( !PersistLayoutQuiet() )
		{
			return;
		}

		var existing = RoomLayoutGeometryBuilder.FindGeneratedRoots( Scene );
		using ( SceneEditorSession.Active.UndoScope( "Clear Generated Interior Layout" )
			.WithGameObjectDestructions( existing )
			.Push() )
		{
			foreach ( var root in existing )
			{
				root.Destroy();
			}
		}
	}

	public void RefreshGeneratedGeometry()
	{
		RebuildGeneratedGeometryIfPresent();
	}

	private void RebuildGeneratedGeometryIfPresent( bool allowEmptyGeneratedClear = false )
	{
		var existing = RoomLayoutGeometryBuilder.FindGeneratedRoots( Scene );
		if ( existing.Count == 0 && (!document.GeneratedGeometryEnabled || !HasLayoutContent()) )
		{
			return;
		}

		if ( !allowEmptyGeneratedClear && !HasLayoutContent() )
		{
			Log.Warning( "Interior Layout Builder refused to rebuild generated geometry from an empty layout document." );
			return;
		}

		RebuildGeneratedGeometry( existing, HasLayoutContent(), false );
	}

	private void RebuildGeneratedGeometry( IReadOnlyList<GameObject> existing, bool buildNewRoot, bool recordUndo )
	{
		if ( !recordUndo )
		{
			foreach ( var root in existing )
			{
				root.Destroy();
			}

			if ( buildNewRoot )
			{
				builder.Build( Scene, document );
			}

			return;
		}

		using ( SceneEditorSession.Active.UndoScope( "Rebuild Interior Layout" )
			.WithGameObjectDestructions( existing )
			.WithGameObjectCreations()
			.Push() )
		{
			foreach ( var root in existing )
			{
				root.Destroy();
			}

			if ( buildNewRoot )
			{
				builder.Build( Scene, document );
			}
		}
	}

	private bool HasLayoutContent()
	{
		return document.Rooms.Count > 0 ||
			document.Doors.Count > 0 ||
			document.Windows.Count > 0 ||
			document.Corridors.Count > 0 ||
			document.FloorCutouts.Count > 0;
	}

	private void PushLayoutUndo()
	{
		PushLayoutUndo( SerializeLayout() );
	}

	private void PushLayoutUndo( string snapshot )
	{
		pendingLayoutUndoSnapshot = snapshot;
	}

	private void BeginLayoutEdit()
	{
		activeLayoutEditSnapshot ??= SerializeLayout();
	}

	private void CommitLayoutEdit()
	{
		if ( activeLayoutEditSnapshot is null )
		{
			return;
		}

		var current = SerializeLayout();
		if ( activeLayoutEditSnapshot != current )
		{
			PushLayoutUndo( activeLayoutEditSnapshot );
			if ( !CommitLayoutChange() )
			{
				return;
			}
		}

		activeLayoutEditSnapshot = null;
	}

	private void RegisterLayoutEditorUndo( string before, string after )
	{
		if ( applyingLayoutHistory ||
			string.IsNullOrWhiteSpace( before ) ||
			before == after )
		{
			return;
		}

		var session = SceneEditorSession.Active;
		if ( session is null )
		{
			return;
		}

		session.UndoSystem.Insert(
			"Interior Layout",
			() => ApplyLayoutHistorySnapshot( before ),
			() => ApplyLayoutHistorySnapshot( after ) );
	}

	private void ApplyLayoutHistorySnapshot( string snapshot )
	{
		var current = SerializeLayout();
		applyingLayoutHistory = true;

		try
		{
			RestoreLayout( snapshot );
			if ( !CommitLayoutChange( !HasLayoutContent() ) )
			{
				RestoreLayout( current );
			}
		}
		finally
		{
			applyingLayoutHistory = false;
			ClearPendingLayoutRollback();
		}
	}

	private bool CancelLayoutInteraction( bool restoreActiveEdit )
	{
		var hadInteraction = drawingRoom ||
			movingRoom ||
			movingDoor ||
			movingWindow ||
			drawingFloorCutout ||
			movingFloorCutout ||
			selectMovePending ||
			resizingRoom ||
			resizingCorridor ||
			resizingFloorCutout ||
			drawingCorridor ||
			activeLayoutEditSnapshot is not null;

		if ( !hadInteraction )
		{
			return false;
		}

		var snapshot = activeLayoutEditSnapshot;
		activeLayoutEditSnapshot = null;

		if ( restoreActiveEdit && snapshot is not null )
		{
			RestoreLayout( snapshot );
		}

		ResetLayoutInteractionState();
		return true;
	}

	private void ResetLayoutInteractionState()
	{
		var wasDrawingCorridor = drawingCorridor;
		drawingRoom = false;
		movingRoom = false;
		movingDoor = false;
		movingWindow = false;
		drawingFloorCutout = false;
		movingFloorCutout = false;
		selectMovePending = false;
		resizingRoom = false;
		resizingCorridor = false;
		resizingFloorCutout = false;

		if ( wasDrawingCorridor )
		{
			CancelCorridorDraft();
			return;
		}

		drawingCorridor = false;
		corridorDraftBendPoints.Clear();
		corridorDraftCurrent = default;
		corridorDraftForcedHorizontal = null;
	}

	private bool AcceptsLayoutInput()
	{
		return Manager?.IsCurrentViewFocused == true && global::Editor.Application.FocusWidget.IsValid();
	}

}

public enum RoomLayoutToolMode
{
	Select,
	Rooms,
	Doors,
	Windows,
	Corridors,
	FloorCutouts
}