EmulatorComponent.cs
using System.Diagnostics;
using System.Threading;
using Sandbox.Rendering;

namespace Emulotl;

public sealed partial class EmulatorComponent : Component
{
	[Property, Title( "ROM Path" )]
	public string RomPath { get; set; }

	public string GameCode { get; private set; }

	public static EmulatorComponent Current { get; private set; }
	public IEmulatorCore Core => _coreThread?.Core;
	public Texture ScreenTexture { get; private set; }
	public IReadOnlyList<Texture> ScreenTextures { get; private set; } = [];
	public bool IsReady { get; private set; }
	public string ErrorMessage { get; private set; }

	private SoundStream _audioStream;
	private SoundHandle _soundHandle;
	private string _savePath;
	private CameraComponent _camera;
	private object _coreLock = new();
	private EmulatorCoreThread _coreThread;
	private SystemProfile _profile;
	private double _frameTime;
	private Stopwatch _videoClock;
	private double _nextVideoFrameDue;

	private const int AudioPrefillFrames = 4;
	private const int AudioHighWaterFrames = 6;
	private const int MaxPendingFrames = 6;
	private const int SyncWaitMilliseconds = 50;
	private const float StickDeadzone = 0.3f;

	private int _inputKeys;
	private int _touchState;
	private bool _videoFramePending;
	private bool _paused;
	private bool _coreHalted;
	private int _inputCooldown;
	private bool _initCoreOnUpdate;
	private int _initDeferFrames;
	private string _stateBasePath;
	private bool _appliedReproduceClassicFeel;
	private RealTimeSince _sessionTime;

	protected override void OnStart()
	{
		Current = this;
		EmulatorSystems.EnsureRegistered();
		EmuLog.SetBackend( LogBackend );
		_camera = Scene.Camera;
		if ( !string.IsNullOrEmpty( RomPath ) )
			_initCoreOnUpdate = true;
	}

	private bool EnsureProfile()
	{
		EmulatorSystems.EnsureRegistered();
		_profile = SystemRegistry.ResolveByPath( RomPath ) ?? _profile;
		if ( _profile != null )
			_frameTime = 1.0 / _profile.NativeFps;
		return _profile != null;
	}

	public void Restart( string romPath )
	{
		if ( _linkedHost || _linkedClient )
			EndLinkedMode();

		TearDownCore();
		RomPath = romPath;
		IsReady = false;
		ErrorMessage = null;
		_initCoreOnUpdate = false;
		_initDeferFrames = 0;
		InitCore();
	}

	public void Unload()
	{
		TearDownCore();
		RomPath = null;
		GameCode = null;
		_initCoreOnUpdate = false;
		_initDeferFrames = 0;
	}

	private void TearDownCore()
	{
		EmulatorCoreThread coreThread = _coreThread;
		_coreThread = null;
		coreThread?.End();

		lock ( CoreLock )
		{
			IEmulatorCore core = coreThread?.Core;
			if ( core?.SaveData != null && core.SaveData.Data.Length > 0 && _savePath != null )
				FileSystem.Data.WriteAllBytes( _savePath, core.SaveData.Data );

			if ( core != null )
			{
				foreach ( IVideoOutput screen in core.Screens )
				{
					if ( _camera.IsValid() && screen.RenderCommandList != null )
						_camera.RemoveCommandList( screen.RenderCommandList );
					screen.DisposeGpu();
				}
			}

			ScreenTexture = null;
			ScreenTextures = [];
		}

		if ( _soundHandle is { IsValid: true } )
			_soundHandle.Volume = 0;
		_audioStream?.Dispose();
		_audioStream = null;
		_videoClock = null;
		_nextVideoFrameDue = 0;
		_videoFramePending = false;
		_inputCooldown = 0;
		_initDeferFrames = 0;
		_paused = false;
		_coreHalted = false;
		Interlocked.Exchange( ref _inputKeys, 0 );
		IsReady = false;
	}

