using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;

namespace NixUniversalSDK.Wrapper;

/// <summary>
/// Exported static functions
/// </summary>
public static class Exported
{
    #region License and activation
    /// <summary>
    /// Gets the vendor identifier for the current license.
    /// </summary>
    [Obsolete(
        message: "Use License_GetUuid() instead",
        error: false)]
    [UnmanagedCallersOnly(
        EntryPoint = nameof(GetSdkId),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint GetSdkId() => 
        Utils.MarshalStringToPointer(LicenseManager.Uuid);

    /// <summary>
    /// Gets the current <see cref="LicenseManager.State"/> as an <see cref="Int32"/>. See <see cref="LicenseManagerState"/> for possible values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetState),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int License_GetState() => (int) LicenseManager.State;

    /// <summary>
    /// Gets a list of Nix device allocation codes associated with the current license formatted as a JSON array.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetAllocations),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint License_GetAllocations() => 
        Utils.MarshalStringToPointer(
            LicenseManager.Allocations.ToArray().AsJsonString());

    /// <summary>
    /// Gets a list of device types supported by the current license, formatted as a JSON array of integer values. See <see cref="DeviceType"/> for possible integer values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetAllowedDeviceTypes),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint License_GetAllowedDeviceTypes() => 
        Utils.MarshalStringToPointer(
            Utils.DeviceTypesToJson(LicenseManager.AllowedDeviceTypes));

    /// <summary>
    /// Gets the expiry time for the current license represented as a <see cref="UInt64"/>. This corresponds to the milliseconds since the Unix epoch (January 1, 1970 00:00 UTC).
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetExpiryJavaTicks),
        CallConvs = [typeof(CallConvCdecl)])]
    public static ulong License_GetExpiryJavaTicks() => 
        LicenseManager.Expiry.GetJavaTicks();

    /// <summary>
    /// Gets a list of supported features for the current license, formatted as a JSON array of integer values. See <see cref="LicenseFeature"/> for possible integer values.
    /// </summary>
    /// <returns></returns>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetFeatures),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint License_GetFeatures() => 
        Utils.MarshalStringToPointer(
            Utils.LicenseFeaturesToJson(LicenseManager.Features));

    /// <summary>
    /// Gets the vendor identifier for the current license.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetUuid),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint License_GetUuid() => 
        Utils.MarshalStringToPointer(LicenseManager.Uuid);

    /// <summary>
    /// Gets the current version of the `NixUniversalSDK`.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetLibraryVersion),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint License_GetLibraryVersion() => 
        Utils.MarshalStringToPointer(LicenseManager.LibraryVersion);

    /// <summary>
    /// Gets the current version of the wrapper library for the `NixUniversalSDK`.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_GetLibraryWrapperVersion),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint License_GetLibraryWrapperVersion() =>
        Utils.MarshalStringToPointer(Utils.WrapperVersion);

    /// <summary>
    /// Checks if the current license supports the specified device type. See <see cref="DeviceType"/> for valid device type values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_IsDeviceTypeSupported),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool License_IsDeviceTypeSupported(int typeInt) => 
        LicenseManager.IsDeviceTypeSupported(((byte)typeInt).GetDeviceType());

    /// <summary>
    /// Checks if the current license supports the specified feature. See <see cref="LicenseFeature"/> for valid feature values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_IsFeatureEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool License_IsFeatureEnabled(int featureInt) =>
        LicenseManager.IsFeatureEnabled(featureInt.GetLicenseFeature());

    /// <summary>
    /// Activates a license. The options and signature parameters must exactly match the values provided in the SDK license. Calling this function invalidates any currently active license. Any connected device will be disconnected.
    /// </summary>
    /// <param name="optionsPtr">Pointer to license options string.</param>
    /// <param name="signaturePtr">Pointer to license signature, used to validate the license options.</param>
    /// <returns>License manager state after activation as an integer. See <see cref="LicenseManagerState"/> for possible values.</returns>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_Activate),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int License_Activate(
        nint optionsPtr,
        nint signaturePtr)
    {
        // Disconnect any connected device
        DeviceCompatModule.Instance?.Device?.Disconnect();

        // Activate the license
        return (int)LicenseManager.Activate(
            Utils.MarshalPointerAsString(optionsPtr),
            Utils.MarshalPointerAsString(signaturePtr));
    }

    /// <summary>
    /// Deactivates the current license. Any connected device will be disconnected.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(License_Deactivate),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void License_Deactivate()
    {
        DeviceCompatModule.Instance?.Device?.Disconnect();
        LicenseManager.Deactivate();
    }
    #endregion

    #region IDeviceScanner - properties        
    /// <summary>
    /// Gets the current <see cref="IDeviceScanner.State"/> as an <see cref="Int32">Int32</see>
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_GetState), 
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Scanner_GetState() =>
        (int)(DeviceScannerModule.Instance?.Scanner?.State ?? DeviceScannerState.Cold);

    /// <summary>
    /// Checks if the specified device ID has been found by the <see cref="IDeviceScanner"/>.
    /// </summary>
    /// <param name="idPtr">Pointer to ID string, corresponding to (<see cref="IDeviceCompat.Id"/>)</param>
    /// <returns>True if the device has been found</returns>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_HasFoundDevice),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Scanner_HasFoundDevice(nint idPtr) =>
        DeviceScannerModule.Instance?.Devices.ContainsKey(
            key: Utils.MarshalPointerAsString(idPtr)) ?? false;

    /// <summary>
    /// Gets a snapshot of the nearby devices list, sorted by signal strength and serialized as a JSON string. This command only provides a snapshot of the list, but does not update it. The list is updated while the <see cref="IDeviceScanner"/> is running; to update the list, call <see cref="Scanner_Start(int, int, int)"/> or <see cref="Scanner_SearchForId(nint, int)"/>.
    /// </summary>
    /// <param name="maxCount">Maximum number of devices to report. Set to -1 to report all devices</param>
    /// <returns>List of nearby devices serialized as JSON string</returns>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string list = Scanner_GetSortedResults(5);
    /// </code>
    /// Example JSON output:
    /// <code>
    /// [
    ///     {
    ///         "id": "COM3",
    ///         "interfaceType": 2,
    ///         "name": "Nix Spectro 2",
    ///         "rssi": 0,
    ///         "type": 5
    ///     },
    ///     {
    ///         "id": "D2:4F:5C:5C:54:40",
    ///         "interfaceType": 1,
    ///         "name": "Nix Mini 3",
    ///         "rssi": -50,
    ///         "type": 6
    ///     },
    ///     {
    ///         "id": "F8:CA:CF:4A:3C:03",
    ///         "interfaceType": 1,
    ///         "name": "Nix Spectro 2",
    ///         "rssi": -53,
    ///         "type": 5
    ///     },
    ///     {
    ///         "id": "D0:92:BA:D3:13:3A",
    ///         "interfaceType": 1,
    ///         "name": "Nix Spectro 2",
    ///         "rssi": -54,
    ///         "type": 5
    ///     },
    ///     {
    ///         "id": "CB:80:63:06:CD:9F",
    ///         "interfaceType": 1,
    ///         "name": "Nix Spectro 2",
    ///         "rssi": -55,
    ///         "type": 5
    ///     }
    /// ]
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_GetSortedResults),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Scanner_GetSortedResults(int maxCount = -1) => Utils.MarshalStringToPointer(
        (DeviceScannerModule.Instance?.GetSortedResults(maxCount) ?? []).AsJsonString());
    #endregion

    #region IDeviceScanner - functions
    /// <summary>
    /// Checks the Bluetooth adapter state again and resets the <see cref="IDeviceScanner"/> instance. This will invoke the <see cref="DeviceScannerModule.ScannerCreatedDelegate"/> callback upon completion.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_Reset),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_Reset() =>
        Task.Run(() => DeviceScannerModule.Instance?.ResetAsync());

    /// <summary>
    /// Starts a device search for the specified length of time, maximum callback interval, and report length. This will invoke the <see cref="DeviceScannerModule.ScannerStartedDelegate"/> callback on start, <see cref="DeviceScannerModule.ScannerStoppedDelegate"/> callback on stop, and <see cref="DeviceScannerModule.ScanResultDelegate"/> callback when the device list updates.
    /// </summary>
    /// <param name="scanPeriodMs">Length of time in milliseconds to run the device search, as an <see cref="Int32">Int32</see></param>
    /// <param name="maxReportIntervalMs">Maximum interval in milliseconds to invoke the <see cref="DeviceScannerModule.ScanResultDelegate"/> callback, as an <see cref="Int32">Int32</see></param>
    /// <param name="maxReportCount">Maximum number of devices to report in the <see cref="DeviceScannerModule.ScanResultDelegate"/> callback, as an <see cref="Int32">Int32</see></param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_Start),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_Start(
        int scanPeriodMs = (int)DeviceScanner.DefaultGeneralScanPeriodMs,
        int maxReportIntervalMs = DeviceScannerModule.DefaultMaxReportIntervalMs,
        int maxReportCount = DeviceScannerModule.DefaultMaxReportCount) =>
        DeviceScannerModule.Instance?.StartDeviceSearchAsync(scanPeriodMs, maxReportIntervalMs, maxReportCount);

    /// <summary>
    /// Stops a device search if one is currently running. This will invoke the <see cref="DeviceScannerModule.ScannerStoppedDelegate"/> callback once the scanner changes from a running state to a stopped state.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_Stop),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_Stop() => DeviceScannerModule.Instance?.Scanner?.Stop();

    /// <summary>
    /// Searches for a specific Nix device via USB and Bluetooth. The search will run until the specified device is found, or until the specified time interval elapses, whichever is shorter. This will invoke the <see cref="DeviceScannerModule.ScanResultDelegate"/> callback when the device is found.
    /// </summary>
    /// <param name="idPtr">Pointer to specific device ID to find</param>
    /// <param name="scanPeriodMs">Maximum time period in milliseconds to run the search, as an <see cref="Int32">Int32</see></param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_SearchForId),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_SearchForId(
        nint idPtr,
        int scanPeriodMs = (int)DeviceScanner.DefaultSingleDevicePeriodMs) =>
        Task.Run(async () =>
        {
            if (DeviceScannerModule.Instance is DeviceScannerModule module)
            {
                // Get `id` string from pointer
                var id = Utils.MarshalPointerAsString(idPtr);

                // Run a search if this device ID is not already in the list
                if (module.Devices.ContainsKey(id) != true)
                {
                    await module.SearchForId(id, scanPeriodMs);
                }

                var results = new List<ScannerResult>();
                if (module.Devices.TryGetValue(id, out var device) && device.AsScannerResult() is ScannerResult result)
                {                        
                    results.Add(result);
                }

                // Callback with this result
                module.ScanResultDelegate?.Invoke(
                    senderName: nameof(IDeviceScanner.SearchForIdAsync),
                    stringValue: results.ToArray().AsJsonString());
            }
        });

    /// <summary>
    /// Runs a listing for all available Nix devices attached via USB. This will invoke the <see cref="DeviceScannerModule.ScanResultDelegate"/> callback when the listing is complete.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_ListUsbDevices),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_ListUsbDevices() => Task.Run(async () =>
    {
        // Update devices list with USB attached devices only
        await DeviceScannerModule.Instance.UpdateUsbDeviceListAsync();

        // Callback with results
        var results = DeviceScannerModule.Instance?.GetSortedResults() ?? [];
        DeviceScannerModule.Instance?.ScanResultDelegate?.Invoke(
            senderName: nameof(IDeviceScanner.ListUsbDevicesAsync),
            stringValue: results.AsJsonString());
    });
    #endregion

    #region IDeviceScanner - callback registration
    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceScannerModule.ScannerCreatedDelegate"/>, which is linked to the <see cref="IDeviceScannerEvents.ScannerCreated"/> event on the internal <see cref="DeviceScanner"/> instance. The handler takes two arguments: a <see cref="string"/> argument (name of the calling event) and an <see cref="Int32"/> corresponding to the <see cref="DeviceScannerState"/> after initialization. See <see cref="DeviceScannerModule.ScannerCreatedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_RegisterCreated),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_RegisterCreated(nint handlerPtr) => 
        DeviceScannerModule.Instance.ScannerCreatedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.IntValue>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceScannerModule.ScannerStartedDelegate"/>, which is linked to the <see cref="IDeviceScannerEvents.ScannerStarted"/> event on the internal <see cref="DeviceScanner"/> instance. The handler takes one <see cref="string"/> argument (name of the calling event). See <see cref="DeviceScannerModule.ScannerStartedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_RegisterStarted),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_RegisterStarted(nint handlerPtr) =>
        DeviceScannerModule.Instance.ScannerStartedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.Empty>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceScannerModule.ScannerStoppedDelegate"/>, which is linked to the <see cref="IDeviceScannerEvents.ScannerStopped"/> event on the internal <see cref="DeviceScanner"/> instance. The handler takes one <see cref="string"/> argument (name of the calling event). See <see cref="DeviceScannerModule.ScannerStoppedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_RegisterStopped),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_RegisterStopped(nint handlerPtr) =>
        DeviceScannerModule.Instance.ScannerStoppedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.Empty>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceScannerModule.ScanResultDelegate"/>, which is linked to the <see cref="IDeviceScannerEvents.ScanResult"/> event on the internal <see cref="DeviceScanner"/> instance. The handler takes two arguments: a <see cref="string"/> argument (name of the calling event) and a string containing scan results. The scan results are formatted in the same manner as <see cref="Scanner_GetSortedResults(int)"/>. See <see cref="DeviceScannerModule.ScanResultDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Scanner_RegisterScanResult),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Scanner_RegisterScanResult(nint handlerPtr) =>
        DeviceScannerModule.Instance.ScanResultDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.StringValue>(handlerPtr);
    #endregion

    #region IDeviceCompat - properties
    /// <summary>
    /// <see cref="IDeviceCompat.Id"/> for the current device as an ANSI <see cref="string"/>, or empty if no device is selected.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetId),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetId() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.Id);

    /// <summary>
    /// <see cref="IDeviceCompat.State"/> for the current device as an <see cref="Int32">Int32</see>. See <see cref="DeviceState"/> for the possible values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetState),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Device_GetState() =>
        (int)(DeviceCompatModule.Instance?.Device?.State ?? DeviceState.Disconnected);

    /// <summary>
    /// <see cref="IDeviceCompat.Type"/> for the current device as an <see cref="Int32">Int32</see>. See <see cref="DeviceType"/> for the possible values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetType),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Device_GetType() =>
        (int)(DeviceCompatModule.Instance?.Device?.Type ?? DeviceType.Unknown);

    /// <summary>
    /// <see cref="IDeviceCompat.InterfaceType"/> for the current device as an <see cref="Int32">Int32</see>. See <see cref="InterfaceType"/> for the possible values.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetInterfaceType),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Device_GetInterfaceType() =>
        (int)(DeviceCompatModule.Instance?.Device?.InterfaceType ?? InterfaceType.Undefined);

    /// <summary>
    /// <see cref="IDeviceCompat.Name"/> for the current device as an ANSI <see cref="string"/>, or empty if no device is selected.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetName),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetName() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.Name);

    /// <summary>
    /// <see cref="IDeviceCompat.Note"/> for the current device as an ANSI <see cref="string"/>. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetNote),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetNote() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.Note);

    /// <summary>
    /// <see cref="IDeviceCompat.SerialNumber"/> for the current device as an ANSI <see cref="string"/>. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSerialNumber),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetSerialNumber() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.SerialNumber);

    /// <summary>
    /// <see cref="IDeviceCompat.FirmwareVersion"/> for the current device as an ANSI <see cref="string"/>. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetFirmwareVersion),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetFirmwareVersion() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.FirmwareVersion?.String);

    /// <summary>
    /// <see cref="IDeviceCompat.HardwareVersion"/> for the current device as an ANSI <see cref="string"/>. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetHardwareVersion),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetHardwareVersion() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.HardwareVersion?.String);

    /// <summary>
    /// <see cref="IDeviceCompat.SoftwareVersion"/> for the current device as an ANSI <see cref="string"/>. This value is valid only after a connection been opened.
    /// </summary>
    /// <returns></returns>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSoftwareVersion),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetSoftwareVersion() =>
        Utils.MarshalStringToPointer(DeviceCompatModule.Instance?.Device?.SoftwareVersion?.String);

    /// <summary>
    /// <see cref="IDeviceCompat.BatteryLevel"/> for the current device as an <see cref="Int32">Int32</see>. Range is 0 - 100, or -1 if not available. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetBatteryLevel),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Device_GetBatteryLevel() =>
        DeviceCompatModule.Instance?.Device?.BatteryLevel ?? -1;

    /// <summary>
    /// <see cref="IDeviceCompat.PowerState"/> for the current device; `true` if sufficient power is available to complete a measurement, `false` if a measurement will be aborted. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetPowerState),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetPowerState() =>
        DeviceCompatModule.Instance?.Device?.PowerState ?? false;

    /// <summary>
    /// <see cref="IDeviceCompat.ExtPowerState"/> for the current device. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetExtPowerState),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetExtPowerState() =>
        DeviceCompatModule.Instance?.Device?.ExtPowerState ?? false;

    /// <summary>
    /// <see cref="IDeviceCompat.ScanTemperature"/> for the current device as a 32-bit <see cref="float"/>, or <see cref="Constants.TemperatureError"/> if not available. This value is valid only if a device is connected and a measurement has completed.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetScanTemperature),
        CallConvs = [typeof(CallConvCdecl)])]
    public static float Device_GetScanTemperature() =>
        DeviceCompatModule.Instance?.Device?.ScanTemperature ?? Constants.TemperatureError;

    /// <summary>
    /// <see cref="IDeviceCompat.ReferenceTemperature"/> for the current device a 32-bit <see cref="float"/>, or <see cref="Constants.TemperatureError"/> it not available. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetReferenceTemperature),
        CallConvs = [typeof(CallConvCdecl)])]
    public static float Device_GetReferenceTemperature() =>
        DeviceCompatModule.Instance?.Device?.ReferenceTemperature ?? Constants.TemperatureError;

    /// <summary>
    /// <see cref="IDeviceCompat.ReferenceDate"/> for the current device, represented as a <see cref="UInt64">UInt64</see>, or 0 if not supported. This corresponds to the milliseconds since the Unix epoch (January 1, 1970 00:00 UTC).
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetReferenceJavaTicks),
        CallConvs = [typeof(CallConvCdecl)])]
    public static ulong Device_GetReferenceJavaTicks() =>
        DeviceCompatModule.Instance?.Device?.ReferenceDate.GetJavaTicks() ?? 0;

    /// <summary>
    /// <see cref="IDeviceCompat.ScanCount"/> for the current device as an <see cref="UInt32">UInt32</see>, or 0 if not supported.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetScanCount),
        CallConvs = [typeof(CallConvCdecl)])]
    public static uint Device_GetScanCount() =>
        DeviceCompatModule.Instance?.Device?.ScanCount ?? 0;

    /// <summary>
    /// <see cref="IDeviceCompat.SupportedModes"/> for the current device, formatted as a JSON array of string values (scan mode names). This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSupportedModes),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetSupportedModes()
    {
        var modeNames = new List<string>();
        if (DeviceCompatModule.Instance?.Device is IDeviceCompat device)
            foreach (var mode in device.SupportedModes)
                modeNames.Add(mode.GetFullName());
        return Utils.MarshalStringToPointer(modeNames.ToArray().AsJsonString());
    }

    /// <summary>
    /// <see cref="IDeviceCompat.ProvidesSpectral"/> for the current device. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetProvidesSpectral),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetProvidesSpectral() =>
        DeviceCompatModule.Instance?.Device?.ProvidesSpectral ?? false;

    /// <summary>
    /// <see cref="IDeviceCompat.ProvidesDensity"/> for the current device. This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetProvidesDensity),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetProvidesDensity() =>
        DeviceCompatModule.Instance?.Device?.ProvidesDensity ?? false;

    /// <summary>
    /// <see cref="IDeviceCompat.SupportedReferences"/> for the current device, formatted as a JSON array of string values (reference names). This value is valid only after a connection been opened.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSupportedReferences),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetSupportedReferences()
    {
        var referenceNames = new List<string>();
        if (DeviceCompatModule.Instance?.Device is IDeviceCompat device)
            foreach (var reference in device.SupportedReferences)
                referenceNames.Add(reference.ToString());
        return Utils.MarshalStringToPointer(referenceNames.ToArray().AsJsonString());
    }

    /// <summary>
    /// Flag to indicate if any on-device options are available. Value is `true` if any one of <see cref="IDeviceCompat.SupportsFieldCalibration"/>, <see cref="IDeviceCompat.SupportsTemperatureCompensation"/>, <see cref="IDeviceCompat.SupportsHapticFeedback"/>, or <see cref="IDeviceCompat.SupportsRgbFeedback"/> is true.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_HasOptions),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_HasOptions() =>
        DeviceCompatModule.Instance?.Device?.HasOptions() ?? false;

    /// <summary>
    /// Flag to indicate if this device supports in-field calibration using the provided reference tile.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSupportsFieldCalibration),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetSupportsFieldCalibration() =>
        DeviceCompatModule.Instance?.Device?.SupportsFieldCalibration ?? false;

    /// <summary>
    /// Flag to indicate if in-field calibration is recommended for this device at this time (if supported). This value is only valid after opening a connection and is a function of both time and ambient temperature. This value also updates each time a measurement is completed.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetFieldCalibrationDue),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetFieldCalibrationDue() =>
        DeviceCompatModule.Instance?.Device?.FieldCalibrationDue ?? false;

    /// <summary>
    /// Flag to indicate if in-field calibration results are applied (`true`) or bypassed (`false`) when evaluating the final calibrated measurement result. Value is `true` by default if supported by the device. It is recommended to leave this set to `true` if supported.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetFieldCalibrationEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetFieldCalibrationEnabled() =>
        DeviceCompatModule.Instance?.Device?.FieldCalibrationEnabled ?? false;

    /// <summary>
    /// Flag to indicate if this device supports automatic temperature compensation to correct for small changes in ambient temperature.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSupportsTemperatureCompensation),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetSupportsTemperatureCompensation() =>
        DeviceCompatModule.Instance?.Device?.SupportsTemperatureCompensation ?? false;

    /// <summary>
    /// Flag to indicate if ambient temperature compensation / correction is applied (`true`) or bypassed (`false`) when evaluating the final calibrated measurement. Value is `true` by default if supported by the device. It is recommended to leave this set to `true` if supported.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetTemperatureCompensationEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetTemperatureCompensationEnabled() =>
        DeviceCompatModule.Instance?.Device?.TemperatureCompensationEnabled ?? false;

    /// <summary>
    /// Flag to indicate if the device supports built-in haptic feedback.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSupportsHapticFeedback),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetSupportsHapticFeedback() =>
        DeviceCompatModule.Instance?.Device?.SupportsHapticFeedback ?? false;

    /// <summary>
    /// Flag to indicate if device haptic feedback is enabled.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetHapticFeedbackEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetHapticFeedbackEnabled() =>
        DeviceCompatModule.Instance?.Device?.HapticFeedbackEnabled ?? false;

    /// <summary>
    /// Flag to indicate if the device supports built-in RGB feedback.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetSupportsRgbFeedback),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetSupportsRgbFeedback() =>
        DeviceCompatModule.Instance?.Device?.SupportsRgbFeedback ?? true;

    /// <summary>
    /// Flag to indicate if device RGB feedback is enabled.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetRgbFeedbackEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_GetRgbFeedbackEnabled() =>
        DeviceCompatModule.Instance?.Device?.RgbFeedbackEnabled ?? false;
    #endregion

    #region IDeviceCompat - functions
    /// <summary>
    /// Opens a connection to the device with the specified ID. Only a single device can be connected at one time; opening a connection to a second device will disconnect the first. Connecting is an async process; either the <see cref="DeviceCompatModule.ConnectedDelegate"/> will be invoked on success, or the <see cref="DeviceCompatModule.DisconnectedDelegate"/> will be invoked on failure. To cancel a connection in progress, call <see cref="Device_Disconnect"/>.
    /// </summary>
    /// <param name="idPtr">Pointer to device ID string. This should match the <see cref="IDeviceCompat.Id"/> of a device found by the <see cref="DeviceScanner"/> in this app session. If this device has not already been found, a search will automatically be started before starting the connection process.</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_Connect),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_Connect(nint idPtr) =>
        Task.Run(async () => 
        {
            if (DeviceCompatModule.Instance is DeviceCompatModule module)
            {
                await module.Connect(Utils.MarshalPointerAsString(idPtr));
            }
        });

    /// <summary>
    /// Closes the current device connection.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_Disconnect),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_Disconnect() =>
        DeviceCompatModule.Instance?.Device?.Disconnect();

    /// <summary>
    /// Calls <see cref="IDeviceCompat.LedTestAsync"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an <see cref="Int32">Int32</see>.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_LedTest),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_LedTest() =>
        DeviceCompatModule.Instance?.LedTest();

    /// <summary>
    /// Calls <see cref="IDeviceCompat.MeasureAsync(ScanMode[])"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an <see cref="Int32">Int32</see>.
    /// </summary>
    /// <param name="modesJsonPtr">Pointer to string with scan mode(s) to select, formatted as a JSON array, or empty string to select all supported modes. Scan mode names must match the names in the <see cref="ScanMode"/> enum.</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_Measure),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_Measure(nint modesJsonPtr)
    {
        // Try to parse modes string as JSON array
        var modesJson = Utils.MarshalPointerAsString(modesJsonPtr);
        var modeList = new List<ScanMode>();
        try
        {
            // De-serialize string array of mode names
            var modeNames = JsonSerializer.Deserialize(
                modesJson, 
                typeof(string[]), 
                SourceGenerationContext.Default) as string[];

            // Parse each name into a list of ScanMode
            if (modeNames is not null)
            {
                foreach (var modeName in modeNames)
                    if (Enum.TryParse<ScanMode>(
                        value: modeName,
                        ignoreCase: true,
                        result: out var mode))
                        modeList.Add(mode);
            }                
        }
        catch
        {
            // Pass an empty array as a fallback
            modeList.Clear();
        }
        finally
        {
            DeviceCompatModule.Instance?.Measure([.. modeList]);
        }
    }

    /// <summary>
    /// Calls <see cref="IDeviceCompat.RunFieldCalibrationAsync(string)"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an integer.
    /// </summary>
    /// <param name="tileStringPtr">Pointer to string value decoded from the reference tile QR code. For <see cref="DeviceType.Spectro2"/> devices, the 5 digit code printed on the reference tile case is also accepted</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_RunFieldCalibration),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_RunFieldCalibration(nint tileStringPtr) =>
        DeviceCompatModule.Instance?.RunFieldCalibration(Utils.MarshalPointerAsString(tileStringPtr));

    /// <summary>
    /// Calls <see cref="IDeviceCompat.InvalidateFieldCalibrationAsync"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an integer.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_InvalidateFieldCalibration),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_InvalidateFieldCalibration() =>
        DeviceCompatModule.Instance?.InvalidateFieldCalibration();

    /// <summary>
    /// Used to check if a decoded string from the reference tile is valid.
    /// </summary>
    /// <param name="tileStringPtr">Pointer to string value decoded from the reference tile QR code</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_IsTileStringValid),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_IsTileStringValid(nint tileStringPtr) =>
        DeviceCompatModule.Instance?.Device?.IsTileStringValid(Utils.MarshalPointerAsString(tileStringPtr)) ?? false;

    /// <summary>
    /// Gets the <see cref="CommandStatus"/> for the last field calibration performed in this application session, represented as an <see cref="Int32">Int32</see>.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetLastCalibrationStatus),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Device_GetLastCalibrationStatus() =>
        (int)(DeviceCompatModule.Instance?.LastCalibrationResult?.Status ?? CommandStatus.ErrorInternal);

    /// <summary>
    /// Gets debug information for the last field calibration performed in this application session, formatted as a JSON string
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetLastCalibrationDebug),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Device_GetLastCalibrationDebug() => Utils.MarshalStringToPointer(
        DeviceCompatModule.Instance?.LastCalibrationResult?
        .GetDebug()
        .AsJsonString() ?? Constants.EmptyJson);

    /// <summary>
    /// Checks if a specific scan mode is supported. This value is valid only after a connection been opened.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to scan mode string to query. Must match names defined in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <returns>True if mode is supported, false if not supported or invalid</returns>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_IsModeSupported),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_IsModeSupported(nint scanModeNamePtr)
    {
        var isNameValid = Enum.TryParse<ScanMode>(
            value: Utils.MarshalPointerAsString(scanModeNamePtr),
            ignoreCase: true,
            result: out var scanMode);
        if (DeviceCompatModule.Instance?.Device is IDeviceCompat device && isNameValid)
            return device.IsModeSupported(scanMode);
        else
            return false;
    }

    /// <summary>
    /// Helper to check if a particular reference white point is supported by the colorimetry data from this device.
    /// </summary>
    /// <param name="referenceNamePtr">Pointer to reference white string, by its name defined in the <see cref="ReferenceWhite"/> enum (e.g. - "D50_2") to query</param>
    /// <returns>True if reference is supported, false if not supported or invalid</returns>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_ProvidesColor),
        CallConvs = [typeof(CallConvCdecl)])]
    public static bool Device_ProvidesColor(nint referenceNamePtr)
    {
        var isNameValid = Enum.TryParse<ReferenceWhite>(
            value: Utils.MarshalPointerAsString(referenceNamePtr),
            ignoreCase: true,
            result: out var reference);
        if (DeviceCompatModule.Instance?.Device is IDeviceCompat device && isNameValid)
            return device.ProvidesColor(reference);
        else
            return false;
    }

    /// <summary>
    /// Gets the current threshold/maximum Delta E threshold used when performing in-field calibration. Value is NaN if the device does not support field calibration.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_GetFieldCalibrationMaxDelta),
        CallConvs = [typeof(CallConvCdecl)])]
    public static double Device_GetFieldCalibrationMaxDelta()
    {
        return DeviceCompatModule.Instance?.Device?.FieldCalibrationMaxDelta ?? double.NaN;
    }

    /// <summary>
    /// Updates the maximum Delta E threshold used when performing in-field calibration. This is ignored if the device does not support field calibration.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_SetFieldCalibrationMaxDelta),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_SetFieldCalibrationMaxDelta(double newValue)
    {
        if (DeviceCompatModule.Instance?.Device is IDeviceCompat device)
            device.FieldCalibrationMaxDelta = newValue;
    }

    /// <summary>
    /// Calls <see cref="IDeviceCompat.SetFieldCalibrationEnabledAsync(bool)"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an integer.
    /// </summary>
    /// <param name="enabled">New state for this option</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_SetFieldCalibrationEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_SetFieldCalibrationEnabled(bool enabled) =>
        DeviceCompatModule.Instance?.SetFieldCalibrationEnabled(enabled);

    /// <summary>
    /// Calls <see cref="IDeviceCompat.SetTemperatureCompensationEnabledAsync(bool)"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an integer.
    /// </summary>
    /// <param name="enabled">New state for this option</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_SetTemperatureCompensationEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_SetTemperatureCompensationEnabled(bool enabled) =>
        DeviceCompatModule.Instance?.SetTemperatureCompensationEnabled(enabled);

    /// <summary>
    /// Calls <see cref="IDeviceCompat.SetHapticFeedbackEnabledAsync(bool)"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an integer.
    /// </summary>
    /// <param name="enabled">New state for this option</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_SetHapticFeedbackEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_SetHapticFeedbackEnabled(bool enabled) =>
        DeviceCompatModule.Instance?.SetHapticFeedbackEnabled(enabled);

    /// <summary>
    /// Calls <see cref="IDeviceCompat.SetRgbFeedbackEnabledAsync(bool)"/> on the current device. This is an async operation; it will invoke the <see cref="DeviceCompatModule.CommandCompletedDelegate"/> on completion with the <see cref="CommandStatus"/> of the result passed as an integer.
    /// </summary>
    /// <param name="enabled">New state for this option</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_SetRgbFeedbackEnabled),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_SetRgbFeedbackEnabled(bool enabled) =>
        DeviceCompatModule.Instance?.SetRgbFeedbackEnabled(enabled);
    #endregion

    #region IDeviceCompat - callback registration
    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceCompatModule.ConnectedDelegate"/>, which is linked to the <see cref="IDeviceCompatEvents.Connected"/> event on the internal <see cref="IDeviceCompat"/> instance. The handler takes one <see cref="string"/> argument (name of the calling event). See <see cref="DeviceCompatModule.ConnectedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_RegisterConnected),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_RegisterConnected(nint handlerPtr) =>
        DeviceCompatModule.Instance.ConnectedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.Empty>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceCompatModule.DisconnectedDelegate"/>, which is linked to the <see cref="IDeviceCompatEvents.Disconnected"/> event on the internal <see cref="IDeviceCompat"/> instance. The handler takes two arguments: a <see cref="string"/> argument (name of the calling event) and an <see cref="Int32"/> corresponding to the <see cref="DeviceStatus"/> reason for disconnection. See <see cref="DeviceCompatModule.DisconnectedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_RegisterDisconnected),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_RegisterDisconnected(nint handlerPtr) =>
        DeviceCompatModule.Instance.DisconnectedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.IntValue>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceCompatModule.BatteryStateChangedDelegate"/>, which is linked to the <see cref="IDeviceCompatEvents.BatteryStateChanged"/> event on the internal <see cref="IDeviceCompat"/> instance. The handler takes two arguments: a <see cref="string"/> argument (name of the calling event) and an <see cref="Int32"/> corresponding to the updated battery level (0 - 100). See <see cref="DeviceCompatModule.BatteryStateChangedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_RegisterBatteryStateChanged),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_RegisterBatteryStateChanged(nint handlerPtr) =>
        DeviceCompatModule.Instance.BatteryStateChangedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.IntValue>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceCompatModule.ExtPowerStageChangedDelegate"/>, which is linked to the <see cref="IDeviceCompatEvents.ExtPowerStateChanged"/> event on the internal <see cref="IDeviceCompat"/> instance. The handler takes two arguments: a <see cref="string"/> argument (name of the calling event) and an <see cref="bool"/> corresponding to the external power state. See <see cref="DeviceCompatModule.ExtPowerStageChangedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_RegisterExtPowerStateChanged),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_RegisterExtPowerStateChanged(nint handlerPtr) =>
        DeviceCompatModule.Instance.ExtPowerStageChangedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.BoolValue>(handlerPtr);

    /// <summary>
    /// Registers a callback handler for the <see cref="DeviceCompatModule.CommandCompletedDelegate"/>, which is invoked on completion of any async operations on the internal <see cref="IDeviceCompat"/> instance. The handler takes two arguments: a <see cref="string"/> argument (name of the internal calling function) and an <see cref="Int32"/> corresponding to the <see cref="CommandStatus"/> of the operation. See <see cref="DeviceCompatModule.CommandCompletedDelegate"/> for an example handler definition.
    /// </summary>
    /// <param name="handlerPtr">Pointer to callback function</param>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Device_RegisterCommandCompleted),
        CallConvs = [typeof(CallConvCdecl)])]
    public static void Device_RegisterCommandCompleted(nint handlerPtr) =>
        DeviceCompatModule.Instance.CommandCompletedDelegate = 
        Utils.GetDelegateForFunctionPointer<Delegates.IntValue>(handlerPtr);
    #endregion

    #region IMeasurementData - last measurements
    /// <summary>
    /// Gets the time stamp for the last measurement, represented as a <see cref="UInt64">UInt64</see>, or 0 if not available. This corresponds to the milliseconds since the Unix epoch (January 1, 1970 00:00 UTC).
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastJavaTicks),
        CallConvs = [typeof(CallConvCdecl)])]
    public static ulong Measurement_GetLastJavaTicks() =>
        (DeviceCompatModule.Instance?.LastMeasurementTime).GetJavaTicks();

    /// <summary>
    /// Gets the <see cref="CommandStatus"/> of the last measurement, represented as an <see cref="Int32">Int32</see>.
    /// </summary>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastStatus),
        CallConvs = [typeof(CallConvCdecl)])]
    public static int Measurement_GetLastStatus() =>
        (int)(DeviceCompatModule.Instance?.LastMeasurementResult?.Status ?? CommandStatus.ErrorInternal);

    /// <summary>
    /// Gets a list of <see cref="ScanMode">modes</see> available for the last measurement, formatted as JSON.
    /// </summary>
    /// <example>
    /// Example JSON output:
    /// <code>
    /// ["M0","M1","M2"]
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastModesJson),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastModesJson()
    {
        var modeNames = new List<string>();
        if (DeviceCompatModule.Instance?.LastMeasurementResult?.Measurements?.Keys is ICollection<ScanMode> modes)
            foreach (var mode in modes)
                modeNames.Add(mode.ToString());
        return Utils.MarshalStringToPointer(modeNames.ToArray().AsJsonString());
    }

    /// <summary>
    /// Gets metadata (device type, device settings, temperature) for the last measurement, formatted as JSON.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to name of the scan mode to select, which must match one of the names in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <returns>Measurement metadata formatted as JSON</returns>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string metadata = Measurement_GetLastMetadataJson("M2");
    /// </code>
    /// Example JSON output:
    /// <code>
    /// {
    ///     "dateMs": 1682005505841,
    ///     "status": 1,
    ///     "deviceName": "Nix Spectro 2",
    ///     "deviceType": 5,
    ///     "mode": "M2",
    ///     "tRef": 24.25,
    ///     "tScan": 24.5625,
    ///     "tReal": true,
    ///     "tCompEnabled": true,
    ///     "tileEnabled": true
    /// }
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastMetadataJson),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastMetadataJson(nint scanModeNamePtr)
    {
        // Try to get the specified measurement
        string? result;
        if (DeviceCompatModule.Instance?.TryGetMeasurement(
            Utils.MarshalPointerAsString(scanModeNamePtr),
            out var measurement) == true)
        {
            // Form JSON string
            result = measurement?
                .GetMetadata(DeviceCompatModule.Instance?.LastMeasurementTime)
                .AsJsonString() ?? Constants.EmptyJson;
        }
        else
        {
            result = Constants.EmptyJson;
        }
        return Utils.MarshalStringToPointer(result);
    }

    /// <summary>
    /// Gets spectral data for the last measurement in the selected scan mode, formatted as JSON. Result is empty ("{}") if spectral data is not available.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to name of the scan mode to select, which must match one of the names in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <returns>Spectral data formatted as JSON</returns>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string spectral = Measurement_GetLastSpectralJson("M2");
    /// </code>
    /// Example JSON output:
    /// <code>
    /// {
    ///     "mode": "M2",
    ///     "lambdaMin": 400,
    ///     "lambdaInterval": 10,
    ///     "value": [
    ///         0.09571632,
    ///         0.143246919,
    ///         0.201270252,
    ///         0.233701661,
    ///         0.229667112,
    ///         0.219201699,
    ///         0.197433069,
    ///         0.171539515,
    ///         0.14366518,
    ///         0.118656777,
    ///         0.102365054,
    ///         0.0851489082,
    ///         0.066573441,
    ///         0.0530253462,
    ///         0.0496698469,
    ///         0.0497059524,
    ///         0.0470496677,
    ///         0.0388559736,
    ///         0.0423551463,
    ///         0.123382688,
    ///         0.342879653,
    ///         0.608318865,
    ///         0.770111382,
    ///         0.84290266,
    ///         0.88257581,
    ///         0.898779333,
    ///         0.898909926,
    ///         0.902816713,
    ///         0.904295564,
    ///         0.906794488,
    ///         0.906649113
    ///     ]
    /// }
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastSpectralJson),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastSpectralJson(
        nint scanModeNamePtr)
    {
        // Try to get the specified measurement
        string? result;
        if (DeviceCompatModule.Instance?.TryGetMeasurement(
            Utils.MarshalPointerAsString(scanModeNamePtr),
            out var measurement) == true)
        {
            // Get spectral data and form JSON string                
            result = measurement?
                .SpectralData?
                .GetSerializable()
                .AsJsonString() ?? Constants.EmptyJson;
        }
        else
        {
            result = Constants.EmptyJson;
        }
        return Utils.MarshalStringToPointer(result);
    }

    /// <summary>
    /// Gets ISO density data for the last measurement in the selected scan mode and density status setting, formatted as JSON. Result is empty ("{}") if density data is not available.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to name of the scan mode to select, which must match one of the names in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <param name="densityStatusNamePtr">Pointer to name of the ISO density setting to select, which must match one of the names in the <see cref="DensityStatus"/> enum (e.g. - "A", "E", "I", "T")</param>
    /// <returns>Density data formatted as JSON</returns>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string density = Measurement_GetLastDensityJson("M2", "T");
    /// </code>
    /// Example JSON output:
    /// <code>
    /// {
    ///     "mode": "M2",
    ///     "isoStatus": "T",
    ///     "autoIndex": 1,
    ///     "value": [
    ///         0.2709233297197278,
    ///         1.2105773026488242,
    ///         0.7482882315988111,
    ///         0.6266073723704548
    ///     ]
    /// }
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastDensityJson),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastDensityJson(
        nint scanModeNamePtr,
        nint densityStatusNamePtr)
    {
        // Try parsing density status name
        var isDensityNameValid = Enum.TryParse<DensityStatus>(
            value: Utils.MarshalPointerAsString(densityStatusNamePtr),
            ignoreCase: true,
            result: out var densityStatus);

        // Try to get the specified measurement
        string? result;
        if (isDensityNameValid &&
            (DeviceCompatModule.Instance?.TryGetMeasurement(
                Utils.MarshalPointerAsString(scanModeNamePtr),
                out var measurement) == true))
        {
            // Get density data and form JSON string 
            result = measurement?
                .ToDensityData(densityStatus)?
                .GetSerializable()
                .AsJsonString() ?? Constants.EmptyJson;
        }
        else
        {
            result = Constants.EmptyJson;
        }
        return Utils.MarshalStringToPointer(result);
    }

    /// <summary>
    /// Gets a list of <see cref="ReferenceWhite">reference white</see> names available for the last measurement, formatted as JSON.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to scan mode name to select, which must match one of the names in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string references = Measurement_GetLastReferencesJson("M2");
    /// </code>
    /// Example JSON output:
    /// <code>
    /// ["D50_2","D65_10"]
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastReferencesJson),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastReferencesJson(nint scanModeNamePtr)
    {
        // Try to get the specified measurement and collect reference names
        var modeName = Utils.MarshalPointerAsString(scanModeNamePtr);
        var refNames = new List<string>();
        if ((DeviceCompatModule.Instance?.TryGetMeasurement(modeName, out var measurement) == true) && 
            measurement != null) 
            foreach (var reference in measurement.SupportedReferences)
                refNames.Add(reference.ToString());
        return Utils.MarshalStringToPointer(refNames.ToArray().AsJsonString());
    }

    /// <summary>
    /// Gets tristimulus colorimetry data for the last measurement in the selected scan mode, reference white, and color type, formatted as JSON. Result is empty ("{}") if the measurement mode or reference white is not available for the last measurement.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to scan mode name to select, which must match one of the names in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <param name="referenceNamePtr">Pointer to reference white to select, which must match one of the names in the <see cref="ReferenceWhite"/> enum (e.g. - "D50_2")</param>
    /// <param name="typeNamePtr">Pointer to color type to select, which must match one of the names in the <see cref="ColorType"/> enum (e.g. - "CIEXYZ", "CIELAB", "CIELCH", "CIELUV")</param>
    /// <returns>Colorimetry data formatted as JSON</returns>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string color = Measurement_GetLastColorJson("M2", "D50_2", "CIEXYZ");
    /// </code>
    /// Example JSON output:
    /// <code>
    /// {
    ///     "mode": "M2",
    ///     "reference": "D50_2",
    ///     "type": "CIEXYZ",
    ///     "value": [
    ///         0.3444101768980547,
    ///         0.18339852869976314,
    ///         0.15600230435248466
    ///     ]
    /// }
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastColorJson),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastColorJson(
        nint scanModeNamePtr,
        nint referenceNamePtr,
        nint typeNamePtr)
    {
        // Try parsing reference white name
        var isReferenceValid = Enum.TryParse<ReferenceWhite>(
            value: Utils.MarshalPointerAsString(referenceNamePtr),
            ignoreCase: true,
            result: out var reference);

        // Try parsing color type name
        var isTypeValid = Enum.TryParse<ColorType>(
            value: Utils.MarshalPointerAsString(typeNamePtr),
            ignoreCase: true,
            result: out var type);

        // Try to get the specified measurement
        string? result;
        if (isReferenceValid &&
            isTypeValid &&
            (DeviceCompatModule.Instance?.TryGetMeasurement(
                Utils.MarshalPointerAsString(scanModeNamePtr),
                out var measurement) == true))
        {
            // Get color data and form JSON string
            result = measurement?
                .ToColorData(reference, type)?
                .GetSerializable()
                .AsJsonString();
        }
        else
        {
            result = Constants.EmptyJson;
        }
        return Utils.MarshalStringToPointer(result);
    }

    /// <summary>
    /// Gets sRGB value for the last measurement in the selected scan mode and reference white, as a HEX triplet. Result is empty ("") if the measurement mode or reference white is not available for the last measurement.
    /// </summary>
    /// <param name="scanModeNamePtr">Pointer to scan mode name to select, which must match one of the names in the <see cref="ScanMode"/> enum (e.g. - "M0", "M1", or "M2")</param>
    /// <param name="referenceNamePtr">Pointer to reference white to select, which must match one of the names in the <see cref="ReferenceWhite"/> enum (e.g. - "D50_2")</param>
    /// <returns>sRGB value as a HEX triplet (e.g. "#RRGGBB"), or empty string ("") on error</returns>
    /// <example>
    /// An example of calling this function is shown below:
    /// <code>
    /// string hexCode = Measurement_GetLastHexCode("M2", "D50_2");
    /// </code>
    /// Example output:
    /// <code>
    /// #DB267C
    /// </code>
    /// </example>
    [UnmanagedCallersOnly(
        EntryPoint = nameof(Measurement_GetLastHexCode),
        CallConvs = [typeof(CallConvCdecl)])]
    public static nint Measurement_GetLastHexCode(
        nint scanModeNamePtr,
        nint referenceNamePtr)
    {
        // Try parsing reference white name
        var isReferenceValid = Enum.TryParse<ReferenceWhite>(
            value: Utils.MarshalPointerAsString(referenceNamePtr),
            ignoreCase: true,
            result: out var reference);

        // Try to get the specified measurement
        string? result;
        if (isReferenceValid && (DeviceCompatModule.Instance?.TryGetMeasurement(
            Utils.MarshalPointerAsString(scanModeNamePtr),
            out var measurement) == true))
        {
            // Get color data
            var colorData = measurement?.ToColorData(reference);

            // Return HEX code as string
            result = colorData?.HexRgbValue() ?? string.Empty;
        }
        else
        {
            result = string.Empty;
        }
        return Utils.MarshalStringToPointer(result);
    }
    #endregion
}