﻿using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using System;
using System.Text;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using System.Linq;

namespace Aidlab.BLE
{
    public class BLEConnector : IBleConnector
    {
        private readonly object devicesLock = new object();
        private Dictionary<string, BLEApi.DeviceUpdate> devices;
        private string foundDevice;
        public Thread bleConnectorThread;
        private BLEStatus currentBLEStatus = BLEStatus.None;
	        private AidlabSDK aidlabSDK;
	        private string deviceNameToConnect;      
	        private bool isConnected = false;  
	        private string lastError = "";

	        private readonly Queue<BLEApi.BLEData> rxQueue = new Queue<BLEApi.BLEData>();
	        private int droppedRxPackets;

	        private readonly Queue<byte[]> writeQueue = new Queue<byte[]>();
	        private readonly AutoResetEvent writeSignal = new AutoResetEvent(false);
	        private Thread writerThread;
	        private volatile bool writerRunning = true;
	        private const int DefaultChunkSize = 20;
	        private const int DefaultChunkSizeV4 = 244;
	        private const int MaxChunkSize = 512;
	        private const int DefaultInterChunkDelayMs = 5;
	        private volatile int cachedFirmwareMajor;
	        private volatile int currentV4ChunkSize = DefaultChunkSizeV4;

        public BLEStatus CurrentStatus => currentBLEStatus;
        public bool IsConnected => isConnected;
        public string LastError => lastError ?? "";
        public string ConnectedDeviceId => foundDevice ?? "";

        public List<BLEApi.DeviceUpdate> GetKnownDevicesSnapshot()
        {
            lock (devicesLock)
            {
                return devices.Values.Select(BLEApi.DeviceUpdateCopy).ToList();
            }
        }

