#if UNITY_IOS
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using AOT;

namespace Aidlab.BLE
{
    public class IosBLEConnector : IBleConnector
    {
        private readonly AidlabSDK aidlabSdk;

        private volatile bool isConnected;
        private volatile bool firmwareReceived;
        private volatile bool timeSetConfirmed;
        private volatile bool timeSetAttempted;
        private volatile bool timeSetFailed;
        private int connectedAtTick = -1;

        private readonly Queue<byte[]> writeQueue = new Queue<byte[]>();
        private readonly AutoResetEvent writeSignal = new AutoResetEvent(false);
        private readonly Thread writerThread;
        private volatile bool writerRunning = true;

        public bool IsConnected => isConnected;
        public bool HasFirmwareRevision => firmwareReceived;

        private static IosBLEConnector current;
        private static readonly IosBLEApi.DataCallback DataCallback = OnData;
        private static readonly IosBLEApi.EventCallback EventCallback = OnEvent;

        public IosBLEConnector(AidlabSDK aidlabSdk, string deviceNameToConnect)
        {
            this.aidlabSdk = aidlabSdk;

            current = this;
            IosBLEApi.SetDataCallback(DataCallback);
            IosBLEApi.SetEventCallback(EventCallback);
            IosBLEApi.Start(deviceNameToConnect);

            writerThread = new Thread(WriterLoop);
            writerThread.IsBackground = true;
            writerThread.Start();
        }

        public void ConnectionProcess()
        {
            MaybeNotifyConnectedAfterTimeout();
        }

        public void Disconnect()
        {
            isConnected = false;
            writerRunning = false;
            writeSignal.Set();
            current = null;
            IosBLEApi.Stop();
            if (writerThread.IsAlive)
                writerThread.Join(500);
        }

        public void Write(byte[] payload)
        {
            if (payload == null || payload.Length == 0)
                return;

            lock (writeQueue)
            {
                writeQueue.Enqueue(payload);
            }
            writeSignal.Set();
        }

        private void WriterLoop()
        {
            while (writerRunning)
            {
                writeSignal.WaitOne();

                while (true)
                {
                    byte[] payload = null;
                    lock (writeQueue)
                    {
                        if (writeQueue.Count > 0)
                            payload = writeQueue.Dequeue();
                    }

                    if (payload == null)
                        break;

                    if (!isConnected)
                        continue;

                    SendChunked(payload);
                }
            }
        }

        private void SendChunked(byte[] payload)
        {
            int chunkSize = 20;
            if (aidlabSdk.UsesV4Protocol())
            {
                int max = IosBLEApi.GetMaxWriteLength();
                if (max > 0)
                    chunkSize = Math.Min(max, 512);
            }

            int offset = 0;
            int chunkIndex = 0;
            while (offset < payload.Length)
            {
                int size = Math.Min(chunkSize, payload.Length - offset);
                byte[] chunk = payload;
                if (size < payload.Length)
                {
                    chunk = new byte[size];
                    Buffer.BlockCopy(payload, offset, chunk, 0, size);
                }
                bool ok = IosBLEApi.Write(chunk, size);
                chunkIndex++;
                if (!ok)
                {
                    lock (writeQueue)
                    {
                        writeQueue.Clear();
                    }
                    writerRunning = false;
                    aidlabSdk.OnTransportFailure(
                        $"IosBLEConnector: Write failed (chunk #{chunkIndex}, {size}B)", this);
                    return;
                }
                offset += size;
            }
            aidlabSdk.OnTransportFrameWritten(payload);
        }

        [MonoPInvokeCallback(typeof(IosBLEApi.DataCallback))]
        private static void OnData(IntPtr data, int size)
        {
            IosBLEConnector instance = current;
            if (instance == null)
                return;

            if (data == IntPtr.Zero || size <= 0)
                return;

            if (!instance.timeSetConfirmed)
                return;

            byte[] chunk = new byte[size];
            Marshal.Copy(data, chunk, 0, size);
            // On the CoreBluetooth queue, so that acknowledgements do not wait for a Unity frame.
            instance.aidlabSdk.OnAidlabDataReceived(chunk, chunk.Length);
        }

