package com.adafruit.bluefruit.le.connect.ble.central;

import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import android.os.ParcelUuid;
import android.util.Log;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import com.adafruit.bluefruit.le.connect.ble.BleUtils;

import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

public class BlePeripheralUart {
    // Log
    private final static String TAG = BlePeripheralUart.class.getSimpleName();

    // Constants
    private static final UUID kUartServiceUUID = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
    private static final UUID kUartTxCharacteristicUUID = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
    private static final UUID kUartRxCharacteristicUUID = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");

    //private static final int kUartTxMaxBytes = 20;
    private static final int kUartReplyDefaultTimeout = 2000;       // in millis

    // Interfaces
    public interface UartRxHandler {
        void onRxDataReceived(@NonNull byte[] data, @Nullable String identifier, int status);
    }

    // Data
    @NonNull
    private BlePeripheral mBlePeripheral;
    private BluetoothGattCharacteristic mUartTxCharacteristic;
    private BluetoothGattCharacteristic mUartRxCharacteristic;
    private int mUartTxCharacteristicWriteType;
    private boolean mIsSendSequentiallyCancelled = false;

    // region Initialization
    public BlePeripheralUart(@NonNull BlePeripheral blePeripheral) {
        super();
        mBlePeripheral = blePeripheral;
    }

