#include <string.h>
#include <Esp.h>
#include <stdint.h>

#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>

#include "bluetooth.h"
#include "storage.h"
#include "config.h"
#include "temperature.h"
#include "main.h"

constexpr size_t bluetoothMaxDataSize = 200;

constexpr uint16_t DEVINFO_UUID = 0x180a;
constexpr uint16_t DEVINFO_MANUFACTURER_UUID = 0x2a29;
constexpr uint16_t DEVINFO_NAME_UUID = 0x2a24;
constexpr uint16_t DEVINFO_SERIAL_UUID = 0x2a25;

const char* deviceName = "TempTrack";
const char* manufacturerName = "CH";
const char* serviceUUID = "22071991-cccc-cccc-cccc-000000000001";
const char* characteristicUUID = "22071991-cccc-cccc-cccc-000000000002";

bool isConnected = false;
uint8_t bluetoothOutgoingBuffer[bluetoothMaxDataSize + 1];
uint8_t* bluetoothDataBuffer;
uint8_t* bluetoothResponse;
size_t bluetoothDataCount = 0;

void bluetoothStartAdvertising();
void bluetoothDidReceiveData(uint8_t* buffer, uint16_t count);

class BluetoothConnection: public BLEServerCallbacks {

public:
    BluetoothConnection() {}

    void onConnect(BLEServer *server) override {
        isConnected = true;
        Serial.println("Connected");
    }

    void onDisconnect(BLEServer *server) override {
        isConnected = false;
        bluetoothStartAdvertising();
        Serial.println("Disconnected");
    }
};

class Receiver : public BLECharacteristicCallbacks {
    
public:
    Receiver() {}

    void onWrite(BLECharacteristic *characteristic) {
        size_t bytesReceived = characteristic->getLength();
        uint8_t* data = characteristic->getData();
        bluetoothDidReceiveData(data, bytesReceived);
    }

    void onRead(BLECharacteristic *characteristic) {
        characteristic->setValue(bluetoothOutgoingBuffer, bluetoothDataCount + 1); // Add 1 byte for response
    }
};

BluetoothConnection bluetooth{};
BLE2902 descriptor{};
BLEServer *server;
Receiver receiver{};

bool bluetoothIsConfigured = false;

void bluetoothConfigure() {
    if (bluetoothIsConfigured) {
        return;
    }
    bluetoothIsConfigured = true;

    bluetoothDataBuffer = bluetoothOutgoingBuffer + 1;
    bluetoothResponse = bluetoothOutgoingBuffer;

    // Setup BLE Server
    BLEDevice::init(deviceName);
    server = BLEDevice::createServer();
    server->setCallbacks(&bluetooth);
    // Register message service that can receive messages and reply with a static message.
    BLEService *service = server->createService(serviceUUID);
    uint32_t properties = BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_WRITE;
    BLECharacteristic* characteristicMessage = service->createCharacteristic(characteristicUUID, properties);
    characteristicMessage->setCallbacks(&receiver);
    characteristicMessage->addDescriptor(&descriptor);
    service->start();

    // Register device info service, that contains the device's UUID, manufacturer and name.
    service = server->createService(DEVINFO_UUID);
    BLECharacteristic *characteristic = service->createCharacteristic(DEVINFO_MANUFACTURER_UUID, BLECharacteristic::PROPERTY_READ);
    characteristic->setValue(manufacturerName);
    characteristic = service->createCharacteristic(DEVINFO_NAME_UUID, BLECharacteristic::PROPERTY_READ);
    characteristic->setValue(deviceName);
    characteristic = service->createCharacteristic(DEVINFO_SERIAL_UUID, BLECharacteristic::PROPERTY_READ);

    uint32_t id = ESP.getEfuseMac() >> 24;
    char chipId[9] = {0};
    sprintf(chipId, "%x", id);

    characteristic->setValue(chipId);
    service->start();

    bluetoothStartAdvertising();
}

void bluetoothStop() {
    BLEDevice::deinit();
    esp_bt_controller_disable();
}

void bluetoothStartAdvertising() {
    // Advertise services
    BLEAdvertising *advertisement = server->getAdvertising();
    BLEAdvertisementData adv;
    adv.setName(deviceName);
    adv.setCompleteServices(BLEUUID(serviceUUID));
    advertisement->setAdvertisementData(adv);
    advertisement->start();
}

