Editor/CitizenRetarget/UfbxNative.cs
using System.Runtime.InteropServices;
using System.Text.Json;

namespace Editor.CitizenRetarget;

internal sealed class UfbxNative : IDisposable
{
	private readonly JsonSerializerOptions _jsonOptions = new()
	{
		PropertyNameCaseInsensitive = true
	};

	private IntPtr _libraryHandle;
	private ScanFbxDelegate _scanFbx;
	private SampleClipDelegate _sampleClip;
	private SampleClipExDelegate _sampleClipEx;
	private InspectSceneDelegate _inspectScene;
	private FreeStringDelegate _freeString;

	[UnmanagedFunctionPointer( CallingConvention.Cdecl )]
	private delegate IntPtr ScanFbxDelegate( [MarshalAs( UnmanagedType.LPUTF8Str )] string sourcePath );

	[UnmanagedFunctionPointer( CallingConvention.Cdecl )]
	private delegate IntPtr SampleClipDelegate(
		[MarshalAs( UnmanagedType.LPUTF8Str )] string sourcePath,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string clipName,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string targetPath );

	[UnmanagedFunctionPointer( CallingConvention.Cdecl )]
	private delegate IntPtr SampleClipExDelegate(
		[MarshalAs( UnmanagedType.LPUTF8Str )] string sourcePath,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string clipName,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string targetPath,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string sourceProfile,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string targetProfile );

	[UnmanagedFunctionPointer( CallingConvention.Cdecl )]
	private delegate IntPtr InspectSceneDelegate(
		[MarshalAs( UnmanagedType.LPUTF8Str )] string scenePath,
		[MarshalAs( UnmanagedType.LPUTF8Str )] string profile );

	[UnmanagedFunctionPointer( CallingConvention.Cdecl )]
	private delegate void FreeStringDelegate( IntPtr pointer );

	public UfbxNative()
	{
		LoadLibrary();
	}

	public NativeScanResult Scan( string sourcePath )
	{
		var json = InvokeUtf8( () => _scanFbx( sourcePath ) );
		var result = JsonSerializer.Deserialize<NativeScanResult>( json, _jsonOptions ) ?? new NativeScanResult();
		if ( !string.IsNullOrWhiteSpace( result.Error ) )
			throw new InvalidOperationException( result.Error );

		return result;
	}

	public NativeSampleResult SampleClip( string sourcePath, string clipName, string targetPath )
	{
		return SampleClip( sourcePath, clipName, targetPath, TargetBindProfile.Current, TargetBindProfile.Current );
	}

	public NativeSampleResult SampleClip(
		string sourcePath,
		string clipName,
		string targetPath,
		TargetBindProfile sourceProfile,
		TargetBindProfile targetProfile )
	{
		var json = InvokeUtf8( () => _sampleClipEx(
			sourcePath,
			clipName,
			targetPath,
			sourceProfile.ToString(),
			targetProfile.ToString() ) );
		var result = JsonSerializer.Deserialize<NativeSampleResult>( json, _jsonOptions ) ?? new NativeSampleResult();
		if ( !string.IsNullOrWhiteSpace( result.Error ) )
			throw new InvalidOperationException( result.Error );

		return result;
	}

	public NativeSceneAuditResult InspectScene( string scenePath, TargetBindProfile profile )
	{
		var json = InvokeUtf8( () => _inspectScene( scenePath, profile.ToString() ) );
		var result = JsonSerializer.Deserialize<NativeSceneAuditResult>( json, _jsonOptions ) ?? new NativeSceneAuditResult();
		if ( !string.IsNullOrWhiteSpace( result.Error ) )
			throw new InvalidOperationException( result.Error );

		return result;
	}

	public void Dispose()
	{
		if ( _libraryHandle == IntPtr.Zero )
			return;

		NativeLibrary.Free( _libraryHandle );
		_libraryHandle = IntPtr.Zero;
		GC.SuppressFinalize( this );
	}

	private void LoadLibrary()
	{
		if ( _libraryHandle != IntPtr.Zero )
			return;

		var dllPath = Path.Combine( CitizenRetargetPaths.NativeRoot, "win-x64", "ual2_ufbx_helper.dll" );
		if ( !File.Exists( dllPath ) )
			throw new FileNotFoundException( $"Missing native helper DLL at '{dllPath}'" );

		_libraryHandle = NativeLibrary.Load( dllPath );
		_scanFbx = LoadFunction<ScanFbxDelegate>( "ual2_scan_fbx_json" );
		_sampleClip = LoadFunction<SampleClipDelegate>( "ual2_sample_clip_json" );
		_sampleClipEx = LoadFunction<SampleClipExDelegate>( "ual2_sample_clip_json_ex" );
		_inspectScene = LoadFunction<InspectSceneDelegate>( "ual2_inspect_scene_json" );
		_freeString = LoadFunction<FreeStringDelegate>( "ual2_free_string" );
	}

	private T LoadFunction<T>( string exportName ) where T : Delegate
	{
		var symbol = NativeLibrary.GetExport( _libraryHandle, exportName );
		return Marshal.GetDelegateForFunctionPointer<T>( symbol );
	}

	private string InvokeUtf8( Func<IntPtr> callback )
	{
		var pointer = callback();
		if ( pointer == IntPtr.Zero )
			throw new InvalidOperationException( "Native helper returned a null pointer" );

		try
		{
			var text = Marshal.PtrToStringUTF8( pointer );
			if ( string.IsNullOrWhiteSpace( text ) )
				throw new InvalidOperationException( "Native helper returned an empty payload" );

			return text;
		}
		finally
		{
			_freeString( pointer );
		}
	}
}