#if UNITY_ANDROID && !UNITY_EDITOR
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Android;

namespace Aidlab
{
    internal sealed class AndroidAidlabBackend
    {
        private const string BluetoothScanPermission = "android.permission.BLUETOOTH_SCAN";
        private const string BluetoothConnectPermission = "android.permission.BLUETOOTH_CONNECT";

        private readonly AidlabSDK owner;
        private readonly object commandCallbacksLock = new object();
        private readonly HashSet<CommandCallbackProxy> commandCallbacks = new HashSet<CommandCallbackProxy>();

        private AndroidJavaObject aidlabManager;
        private AndroidJavaObject device;
        private DeviceDelegateProxy deviceDelegateProxy;
        private string targetDeviceName;
        private string status = "WaitingForPermissions";
        private string lastError = "";
        private bool permissionRequestIssued;
        private bool scanStarted;
        private bool deviceDetected;
        private bool disposed;
        private int? androidSdkInt;

        internal AndroidAidlabBackend(AidlabSDK owner, string targetDeviceName)
        {
            this.owner = owner ?? throw new ArgumentNullException(nameof(owner));
            this.targetDeviceName = targetDeviceName ?? "";
        }

        internal bool IsConnected => device != null;
        internal string Status => status;
        internal string LastError => lastError;

        internal void Update()
        {
            if (disposed || scanStarted || deviceDetected)
                return;

            if (!HasBluetoothPermissions())
            {
                status = "WaitingForPermissions";
                RequestBluetoothPermissionsOnce();
                return;
            }

            EnsureManager();
            StartScan();
        }

        internal void SetTargetDeviceName(string value)
        {
            targetDeviceName = value ?? "";
            ResetConnection();
        }

        internal Task<int?> Collect(Signal[] liveSignals, Signal[] syncSignals)
        {
            if (device == null)
                return NotConnectedTask();

            AndroidJavaObject live = BuildCollectEnumSet(liveSignals);
            AndroidJavaObject sync = BuildCollectEnumSet(syncSignals);
            try
            {
                return CallCommand("collect", live, sync);
            }
            finally
            {
                live.Dispose();
                sync.Dispose();
            }
        }

        internal Task<int?> StartSynchronization()
        {
            return CallCommand("startSynchronization");
        }

        internal Task<int?> StopSynchronization()
        {
            return CallCommand("stopSynchronization");
        }

        internal Task<int?> ClearSynchronization()
        {
            return CallCommand("clearSynchronization");
        }

        internal Task<int?> StopCollect()
        {
            return CallCommand("stopCollect");
        }

        internal void Send(byte[] payload, int processId)
        {
            if (device == null || payload == null || payload.Length == 0)
                return;

            device.Call("send", payload, processId);
        }

        internal Task<int?> SendCommand(string command)
        {
            if (string.IsNullOrEmpty(command))
                return Task.FromException<int?>(new ArgumentException("Command must not be empty", nameof(command)));

            Send(System.Text.Encoding.UTF8.GetBytes(command + "\0"), 0);
            return Task.FromResult<int?>(null);
        }

        internal void Shutdown()
        {
            disposed = true;
            ResetConnection();

            if (aidlabManager != null)
            {
                try
                {
                    aidlabManager.Call("stopScan");
                }
                catch (Exception)
                {
                    // The Java object may already have been released by Android.
                }
                aidlabManager.Dispose();
                aidlabManager = null;
            }

            lock (commandCallbacksLock)
            {
                var pending = new List<CommandCallbackProxy>(commandCallbacks);
                commandCallbacks.Clear();
                foreach (CommandCallbackProxy callback in pending)
                    callback.Fail(new InvalidOperationException("Android backend was shut down"));
            }
        }