    public void uartEnable(@Nullable UartRxHandler uartRxHandler, @Nullable BlePeripheral.CompletionHandler completionHandler) {

        // Get uart communications characteristic
        mUartTxCharacteristic = mBlePeripheral.getCharacteristic(kUartTxCharacteristicUUID, kUartServiceUUID);
        mUartRxCharacteristic = mBlePeripheral.getCharacteristic(kUartRxCharacteristicUUID, kUartServiceUUID);
        if (mUartTxCharacteristic != null && mUartRxCharacteristic != null) {
            Log.d(TAG, "Uart Enable for: " + getName());

            mUartTxCharacteristicWriteType = mUartTxCharacteristic.getWriteType();

            // Prepare notification handler
            WeakReference<UartRxHandler> weakUartRxHandler = new WeakReference<>(uartRxHandler);
            final String identifier = mBlePeripheral.getIdentifier();
            BlePeripheral.NotifyHandler notifyHandler = uartRxHandler == null ? null : status -> {
                UartRxHandler handler = weakUartRxHandler.get();
                if (handler != null) {
                    byte[] data = mUartRxCharacteristic.getValue();
                    handler.onRxDataReceived(data, identifier, status);
                }
            };

            // Check if already notifying (read client characteristic config descriptor to check it)
            mBlePeripheral.readDescriptor(mUartRxCharacteristic, BlePeripheral.kClientCharacteristicConfigUUID, status -> {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    // Enable notifications
                    if (!BlePeripheral.isCharacteristicNotifyingForCachedClientConfigDescriptor(mUartRxCharacteristic)) {
                        mBlePeripheral.characteristicEnableNotify(mUartRxCharacteristic, notifyHandler, completionHandler);
                    } else {
                        mBlePeripheral.characteristicUpdateNotify(mUartRxCharacteristic, notifyHandler);
                        if (completionHandler != null) {
                            completionHandler.completion(BluetoothGatt.GATT_SUCCESS);
                        }
                    }
                } else {
                    if (completionHandler != null) {
                        completionHandler.completion(status);
                    }
                }
            });
        }
    }

    public boolean isUartEnabled() {
        return mUartRxCharacteristic != null && mUartTxCharacteristic != null && BlePeripheral.isCharacteristicNotifyingForCachedClientConfigDescriptor(mUartRxCharacteristic);
    }

    public void uartDisable() {
        // Disable notify
        if (mUartRxCharacteristic != null) {
            Log.d(TAG, "Uart Disable");
            mBlePeripheral.characteristicDisableNotify(mUartRxCharacteristic, null);
        }

        // Clear all Uart specific data
        mUartRxCharacteristic = null;
        mUartTxCharacteristic = null;
    }

    public void disconnect() {
        mBlePeripheral.disconnect();
    }

    public String getIdentifier() {
        return mBlePeripheral.getIdentifier();
    }

    public String getName() {
        return mBlePeripheral.getName();
    }

    public void requestMtu(@IntRange(from = 23, to = 517) int mtuSize, BlePeripheral.CompletionHandler completionHandler) {
        mBlePeripheral.requestMtu(mtuSize, completionHandler);
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    public void readPhy() {
        mBlePeripheral.readPhy();
    }

    // endregion

    // region Send
    void uartSend(@NonNull byte[] data, @Nullable BlePeripheral.CompletionHandler completionHandler) {
        if (mUartTxCharacteristic == null) {
            Log.e(TAG, "Command Error: characteristic no longer valid");
            if (completionHandler != null) {
                completionHandler.completion(BluetoothGatt.GATT_FAILURE);
            }
            return;
        }

        // Split data in kUartTxMaxBytes bytes packets
        int offset = 0;
        AtomicInteger worseStatus = new AtomicInteger(BluetoothGatt.GATT_SUCCESS);

        do {
            final int packetSize = Math.min(data.length - offset, mBlePeripheral.getMaxPacketLength());
            final byte[] packet = Arrays.copyOfRange(data, offset, offset + packetSize);
            offset += packetSize;

            final int finalOffset = offset;
            mBlePeripheral.writeCharacteristic(mUartTxCharacteristic, mUartTxCharacteristicWriteType, packet, status -> {
                //Log.d(TAG, "next offset:"+finalOffset);
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.d(TAG, "uart tx write (hex): " + BleUtils.bytesToHex2(packet));
                } else {
                    worseStatus.set(status);
                    Log.w(TAG, "Error " + status + " writing packet");
                }

                if (finalOffset >= data.length && completionHandler != null) {
                    completionHandler.completion(worseStatus.get());
                }
            });

        } while (offset < data.length);
    }

    void sendEachPacketSequentially(@NonNull byte[] data, int withResponseEveryPacketCount, BlePeripheral.ProgressHandler progressHandler, BlePeripheral.CompletionHandler completionHandler) {
        if (mUartTxCharacteristic == null) {
            Log.e(TAG, "Command Error: characteristic no longer valid");
            if (completionHandler != null) {
                completionHandler.completion(BluetoothGatt.GATT_FAILURE);
            }
            return;
        }

        mIsSendSequentiallyCancelled = false;

        uartSendPacket(data, 0, mUartTxCharacteristic, withResponseEveryPacketCount, withResponseEveryPacketCount, progressHandler, completionHandler);
    }

    void cancelOngoingSendPacketSequentiallyInThread() {
        mIsSendSequentiallyCancelled = true;
    }

    private void uartSendPacket(@NonNull byte[] data, int offset, BluetoothGattCharacteristic uartTxCharacteristic, int withResponseEveryPacketCount, int numPacketsRemainingForDelay, BlePeripheral.ProgressHandler progressHandler, BlePeripheral.CompletionHandler completionHandler) {
        final int packetSize = Math.min(data.length - offset, mBlePeripheral.getMaxPacketLength());
        final byte[] packet = Arrays.copyOfRange(data, offset, offset + packetSize);
        final int writeStartingOffset = offset;
        final int uartTxCharacteristicWriteType = numPacketsRemainingForDelay <= 0 ? BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT : BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE;          // Send a packet WRITE_TYPE_DEFAULT to force wait until receive response and avoid dropping packets if the peripheral is not processing them fast enough

        mBlePeripheral.writeCharacteristic(uartTxCharacteristic, uartTxCharacteristicWriteType, packet, status -> {
            int writtenSize = writeStartingOffset;

            if (status != BluetoothGatt.GATT_SUCCESS) {
                Log.w(TAG, "Error " + status + " writing packet at offset" + writeStartingOffset + " Error: " + status);
            } else {
                Log.d(TAG, "uart tx " + (uartTxCharacteristicWriteType == BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE ? "withoutResponse" : "withResponse") + " offset " + writeStartingOffset + ": " + BleUtils.bytesToHex2(packet));

                writtenSize += packet.length;

                if (!mIsSendSequentiallyCancelled && writtenSize < data.length) {
                    //int finalWrittenSize = writtenSize;
                    //handler.postDelayed(() -> uartSendPacket(handler, data, finalWrittenSize, uartTxCharacteristic, uartTxCharacteristicWriteType, delayBetweenPackets, progressHandler, completionHandler), delayBetweenPackets);
                    uartSendPacket(data, writtenSize, uartTxCharacteristic, withResponseEveryPacketCount, numPacketsRemainingForDelay <= 0 ? withResponseEveryPacketCount : numPacketsRemainingForDelay - 1, progressHandler, completionHandler);
                }
            }

            if (mIsSendSequentiallyCancelled) {
                completionHandler.completion(BluetoothGatt.GATT_SUCCESS);
            } else if (writtenSize >= data.length) {
                progressHandler.progress(1);
                completionHandler.completion(status);
            } else {
                progressHandler.progress(writtenSize / (float) data.length);
            }
        });
    }

    @SuppressWarnings("SameParameterValue")
    void uartSendAndWaitReply(@NonNull byte[] data, @Nullable BlePeripheral.CompletionHandler writeCompletionHandler, @NonNull BlePeripheral.CaptureReadCompletionHandler readCompletionHandler) {
        uartSendAndWaitReply(data, writeCompletionHandler, kUartReplyDefaultTimeout, readCompletionHandler);
    }

    void uartSendAndWaitReply(@NonNull byte[] data, @Nullable BlePeripheral.CompletionHandler writeCompletionHandler, int readTimeout, @NonNull BlePeripheral.CaptureReadCompletionHandler readCompletionHandler) {
        if (mUartTxCharacteristic == null || mUartRxCharacteristic == null) {
            Log.e(TAG, "Error: uart characteristics no longer valid");
            if (writeCompletionHandler != null) {
                writeCompletionHandler.completion(BluetoothGatt.GATT_FAILURE);
            } else {  // If no writeCompletion defined, move the error result to the readCompletion
                readCompletionHandler.read(BluetoothGatt.GATT_FAILURE, null);
            }
            return;
        }

        // Split data in kUartTxMaxBytes bytes packets
        int offset = 0;
        do {
            final int packetSize = Math.min(data.length - offset, mBlePeripheral.getMaxPacketLength());
            final byte[] packet = Arrays.copyOfRange(data, offset, offset + packetSize);
            offset += packetSize;

            final int finalOffset = offset;
            mBlePeripheral.writeCharacteristicAndCaptureNotify(mUartTxCharacteristic, mUartTxCharacteristicWriteType, packet, status -> {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    Log.d(TAG, "uart tx writeAndWait (hex): " + BleUtils.bytesToHex2(packet));
                } else {
                    Log.w(TAG, "Error " + status + " writing packet");
                }

                if (finalOffset >= data.length && writeCompletionHandler != null) {
                    writeCompletionHandler.completion(status);
                }
            }, mUartRxCharacteristic, readTimeout, readCompletionHandler);
        } while (offset < data.length);
    }

    public static byte[] appendCrc(byte[] data) {
        // Calculate checksum
        byte checksum = 0;
        for (byte aData : data) {
            checksum += aData;
        }
        checksum = (byte) (~checksum);       // Invert

        // Add crc to data
        byte[] dataCrc = new byte[data.length + 1];
        System.arraycopy(data, 0, dataCrc, 0, data.length);
        dataCrc[data.length] = checksum;

        return dataCrc;
    }

    // endregion

    // region Utils
    @SuppressWarnings("unused")
    public static boolean isUartAdvertised(@NonNull BlePeripheral blePeripheral) {
        List<ParcelUuid> serviceUuids = blePeripheral.getScanRecord().getServiceUuids();
        boolean found = false;

        if (serviceUuids != null) {
            int i = 0;
            while (i < serviceUuids.size() && !found) {
                found = serviceUuids.get(i).getUuid().equals(kUartServiceUUID);
                i++;
            }
        }
        return found;
    }

    public static boolean hasUart(@NonNull BlePeripheral peripheral) {
        return peripheral.getService(kUartServiceUUID) != null;
    }

    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
    public static boolean isUartInitialized(@NonNull BlePeripheral blePeripheral, @NonNull List<BlePeripheralUart> blePeripheralUarts) {
        BlePeripheralUart blePeripheralUart = getBlePeripheralUart(blePeripheral, blePeripheralUarts);
        return blePeripheralUart != null && blePeripheralUart.isUartEnabled();
    }

    private @Nullable
    static BlePeripheralUart getBlePeripheralUart(@NonNull BlePeripheral blePeripheral, @NonNull List<BlePeripheralUart> blePeripheralUarts) {

        BlePeripheralUart result = null;

        String identifier = blePeripheral.getIdentifier();
        boolean found = false;
        int i = 0;
        while (i < blePeripheralUarts.size() && !found) {
            BlePeripheralUart blePeripheralUart = blePeripheralUarts.get(i);
            if (blePeripheralUart.getIdentifier().equals(identifier)) {
                found = true;
                result = blePeripheralUart;
            } else {
                i++;
            }
        }
        return result;
    }

    // endregion
}