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

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutTool
{
	public void EnsureLayoutLoadedForControls()
	{
		if ( IsLayoutInteractionActive() )
		{
			return;
		}

		EnsureCurrentSceneLayoutLoaded();

		if ( HasLayoutContent() )
		{
			return;
		}

		if ( !layoutLoadAttempted )
		{
			LoadLayoutQuiet();
		}
	}

	private bool LoadLayoutQuiet()
	{
		layoutLoadAttempted = true;
		var layoutPath = CurrentLayoutPath();

		foreach ( var store in LayoutStores() )
		{
			if ( !TryReadLayout( store, layoutPath, out var json ) )
			{
				continue;
			}

			RestoreLayoutAndResetHistory( json );
			loadedLayoutPath = layoutPath;
			return true;
		}

		if ( TryLoadLegacyLayout( layoutPath ) )
		{
			return true;
		}

		ResetLayoutDocument();
		loadedLayoutPath = layoutPath;
		return false;
	}

	private bool PersistLayoutQuiet( bool allowEmptySave = false, string layoutPath = null )
	{
		layoutPath ??= CurrentLayoutPath();
		document.NormalizeIds();

		if ( !allowEmptySave && !HasLayoutContent() && SavedLayoutHasContent( layoutPath ) )
		{
			Log.Warning( "Interior Layout Builder refused to overwrite a saved non-empty layout with an empty layout." );
			return false;
		}

		var json = SerializeLayout();
		var saved = false;
		var failures = new List<string>();

		foreach ( var store in LayoutStores() )
		{
			if ( TryWriteLayout( store, layoutPath, json, out var failure ) )
			{
				saved = true;
				continue;
			}

			failures.Add( failure );
		}

		if ( saved )
		{
			return true;
		}

		Log.Warning( $"Unable to save interior layout to {layoutPath}: {string.Join( "; ", failures )}" );
		return false;
	}

	private bool CommitLayoutChange( bool allowEmptyGeneratedClear = false )
	{
		var existing = RoomLayoutGeometryBuilder.FindGeneratedRoots( Scene );
		if ( existing.Count > 0 && !allowEmptyGeneratedClear && !HasLayoutContent() )
		{
			if ( !TryReloadNonEmptyLayout() )
			{
				Log.Warning( "Interior Layout Builder refused to save or rebuild an empty layout over existing generated geometry." );
				RollbackPendingLayoutChange();
				return false;
			}
		}

		if ( !PersistLayoutQuiet( allowEmptyGeneratedClear ) )
		{
			RollbackPendingLayoutChange();
			return false;
		}

		RebuildGeneratedGeometryIfPresent( allowEmptyGeneratedClear );
		RegisterLayoutEditorUndo( pendingLayoutUndoSnapshot, SerializeLayout() );
		ClearPendingLayoutRollback();
		return true;
	}

	private void RollbackPendingLayoutChange()
	{
		if ( pendingLayoutUndoSnapshot is null )
		{
			return;
		}

		var snapshot = pendingLayoutUndoSnapshot;
		ClearPendingLayoutRollback();
		RestoreLayout( snapshot );
	}

	private void ClearPendingLayoutRollback()
	{
		pendingLayoutUndoSnapshot = null;
	}

	private bool TryReloadNonEmptyLayout()
	{
		if ( HasLayoutContent() )
		{
			return true;
		}

		return LoadLayoutQuiet() && HasLayoutContent();
	}

	private string SerializeLayout()
	{
		return JsonSerializer.Serialize( document, JsonOptions );
	}

	private void EnsureCurrentSceneLayoutLoaded()
	{
		if ( IsLayoutInteractionActive() )
		{
			return;
		}

		var layoutPath = CurrentLayoutPath();
		if ( string.Equals( loadedLayoutPath, layoutPath, StringComparison.OrdinalIgnoreCase ) )
		{
			return;
		}

		if ( HasLayoutContent() && !string.IsNullOrWhiteSpace( loadedLayoutPath ) )
		{
			PersistLayoutQuiet( layoutPath: loadedLayoutPath );
		}

		layoutLoadAttempted = false;
		LoadLayoutQuiet();
	}

	private void RestoreLayout( string json )
	{
		document = JsonSerializer.Deserialize<RoomLayoutDocument>( json, JsonOptions ) ?? new RoomLayoutDocument();
		document.NormalizeIds();
		var floors = document.UsedFloors();
		if ( !floors.Contains( activeFloor ) )
		{
			activeFloor = floors[0];
		}

		selectedRoomId = document.Rooms.FirstOrDefault( IsActiveFloor )?.Id ?? 0;
		selectedDoorId = 0;
		selectedWindowId = 0;
		selectedFloorCutoutId = 0;
		selectedCorridorId = 0;
		drawingRoom = false;
		movingRoom = false;
		movingDoor = false;
		movingWindow = false;
		drawingFloorCutout = false;
		movingFloorCutout = false;
		selectMovePending = false;
		resizingRoom = false;
		resizingCorridor = false;
		resizingFloorCutout = false;
		drawingCorridor = false;
		corridorDraftBendPoints.Clear();
		corridorDraftForcedHorizontal = null;
		activeLayoutEditSnapshot = null;
	}

	private void ResetLayoutDocument()
	{
		document = new RoomLayoutDocument();
		selectedRoomId = 0;
		selectedDoorId = 0;
		selectedWindowId = 0;
		selectedFloorCutoutId = 0;
		selectedCorridorId = 0;
		drawingRoom = false;
		movingRoom = false;
		movingDoor = false;
		movingWindow = false;
		drawingFloorCutout = false;
		movingFloorCutout = false;
		selectMovePending = false;
		resizingRoom = false;
		resizingCorridor = false;
		resizingFloorCutout = false;
		drawingCorridor = false;
		corridorDraftBendPoints.Clear();
		corridorDraftForcedHorizontal = null;
		activeLayoutEditSnapshot = null;
	}

	private void RestoreLayoutAndResetHistory( string json )
	{
		RestoreLayout( json );
	}

	private bool IsLayoutInteractionActive()
	{
		return drawingRoom ||
			movingRoom ||
			movingDoor ||
			movingWindow ||
			drawingFloorCutout ||
			movingFloorCutout ||
			selectMovePending ||
			resizingRoom ||
			resizingCorridor ||
			resizingFloorCutout ||
			drawingCorridor ||
			activeLayoutEditSnapshot is not null;
	}

	private bool SavedLayoutHasContent( string layoutPath )
	{
		foreach ( var store in LayoutStores() )
		{
			if ( TryReadLayout( store, layoutPath, out var json ) && LayoutJsonHasContent( json ) )
			{
				return true;
			}
		}

		return false;
	}

	private static bool LayoutJsonHasContent( string json )
	{
		try
		{
			var saved = JsonSerializer.Deserialize<RoomLayoutDocument>( json, JsonOptions );
			return saved is not null &&
				(saved.Rooms.Count > 0 || saved.Doors.Count > 0 || saved.Windows.Count > 0 || saved.Corridors.Count > 0 || saved.FloorCutouts.Count > 0);
		}
		catch
		{
			return false;
		}
	}

	private bool TryLoadLegacyLayout( string layoutPath )
	{
		if ( string.Equals( layoutPath, LegacyLayoutPath, StringComparison.OrdinalIgnoreCase ) )
		{
			return false;
		}

		var scene = CurrentSceneForLayout();
		if ( scene is null || RoomLayoutGeometryBuilder.FindGeneratedRoots( scene ).Count == 0 )
		{
			return false;
		}

		foreach ( var store in LayoutStores() )
		{
			if ( !TryReadLayout( store, LegacyLayoutPath, out var json ) || !LayoutJsonHasContent( json ) )
			{
				continue;
			}

			RestoreLayoutAndResetHistory( json );
			loadedLayoutPath = layoutPath;
			PersistLayoutQuiet( layoutPath: layoutPath );
			return true;
		}

		return false;
	}

	private static bool TryReadLayout( LayoutStore store, string layoutPath, out string json )
	{
		json = null;
		try
		{
			if ( !store.FileSystem.FileExists( layoutPath ) )
			{
				return false;
			}

			json = store.FileSystem.ReadAllText( layoutPath );
			return !string.IsNullOrWhiteSpace( json );
		}
		catch ( Exception exception )
		{
			Log.Warning( $"Unable to read interior layout from {store.Name}: {exception.Message}" );
			return false;
		}
	}

	private static bool TryWriteLayout( LayoutStore store, string layoutPath, string json, out string failure )
	{
		failure = "";

		if ( store.FileSystem.IsReadOnly )
		{
			failure = $"{store.Name} is read-only";
			return false;
		}

		try
		{
			var directory = LayoutDirectoryForPath( layoutPath );
			if ( !string.IsNullOrWhiteSpace( directory ) )
			{
				store.FileSystem.CreateDirectory( directory );
			}

			store.FileSystem.WriteAllText( layoutPath, json );
			return true;
		}
		catch ( Exception exception )
		{
			failure = $"{store.Name}: {exception.Message}";
			return false;
		}
	}

	private string CurrentLayoutPath()
	{
		var scene = CurrentSceneForLayout();
		var scenePath = FileSystem.NormalizeFilename( scene?.Source?.ResourcePath ?? "" ).Trim( '/' );
		if ( !string.IsNullOrWhiteSpace( scenePath ) )
		{
			return LayoutPathForScenePath( scenePath );
		}

		var sceneId = scene?.Id.ToString() ?? "inactive";
		return $"{SceneLayoutDirectory}/unsaved/{SanitizeLayoutPathSegment( sceneId )}{LayoutFileExtension}";
	}

	private Sandbox.Scene CurrentSceneForLayout()
	{
		var scene = Manager?.CurrentSession?.Scene;
		if ( scene is null )
		{
			return null;
		}

		return global::Editor.SceneEditorSession.Resolve( scene )?.Scene ?? scene;
	}

	private static string LayoutPathForScenePath( string scenePath )
	{
		var normalized = FileSystem.NormalizeFilename( scenePath ?? "" ).Trim( '/' );
		if ( normalized.EndsWith( ".scene", StringComparison.OrdinalIgnoreCase ) )
		{
			normalized = normalized[..^".scene".Length];
		}

		if ( string.IsNullOrWhiteSpace( normalized ) )
		{
			normalized = "scene";
		}

		return $"{SceneLayoutDirectory}/{SanitizeLayoutRelativePath( normalized )}{LayoutFileExtension}";
	}

	private static string SanitizeLayoutRelativePath( string path )
	{
		var segments = path.Split( '/', StringSplitOptions.RemoveEmptyEntries )
			.Select( SanitizeLayoutPathSegment )
			.Where( segment => !string.IsNullOrWhiteSpace( segment ) );

		var sanitized = string.Join( "/", segments );
		return string.IsNullOrWhiteSpace( sanitized ) ? "scene" : sanitized;
	}

	private static string SanitizeLayoutPathSegment( string segment )
	{
		if ( string.IsNullOrWhiteSpace( segment ) )
		{
			return "scene";
		}

		var builder = new StringBuilder( segment.Length );
		foreach ( var character in segment )
		{
			if ( char.IsLetterOrDigit( character ) || character is '-' or '_' or '.' )
			{
				builder.Append( char.ToLowerInvariant( character ) );
				continue;
			}

			builder.Append( '_' );
		}

		return builder.Length == 0 ? "scene" : builder.ToString();
	}

	private static string LayoutDirectoryForPath( string layoutPath )
	{
		var normalized = FileSystem.NormalizeFilename( layoutPath ?? "" );
		var slashIndex = normalized.LastIndexOf( '/' );
		return slashIndex <= 0 ? "" : normalized[..slashIndex];
	}

	private static IEnumerable<LayoutStore> LayoutStores()
	{
		yield return new LayoutStore( "ProjectSettings", global::Editor.FileSystem.ProjectSettings );
		yield return new LayoutStore( "Content", global::Editor.FileSystem.Content );
	}

	private readonly record struct LayoutStore( string Name, BaseFileSystem FileSystem );
}