bool bluetoothIsConnected() {
    return isConnected;
}

enum class BluetoothRequest: uint8_t {

    /**
     * @brief Request the number of bytes already recorded
     * 
     * Request:
     * - No additional bytes expected
     * 
     * Response: 
     * - BluetoothResponse::success
     * - the number of bytes as a uint16_t (2 bytes)
     * - the number of seconds until the next measurement as a uint16_t (2 bytes)
     * - the number of seconds between measurements as a uint16_t (2 bytes)
     * - the number of measurements as a uint16_t (2 bytes)
     * - the number of seconds since power on as a uint32_t (4 bytes)
     */
    getInfo = 0,

    /**
     * @brief Request recording data
     * 
     * Request:
     * - Bytes 1-2: Memory offset (uint16_t)
     * - Bytes 3-4: Number of bytes (uint16_t)
     * 
     * Response: 
     * - BluetoothResponse::success, plus the requested bytes
     * - BluetoothResponse::responseTooLarge if too many bytes are requested
     */
    getRecordingData = 1,

    /**
     * @brief Request deletion of recordings
     * 
     * Request:
     * - Bytes 1-2: Number of bytes to clear (uint16_t)
     * 
     * Response: 
     * - BluetoothResponse::success
     * - BluetoothResponse::invalidNumberOfBytesToDelete, if the number of bytes does not match. 
     *   This may happen when a new temperature recording is performed in between calls
     */
    clearRecordingBuffer = 2,
};

enum class BluetoothResponse: uint8_t {

    /** The response to the last request is provided */
    success = 0,

    /** Invalid command received */
    invalidCommand = 1,

    responseTooLarge = 2,

    unknownCommand = 3,

    invalidNumberOfBytesToDelete = 4,

    /* The request has not been completed yet */
    responseInProgress = 5,
};

void setResponse(BluetoothResponse response) {
    *bluetoothResponse = static_cast<uint8_t>(response);
}

void setResponseWithoutData(BluetoothResponse response) {
    setResponse(response);
    bluetoothDataCount = 0;
}

void bluetoothSetOutgoingData(BluetoothResponse response, const uint8_t* data, size_t count) {
    if (count > bluetoothMaxDataSize) {
        setResponseWithoutData(BluetoothResponse::responseTooLarge);
        return;
    }
    memcpy(bluetoothDataBuffer, data, count);
    bluetoothDataCount = count;
    setResponse(response);
}

void setSuccessResponseWithNumber(uint16_t number) {
    bluetoothSetOutgoingData(BluetoothResponse::success, (uint8_t*) &number, sizeof(uint16_t));
}

void setSuccessResponseWithUInt32(uint32_t number) {
    bluetoothSetOutgoingData(BluetoothResponse::success, (uint8_t*) &number, sizeof(uint32_t));
}

uint16_t readNumberFromReceivedBuffer(uint8_t* buffer) {
    return *((uint16_t*) buffer + 1);
}

void inline addToBluetoothBuffer(const void* data, size_t byteCount) {
    memcpy(bluetoothDataBuffer + bluetoothDataCount, data, byteCount);
    bluetoothDataCount += byteCount;
}

void inline addValueToBluetoothBuffer8(uint8_t value) {
    bluetoothDataBuffer[bluetoothDataCount] = value;
    bluetoothDataCount += sizeof(uint8_t);
}

void inline addValueToBluetoothBuffer16(uint16_t value) {
    addToBluetoothBuffer(&value, sizeof(uint16_t));
}

void inline addValueToBluetoothBuffer32(uint32_t value) {
    addToBluetoothBuffer(&value, sizeof(uint32_t));
}

void inline addSensorToBluetoothBuffer(uint8_t index) {
    // Temperature sensor addresses
    uint8_t* address = getSensorAddress(index);
    addToBluetoothBuffer(address, TEMPERATURE_SENSOR_ADDRESS_SIZE);

    // The last temperatures
    uint8_t tempByte = getLastTemperature(index);
    addValueToBluetoothBuffer8(tempByte);

    // Time since measurement of sensor
    uint16_t timeSeconds = getTimeSinceValidTemperature(index);
    addValueToBluetoothBuffer16(timeSeconds);
}

