Editor/Lifecycle/WelcomeDialogueTrigger.cs
using System;
using System.IO;
using Editor;
using Sandbox.SecBox.Bridge;
using Sandbox.SecBox.UI;

namespace Sandbox.SecBox.Lifecycle;

// Decides whether to show the post-install welcome and fires it exactly once
// per editor session. Frame-driven so we can self-heal - we wait until our
// library is loaded before showing. Same throttle / latch pattern as
// LibraryManagerInjector.
//
// Show rules:
//   - Suppressed entirely if SecboxConfig.WelcomeDialogueDismissedGlobally.
//   - Suppressed when <libraryRoot>/.welcome-shown exists.
//   - Otherwise show once. Marker is written on every close path.
//
// Marker lives inside the library folder so it auto-vanishes when the
// user uninstalls via the Library Manager - the engine deletes the whole
// folder, marker dies with it, reinstall shows the welcome again. No
// uninstall-detection logic needed.
public static class WelcomeDialogueTrigger
{
	const string MarkerFileName = ".welcome-shown";

	static bool _decided;
	static int _frameCounter;

	[EditorEvent.Frame]
	public static void OnFrame()
	{
		if ( _decided ) return;

		// 8x throttle, same as LibraryManagerInjector. Frame fires at editor
		// render rate so the unthrottled cost would be silly for a one-shot
		// decision.
		if ( (_frameCounter++ & 0b111) != 0 ) return;

		DiagnosticsLog.Wrap( "WelcomeDialogueTrigger.OnFrame", DecideAndMaybeShow );
	}

	[EditorEvent.Hotload]
	public static void OnHotload()
	{
		// Do NOT reset _decided - hot-reloading secbox code must not re-fire
		// the welcome. Only the throttle counter is safe to reset.
		_frameCounter = 0;
	}

	// Public entry used by MenuItems > "Show Welcome..." to force-show
	// regardless of marker / global flag. Does NOT write the marker (it's
	// already there if needed; manual re-opens shouldn't change persistence).
	public static void ShowNow( bool isManualInvocation )
	{
		MainThread.Queue( () =>
		{
			try
			{
				var libRoot = PackageLocator.CurrentSecboxLibraryRoot();
				var markerPath = string.IsNullOrEmpty( libRoot ) ? null : MarkerPathFor( libRoot );
				ShowDialog( markerPath, isManualInvocation );
			}
			catch ( Exception ex )
			{
				DiagnosticsLog.Error( "[secbox] welcome: manual show failed", ex );
			}
		} );
	}

	static void DecideAndMaybeShow()
	{
		// Bail cheap if our library isn't enumerable yet - avoids re-reading
		// config every tick during editor warm-up.
		var libRoot = PackageLocator.CurrentSecboxLibraryRoot();
		if ( string.IsNullOrEmpty( libRoot ) )
			return;

		var cfg = SecboxConfig.Load();
		if ( cfg.WelcomeDialogueDismissedGlobally )
		{
			_decided = true;
			DiagnosticsLog.Info( "[secbox] welcome: skipped - globally dismissed" );
			return;
		}

		var markerPath = MarkerPathFor( libRoot );
		if ( MarkerExists( markerPath ) )
		{
			_decided = true;
			DiagnosticsLog.Trace( $"[secbox] welcome: already shown for {libRoot}" );
			return;
		}

		// Latch BEFORE marshalling so a second frame tick can't race a
		// second dialogue into existence while MainThread.Queue is in flight.
		_decided = true;

		MainThread.Queue( () =>
		{
			try { ShowDialog( markerPath, isManualInvocation: false ); }
			catch ( Exception ex ) { DiagnosticsLog.Error( "[secbox] welcome: dialog create failed", ex ); }
		} );
	}

	static void ShowDialog( string markerPath, bool isManualInvocation )
	{
		var dlg = new WelcomeDialogue();
		dlg.Closed = result => OnDialogueClosed( markerPath, isManualInvocation, result );
		dlg.Show();
		DiagnosticsLog.Info( $"[secbox] welcome: dialogue shown (manual={isManualInvocation})" );
	}

	static void OnDialogueClosed( string markerPath, bool isManualInvocation, WelcomeDialogueResult result )
	{
		DiagnosticsLog.Wrap( "WelcomeDialogueTrigger.OnClose", () =>
		{
			// Write marker on every auto-shown close - even "Got it" without
			// the don't-show checkbox should stop the welcome reappearing in
			// THIS project. Manual re-opens via menu skip the write (the
			// marker is already there, and re-opening shouldn't change
			// persistence for projects that may not have one yet).
			if ( !isManualInvocation && !string.IsNullOrEmpty( markerPath ) )
				WriteMarker( markerPath );

			if ( result != null && result.DontShowAgainGlobally )
			{
				var cfg = SecboxConfig.Load();
				cfg.WelcomeDialogueDismissedGlobally = true;
				cfg.Save();
				DiagnosticsLog.Info( "[secbox] welcome: globally dismissed by user" );
			}
		} );
	}

	static string MarkerPathFor( string libraryRoot )
		=> Path.Combine( libraryRoot, MarkerFileName );

	static bool MarkerExists( string markerPath )
	{
		try { return File.Exists( markerPath ); }
		catch { return false; }
	}

	static void WriteMarker( string markerPath )
	{
		try { File.WriteAllText( markerPath, string.Empty ); }
		catch ( Exception ex )
		{
			DiagnosticsLog.Warn( $"[secbox] welcome: failed to write marker at {markerPath}: {ex.Message}" );
			// _decided is already true; we won't loop. Welcome may reappear
			// next session if the library root is unwritable - acceptable.
		}
	}
}