#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;

        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, DeviceEventType[] events)
        {
            if (device == null)
                return NotConnectedTask();

            AndroidJavaObject live = BuildEnumSet("com.aidlab.sdk.DataType", liveSignals, AndroidDataTypeName);
            AndroidJavaObject sync = BuildEnumSet("com.aidlab.sdk.DataType", syncSignals, AndroidDataTypeName);
            AndroidJavaObject javaEvents = BuildEnumSet("com.aidlab.sdk.DeviceEventType", events, AndroidEventTypeName);
            try
            {
                return CallCommand("collect", live, sync, javaEvents);
            }
            finally
            {
                live.Dispose();
                sync.Dispose();
                javaEvents.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 Task Send(byte[] payload, int processId)
        {
            return CallCommand("send", payload, processId);
        }

        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.
            }

            // The Android scan finds only Aidlab devices, so an empty target accepts any of them.
            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, new ConnectCallbackProxy(this));
        }

        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);
        }

        // A failed connection never reached didConnect, so it reports an error instead of a disconnect.
        private void OnConnectFailed(string message)
        {
            device?.Dispose();
            device = null;
            deviceDelegateProxy = null;
            deviceDetected = false;
            scanStarted = false;
            ReportError(AidlabAPI.AidlabErrorCode.Transport, message ?? "Connection failed");
        }

        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 static AndroidJavaObject BuildEnumSet<T>(string className, T[] values, Func<T, string> constantName)
        {
            using (var classClass = new AndroidJavaClass("java.lang.Class"))
            using (var enumSetClass = new AndroidJavaClass("java.util.EnumSet"))
            using (var enumClass = new AndroidJavaClass(className))
            using (AndroidJavaObject javaEnumClass = classClass.CallStatic<AndroidJavaObject>("forName", className))
            {
                AndroidJavaObject result =
                    enumSetClass.CallStatic<AndroidJavaObject>("noneOf", javaEnumClass);

                if (values == null)
                    return result;

                foreach (T value in values)
                {
                    string name = constantName(value);
                    if (name == null)
                        continue;

                    using (AndroidJavaObject constant = enumClass.GetStatic<AndroidJavaObject>(name))
                    {
                        result.Call<bool>("add", constant);
                    }
                }

                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.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 static string AndroidEventTypeName(DeviceEventType eventType)
        {
            switch (eventType)
            {
                case DeviceEventType.WearState: return "WEAR_STATE";
                case DeviceEventType.PressureWearState: return "PRESSURE_WEAR_STATE";
                case DeviceEventType.User: return "USER";
                case DeviceEventType.Exercise: return "EXERCISE";
                case DeviceEventType.Activity: return "ACTIVITY";
                case DeviceEventType.Ecg: return "ECG";
                case DeviceEventType.Sleep: return "SLEEP";
                default: return null;
            }
        }

        private bool HasBluetoothPermissions()
        {
            return Permission.HasUserAuthorizedPermission(BluetoothScanPermission) &&
                   Permission.HasUserAuthorizedPermission(BluetoothConnectPermission);
        }

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

            permissionRequestIssued = true;
            if (!Permission.HasUserAuthorizedPermission(BluetoothScanPermission))
                Permission.RequestUserPermission(BluetoothScanPermission);
            if (!Permission.HasUserAuthorizedPermission(BluetoothConnectPermission))
                Permission.RequestUserPermission(BluetoothConnectPermission);
        }

        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.ReportError(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;
                }
            }
        }

        // A rejection keeps the reason of the Java ProcessRejectedException, as on the other platforms.
        private static Exception CommandError(AndroidJavaObject error)
        {
            string message = error.Call<string>("getMessage") ?? error.Call<string>("toString");
            string className;
            using (AndroidJavaObject javaClass = error.Call<AndroidJavaObject>("getClass"))
                className = javaClass.Call<string>("getName");
            if (className != "com.aidlab.sdk.ProcessRejectedException")
                return new InvalidOperationException(message);
            RejectionReason reason;
            using (AndroidJavaObject value = error.Call<AndroidJavaObject>("getReason"))
                reason = (RejectionReason)EnumValue(value, (int)RejectionReason.Unknown);
            return new ProcessRejectedException(reason, message);
        }

        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;

            // Unity unboxes a java.lang.Integer PID to int and passes null as an AndroidJavaObject.
            public void onResult(int pid) => Complete(pid);

            public void onResult(AndroidJavaObject pid) => Complete(null);

            private void Complete(int? pid)
            {
                completion.TrySetResult(pid);
                onCompleted?.Invoke(this);
            }

            public void onError(AndroidJavaObject error) => Fail(CommandError(error));

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

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

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

            // Success is reported through didConnect. Unity unboxes a java.lang.Integer PID to int and passes null as
            // an AndroidJavaObject.
            public void onResult(int pid) { }

            public void onResult(AndroidJavaObject pid) { }

            public void onError(AndroidJavaObject error) => backend.OnConnectFailed(CommandError(error).Message);
        }

        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 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 didReceiveEvent(AndroidJavaObject device, AndroidJavaObject deviceEvent)
            {
                DeviceEvent parsed = ParseEvent(deviceEvent);
                if (parsed != null)
                    AidlabSDK.aidlabDelegate.DidReceiveEvent(parsed);
            }

            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, long size, float bytesPerSecond) =>
                AidlabSDK.aidlabSyncDelegate.DidReceiveUnsynchronizedSize(unchecked((uint)size), bytesPerSecond);

            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 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 didReceiveError(AndroidJavaObject device, AndroidJavaObject error) =>
                backend.owner.ReportError(ParseError(error));

            void didReceiveProcessOutput(
                AndroidJavaObject device,
                int pid,
                AndroidJavaObject payload,
                long options) =>
                backend.owner.AndroidDidReceiveProcessOutput(
                    pid,
                    ConvertByteArray(payload),
                    unchecked((ulong)options)
                );

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

            // Unity unboxes a java.lang.Integer exit status to int and passes null as an AndroidJavaObject.
            void processDidExit(AndroidJavaObject device, int pid, int exitStatus) =>
                backend.owner.AndroidProcessDidExit(pid, exitStatus);

            void processDidExit(AndroidJavaObject device, int pid, AndroidJavaObject exitStatus) =>
                backend.owner.AndroidProcessDidExit(pid, null);

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

            // Maps the com.aidlab.sdk.DeviceEvent data classes. A newer Android SDK may add event types; they are not
            // delivered.
            private static DeviceEvent ParseEvent(AndroidJavaObject deviceEvent)
            {
                ulong timestamp = ToUInt64Bits(deviceEvent.Call<long>("getTimestamp"));
                bool isPast = deviceEvent.Call<bool>("isPast");
                // Nullable Kotlin properties: java.lang.Long and java.lang.Float, null while a period lasts or when
                // the detector reports no confidence.
                ulong? end;
                using (AndroidJavaObject value = deviceEvent.Call<AndroidJavaObject>("getEndTimestamp"))
                    end = value == null ? (ulong?)null : ToUInt64Bits(value.Call<long>("longValue"));
                float? confidence;
                using (AndroidJavaObject value = deviceEvent.Call<AndroidJavaObject>("getConfidence"))
                    confidence = value == null ? (float?)null : value.Call<float>("floatValue");
                string className;
                using (AndroidJavaObject javaClass = deviceEvent.Call<AndroidJavaObject>("getClass"))
                    className = javaClass.Call<string>("getName");

                switch (className)
                {
                    case "com.aidlab.sdk.WearStateEvent":
                        return new WearStateEvent(
                            timestamp,
                            end,
                            confidence,
                            isPast,
                            (WearState)EnumProperty(deviceEvent, "getWearState", (int)WearState.Unknown)
                        );
                    case "com.aidlab.sdk.PressureWearStateEvent":
                        return new PressureWearStateEvent(
                            timestamp,
                            end,
                            confidence,
                            isPast,
                            (WearState)EnumProperty(deviceEvent, "getWearState", (int)WearState.Unknown)
                        );
                    case "com.aidlab.sdk.UserEvent":
                        return new UserEvent(timestamp, end, confidence, isPast);
                    case "com.aidlab.sdk.ExerciseEvent":
                        return new ExerciseEvent(
                            timestamp,
                            end,
                            confidence,
                            isPast,
                            (Exercise)EnumProperty(deviceEvent, "getExercise", (int)Exercise.None)
                        );
                    case "com.aidlab.sdk.ActivityEvent":
                        return new ActivityEvent(
                            timestamp,
                            end,
                            confidence,
                            isPast,
                            (ActivityType)EnumProperty(deviceEvent, "getActivity", (int)ActivityType.Unspecific)
                        );
                    case "com.aidlab.sdk.EcgEvent":
                        return new EcgEvent(
                            timestamp,
                            end,
                            confidence,
                            isPast,
                            deviceEvent.Call<int>("getAnntyp"),
                            deviceEvent.Call<int>("getSubtyp"),
                            deviceEvent.Call<string>("getAux"),
                            EnumNameProperty<EcgRhythm>(deviceEvent, "getRhythm"),
                            deviceEvent.Call<string>("getModelVersion")
                        );
                    case "com.aidlab.sdk.SleepEvent":
                        return new SleepEvent(
                            timestamp,
                            end,
                            confidence,
                            isPast,
                            EnumNameProperty<SleepPeriod>(deviceEvent, "getPeriod") ?? SleepPeriod.Unknown,
                            deviceEvent.Call<string>("getModelVersion")
                        );
                    default:
                        return null;
                }
            }

            private static int EnumProperty(AndroidJavaObject source, string getter, int fallback)
            {
                using (AndroidJavaObject value = source.Call<AndroidJavaObject>(getter))
                    return EnumValue(value, fallback);
            }

            // Kotlin enum constants are the C# names in SCREAMING_SNAKE_CASE, e.g. ATRIAL_FIBRILLATION. Null when the
            // property is null or its constant is unknown.
            private static T? EnumNameProperty<T>(AndroidJavaObject source, string getter) where T : struct, Enum
            {
                using (AndroidJavaObject value = source.Call<AndroidJavaObject>(getter))
                {
                    string name = value?.Call<string>("name")?.Replace("_", "");
                    return name != null && Enum.TryParse(name, true, out T result) ? result : (T?)null;
                }
            }

            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