        [MonoPInvokeCallback(typeof(IosBLEApi.EventCallback))]
        private static void OnEvent(int evt, IntPtr messagePtr)
        {
            IosBLEConnector instance = current;
            if (instance == null)
                return;

            string message = messagePtr == IntPtr.Zero ? "" : Marshal.PtrToStringAnsi(messagePtr);
            switch (evt)
            {
                case 1: // connected
                    instance.isConnected = true;
                    instance.connectedAtTick = Environment.TickCount;
                    instance.timeSetConfirmed = false;
                    instance.timeSetAttempted = false;
                    instance.timeSetFailed = false;
                    if (instance.firmwareReceived)
                        instance.MaybeNotifyConnected();
                    break;
                case 2: // disconnected
                    instance.isConnected = false;
                    instance.OnMainThread(instance.aidlabSdk.OnAidlabDisconnected);
                    break;
                case 3: // firmware
                    instance.aidlabSdk.firmwareRevisionStr = message;
                    instance.firmwareReceived = true;
                    if (instance.isConnected)
                        instance.MaybeNotifyConnected();
                    break;
                case 4: // hardware
                    instance.aidlabSdk.hardwareRevisionStr = message;
                    break;
                case 5: // error
                    UnityEngine.Debug.LogWarning($"Aidlab BLE (iOS): {message}");
                    break;
                case 6: // debug
                    if (!string.IsNullOrEmpty(message) &&
                        message.IndexOf("Set time sent", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        instance.timeSetAttempted = true;
                    }
                    break;
                case 7: // time set confirmed (or failed)
                    if (!string.IsNullOrEmpty(message) &&
                        message.IndexOf("failed", StringComparison.OrdinalIgnoreCase) >= 0)
                    {
                        instance.timeSetFailed = true;
                        UnityEngine.Debug.LogWarning($"Aidlab BLE (iOS): {message}");
                        break;
                    }

                    instance.timeSetConfirmed = true;
                    if (instance.isConnected && instance.firmwareReceived)
                        instance.MaybeNotifyConnected();
                    break;
                default:
                    break;
            }
        }

        private void MaybeNotifyConnected()
        {
            if (!timeSetConfirmed)
                return;

            OnMainThread(aidlabSdk.OnAidlabConnected);
        }

        private void MaybeNotifyConnectedAfterTimeout()
        {
            if (!isConnected || !firmwareReceived || timeSetConfirmed || timeSetFailed)
                return;

            if (connectedAtTick < 0)
                return;

            int elapsedMs = unchecked(Environment.TickCount - connectedAtTick);
            if (timeSetAttempted)
            {
                if (elapsedMs < 10000)
                    return;

                timeSetFailed = true;
                UnityEngine.Debug.LogWarning("Aidlab BLE (iOS): time set not confirmed within 10s; disconnecting");
                // Disconnects this connector too.
                OnMainThread(aidlabSdk.OnAidlabDisconnected);
                return;
            }

            if (elapsedMs < 2000)
                return;

            timeSetConfirmed = true;
            UnityEngine.Debug.LogWarning("Aidlab BLE (iOS): time service not available; proceeding without set time");
            OnMainThread(aidlabSdk.OnAidlabConnected);
        }

        // Runs action on the main thread unless this connector has been replaced by then; the SDK's connector is
        // the current one.
        private void OnMainThread(Action action)
        {
            AidlabSDK.EnqueueMainThreadEvent(() =>
            {
                if (current == this)
                    action();
            });
        }
    }
}
#else
namespace Aidlab.BLE
{
    public class IosBLEConnector : IBleConnector
    {
        public IosBLEConnector(AidlabSDK _, string __) { }
        public void ConnectionProcess() { }
        public void Disconnect() { }
        public void Write(byte[] payload) { }
    }
}
#endif