        private void EnsureManager()
        {
            if (aidlabManager != null)
                return;

            using (var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer"))
            using (AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity"))
            {
                aidlabManager = new AndroidJavaObject(
                    "com.aidlab.sdk.AidlabManager",
                    currentActivity,
                    new AidlabManagerDelegateProxy(this)
                );
            }
        }

        private void StartScan()
        {
            if (aidlabManager == null || scanStarted || disposed)
                return;

            lastError = "";
            status = "Scanning";
            scanStarted = true;
            aidlabManager.Call("scan");
        }

        private void StopScan()
        {
            if (aidlabManager == null || !scanStarted)
                return;

            aidlabManager.Call("stopScan");
            scanStarted = false;
        }

        private void OnDeviceDiscovered(AndroidJavaObject discoveredDevice, int rssi)
        {
            _ = rssi;
            if (disposed || discoveredDevice == null || deviceDetected)
                return;

            string discoveredName = "";
            try
            {
                discoveredName = discoveredDevice.Call<string>("name") ?? "";
            }
            catch (Exception)
            {
                // A device without a readable name cannot match an explicit target.
            }

            if (!string.IsNullOrEmpty(targetDeviceName) &&
                !string.Equals(discoveredName, targetDeviceName, StringComparison.OrdinalIgnoreCase))
            {
                return;
            }

            deviceDetected = true;
            status = "Connecting";
            StopScan();

            device = discoveredDevice;
            deviceDelegateProxy = new DeviceDelegateProxy(this);
            device.Call("connect", deviceDelegateProxy);
        }

        private void OnScanStarted()
        {
            scanStarted = true;
            status = "Scanning";
        }

        private void OnScanStopped()
        {
            scanStarted = false;
            if (!deviceDetected)
                status = "Disconnected";
        }

        private void OnScanFailed(int errorCode)
        {
            scanStarted = false;
            ReportError(
                AidlabAPI.AidlabErrorCode.Transport,
                $"Android BLE scan failed (code={errorCode})"
            );
        }

        private void OnConnected(AndroidJavaObject connectedDevice)
        {
            device = connectedDevice;
            status = "Connected";
            lastError = "";

            string firmwareRevision = ReadStringProperty(connectedDevice, "firmwareRevision");
            string hardwareRevision = ReadStringProperty(connectedDevice, "hardwareRevision");
            owner.AndroidDidConnect(firmwareRevision, hardwareRevision);
        }

        private void OnDisconnected(AndroidJavaObject disconnectReason)
        {
            _ = disconnectReason;
            device?.Dispose();
            device = null;
            deviceDelegateProxy = null;
            deviceDetected = false;
            scanStarted = false;
            status = "Disconnected";
            owner.AndroidDidDisconnect();
        }

        private void ResetConnection()
        {
            if (device != null)
            {
                try
                {
                    device.Call("disconnect");
                }
                catch (Exception)
                {
                    // Disconnect is best effort while tearing down the managed wrapper.
                }
                device.Dispose();
                device = null;
            }

            deviceDelegateProxy = null;
            deviceDetected = false;
            scanStarted = false;
            status = disposed ? "Stopped" : "Disconnected";
        }

        private Task<int?> CallCommand(string methodName, params object[] args)
        {
            if (device == null)
                return NotConnectedTask();

            var callback = new CommandCallbackProxy(RemoveCommandCallback);
            lock (commandCallbacksLock)
                commandCallbacks.Add(callback);

            var callArgs = new object[args.Length + 1];
            Array.Copy(args, callArgs, args.Length);
            callArgs[args.Length] = callback;

            try
            {
                device.Call(methodName, callArgs);
            }
            catch (Exception exception)
            {
                RemoveCommandCallback(callback);
                callback.Fail(exception);
            }

            return callback.Task;
        }

        private void RemoveCommandCallback(CommandCallbackProxy callback)
        {
            lock (commandCallbacksLock)
                commandCallbacks.Remove(callback);
        }

        private static Task<int?> NotConnectedTask()
        {
            return Task.FromException<int?>(new InvalidOperationException("SDK is not initialized"));
        }

        private AndroidJavaObject BuildCollectEnumSet(Signal[] signals)
        {
            using (var classClass = new AndroidJavaClass("java.lang.Class"))
            using (var enumSetClass = new AndroidJavaClass("java.util.EnumSet"))
            using (var dataTypeClass = new AndroidJavaClass("com.aidlab.sdk.DataType"))
            using (AndroidJavaObject javaDataTypeClass =
                classClass.CallStatic<AndroidJavaObject>("forName", "com.aidlab.sdk.DataType"))
            {
                AndroidJavaObject result =
                    enumSetClass.CallStatic<AndroidJavaObject>("noneOf", javaDataTypeClass);

                if (signals == null)
                    return result;

                foreach (Signal signal in signals)
                {
                    string dataTypeName = AndroidDataTypeName(signal);
                    if (dataTypeName == null)
                        continue;

                    using (AndroidJavaObject dataType =
                        dataTypeClass.GetStatic<AndroidJavaObject>(dataTypeName))
                    {
                        result.Call<bool>("add", dataType);
                    }
                }

                return result;
            }
        }

        private static string AndroidDataTypeName(Signal signal)
        {
            switch (signal)
            {
                case Signal.Ecg: return "ECG";
                case Signal.Respiration: return "RESPIRATION";
                case Signal.Temperature: return "SKIN_TEMPERATURE";
                case Signal.Motion: return "MOTION";
                case Signal.Activity: return "ACTIVITY";
                case Signal.Orientation: return "ORIENTATION";
                case Signal.Steps: return "STEPS";
                case Signal.HeartRate: return "HEART_RATE";
                case Signal.SoundVolume: return "SOUND_VOLUME";
                case Signal.Rr: return "RR";
                case Signal.Pressure: return "PRESSURE";
                case Signal.RespirationRate: return "RESPIRATION_RATE";
                case Signal.BodyPosition: return "BODY_POSITION";
                case Signal.Eda: return "EDA";
                case Signal.Gps: return "GPS";
                default: return null;
            }
        }

        private bool HasBluetoothPermissions()
        {
            if (AndroidSdkInt() >= 31)
            {
                return Permission.HasUserAuthorizedPermission(BluetoothScanPermission) &&
                       Permission.HasUserAuthorizedPermission(BluetoothConnectPermission);
            }

            return Permission.HasUserAuthorizedPermission(Permission.FineLocation);
        }

        private void RequestBluetoothPermissionsOnce()
        {
            if (permissionRequestIssued)
                return;

            permissionRequestIssued = true;
            if (AndroidSdkInt() >= 31)
            {
                if (!Permission.HasUserAuthorizedPermission(BluetoothScanPermission))
                    Permission.RequestUserPermission(BluetoothScanPermission);
                if (!Permission.HasUserAuthorizedPermission(BluetoothConnectPermission))
                    Permission.RequestUserPermission(BluetoothConnectPermission);
            }
            else if (!Permission.HasUserAuthorizedPermission(Permission.FineLocation))
            {
                Permission.RequestUserPermission(Permission.FineLocation);
            }
        }

        private int AndroidSdkInt()
        {
            if (androidSdkInt.HasValue)
                return androidSdkInt.Value;

            using (var version = new AndroidJavaClass("android.os.Build$VERSION"))
                androidSdkInt = version.GetStatic<int>("SDK_INT");
            return androidSdkInt.Value;
        }

        private static string ReadStringProperty(AndroidJavaObject source, string property)
        {
            if (source == null)
                return "";

            try
            {
                return source.Get<string>(property) ?? "";
            }
            catch (Exception)
            {
                return "";
            }
        }

        private void ReportError(AidlabAPI.AidlabErrorCode code, string message)
        {
            lastError = message ?? "";
            status = "Error";
            owner.AndroidDidReceiveError(new AidlabError(code, lastError));
        }

        private static ulong ToUInt64Bits(long value)
        {
            return unchecked((ulong)value);
        }

        private static int EnumValue(AndroidJavaObject value, int fallback)
        {
            if (value == null)
                return fallback;

            try
            {
                return value.Get<int>("value");
            }
            catch (Exception)
            {
                try
                {
                    return value.Call<int>("getValue");
                }
                catch (Exception)
                {
                    return fallback;
                }
            }
        }

        private sealed class AidlabManagerDelegateProxy : AndroidJavaProxy
        {
            private readonly AndroidAidlabBackend backend;

            internal AidlabManagerDelegateProxy(AndroidAidlabBackend backend)
                : base("com.aidlab.sdk.AidlabManagerDelegate")
            {
                this.backend = backend;
            }

            void onDeviceScanStarted() => backend.OnScanStarted();
            void onDeviceScanStopped() => backend.OnScanStopped();
            void onDeviceScanFailed(int errorCode) => backend.OnScanFailed(errorCode);
            void didDiscover(AndroidJavaObject discoveredDevice, int rssi) =>
                backend.OnDeviceDiscovered(discoveredDevice, rssi);
        }

        private sealed class CommandCallbackProxy : AndroidJavaProxy
        {
            private readonly TaskCompletionSource<int?> completion = new TaskCompletionSource<int?>();
            private readonly Action<CommandCallbackProxy> onCompleted;

            internal CommandCallbackProxy(Action<CommandCallbackProxy> onCompleted)
                : base("com.aidlab.sdk.CommandCallback")
            {
                this.onCompleted = onCompleted;
            }

            internal Task<int?> Task => completion.Task;

            public void onResult(AndroidJavaObject pid)
            {
                try
                {
                    completion.TrySetResult(pid == null ? (int?)null : pid.Call<int>("intValue"));
                }
                catch (Exception exception)
                {
                    completion.TrySetException(exception);
                }
                finally
                {
                    onCompleted?.Invoke(this);
                }
            }

            public void onError(string message)
            {
                completion.TrySetException(new InvalidOperationException(message ?? "Command failed"));
                onCompleted?.Invoke(this);
            }

            internal void Fail(Exception exception)
            {
                completion.TrySetException(exception);
                onCompleted?.Invoke(this);
            }
        }

        private sealed class DeviceDelegateProxy : AndroidJavaProxy
        {
            private readonly AndroidAidlabBackend backend;

            internal DeviceDelegateProxy(AndroidAidlabBackend backend)
                : base("com.aidlab.sdk.DeviceDelegate")
            {
                this.backend = backend;
            }

            void didConnect(AndroidJavaObject device) => backend.OnConnected(device);
            void didDisconnect(AndroidJavaObject device, AndroidJavaObject reason) => backend.OnDisconnected(reason);

            void didReceiveECG(AndroidJavaObject device, long timestamp, float value)
            {
                backend.owner.AndroidDidReceiveData();
                AidlabSDK.aidlabDelegate.DidReceiveECG(ToUInt64Bits(timestamp), value);
            }

            void didReceiveRespiration(AndroidJavaObject device, long timestamp, float value)
            {
                backend.owner.AndroidDidReceiveData();
                AidlabSDK.aidlabDelegate.DidReceiveRespiration(ToUInt64Bits(timestamp), value);
            }

            void didReceiveRespirationRate(AndroidJavaObject device, long timestamp, int value)
            {
                backend.owner.AndroidDidReceiveData();
                AidlabSDK.aidlabDelegate.DidReceiveRespirationRate(ToUInt64Bits(timestamp), unchecked((uint)value));
            }

            void didReceiveBatteryLevel(AndroidJavaObject device, int value) =>
                AidlabSDK.aidlabDelegate.DidReceiveBatteryLevel(value);

            void didReceiveSkinTemperature(AndroidJavaObject device, long timestamp, float value)
            {
                backend.owner.AndroidDidReceiveData();
                AidlabSDK.aidlabDelegate.DidReceiveSkinTemperature(ToUInt64Bits(timestamp), value);
            }

            void didReceiveAccelerometer(AndroidJavaObject device, long timestamp, float x, float y, float z) =>
                AidlabSDK.aidlabDelegate.DidReceiveAccelerometer(ToUInt64Bits(timestamp), x, y, z);

            void didReceiveGyroscope(AndroidJavaObject device, long timestamp, float x, float y, float z) =>
                AidlabSDK.aidlabDelegate.DidReceiveGyroscope(ToUInt64Bits(timestamp), x, y, z);

            void didReceiveMagnetometer(AndroidJavaObject device, long timestamp, float x, float y, float z) =>
                AidlabSDK.aidlabDelegate.DidReceiveMagnetometer(ToUInt64Bits(timestamp), x, y, z);

            void didReceiveQuaternion(AndroidJavaObject device, long timestamp, float w, float x, float y, float z) =>
                AidlabSDK.aidlabDelegate.DidReceiveQuaternion(ToUInt64Bits(timestamp), w, x, y, z);

            void didReceiveOrientation(AndroidJavaObject device, long timestamp, float roll, float pitch, float yaw) =>
                AidlabSDK.aidlabDelegate.DidReceiveOrientation(ToUInt64Bits(timestamp), roll, pitch, yaw);

            void didReceiveEDA(AndroidJavaObject device, long timestamp, float value) =>
                AidlabSDK.aidlabDelegate.DidReceiveEda(ToUInt64Bits(timestamp), value);

            void didReceiveGPS(
                AndroidJavaObject device,
                long timestamp,
                double latitude,
                double longitude,
                double altitude,
                float speed,
                float heading,
                float hdop) =>
                AidlabSDK.aidlabDelegate.DidReceiveGps(
                    ToUInt64Bits(timestamp),
                    (float)latitude,
                    (float)longitude,
                    (float)altitude,
                    speed,
                    heading,
                    hdop
                );

            void didReceiveBodyPosition(AndroidJavaObject device, long timestamp, AndroidJavaObject value) =>
                AidlabSDK.aidlabDelegate.DidReceiveBodyPosition(
                    ToUInt64Bits(timestamp),
                    (BodyPosition)EnumValue(value, (int)BodyPosition.Undefined)
                );

            void didReceiveActivity(AndroidJavaObject device, long timestamp, AndroidJavaObject value) =>
                AidlabSDK.aidlabDelegate.DidReceiveActivity(
                    ToUInt64Bits(timestamp),
                    (ActivityType)EnumValue(value, (int)ActivityType.Unspecific)
                );

            void didReceiveSteps(AndroidJavaObject device, long timestamp, long value) =>
                AidlabSDK.aidlabDelegate.DidReceiveSteps(ToUInt64Bits(timestamp), ToUInt64Bits(value));

            void didReceiveHeartRate(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabDelegate.DidReceiveHeartRate(ToUInt64Bits(timestamp), value);

            void didReceiveRr(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabDelegate.DidReceiveRr(ToUInt64Bits(timestamp), value);

            void didReceivePressure(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabDelegate.DidReceivePressure(ToUInt64Bits(timestamp), value);

            void pressureWearStateDidChange(AndroidJavaObject device, AndroidJavaObject value) =>
                AidlabSDK.aidlabDelegate.PressureWearStateDidChange(
                    (WearState)EnumValue(value, (int)WearState.Unknown)
                );

            void wearStateDidChange(AndroidJavaObject device, AndroidJavaObject value) =>
                AidlabSDK.aidlabDelegate.WearStateDidChange(
                    (WearState)EnumValue(value, (int)WearState.Unknown)
                );

            void didDetectExercise(AndroidJavaObject device, AndroidJavaObject value) =>
                AidlabSDK.aidlabDelegate.DidDetectExercise(
                    (Exercise)EnumValue(value, (int)Exercise.None)
                );

            void didDetectUserEvent(AndroidJavaObject device, long timestamp) =>
                AidlabSDK.aidlabDelegate.DidDetectUserEvent(ToUInt64Bits(timestamp));

            void didReceiveSoundVolume(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabDelegate.DidReceiveSoundVolume(ToUInt64Bits(timestamp), unchecked((ushort)value));

            void didReceiveSignalQuality(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabDelegate.DidReceiveSignalQuality(ToUInt64Bits(timestamp), unchecked((byte)value));

            void syncStateDidChange(AndroidJavaObject device, AndroidJavaObject value) =>
                AidlabSDK.aidlabSyncDelegate.SyncStateDidChange(
                    (SyncState)EnumValue(value, (int)SyncState.Unavailable)
                );

            void didReceiveUnsynchronizedSize(AndroidJavaObject device, int size, float progress) =>
                AidlabSDK.aidlabSyncDelegate.DidReceiveUnsynchronizedSize(unchecked((uint)size), progress);

            void didReceivePastECG(AndroidJavaObject device, long timestamp, float value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastEcg(ToUInt64Bits(timestamp), value);

            void didReceivePastRespiration(AndroidJavaObject device, long timestamp, float value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastRespiration(ToUInt64Bits(timestamp), value);

            void didReceivePastSkinTemperature(AndroidJavaObject device, long timestamp, float value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastTemperature(ToUInt64Bits(timestamp), value);

            void didReceivePastHeartRate(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastHeartRate(ToUInt64Bits(timestamp), value);

            void didReceivePastRr(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastRr(ToUInt64Bits(timestamp), value);

            void didReceivePastRespirationRate(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastRespirationRate(ToUInt64Bits(timestamp), unchecked((uint)value));

            void didReceivePastActivity(AndroidJavaObject device, long timestamp, AndroidJavaObject value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastActivity(
                    ToUInt64Bits(timestamp),
                    (ActivityType)EnumValue(value, (int)ActivityType.Unspecific)
                );

            void didReceivePastSteps(AndroidJavaObject device, long timestamp, long value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastSteps(ToUInt64Bits(timestamp), ToUInt64Bits(value));

            void didReceivePastAccelerometer(AndroidJavaObject device, long timestamp, float x, float y, float z) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastAccelerometer(ToUInt64Bits(timestamp), x, y, z);

            void didReceivePastGyroscope(AndroidJavaObject device, long timestamp, float x, float y, float z) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastGyroscope(ToUInt64Bits(timestamp), x, y, z);

            void didReceivePastMagnetometer(AndroidJavaObject device, long timestamp, float x, float y, float z) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastMagnetometer(ToUInt64Bits(timestamp), x, y, z);

            void didReceivePastQuaternion(AndroidJavaObject device, long timestamp, float w, float x, float y, float z) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastQuaternion(ToUInt64Bits(timestamp), w, x, y, z);

            void didReceivePastOrientation(
                AndroidJavaObject device,
                long timestamp,
                float roll,
                float pitch,
                float yaw) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastOrientation(
                    ToUInt64Bits(timestamp),
                    roll,
                    pitch,
                    yaw
                );

            void didReceivePastEDA(AndroidJavaObject device, long timestamp, float value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastEda(ToUInt64Bits(timestamp), value);

            void didReceivePastGPS(
                AndroidJavaObject device,
                long timestamp,
                double latitude,
                double longitude,
                double altitude,
                float speed,
                float heading,
                float hdop) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastGps(
                    ToUInt64Bits(timestamp),
                    (float)latitude,
                    (float)longitude,
                    (float)altitude,
                    speed,
                    heading,
                    hdop
                );

            void didReceivePastBodyPosition(AndroidJavaObject device, long timestamp, AndroidJavaObject value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastBodyPosition(
                    ToUInt64Bits(timestamp),
                    (BodyPosition)EnumValue(value, (int)BodyPosition.Undefined)
                );

            void didReceivePastSoundVolume(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastSoundVolume(ToUInt64Bits(timestamp), unchecked((ushort)value));

            void didReceivePastPressure(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastPressure(ToUInt64Bits(timestamp), value);

            void didReceivePastSignalQuality(AndroidJavaObject device, long timestamp, int value) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastSignalQuality(ToUInt64Bits(timestamp), unchecked((byte)value));

            void didDetectPastUserEvent(AndroidJavaObject device, long timestamp) =>
                AidlabSDK.aidlabSyncDelegate.DidReceivePastUserEvent(ToUInt64Bits(timestamp));

            void didReceiveError(AndroidJavaObject device, AndroidJavaObject error) =>
                backend.owner.AndroidDidReceiveError(ParseError(error));

            void didReceivePayload(
                AndroidJavaObject device,
                string process,
                AndroidJavaObject payload,
                long options) =>
                backend.owner.AndroidDidReceivePayload(
                    process,
                    ConvertByteArray(payload),
                    unchecked((ulong)options)
                );

            void didReceiveProcessError(
                AndroidJavaObject device,
                string process,
                int pid,
                AndroidJavaObject payload,
                long options) =>
                backend.owner.AndroidDidReceiveProcessError(
                    process,
                    pid,
                    ConvertByteArray(payload),
                    unchecked((ulong)options)
                );

            void processDidTerminate(AndroidJavaObject device, int pid) =>
                backend.owner.AndroidDidTerminateProcess(pid);

            private static byte[] ConvertByteArray(AndroidJavaObject payload)
            {
                if (payload == null)
                    return Array.Empty<byte>();
                return AndroidJNIHelper.ConvertFromJNIArray<byte[]>(payload.GetRawObject());
            }

            private static AidlabError ParseError(AndroidJavaObject error)
            {
                if (error == null)
                    return new AidlabError(AidlabAPI.AidlabErrorCode.Sdk, "");

                string message;
                try
                {
                    message = error.Call<string>("getMessage") ?? "";
                }
                catch (Exception)
                {
                    message = error.Call<string>("toString") ?? "";
                }

                int rawCode = (int)AidlabAPI.AidlabErrorCode.Sdk;
                try
                {
                    using (AndroidJavaObject code = error.Call<AndroidJavaObject>("getCode"))
                        rawCode = code?.Call<int>("getValue") ?? rawCode;
                }
                catch (Exception)
                {
                    // Unknown Android error code is represented as a generic SDK error.
                }

                var parsedCode = Enum.IsDefined(typeof(AidlabAPI.AidlabErrorCode), rawCode)
                    ? (AidlabAPI.AidlabErrorCode)rawCode
                    : AidlabAPI.AidlabErrorCode.Sdk;
                return new AidlabError(parsedCode, message);
            }
        }
    }
}
#endif