	private void InitCore()
	{
		try
		{
			if ( !FileSystem.Mounted.FileExists( RomPath ) && !FileSystem.Data.FileExists( RomPath ) )
			{
				ErrorMessage = $"ROM not found: {RomPath}";
				EmuLog.Write( LogCategory.Core, LogLevel.Error, ErrorMessage );
				return;
			}

			if ( !EnsureProfile() )
			{
				ErrorMessage = $"No emulator system handles ROM: {RomPath}";
				EmuLog.Write( LogCategory.Core, LogLevel.Error, ErrorMessage );
				return;
			}

			BaseFileSystem romFs = FileSystem.Mounted.FileExists( RomPath ) ? FileSystem.Mounted : FileSystem.Data;
			byte[] romData = romFs.ReadAllBytes( RomPath ).ToArray();
			if ( romData.Length < 192 )
			{
				ErrorMessage = $"ROM file is too small to be a valid {_profile.DisplayName} ROM.";
				EmuLog.Write( LogCategory.Core, LogLevel.Error, ErrorMessage );
				return;
			}

			GameCode = _profile.ReadGameCode( romData );

			IEmulatorCore core = _profile.CreateCore();
			core.LoadRom( romData );

			_savePath = "saves/" + System.IO.Path.GetFileNameWithoutExtension( RomPath ) + ".sav";
			if ( FileSystem.Data.FileExists( _savePath ) )
			{
				byte[] saveData = FileSystem.Data.ReadAllBytes( _savePath ).ToArray();
				core.SaveData.Load( saveData );
			}

			core.Reset();

			int scale = ComputeAutoScale();
			DisplayOptions display = CurrentDisplayOptions();
			foreach ( IVideoOutput screen in core.Screens )
			{
				screen.InitGpu( scale );
				screen.ApplyDisplayOptions( display );
				if ( _camera.IsValid() && screen.RenderCommandList != null )
					_camera.AddCommandList( screen.RenderCommandList, Stage.AfterOpaque, 0 );
			}
			_appliedReproduceClassicFeel = GamePreferences.ReproduceClassicFeel;
			ScreenTexture = core.Screens[0].OutputTexture;
			ScreenTextures = [.. core.Screens.Select( s => s.OutputTexture )];

			_stateBasePath = "states/" + System.IO.Path.GetFileNameWithoutExtension( RomPath );

			try { InitAudioStream(); }
			catch ( Exception audioEx ) { EmuLog.Write( LogCategory.Audio, LogLevel.Warn, $"Audio init failed: {audioEx.Message}" ); }

			_coreThread = new EmulatorCoreThread( core, CoreLock, ReadInputKeysActive, ReadTouchState, LogCoreThreadError, LogCoreThreadReset );
			_coreThread.Sync.LoadCoreOptions( audioSync: true, videoSync: true, fpsTarget: (float)_profile.NativeFps );
			_coreThread.Sync.AudioHighWater = _profile.AudioSamplesPerFrame * AudioHighWaterFrames;
			ResetVideoClock();
			_coreThread.Start();

			_sessionTime = 0;
			AchievementManager.OnSessionStarted();
			IsReady = true;
			ApplyGameplayCursor();
		}
		catch ( Exception ex )
		{
			ErrorMessage = $"Failed to load ROM: {ex.Message}";
			EmuLog.Write( LogCategory.Core, LogLevel.Fatal, ErrorMessage );
		}
	}

	private void InitAudioStream()
	{
		if ( _audioStream != null )
		{
			if ( _soundHandle is { IsValid: true } )
				_soundHandle.Volume = 0;
			_audioStream.Dispose();
			_audioStream = null;
		}

		if ( _profile == null )
			return;

		int sampleRate = _profile.AudioSampleRate;
		int channels = _profile.AudioChannels;
		int samplesPerFrame = _profile.AudioSamplesPerFrame;

		_audioStream = new SoundStream( sampleRate, channels );
		_audioStream.WriteData( new short[samplesPerFrame * channels * AudioPrefillFrames] );
		_soundHandle = _audioStream.Play( volume: _paused ? 0f : 1.0f );
		_soundHandle.SpacialBlend = 0f;
		_soundHandle.OcclusionEnabled = false;
		_soundHandle.DistanceAttenuation = false;
		_soundHandle.AirAbsorption = false;
		_soundHandle.Transmission = false;
		_soundHandle.Stop( float.MaxValue );
	}