        #region MainMethods
	        public BLEConnector(AidlabSDK aidlabSDK, string deviceNameToConnect)
	        {
                      
            this.aidlabSDK = aidlabSDK;
            this.deviceNameToConnect = deviceNameToConnect;
            devices = new Dictionary<string, BLEApi.DeviceUpdate>();

	            bleConnectorThread = new Thread(StartDevicesScan);
	            bleConnectorThread.Start();
	            currentBLEStatus = BLEStatus.ScanningDevices;

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

        public void ConnectionProcess()
        {
            switch (currentBLEStatus)
            {
                case BLEStatus.None:
                    Debug.Log("Something went wrong...");
                    break;
                case BLEStatus.ScanningDevices:
                    if (!bleConnectorThread.IsAlive && !string.IsNullOrEmpty(foundDevice))
                    {
                        currentBLEStatus = BLEStatus.TryingToConnect;
                        bleConnectorThread = new Thread(Connect);
                        bleConnectorThread.Start();
                    }
                    break;
	                case BLEStatus.TryingToConnect:
	                    if (!bleConnectorThread.IsAlive && isConnected)
	                    {
	                        currentBLEStatus = BLEStatus.Connected;
	                        aidlabSDK.OnAidlabConnected();

	                        bleConnectorThread = new Thread(ReceiveData);
	                        bleConnectorThread.Start();
	                    }
	                    break;
            }

	            FlushRxQueue();

            BLEApi.ErrorMessage res = new BLEApi.ErrorMessage();
            BLEApi.GetError(out res);
            if (lastError != res.msg) 
            {
                lastError = res.msg;
                Debug.Log("BleDll: " + res.msg);
            }
        }

        public byte[] AddByteToArray(byte[] bArray, byte newByte)
        {
            byte[] newArray = new byte[bArray.Length + 1];
            bArray.CopyTo(newArray, 0);
            newArray[bArray.Length] = newByte;
            return newArray;
        }

        public void Disconnect()
        {
            isConnected = false;
            cachedFirmwareMajor = 0;
            currentV4ChunkSize = DefaultChunkSizeV4;
            if (bleConnectorThread != null)
                bleConnectorThread.Abort();
            BLEApi.Quit();
            aidlabSDK.OnAidlabDisconnected();
            
            bleConnectorThread = new Thread(StartDevicesScan);
            bleConnectorThread.Start();
            currentBLEStatus = BLEStatus.ScanningDevices;

        }
        #endregion MainMethods


        #region ScanningDevices
        private void StartDevicesScan()
        {
            Debug.Log("Start scanning devices");

            while (true)
            {
                BLEApi.StartDeviceScan();
                BLEApi.DeviceUpdate device = new BLEApi.DeviceUpdate();

                while (BLEApi.PollDevice(ref device, true) != BLEApi.ScanStatus.FINISHED)
                {
                    UpdateDevices(device);
                    foundDevice = CheckDevices();
                    if (!string.IsNullOrEmpty(foundDevice))
                    {
                        Debug.Log("Device found: " + foundDevice);

                        BLEApi.StopDeviceScan();
                        Thread.Sleep(256);
                        return;
                    }
                }

                BLEApi.StopDeviceScan();
                Debug.Log("Devices scanning complete");
                Thread.Sleep(500);
                Debug.Log("Start scanning devices again");
            }
        }


        private void UpdateDevices(BLEApi.DeviceUpdate device)
        {
            lock (devicesLock)
            {
                if (!devices.ContainsKey(device.id))
                {
                    var newDevice = BLEApi.DeviceUpdateCopy(device);
                    devices.Add(device.id, newDevice);
                }
                else
                {
                    var savedDevice = devices[device.id];
                    devices.Remove(device.id);

                    var newDevice = BLEApi.DeviceUpdateCopy(savedDevice);
                    if (device.nameUpdated)
                    {
                        newDevice.name = device.name;
                        newDevice.nameUpdated = true;
                    }
                    if (device.isConnectableUpdated)
                    {
                        newDevice.isConnectable = device.isConnectable;
                        newDevice.isConnectableUpdated = true;
                    }
                    devices.Add(device.id, newDevice);
                }
            }
        }

        private string CheckDevices()
        {
            lock (devicesLock)
            {
                foreach (var device in devices.Values)
                {
                    if (device.name.ToLower().Equals(deviceNameToConnect.ToLower()) && device.isConnectable)
                    {
                        return device.id;
                    }
                }
            }

            return null;
        }
        #endregion ScanningDevices


        #region Connecting
        private void Connect()
        {
            Thread[] characteristicsThreads = new Thread[AidlabCharacteristicsUUID.Characteristics.Length];

            int i = 0;
            foreach (var characteristic in AidlabCharacteristicsUUID.Characteristics)
            {
                characteristicsThreads[i] = new Thread(() => ConnectToCharacteristic(characteristic));
                characteristicsThreads[i].Start();
                characteristicsThreads[i].Join();
                i++;
                Thread.Sleep(128);
            }

            string fw = Read(AidlabCharacteristicsUUID.FirmwareUUID);
            string hw = Read(AidlabCharacteristicsUUID.HardwareUUID);

            aidlabSDK.firmwareRevisionStr = fw;
            aidlabSDK.hardwareRevisionStr = hw;
            cachedFirmwareMajor = ParseMajorFirmware(fw);
            currentV4ChunkSize = DefaultChunkSizeV4;

            Debug.Log("Connection completed");
            Debug.Log("FW: " + fw);
            Debug.Log("HW: " + hw);
            
            isConnected = true;
        }
  
	        public void Write(byte[] payload)
	        {
	            if (payload == null || payload.Length == 0)
	                return;

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

	        public void RawWrite(byte[] payload) 
	        {
	            RawWriteInternal(payload);
	        }

	        private bool RawWriteInternal(byte[] payload)
	        {
	            if (payload == null || payload.Length == 0)
	                return true;
	            if (payload.Length > MaxChunkSize)
	                return false;

	            BLEApi.BLEData data = new BLEApi.BLEData();
	            data.buf = new byte[MaxChunkSize];
	            data.size = (short)payload.Length;
	            data.deviceId = foundDevice;
	            data.serviceUuid = AidlabCharacteristicsUUID.CmdUUID.serviceUuid;
	            data.characteristicUuid = AidlabCharacteristicsUUID.CmdUUID.characteristicUuid;
	            for (int i = 0; i < payload.Length; i++)
	                data.buf[i] = payload[i];

	            return BLEApi.SendData(in data, false);
	        }

	        private static int ParseMajorFirmware(string firmwareRevision)
	        {
	            if (string.IsNullOrEmpty(firmwareRevision))
	                return 0;

	            int nul = firmwareRevision.IndexOf('\0');
	            if (nul >= 0)
	                firmwareRevision = firmwareRevision.Substring(0, nul);

	            int dash = firmwareRevision.IndexOf('-');
	            if (dash >= 0)
	                firmwareRevision = firmwareRevision.Substring(0, dash);

	            firmwareRevision = firmwareRevision.Trim();
	            if (firmwareRevision.Length == 0)
	                return 0;

	            int dot = firmwareRevision.IndexOf('.');
	            string majorStr = dot >= 0 ? firmwareRevision.Substring(0, dot) : firmwareRevision;
	            if (int.TryParse(majorStr, out int major))
	                return major;
	            return 0;
	        }

        public String Read(FullCharacteristic characteristic)
        {

            BLEApi.ReadDataResult result;
            BLEApi.BLEData data = new BLEApi.BLEData();
            data.deviceId = foundDevice;
            data.serviceUuid = characteristic.serviceUuid;
            data.characteristicUuid = characteristic.characteristicUuid;

            BLEApi.ReadData(in data, out result);

            String value = "";

            if (result.result) {
                byte[] buf = result.buf.Reverse().SkipWhile(x => x == 0).Reverse().ToArray();
                value = System.Text.Encoding.UTF8.GetString(buf);
            }

            return String.Concat(value.Where(c => !Char.IsWhiteSpace(c)));
        }
  
        private void ConnectToCharacteristic(FullCharacteristic fullCharacteristic)
        {
            bool result = BLEApi.SubscribeCharacteristic(foundDevice,
                    fullCharacteristic.serviceUuid,
                    fullCharacteristic.characteristicUuid,
                    true);
        }
        #endregion Connecting

        #region ReceiveData
        private void ReceiveData()
        {

            while (true)
            {
                RawReceiveData();
            }
        }

	        private void RawReceiveData()
	        {

            BLEApi.BLEData BLEData;

            bool result = BLEApi.PollData(out BLEData, true);

	            if (result)
	            {
	                lock (rxQueue)
	                {
	                    if (rxQueue.Count >= 8192)
	                    {
	                        rxQueue.Dequeue();
	                        droppedRxPackets++;
	                    }
	                    rxQueue.Enqueue(BLEData);
	                }
	            }
	        }
	        #endregion ReceiveData

	        private void FlushRxQueue()
	        {
	            int processed = 0;
	            const int maxPerFrame = 4096;

	            while (processed < maxPerFrame)
	            {
	                BLEApi.BLEData data;
	                bool hasItem;
	                lock (rxQueue)
	                {
	                    hasItem = rxQueue.Count > 0;
	                    data = hasItem ? rxQueue.Dequeue() : default;
	                }

	                if (!hasItem)
	                    break;

	                aidlabSDK.OnAidlabDataReceived(data);
	                processed++;
	            }

	            if (droppedRxPackets > 0)
	            {
	                Debug.LogWarning($"BLEConnector: dropped {droppedRxPackets} RX packets (main thread couldn't keep up)");
	                droppedRxPackets = 0;
	            }
	            if (processed >= maxPerFrame)
	            {
	                Debug.LogWarning($"BLEConnector: RX backlog too large; processed {processed} packets this frame");
	            }
	        }

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

	        private void SendChunked(byte[] payload, int interChunkDelayMs)
	        {
	            int major = cachedFirmwareMajor;
	            if (major == 0)
	            {
	                major = ParseMajorFirmware(aidlabSDK?.firmwareRevisionStr);
	                if (major != 0)
	                    cachedFirmwareMajor = major;
	            }

	            int chunkSize = DefaultChunkSize;
	            if (major >= 4)
	                chunkSize = Math.Max(DefaultChunkSize, Math.Min(currentV4ChunkSize, MaxChunkSize));

	            int offset = 0;
	            while (offset < payload.Length)
	            {
	                int remaining = payload.Length - offset;
	                int size = Math.Min(chunkSize, remaining);

	                while (true)
	                {
	                    byte[] chunk = new byte[size];
	                    Buffer.BlockCopy(payload, offset, chunk, 0, size);
	                    bool ok = RawWriteInternal(chunk);
	                    if (ok)
	                    {
	                        offset += size;
	                        break;
	                    }

	                    if (major < 4 || size <= DefaultChunkSize)
	                    {
	                        Debug.LogWarning($"BLEConnector: SendData failed (chunk {size}B)");
	                        return;
	                    }

	                    size = Math.Max(DefaultChunkSize, size / 2);
	                    chunkSize = size;
	                    currentV4ChunkSize = chunkSize;
	                }

	                if (offset < payload.Length && interChunkDelayMs > 0)
	                    Thread.Sleep(interChunkDelayMs);
	            }
	        }
	    }
	}
