Editor/Widgets/SuiPreviewTabWidget.cs
using System;
using Editor;
using Sandbox;
using SboxUiDesigner.Runtime;

namespace SboxUiDesigner.EditorUi.Widgets;

/// <summary>
/// Embedded preview widget — hosts a <see cref="SuiPreviewHost"/> + Editor's
/// <see cref="SceneRenderingWidget"/> inside the Designer center area as a
/// "Preview" tab. Useful for quick design-loop validation without leaving the
/// editor (vs. "Test in Play" which switches to a real Play-mode stage scene).
///
/// Lifecycle is editor-owned (no Game.IsPlaying), so PanelComponent lifecycle
/// is invoked via reflection by <c>SuiPreviewHost</c>. See <c>SuiPreviewHost</c>
/// for the workaround details.
/// </summary>
public sealed class SuiPreviewTabWidget : Widget
{
	private SuiDocument _document;
	private SuiPreviewHost _host;
	private SceneRenderingWidget _sceneWidget;
	private Label _statusLabel;
	private string _lastMountedTypeFqn;

	public SuiPreviewTabWidget( Widget parent ) : base( parent )
	{
		Layout = Layout.Column();
		Layout.Margin = 0;
		Layout.Spacing = 0;

		// Top bar: Refresh button + status label.
		var topBar = new Widget( this );
		topBar.Layout = Layout.Row();
		topBar.Layout.Margin = new Sandbox.UI.Margin( 6, 4, 6, 4 );
		topBar.Layout.Spacing = 6;
		topBar.FixedHeight = 32;

		var refreshBtn = new Button( "Refresh", "refresh", topBar );
		refreshBtn.ToolTip = "Re-run the generator and remount the panel";
		refreshBtn.Clicked += Reload;
		topBar.Layout.Add( refreshBtn );

		_statusLabel = new Label( "(no document)", topBar );
		_statusLabel.SetStyles( "color: #9ca3af; font-size: 11px;" );
		topBar.Layout.Add( _statusLabel, 1 );
		Layout.Add( topBar );

		// SceneRenderingWidget hosts our editor-owned preview scene.
		_host = new SuiPreviewHost();
		_sceneWidget = new SceneRenderingWidget( this );
		_sceneWidget.Scene = _host.Scene;
		_sceneWidget.Camera = _host.Camera;
		_sceneWidget.OnPreFrame += () =>
		{
			// Match camera distance to the widget's current aspect so the panel
			// fits inside the viewport regardless of how tall/wide the editor
			// docked us. Without this, the camera is fixed at the 16:9 sweet
			// spot and any other aspect crops the panel.
			if ( _sceneWidget.IsValid() )
				_host.FitCameraToWidgetSize( _sceneWidget.Size );
			_host.Tick();
		};
		Layout.Add( _sceneWidget, 1 );
	}

	public void SetDocument( SuiDocument document )
	{
		_document = document;
		// Don't auto-Reload — runs the full generation pipeline. Center tab
		// activation triggers it lazily.
	}

	/// <summary>
	/// Re-write the preview cache and (re)mount the resulting type. Idempotent
	/// when the document hasn't changed (cache writer hashes content first).
	/// </summary>
	public void Reload()
	{
		if ( _document == null )
		{
			SetStatus( "(no document loaded)" );
			return;
		}

		var write = SuiPreviewCacheWriter.Write( _document );
		if ( !write.Ok )
		{
			SetStatus( $"compile failed: {string.Join( "; ", write.Errors )}" );
			return;
		}

		// Hot-reload may not have produced the type yet; the host polls
		// TypeLibrary internally on each frame via its mount path.
		var ok = _host.TrySetPanelTypeByName( write.TypeFullName );
		if ( ok )
		{
			_lastMountedTypeFqn = write.TypeFullName;
			SetStatus( $"mounted {write.TypeFullName}" );
		}
		else
		{
			SetStatus( $"waiting for hot-reload of {write.TypeFullName}…" );
			// One-shot retry on next tick — gives the engine a frame to register the type.
			_ = RetryMountAsync( write.TypeFullName );
		}
	}

	private async System.Threading.Tasks.Task RetryMountAsync( string fqn )
	{
		for ( int i = 0; i < 30; i++ )
		{
			await System.Threading.Tasks.Task.Delay( 100 );
			if ( !this.IsValid() ) return;
			if ( _host.TrySetPanelTypeByName( fqn ) )
			{
				_lastMountedTypeFqn = fqn;
				SetStatus( $"mounted {fqn}" );
				return;
			}
		}
		SetStatus( $"timed out waiting for {fqn}" );
	}

	private void SetStatus( string text )
	{
		if ( _statusLabel.IsValid() ) _statusLabel.Text = text;
	}
}