	public bool HasTouchscreen => _profile?.HasTouchscreen ?? false;

	private const float CursorMouseWakeThreshold = 5f;
	private const float CursorStickDeadzone = 0.05f;
	private bool _cursorGamepadMode;

	public void ApplyGameplayCursor()
	{
		if ( !HasTouchscreen )
		{
			Mouse.Visibility = MouseVisibility.Hidden;
			return;
		}

		_cursorGamepadMode = false;
		Mouse.Visibility = MouseVisibility.Visible;
	}

	private void UpdateGameplayCursor( InputButton[] buttons, float stickX, float stickY )
	{
		if ( !HasTouchscreen )
			return;

		float rsx = Input.GetAnalog( InputAnalog.RightStickX );
		float rsy = Input.GetAnalog( InputAnalog.RightStickY );
		bool stickActive =
			Math.Abs( stickX ) > CursorStickDeadzone || Math.Abs( stickY ) > CursorStickDeadzone ||
			Math.Abs( rsx ) > CursorStickDeadzone || Math.Abs( rsy ) > CursorStickDeadzone;
		bool gamepadActive = stickActive || AnyButtonHeld( buttons, stickX, stickY );
		bool mouseMoved = !stickActive && Mouse.Delta.Length > CursorMouseWakeThreshold;

		if ( _cursorGamepadMode )
		{
			if ( mouseMoved )
				_cursorGamepadMode = false;
		}
		else if ( gamepadActive )
		{
			_cursorGamepadMode = true;
		}

		Mouse.Visibility = _cursorGamepadMode ? MouseVisibility.Hidden : MouseVisibility.Visible;
	}

	private DisplayOptions CurrentDisplayOptions() => new( GamePreferences.ReproduceClassicFeel, GamePreferences.DisplayWithSmallScreen );

	protected override void OnUpdate()
	{
		ReconcileLinkedMode();

		if ( _coreThread != null )
			AchievementManager.OnSessionTime( _sessionTime );

		if ( _linkedClient )
		{
			if ( _timeSinceClientPacket > ClientHostTimeoutSeconds )
			{
				ResumeSoloAfterHostLost();
				return;
			}

			if ( _appliedReproduceClassicFeel != GamePreferences.ReproduceClassicFeel )
			{
				_clientVideo?.SetReproduceClassicFeel( GamePreferences.ReproduceClassicFeel );
				_appliedReproduceClassicFeel = GamePreferences.ReproduceClassicFeel;
			}

			PollInput();
			TickLinkedClient();
			RestoreAudioStreamIfNeeded();
			return;
		}

		if ( _linkedHost )
		{
			RescaleGpuIfNeeded();
			if ( _appliedReproduceClassicFeel != GamePreferences.ReproduceClassicFeel )
				ApplyDisplaySettings();
			PollInput();
			TickLinkedHost();
			RestoreAudioStreamIfNeeded();
			return;
		}

		if ( !StartCoreWhenReady() )
			return;

		ReconcileCorePause();
		RescaleGpuIfNeeded();

		if ( _appliedReproduceClassicFeel != GamePreferences.ReproduceClassicFeel )
			ApplyDisplaySettings();

		PollInput();
		RestoreAudioStreamIfNeeded();
	}

	private bool StartCoreWhenReady()
	{
		if ( _initCoreOnUpdate )
		{
			var net = NetworkManager.Current;
			if ( net != null && net.Mode == SessionMode.LinkCable && !Networking.IsHost && net.State != SessionState.Solo )
			{
				_initCoreOnUpdate = false;
				_initDeferFrames = 0;
				return false;
			}

			if ( ShouldDeferInitialCore() )
				return false;

			_initCoreOnUpdate = false;
			_initDeferFrames = 0;
			InitCore();
		}

		return IsReady && _coreThread?.Core != null;
	}