void fillInfo() {
    uint16_t value16;
    uint32_t value32;
    bluetoothDataCount = 0;

    // The unique ID generated on power-on
    value32 = getUniqueID();
    addValueToBluetoothBuffer32(value32);

    // the number of bytes as a uint16_t (2 bytes)
    value16 = getTotalNumberOfStoredBytes();
    addValueToBluetoothBuffer16(value16);

    // the number of seconds until the next measurement as a uint16_t (2 bytes)
    value16 = secondsUntilNextTemperatureMeasurement();
    addValueToBluetoothBuffer16(value16);

    // the number of seconds between measurements as a uint16_t (2 bytes)
    value16 = temperatureMeasurementIntervalSeconds;
    addValueToBluetoothBuffer16(value16);

    // the number of measurements as a uint16_t (2 bytes)
    value16 = getNumberOfMeasurements();
    addValueToBluetoothBuffer16(value16);

    // Total number of measurements since start
    value32 = getTotalNumberOfMeasurements();
    addValueToBluetoothBuffer32(value32);

    // the maximum number of bytes that can be copied
    value16 = bluetoothMaxDataSize;
    addValueToBluetoothBuffer16(value16);

    // Total storage size of device
    value16 = rtcStorageSize;
    addValueToBluetoothBuffer16(value16);

    // the number of seconds since power on as a uint32_t (4 bytes)
    value32 = time(NULL);
    addValueToBluetoothBuffer32(value32);

    // Start time of current recording
    value32 = getStartTimeOfCurrentRecording();
    addValueToBluetoothBuffer32(value32);

    // Checksum for all data
    value16 = getDataChecksum();
    addValueToBluetoothBuffer16(value16);

    // The wakeup cause for the current run
    uint8_t wakeCause = getWakeupCause();
    addValueToBluetoothBuffer8(wakeCause);

    // Sensor 0
    addSensorToBluetoothBuffer(0);
    addSensorToBluetoothBuffer(1);

    setResponse(BluetoothResponse::success);
}

void getRecordingData(uint8_t* buffer, uint16_t count) {
    if (count != 2 * sizeof(uint16_t)) {
        setResponseWithoutData(BluetoothResponse::invalidCommand);
        return;
    }
    uint16_t byteCount;
    memcpy(&byteCount, buffer, sizeof(uint16_t));
    if (byteCount > bluetoothMaxDataSize) {
        setResponseWithoutData(BluetoothResponse::responseTooLarge);
        return;
    }

    uint16_t byteOffset;
    memcpy(&byteOffset, buffer + sizeof(uint16_t), sizeof(uint16_t));
    
    bluetoothDataCount = getRecordedBytesAtOffset(bluetoothDataBuffer, byteOffset, byteCount);
    setResponse(BluetoothResponse::success);
}

void clearRecordingBuffer(uint8_t* buffer, uint16_t count) {
    if (count != sizeof(uint16_t)) {
        setResponseWithoutData(BluetoothResponse::invalidCommand);
        return;
    }
    uint16_t byteCount;
    memcpy(&byteCount, buffer, sizeof(uint16_t));
    if (byteCount != getTotalNumberOfStoredBytes()) {
        setResponseWithoutData(BluetoothResponse::invalidNumberOfBytesToDelete);
        return;
    }

    discardAllRecordedBytes();
    setResponseWithoutData(BluetoothResponse::success);
}

void bluetoothDidReceiveData(uint8_t* buffer, uint16_t count) {
    if (count < 1) {
        setResponseWithoutData(BluetoothResponse::invalidCommand);
        return;
    }
    setResponseWithoutData(BluetoothResponse::responseInProgress);

    BluetoothRequest request = static_cast<BluetoothRequest>(buffer[0]);
    uint8_t* payload = buffer + sizeof(uint8_t);
    uint16_t payloadSize = count - sizeof(uint8_t);
    switch (request) {
    case BluetoothRequest::getInfo:
        fillInfo();
        break;

    case BluetoothRequest::clearRecordingBuffer:
        clearRecordingBuffer(payload, payloadSize);
        break;

    case BluetoothRequest::getRecordingData:
        getRecordingData(payload, payloadSize);
        break;

    default:
        setResponseWithoutData(BluetoothResponse::unknownCommand);
        break;
    }
}