Editor/Canvas/DesignerScene.cs
using System;
using System.Linq;
using System.Reflection;
using Sandbox;
using Sandbox.UI;

namespace Grains.RazorDesigner.Canvas;

public sealed class DesignerScene : IDisposable
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	public static readonly Color ArtboardTintColor   = new( 0.137f, 0.165f, 0.200f ); // ~#232a33
	public static readonly Color WorkspaceClearColor = new( 0.055f, 0.067f, 0.078f ); // ~#0e1114
	public static readonly Color HalvesLineColor    = new( 1f, 1f, 1f, 0.22f );      // halves major-line tint

	// Chrome label tints (workspace ScreenPanel runs at Scale=dpiScale so font sizes etc. are widget-px).
	public static readonly Color ChromeMutedColor   = new( 0.541f, 0.573f, 0.643f );      // #8a92a4
	public static readonly Color ChromeBrightColor  = new( 0.812f, 0.835f, 0.886f );      // #cfd5e2
	public static readonly Color ChromePadBgColor   = new( 0.055f, 0.067f, 0.078f, 0.95f );

	public Scene Scene { get; }
	public CameraComponent Camera { get; }
	public ScreenPanel ScreenPanel { get; }
	public Panel Root => ScreenPanel.GetPanel();

	public ScreenPanel WorkspaceScreenPanel { get; }
	public Panel WorkspaceRoot => WorkspaceScreenPanel.GetPanel();

	// Workspace ScreenPanel was added in the pivot — needs its own isolation + reflection plumbing.
	private bool _workspaceLayoutIsolated;
	private bool _workspaceRootLogged;

	public System.Func<string> FilenameSource { get; set; }

	private Panel _backdrop;
	private bool _backdropProbeLogged;

	private Panel _halfVertical;
	private Panel _halfHorizontal;
	private bool _halvesProbeLogged;

	private Panel _grid;
	private float _gridLastStepCss = -1f;
	private Vector2? _gridLastViewport;
	private bool _gridProbeLogged;
	private const float MinGridStepWidgetPx = 3f; // grid hides below this widget-px step (density clamp)
	private static readonly Color GridLineColor = new( 1f, 1f, 1f, 0.045f );

	public bool GridShow { get; set; }
	public float GridStepCss { get; set; } = 8f;

	public ArtboardFillMode ArtboardFill { get; set; } = ArtboardFillMode.Dark;
	public Color ArtboardCustomColor { get; set; } = new( 0.137f, 0.165f, 0.200f );

	private Panel _workspaceFill;
	private Sandbox.UI.Label _filenameHeader;
	private Sandbox.UI.Label _widthLabel;
	private Panel _widthLabelPad;
	private Sandbox.UI.Label _heightLabel;
	private Panel _heightLabelPad;
	private bool _workspaceChromeProbeLogged;

	public Vector2? ViewportLogical { get; set; }

	public PreviewTargetMode PreviewTarget { get; set; } = PreviewTargetMode.None;

	public float Zoom { get; set; } = 1f;
	public Vector2 PanOffsetWidgetPx { get; set; } = Vector2.Zero;

	public float CurrentScale { get; private set; } = 1f;
	public Rect RootBoundsFb { get; private set; }

	private bool _rootLogged;
	private bool _layoutIsolated;
	private bool _reflectionLogged;

	private MethodInfo _rootLayoutMethod;
	private MethodInfo _rootBuildDescriptorsMethod;
	private PropertyInfo _rootScaleProperty;
	private PropertyInfo _rootPanelBoundsProperty;

	public DesignerScene()
	{
		Log.Info( $"{LogPrefix} DesignerScene ctor (CreateEditorScene + EditorTick + two ScreenPanels)" );

		Scene = Scene.CreateEditorScene();
		Scene.Name = "Razor Designer";

		using ( Scene.Push() )
		{
			var cameraGo = new GameObject( true, "camera" );
			Camera = cameraGo.AddComponent<CameraComponent>();
			Camera.BackgroundColor = WorkspaceClearColor;
			Camera.IsMainCamera = false;

			// Order matters: register Workspace first so it renders BEHIND the artboard.
			var workspaceGo = new GameObject( true, "workspace-ui" );
			WorkspaceScreenPanel = workspaceGo.AddComponent<ScreenPanel>();
			WorkspaceScreenPanel.TargetCamera = Camera;
			ForceLifecycle( WorkspaceScreenPanel );

			var uiGo = new GameObject( true, "artboard-ui" );
			ScreenPanel = uiGo.AddComponent<ScreenPanel>();
			ScreenPanel.TargetCamera = Camera;
			ForceLifecycle( ScreenPanel );
		}
	}

	private static void ForceLifecycle( Component component )
	{
		var type = component.GetType();
		const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

		var awake = type.GetMethod( "OnAwake", flags );
		var enabled = type.GetMethod( "OnEnabled", flags );

		try
		{
			awake?.Invoke( component, null );
			enabled?.Invoke( component, null );
			Log.Info( $"{LogPrefix} ForceLifecycle({type.Name}) ok awake={awake != null} enabled={enabled != null}" );
		}
		catch ( System.Exception ex )
		{
			Log.Error( $"{LogPrefix} ForceLifecycle({type.Name}) threw: {ex.GetType().Name}: {ex.Message}" );
			throw;
		}
	}

	// Call BEFORE Scene.Destroy(); component needs a valid scene to tear down.
	private static void ForceTeardown( Component component )
	{
		if ( component is null ) return;

		var type = component.GetType();
		const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

		var disabled = type.GetMethod( "OnDisabled", flags );
		var destroy = type.GetMethod( "OnDestroy", flags );

		try
		{
			disabled?.Invoke( component, null );
			destroy?.Invoke( component, null );
			Log.Info( $"{LogPrefix} ForceTeardown({type.Name}) ok disabled={disabled != null} destroy={destroy != null}" );
		}
		catch ( System.Exception ex )
		{
			Log.Error( $"{LogPrefix} ForceTeardown({type.Name}) threw: {ex.GetType().Name}: {ex.Message}" );
		}
	}

	private static bool TryIsolateRootFromMenuPump( Panel root )
	{
		if ( root is null ) return false;

		try
		{
			var globalContextType = AppDomain.CurrentDomain.GetAssemblies()
				.Select( a => SafeGetType( a, "Sandbox.Engine.GlobalContext" ) )
				.FirstOrDefault( t => t is not null );

			if ( globalContextType is null )
			{
				Log.Warning( $"{LogPrefix} IsolateRoot: GlobalContext type not found" );
				return false;
			}

			var current = globalContextType
				.GetProperty( "Current", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static )
				?.GetValue( null );
			if ( current is null )
			{
				Log.Warning( $"{LogPrefix} IsolateRoot: GlobalContext.Current is null" );
				return false;
			}

			var uiSystem = current.GetType()
				.GetProperty( "UISystem", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance )
				?.GetValue( current );
			if ( uiSystem is null )
			{
				Log.Warning( $"{LogPrefix} IsolateRoot: GlobalContext.UISystem is null" );
				return false;
			}

			var removeRoot = uiSystem.GetType().GetMethod(
				"RemoveRoot", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );
			if ( removeRoot is null )
			{
				Log.Warning( $"{LogPrefix} IsolateRoot: UISystem.RemoveRoot not found" );
				return false;
			}

			removeRoot.Invoke( uiSystem, new object[] { root } );
			Log.Info( $"{LogPrefix} IsolateRoot: removed from UISystem.RootPanels (engine layout pump will skip it)" );
			return true;
		}
		catch ( Exception ex )
		{
			Log.Warning( $"{LogPrefix} IsolateRoot threw: {ex.GetType().Name}: {ex.Message}" );
			return false;
		}
	}

	private static Type SafeGetType( Assembly a, string fullName )
	{
		try { return a.GetType( fullName, throwOnError: false ); }
		catch { return null; }
	}

	private void DriveLayout( Panel root, float widthPx, float heightPx, float dpiScale )
	{
		if ( widthPx < 1f || heightPx < 1f ) return;
		if ( dpiScale < 0.01f ) dpiScale = 1.0f;

		var pinned = PreviewTarget.IsPinned() ? PreviewTarget.PinnedScale() : 0f;
		var (bounds, scale, _) = ComputeView( new Vector2( widthPx, heightPx ), dpiScale, ViewportLogical, pinned, Zoom, PanOffsetWidgetPx );

		CurrentScale = scale;
		RootBoundsFb = bounds;

		var rootType = root.GetType();
		EnsureReflectionResolved( rootType );

		_rootPanelBoundsProperty?.SetValue( root, bounds );
		_rootScaleProperty?.SetValue( root, scale );

		try
		{
			_rootLayoutMethod?.Invoke( root, null );
			_rootBuildDescriptorsMethod?.Invoke( root, new object[] { 1.0f } );
		}
		catch ( Exception ex )
		{
			Log.Error( $"{LogPrefix} DriveLayout threw: {ex.GetType().Name}: {ex.Message}" );
		}
	}

	public static (Rect Bounds, float Scale, Vector2 ClampedPanWidgetPx) ComputeView(
		Vector2 widgetSize, float dpiScale, Vector2? viewportLogical, float pinnedScale, float zoom, Vector2 panOffsetWidgetPx )
	{
		if ( dpiScale < 0.01f ) dpiScale = 1f;
		var fbW = widgetSize.x * dpiScale;
		var fbH = widgetSize.y * dpiScale;
		if ( fbW < 1f || fbH < 1f ) return ( new Rect( 0, 0, MathF.Max( fbW, 1f ), MathF.Max( fbH, 1f ) ), dpiScale, Vector2.Zero );

		if ( !(viewportLogical is { } vp && vp.x >= 1f && vp.y >= 1f) )
			return ( new Rect( 0, 0, fbW, fbH ), dpiScale, Vector2.Zero );

		var baseScale = pinnedScale > 0.001f
			? pinnedScale                              // grd-z71: 1 logical px == pinnedScale framebuffer px
			: MathF.Min( fbW / vp.x, fbH / vp.y );     // scale-to-fit on the smaller axis
		if ( zoom < 0.001f ) zoom = 1f;
		var scale = baseScale * zoom;
		if ( scale < 0.001f ) scale = dpiScale;

		var panelW = vp.x * scale;
		var panelH = vp.y * scale;
		var panFb = panOffsetWidgetPx * dpiScale;

		var centeredX = (fbW - panelW) * 0.5f;
		var centeredY = (fbH - panelH) * 0.5f;
		var keepX = MathF.Min( 80f * dpiScale, MathF.Min( panelW, fbW ) );
		var keepY = MathF.Min( 80f * dpiScale, MathF.Min( panelH, fbH ) );
		var x = Math.Clamp( centeredX + panFb.x, keepX - panelW, fbW - keepX );
		var y = Math.Clamp( centeredY + panFb.y, keepY - panelH, fbH - keepY );

		var bounds = new Rect( MathF.Floor( x ), MathF.Floor( y ), MathF.Floor( panelW ), MathF.Floor( panelH ) );
		return ( bounds, scale, new Vector2( x - centeredX, y - centeredY ) / dpiScale );
	}

	private void EnsureReflectionResolved( Type rootType )
	{
		_rootLayoutMethod ??= ResolveMethod( rootType, "Layout", Type.EmptyTypes );
		_rootBuildDescriptorsMethod ??= ResolveMethod( rootType, "BuildDescriptors", new[] { typeof( float ) } );
		_rootScaleProperty ??= ResolveProperty( rootType, "Scale", typeof( float ) );
		_rootPanelBoundsProperty ??= ResolveProperty( rootType, "PanelBounds", typeof( Rect ) );

		if ( !_reflectionLogged )
		{
			Log.Info( $"{LogPrefix} DriveLayout reflection resolved: " +
				$"Layout={_rootLayoutMethod is not null} " +
				$"BuildDescriptors={_rootBuildDescriptorsMethod is not null} " +
				$"Scale={_rootScaleProperty is not null} " +
				$"PanelBounds={_rootPanelBoundsProperty is not null}" );
			_reflectionLogged = true;
		}
	}

	// Walk up: methods we want are on RootPanel base, not concrete GameRootPanel.
	private static MethodInfo ResolveMethod( Type startType, string name, Type[] paramTypes )
	{
		for ( var t = startType; t is not null; t = t.BaseType )
		{
			var method = t.GetMethod( name,
				BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
				binder: null,
				types: paramTypes,
				modifiers: null );
			if ( method is not null ) return method;
		}
		return null;
	}

	private static PropertyInfo ResolveProperty( Type startType, string name, Type expectedType )
	{
		for ( var t = startType; t is not null; t = t.BaseType )
		{
			var prop = t.GetProperty( name,
				BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance );
			if ( prop is not null && prop.PropertyType == expectedType )
				return prop;
		}
		return null;
	}

	public void Update( float widthPx, float heightPx, float dpiScale )
	{
		var root = Root;
		var workspaceRoot = WorkspaceRoot;
		if ( !root.IsValid() ) return;

		if ( !_layoutIsolated )
		{
			_layoutIsolated = TryIsolateRootFromMenuPump( root );
		}

		if ( workspaceRoot.IsValid() && !_workspaceLayoutIsolated )
		{
			_workspaceLayoutIsolated = TryIsolateRootFromMenuPump( workspaceRoot );
		}

		if ( !_rootLogged )
		{
			Log.Info( $"{LogPrefix} root valid (width={widthPx} height={heightPx} dpiScale={dpiScale})" );
			_rootLogged = true;
		}

		if ( workspaceRoot.IsValid() && !_workspaceRootLogged )
		{
			Log.Info( $"{LogPrefix} workspace root valid (width={widthPx} height={heightPx} dpiScale={dpiScale})" );
			_workspaceRootLogged = true;
		}

		EnsureBackdrop( root );
		EnsureHalves( root );
		if ( workspaceRoot.IsValid() ) EnsureWorkspaceChrome( workspaceRoot );

		DriveLayout( root, widthPx, heightPx, dpiScale );
		if ( workspaceRoot.IsValid() ) DriveWorkspaceLayout( workspaceRoot, widthPx, heightPx, dpiScale );

		EnsureGrid( root, dpiScale );

		// Position chrome AFTER DriveLayout so RootBoundsFb is current for this frame.
		UpdateWorkspaceChromePositions( widthPx, heightPx, dpiScale );
	}

	private void EnsureBackdrop( Panel root )
	{
		if ( _backdrop is null || !_backdrop.IsValid || _backdrop.Parent != root )
		{
			_backdrop = new Panel( root );
			_appliedFill = (ArtboardFillMode)(-1);
			_backdrop.AddClass( "designer-backdrop" );
			_backdrop.Style.Position = PositionMode.Absolute;
			_backdrop.Style.Left = Length.Pixels( 0 );
			_backdrop.Style.Top = Length.Pixels( 0 );
			_backdrop.Style.Width = Length.Percent( 100 );
			_backdrop.Style.Height = Length.Percent( 100 );
			_backdrop.Style.ZIndex = -1000;
			_backdrop.Style.PointerEvents = PointerEvents.None;

			try { root.SetChildIndex( _backdrop, 0 ); }
			catch ( Exception ex ) { Log.Warning( $"{LogPrefix} EnsureBackdrop SetChildIndex threw: {ex.GetType().Name}: {ex.Message}" ); }

			if ( !_backdropProbeLogged )
			{
				Log.Info( $"{LogPrefix} EnsureBackdrop: backdrop installed (fill={ArtboardFill.DisplayLabel()}, box-shadow DISABLED for triage, target=Root, zIndex=-1000)" );
				_backdropProbeLogged = true;
			}
		}

		ApplyArtboardFill( _backdrop );
	}

	private ArtboardFillMode _appliedFill = (ArtboardFillMode)(-1);
	private Color _appliedCustom = Color.Black;
	private void ApplyArtboardFill( Panel backdrop )
	{
		var mode = ArtboardFill;
		var custom = ArtboardCustomColor;
		if ( mode == _appliedFill && (mode != ArtboardFillMode.Custom || custom == _appliedCustom) )
			return;

		_appliedFill = mode;
		_appliedCustom = custom;

		if ( mode == ArtboardFillMode.Checker )
		{
			backdrop.Style.BackgroundColor = Color.White;
			backdrop.Style.BackgroundImage = GetOrCreateCheckerTexture();
		}
		else
		{
			// Strip the texture if we're switching out of Checker, then apply the flat color.
			backdrop.Style.BackgroundImage = null;
			var color = mode == ArtboardFillMode.Custom ? custom : mode.PresetColor();
			backdrop.Style.BackgroundColor = color;
		}

		Log.Info( $"{LogPrefix} ArtboardFill applied: {mode.DisplayLabel()}{(mode == ArtboardFillMode.Custom ? $" ({custom.Hex})" : "")}" );
	}

	private static Texture _checkerTexture;
	private static Texture GetOrCreateCheckerTexture()
	{
		if ( _checkerTexture is not null && _checkerTexture.IsLoaded ) return _checkerTexture;

		const int size = 32;
		const int half = size / 2;
		// Two-tone cool gray. Same dark family as the Dark preset, just split into two tones.
		var darkRgb  = new byte[] { 42, 45, 51 };   // ~#2a2d33
		var lightRgb = new byte[] { 54, 58, 66 };   // ~#363a42

		var data = new byte[size * size * 4];
		for ( int y = 0; y < size; y++ )
		for ( int x = 0; x < size; x++ )
		{
			var quadrant = (x < half) ^ (y < half);
			var rgb = quadrant ? lightRgb : darkRgb;
			int i = (y * size + x) * 4;
			data[i + 0] = rgb[0];
			data[i + 1] = rgb[1];
			data[i + 2] = rgb[2];
			data[i + 3] = 255;
		}

		_checkerTexture = Texture.Create( size, size, ImageFormat.RGBA8888 )
			.WithName( "designer-artboard-checker" )
			.WithData( data )
			.Finish();

		Log.Info( $"[Grains.RazorDesigner] GetOrCreateCheckerTexture: built {size}x{size} two-tone tile" );
		return _checkerTexture;
	}

	private void EnsureHalves( Panel root )
	{
		EnsureHalf( ref _halfVertical, root, "designer-halves-v", isVertical: true );
		EnsureHalf( ref _halfHorizontal, root, "designer-halves-h", isVertical: false );

		var visible = GridShow;
		var display = visible ? DisplayMode.Flex : DisplayMode.None;
		if ( _halfVertical is not null && _halfVertical.IsValid ) _halfVertical.Style.Display = display;
		if ( _halfHorizontal is not null && _halfHorizontal.IsValid ) _halfHorizontal.Style.Display = display;

		if ( !_halvesProbeLogged )
		{
			Log.Info( $"{LogPrefix} EnsureHalves: halves lines installed (zIndex=-999, alpha=0.22, visible={visible})" );
			_halvesProbeLogged = true;
		}
	}

	private void EnsureHalf( ref Panel slot, Panel root, string className, bool isVertical )
	{
		if ( slot is not null && slot.IsValid && slot.Parent == root )
			return;

		slot = new Panel( root );
		slot.AddClass( className );
		var s = slot.Style;
		s.Position = PositionMode.Absolute;
		s.BackgroundColor = HalvesLineColor;
		s.ZIndex = -999;
		s.PointerEvents = PointerEvents.None;

		if ( isVertical )
		{
			s.Left = Length.Percent( 50 );
			s.Top = Length.Pixels( 0 );
			s.Width = Length.Pixels( 1 );
			s.Height = Length.Percent( 100 );
		}
		else
		{
			s.Left = Length.Pixels( 0 );
			s.Top = Length.Percent( 50 );
			s.Width = Length.Percent( 100 );
			s.Height = Length.Pixels( 1 );
		}
	}

	private void EnsureGrid( Panel root, float dpiScale )
	{
		if ( dpiScale < 0.01f ) dpiScale = 1f;

		// Density clamp: stepWidget = StepCss * (fb-per-css) / (fb-per-widget) = widget-per-css * StepCss.
		var stepWidget = GridStepCss * CurrentScale / dpiScale;
		var viewport = ViewportLogical;
		var shouldShow = GridShow
			&& GridStepCss > 0.001f
			&& stepWidget >= MinGridStepWidgetPx
			&& viewport is { } vpCheck && vpCheck.x >= 1f && vpCheck.y >= 1f;

		if ( !shouldShow )
		{
			if ( _grid is { IsValid: true } ) _grid.Style.Set( "display", "none" );
			return;
		}

		// Lazy-create container (self-healing — covers Repopulate's DeleteChildren wipe).
		if ( _grid is null || !_grid.IsValid || _grid.Parent != root )
		{
			_grid = new Panel( root );
			_grid.AddClass( "designer-grid" );
			var s = _grid.Style;
			s.Position = PositionMode.Absolute;
			s.Left = Length.Pixels( 0 );
			s.Top = Length.Pixels( 0 );
			s.Width = Length.Percent( 100 );
			s.Height = Length.Percent( 100 );
			s.PointerEvents = PointerEvents.None;
			s.ZIndex = -998; // above halves (-999), below user content (0)
			_gridLastStepCss = -1f;       // force line rebuild on next pass
			_gridLastViewport = null;

			if ( !_gridProbeLogged )
			{
				Log.Info( $"{LogPrefix} EnsureGrid: grid container installed (zIndex=-998, thin-panel lines)" );
				_gridProbeLogged = true;
			}
		}

		// Rebuild line children only when step or viewport changes.
		var vp = viewport.Value;
		if ( MathF.Abs( GridStepCss - _gridLastStepCss ) > 0.01f || _gridLastViewport != vp )
		{
			_grid.DeleteChildren( immediate: true );

			var step = GridStepCss;
			var midX = vp.x * 0.5f;
			var midY = vp.y * 0.5f;
			int linesCreated = 0;

			int halfStepsX = (int)MathF.Floor( midX / step );
			float startX = midX - halfStepsX * step;
			for ( float x = startX; x < vp.x - 0.5f; x += step )
			{
				if ( x < 0.5f ) continue;
				if ( MathF.Abs( x - midX ) < 0.5f ) continue; // skip halves overlap
				var line = new Panel( _grid );
				var ls = line.Style;
				ls.Position = PositionMode.Absolute;
				ls.Left = Length.Pixels( x );
				ls.Top = Length.Pixels( 0 );
				ls.Width = Length.Pixels( 1 );
				ls.Height = Length.Percent( 100 );
				ls.BackgroundColor = GridLineColor;
				ls.PointerEvents = PointerEvents.None;
				linesCreated++;
			}

			int halfStepsY = (int)MathF.Floor( midY / step );
			float startY = midY - halfStepsY * step;
			for ( float y = startY; y < vp.y - 0.5f; y += step )
			{
				if ( y < 0.5f ) continue;
				if ( MathF.Abs( y - midY ) < 0.5f ) continue; // skip halves overlap
				var line = new Panel( _grid );
				var ls = line.Style;
				ls.Position = PositionMode.Absolute;
				ls.Left = Length.Pixels( 0 );
				ls.Top = Length.Pixels( y );
				ls.Width = Length.Percent( 100 );
				ls.Height = Length.Pixels( 1 );
				ls.BackgroundColor = GridLineColor;
				ls.PointerEvents = PointerEvents.None;
				linesCreated++;
			}

			_gridLastStepCss = GridStepCss;
			_gridLastViewport = vp;
			Log.Info( $"{LogPrefix} EnsureGrid: rebuilt {linesCreated} line panels (step={step:F1} CSS, vp={vp.x:F0}x{vp.y:F0}, centered)" );
		}

		_grid.Style.Set( "display", "flex" );
	}

	private void DriveWorkspaceLayout( Panel root, float widthPx, float heightPx, float dpiScale )
	{
		if ( widthPx < 1f || heightPx < 1f ) return;
		if ( dpiScale < 0.01f ) dpiScale = 1.0f;

		var fbW = widthPx * dpiScale;
		var fbH = heightPx * dpiScale;
		var fullFb = new Rect( 0, 0, fbW, fbH );

		var rootType = root.GetType();
		EnsureReflectionResolved( rootType );

		_rootPanelBoundsProperty?.SetValue( root, fullFb );
		_rootScaleProperty?.SetValue( root, dpiScale );

		try
		{
			_rootLayoutMethod?.Invoke( root, null );
			_rootBuildDescriptorsMethod?.Invoke( root, new object[] { 1.0f } );
		}
		catch ( Exception ex )
		{
			Log.Error( $"{LogPrefix} DriveWorkspaceLayout threw: {ex.GetType().Name}: {ex.Message}" );
		}
	}

	private void EnsureWorkspaceChrome( Panel root )
	{
		EnsureWorkspaceFill( root );
		EnsureFilenameHeader( root );
		EnsureWidthLabel( root );
		EnsureHeightLabel( root );

		if ( !_workspaceChromeProbeLogged )
		{
			Log.Info( $"{LogPrefix} EnsureWorkspaceChrome: fill + header + width + height labels installed under WorkspaceRoot" );
			_workspaceChromeProbeLogged = true;
		}
	}

	private void EnsureWorkspaceFill( Panel root )
	{
		if ( _workspaceFill is not null && _workspaceFill.IsValid && _workspaceFill.Parent == root ) return;

		_workspaceFill = new Panel( root );
		_workspaceFill.AddClass( "designer-workspace-fill" );
		var s = _workspaceFill.Style;
		s.Position = PositionMode.Absolute;
		s.Left = Length.Pixels( 0 );
		s.Top = Length.Pixels( 0 );
		s.Width = Length.Percent( 100 );
		s.Height = Length.Percent( 100 );
		s.BackgroundColor = WorkspaceClearColor;
		s.ZIndex = -2000;
		s.PointerEvents = PointerEvents.None;

		s.SetBackgroundImage( GetOrCreateDotTexture() );

		// Put it first in the sibling list so chrome labels paint on top.
		try { root.SetChildIndex( _workspaceFill, 0 ); }
		catch ( Exception ex ) { Log.Warning( $"{LogPrefix} EnsureWorkspaceFill SetChildIndex threw: {ex.GetType().Name}: {ex.Message}" ); }
	}

	private static Texture _dotTexture;
	private static Texture GetOrCreateDotTexture()
	{
		if ( _dotTexture is not null && _dotTexture.IsLoaded ) return _dotTexture;

		const int size = 28;
		const int centre = size / 2;
		var data = new byte[size * size * 4]; // RGBA8888

		for ( int y = 0; y < size; y++ )
		for ( int x = 0; x < size; x++ )
		{
			var dx = x - centre;
			var dy = y - centre;
			var d2 = dx * dx + dy * dy;

			byte alpha = 0;
			if ( d2 == 0 ) alpha = 18;       // ~0.07 — exact centre
			else if ( d2 == 1 ) alpha = 10;  // ~0.04 — 4-neighbours
			else if ( d2 == 2 ) alpha = 4;   // ~0.015 — diagonals

			int i = (y * size + x) * 4;
			data[i + 0] = 255; // R
			data[i + 1] = 255; // G
			data[i + 2] = 255; // B
			data[i + 3] = alpha;
		}

		_dotTexture = Texture.Create( size, size, ImageFormat.RGBA8888 )
			.WithName( "designer-workspace-dot" )
			.WithData( data )
			.Finish();

		Log.Info( $"[Grains.RazorDesigner] GetOrCreateDotTexture: built {size}x{size} dot tile (RGBA8888, {data.Length} bytes)" );
		return _dotTexture;
	}

	private void EnsureFilenameHeader( Panel root )
	{
		if ( _filenameHeader is not null && _filenameHeader.IsValid && _filenameHeader.Parent == root ) return;

		_filenameHeader = root.AddChild<Sandbox.UI.Label>();
		_filenameHeader.AddClass( "designer-filename-header" );
		var s = _filenameHeader.Style;
		s.Position = PositionMode.Absolute;
		s.FontColor = ChromeBrightColor; // Sandbox.UI Label can't easily mix colors in one widget; pick the brighter tint.
		s.FontSize = 11f;
		s.PointerEvents = PointerEvents.None;
		s.Set( "white-space", "nowrap" );
		s.Set( "display", "none" ); // hidden until UpdateWorkspaceChromePositions decides to show
	}

	private void EnsureWidthLabel( Panel root )
	{
		if ( _widthLabelPad is not null && _widthLabelPad.IsValid && _widthLabelPad.Parent == root ) return;

		// Pad panel gives the label a subtle dark background pad (legibility over the dot pattern).
		_widthLabelPad = new Panel( root );
		_widthLabelPad.AddClass( "designer-dim-pad" );
		var ps = _widthLabelPad.Style;
		ps.Position = PositionMode.Absolute;
		ps.BackgroundColor = ChromePadBgColor;
		ps.PointerEvents = PointerEvents.None;
		ps.Set( "padding", "1px 6px" );
		ps.Set( "border-radius", "3px" );
		ps.Set( "display", "none" );

		_widthLabel = _widthLabelPad.AddChild<Sandbox.UI.Label>();
		_widthLabel.AddClass( "designer-width-label" );
		var ls = _widthLabel.Style;
		ls.FontColor = ChromeMutedColor;
		ls.FontSize = 10f;
		ls.PointerEvents = PointerEvents.None;
		ls.Set( "white-space", "nowrap" );
	}

	private void EnsureHeightLabel( Panel root )
	{
		if ( _heightLabelPad is not null && _heightLabelPad.IsValid && _heightLabelPad.Parent == root ) return;

		_heightLabelPad = new Panel( root );
		_heightLabelPad.AddClass( "designer-dim-pad" );
		var ps = _heightLabelPad.Style;
		ps.Position = PositionMode.Absolute;
		ps.BackgroundColor = ChromePadBgColor;
		ps.PointerEvents = PointerEvents.None;
		ps.Set( "padding", "4px 2px" );
		ps.Set( "border-radius", "3px" );
		ps.Set( "display", "none" );

		_heightLabel = _heightLabelPad.AddChild<Sandbox.UI.Label>();
		_heightLabel.AddClass( "designer-height-label" );
		var ls = _heightLabel.Style;
		ls.FontColor = ChromeMutedColor;
		ls.FontSize = 10f;
		ls.PointerEvents = PointerEvents.None;
		ls.Set( "white-space", "nowrap" );
		ls.Set( "text-align", "center" );
	}

	private string _filenameLastText;
	private string _widthLastText;
	private string _heightLastText;
	private bool _chromeVisibleLast;

	private void UpdateWorkspaceChromePositions( float widthPx, float heightPx, float dpiScale )
	{
		if ( dpiScale < 0.01f ) dpiScale = 1f;

		var shouldShow = ViewportLogical is { } vpCheck && vpCheck.x >= 1f && vpCheck.y >= 1f;
		if ( !shouldShow )
		{
			if ( _chromeVisibleLast )
			{
				if ( _filenameHeader is { IsValid: true } ) _filenameHeader.Style.Set( "display", "none" );
				if ( _widthLabelPad is { IsValid: true } )  _widthLabelPad.Style.Set( "display", "none" );
				if ( _heightLabelPad is { IsValid: true } ) _heightLabelPad.Style.Set( "display", "none" );
				_chromeVisibleLast = false;
			}
			return;
		}

		var vp = ViewportLogical.Value;

		var artLeft   = RootBoundsFb.Left   / dpiScale;
		var artTop    = RootBoundsFb.Top    / dpiScale;
		var artRight  = RootBoundsFb.Right  / dpiScale;
		var artBottom = RootBoundsFb.Bottom / dpiScale;
		var artCenterX = (artLeft + artRight) * 0.5f;
		var artCenterY = (artTop + artBottom) * 0.5f;

		if ( _filenameHeader is { IsValid: true } )
		{
			var fname = FilenameSource?.Invoke() ?? "Untitled";
			var newText = $"{fname} · {vp.x:F0}×{vp.y:F0}";
			if ( _filenameLastText != newText )
			{
				_filenameHeader.Text = newText;
				_filenameLastText = newText;
			}
			var fs = _filenameHeader.Style;
			fs.Left = artLeft;
			fs.Top = MathF.Max( 4f, artTop - 20f );
			if ( !_chromeVisibleLast ) fs.Set( "display", "flex" );
		}

		// Width label — centered above the artboard's top edge.
		if ( _widthLabelPad is { IsValid: true } && _widthLabel is { IsValid: true } )
		{
			var newText = $"{vp.x:F0}";
			if ( _widthLastText != newText )
			{
				_widthLabel.Text = newText;
				_widthLastText = newText;
			}
			var ps = _widthLabelPad.Style;
			ps.Left = artCenterX;
			ps.Top = MathF.Max( 4f, artTop - 18f );
			ps.Set( "transform", "translateX(-50%)" );
			if ( !_chromeVisibleLast ) ps.Set( "display", "flex" );
		}

		// Height label — centered on the artboard's right edge.
		if ( _heightLabelPad is { IsValid: true } && _heightLabel is { IsValid: true } )
		{
			var newRaw = $"{vp.y:F0}";
			if ( _heightLastText != newRaw )
			{
				_heightLabel.Text = string.Join( "\n", newRaw.ToCharArray() );
				_heightLastText = newRaw;
			}
			var ps = _heightLabelPad.Style;
			ps.Left = artRight + 8f;
			ps.Top = artCenterY;
			ps.Set( "transform", "translateY(-50%)" );
			if ( !_chromeVisibleLast ) ps.Set( "display", "flex" );
		}

		_chromeVisibleLast = true;
	}

	public void Dispose()
	{
		Log.Info( $"{LogPrefix} DesignerScene.Dispose" );

		ForceTeardown( ScreenPanel );
		ForceTeardown( WorkspaceScreenPanel );

		Scene?.Destroy();
	}
}