	private void RestoreAudioStreamIfNeeded()
	{
		if ( _audioStream == null || _soundHandle is { IsValid: true } )
			return;

		try { InitAudioStream(); }
		catch { _audioStream = null; }
	}

	private bool ShouldDeferInitialCore()
	{
		if ( (int)Screen.Width != 1024 || (int)Screen.Height != 1024 )
			return false;

		if ( _initDeferFrames++ >= 5 )
			return false;

		return true;
	}

	private object CoreLock => _coreLock ??= new object();

	private ushort ReadInputKeysActive()
	{
		return (ushort)Interlocked.CompareExchange( ref _inputKeys, 0, 0 );
	}

	private int ReadTouchState()
	{
		return Interlocked.CompareExchange( ref _touchState, 0, 0 );
	}

	public void SetUiTouch( bool down, float fx, float fy )
	{
		if ( !down || !HasTouchscreen )
		{
			Interlocked.Exchange( ref _touchState, 0 );
			return;
		}

		int x = Math.Clamp( (int)(fx * 256f), 0, 255 );
		int y = Math.Clamp( (int)(fy * 192f), 0, 191 );
		Interlocked.Exchange( ref _touchState, unchecked((int)0x80000000) | (x << 8) | y );
	}

	private void ResetVideoClock()
	{
		_videoClock ??= new Stopwatch();
		_videoClock.Restart();
		_nextVideoFrameDue = 0;
	}

	private bool IsVideoFrameDue( EmulatorCoreSync sync )
	{
		if ( _videoClock == null )
			ResetVideoClock();

		if ( sync == null || _nextVideoFrameDue <= 0 )
			return true;

		return _videoClock.Elapsed.TotalSeconds >= _nextVideoFrameDue;
	}

	private void AdvanceVideoClock( EmulatorCoreSync sync )
	{
		double frameTime = sync?.FpsTarget > 0 ? 1.0 / sync.FpsTarget : _frameTime;
		double now = _videoClock?.Elapsed.TotalSeconds ?? 0;

		if ( _nextVideoFrameDue <= 0 || now - _nextVideoFrameDue > frameTime * MaxPendingFrames )
			_nextVideoFrameDue = now + frameTime;
		else
			_nextVideoFrameDue += frameTime;
	}

	private void RunOnCoreThread( Action<IEmulatorCore> action )
	{
		_coreThread?.RunFunction( action );
	}

	private int ComputeAutoScale()
	{
		int screenWidth = Screen.Width > 0 ? (int)Screen.Width : 1920;
		int screenHeight = Screen.Height > 0 ? (int)Screen.Height : 1080;
		ScreenSpec native = _profile?.Screens is { Length: > 0 } screens ? screens[0] : new ScreenSpec( 240, 160 );
		return Math.Clamp( Math.Min( screenWidth / native.Width, screenHeight / native.Height ), 1, 8 );
	}

	private void RescaleGpuIfNeeded()
	{
		if ( _paused )
			return;

		IEmulatorCore core = Core;
		if ( core == null || core.Screens.Count == 0 )
			return;

		int desiredScale = ComputeAutoScale();
		if ( core.Screens[0].GpuScale == desiredScale )
			return;

		lock ( CoreLock )
		{
			if ( Core != core || core.Screens.Count == 0 )
				return;

			if ( core.Screens[0].GpuScale == desiredScale )
				return;

			DisplayOptions display = CurrentDisplayOptions();
			foreach ( IVideoOutput screen in core.Screens )
			{
				if ( _camera.IsValid() && screen.RenderCommandList != null )
					_camera.RemoveCommandList( screen.RenderCommandList );

				screen.InitGpu( desiredScale );
				screen.ApplyDisplayOptions( display );

				if ( _camera.IsValid() && screen.RenderCommandList != null )
					_camera.AddCommandList( screen.RenderCommandList, Stage.AfterOpaque, 0 );
			}

			_appliedReproduceClassicFeel = GamePreferences.ReproduceClassicFeel;
			ScreenTexture = core.Screens[0].OutputTexture;
			ScreenTextures = [.. core.Screens.Select( s => s.OutputTexture )];

			_videoFramePending = true;
			_coreThread?.Sync.ForceFrame();
		}
	}

	protected override void OnPreRender()
	{
		if ( _linkedHost )
		{
			CaptureAndSendHostVideo();
			return;
		}

		if ( _linkedClient )
		{
			PresentClientVideo();
			return;
		}

		WaitForPostedCoreFrame();
		UploadPendingVideoFrame();
	}

	private void WaitForPostedCoreFrame()
	{
		EmulatorCoreThread coreThread = _coreThread;
		EmulatorCoreSync sync = coreThread?.Sync;
		if ( sync == null || !IsVideoFrameDue( sync ) )
			return;

		if ( !sync.WaitFrameStart() )
			return;

		DrainPostedCoreFrames( coreThread );
		sync.WaitFrameEnd();
		AdvanceVideoClock( sync );
	}

	private void DrainPostedCoreFrames( EmulatorCoreThread coreThread )
	{
		bool hasFrame = false;
		int channels = _profile?.AudioChannels ?? 2;
		int highWater = (_profile?.AudioSamplesPerFrame ?? 0) * AudioHighWaterFrames;

		while ( coreThread.PostedFrames.TryDequeue( out FramePacket frame ) )
		{
			if ( _audioStream != null && frame.AudioSamples > 0 && _audioStream.QueuedSampleCount <= highWater )
				_audioStream.WriteData( frame.Audio.AsSpan( 0, frame.AudioSamples * channels ) );

			coreThread.Sync.ConsumeAudio( frame.AudioSamples );

			if ( frame.SaveData != null )
				FileSystem.Data.WriteAllBytes( _savePath, frame.SaveData );

			hasFrame = true;
		}

		if ( hasFrame )
			_videoFramePending = true;
	}

	private void UploadPendingVideoFrame()
	{
		IEmulatorCore core = Core;
		if ( core == null || !_videoFramePending )
			return;

		bool uploaded = false;
		foreach ( IVideoOutput screen in core.Screens )
		{
			if ( screen.UploadAndBuildCommandList() )
				uploaded = true;
		}

		if ( uploaded )
			_videoFramePending = false;
	}

	private void PollInput()
	{
		if ( _paused )
			return;

		InputButton[] buttons = _profile?.Buttons;
		if ( buttons == null )
			return;

		float stickX = Input.GetAnalog( InputAnalog.LeftStickX );
		float stickY = Input.GetAnalog( InputAnalog.LeftStickY );

		UpdateGameplayCursor( buttons, stickX, stickY );

		if ( _inputCooldown > 0 )
		{
			if ( AnyButtonHeld( buttons, stickX, stickY ) )
				return;

			_inputCooldown = 0;
		}

		ulong mask = 0;
		for ( int i = 0; i < buttons.Length; i++ )
		{
			if ( IsButtonPressed( buttons[i], stickX, stickY ) )
				mask |= 1UL << i;
		}

		Interlocked.Exchange( ref _inputKeys, (int)mask );
	}

	private static bool IsButtonPressed( in InputButton button, float stickX, float stickY )
	{
		if ( Input.Down( button.ActionName ) )
			return true;

		return button.Dpad switch
		{
			DpadDirection.Up => stickY < -StickDeadzone,
			DpadDirection.Down => stickY > StickDeadzone,
			DpadDirection.Left => stickX < -StickDeadzone,
			DpadDirection.Right => stickX > StickDeadzone,
			_ => false,
		};
	}

	private static bool AnyButtonHeld( InputButton[] buttons, float stickX, float stickY )
	{
		for ( int i = 0; i < buttons.Length; i++ )
		{
			if ( IsButtonPressed( buttons[i], stickX, stickY ) )
				return true;
		}

		return false;
	}

	public void SetPaused( bool paused )
	{
		_paused = paused;

		if ( paused )
			Interlocked.Exchange( ref _inputKeys, 0 );
		else
			_inputCooldown = 2;

		if ( _soundHandle is { IsValid: true } )
			_soundHandle.Volume = paused ? 0 : 1.0f;

		ReconcileCorePause();
	}

	private void ReconcileCorePause()
	{
		bool networked = NetworkManager.Current?.IsActive == true;
		bool shouldHalt = _paused && !networked;
		if ( shouldHalt == _coreHalted )
			return;

		_coreHalted = shouldHalt;
		_coreThread?.SetPaused( shouldHalt );
		if ( !shouldHalt )
			ResetVideoClock();
	}

	public string GetStatePath( int slot ) => $"{_stateBasePath}.ss{slot}";

	public void CreateSuspendPoint( int slot )
	{
		IEmulatorCore core = Core;
		if ( core == null )
			return;

		string path = GetStatePath( slot );
		try
		{
			byte[] data;
			lock ( CoreLock )
			{
				if ( Core != core )
					return;

				byte[] screenshot = core.Screens[0].CaptureScreenshot();
				data = core.SaveState( screenshot );
			}

			FileSystem.Data.WriteAllBytes( path, data );
			EmuLog.Write( LogCategory.State, LogLevel.Info, $"Suspend point created in slot {slot}" );
		}
		catch ( Exception ex )
		{
			EmuLog.Write( LogCategory.State, LogLevel.Error, $"Failed to create suspend point {slot}: {ex.Message}" );
		}
	}

	public void LoadSuspendPoint( int slot )
	{
		string path = GetStatePath( slot );
		RunOnCoreThread( core =>
		{
			if ( !FileSystem.Data.FileExists( path ) )
			{
				EmuLog.Write( LogCategory.State, LogLevel.Warn, $"No suspend point in slot {slot}" );
				return;
			}

			byte[] data = FileSystem.Data.ReadAllBytes( path ).ToArray();
			core.LoadState( data );
			EmuLog.Write( LogCategory.State, LogLevel.Info, $"Suspend point loaded from slot {slot}" );
		} );
	}

	public void ResetEmulator()
	{
		_coreThread?.Reset();
	}

	public void ApplyDisplaySettings()
	{
		bool reproduceClassicFeel = GamePreferences.ReproduceClassicFeel;
		IEmulatorCore core = Core;
		if ( core != null && core.Screens.Count > 0 )
		{
			lock ( CoreLock )
			{
				if ( Core != core || core.Screens.Count == 0 )
					return;

				DisplayOptions display = CurrentDisplayOptions();
				foreach ( IVideoOutput screen in core.Screens )
					screen.ApplyDisplayOptions( display );
			}
		}
		_appliedReproduceClassicFeel = reproduceClassicFeel;
	}

	protected override void OnDestroy()
	{
		if ( _linkedHost || _linkedClient )
			EndLinkedMode();
		TearDownCore();
		_camera = null;
		if ( Current == this )
			Current = null;
	}

	private void LogCoreThreadError( string message )
	{
		EmuLog.Write( LogCategory.Core, LogLevel.Fatal, message );
	}

	private void LogCoreThreadReset()
	{
		EmuLog.Write( LogCategory.Core, LogLevel.Info, "Emulator reset" );
	}

	private static void LogBackend( LogCategory category, LogLevel level, string message )
	{
		string formatted = $"{EmuLog.GetCategoryName( category )}: {message}";

		if ( (level & (LogLevel.Fatal | LogLevel.Error)) != 0 )
			Log.Error( formatted );
		else if ( (level & (LogLevel.Warn | LogLevel.GameError)) != 0 )
			Log.Warning( formatted );
		else
			Log.Info( formatted );
	}
}