#include "SoilMoisture.h"
#include <bitset>

namespace trygvis {
namespace sensor {

#define BLUETOOTH_UUID_INITIALIZER \
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb };

auto trygvis_io_base_uuid =
    boost::uuids::uuid{0x32, 0xd0, 0x00, 0x00, 0x03, 0x5d, 0x59, 0xc5, 0x70, 0xd3, 0xbc, 0x8e, 0x4a, 0x1f, 0xd8, 0x3f};

auto bluetooth_base_uuid =
    boost::uuids::uuid{0x00, 0x00, 0x18, 0x0f, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb};

const boost::uuids::uuid soil_moisture_service = makeUuid(trygvis_io_base_uuid, 0x00, 0x10);
const boost::uuids::uuid soil_moisture_characteristic = makeUuid(trygvis_io_base_uuid, 0x00, 0x11);
const boost::uuids::uuid temperature_characteristic = makeUuid(bluetooth_base_uuid, 0x2a, 0x1e);
const boost::uuids::uuid light_characteristic = makeUuid(trygvis_io_base_uuid, 0x00, 0x12);

using namespace trygvis::bluetooth;

static
ByteBuffer createGetSensorCount() {
    return ByteBuffer::alloc(100) //
        .write8(static_cast<uint8_t>(sm_cmd_code::SM_CMD_GET_SENSOR_COUNT));
}

static
ByteBuffer createGetValue(uint8_t sensor) {
    return ByteBuffer::alloc(100) //
        .write8(static_cast<uint8_t>(sm_cmd_code::SM_CMD_GET_VALUE)) //
        .write16le(sensor);
}

static
ByteBuffer createSetWarningValue(uint8_t sensor, uint16_t warning_value) {
    return ByteBuffer::alloc(100)
        .write8(static_cast<uint8_t>(sm_cmd_code::SM_CMD_SET_WARNING_VALUE))
        .write8(sensor)
        .write16le(warning_value);
}

static
ByteBuffer createSetSensorName(uint8_t sensor, string name) {
    return ByteBuffer::alloc(100)
        .write8(static_cast<uint8_t>(sm_cmd_code::SM_CMD_SET_SENSOR_NAME))
        .write8(sensor)
        .write(reinterpret_cast<const uint8_t *>(name.c_str()), name.length());
}

static
ByteBuffer createGetSensorName(uint8_t sensor) {
    return ByteBuffer::alloc(100).write8(static_cast<uint8_t>(sm_cmd_code::SM_CMD_GET_SENSOR_NAME)).write8(sensor);
}

static
ByteBuffer createSetUpdateInterval(uint8_t sensor, uint8_t interval_in_seconds) {
    return ByteBuffer::alloc(100)
        .write8(static_cast<uint8_t>(sm_cmd_code::SM_CMD_SET_UPDATE_INTERVAL))
        .write8(sensor)
        .write8(interval_in_seconds);
}

SoilMoisture SoilMoisture::create(shared_ptr<BluetoothGatt> gatt) {
    gatt->discoverServices();

    o<shared_ptr<BluetoothGattService>> s = gatt->findService(soil_moisture_service);

    if (!s) {
        throw runtime_error("The device is missing the soil moisture service");
    }

    shared_ptr<BluetoothGattService> &service = *s;

    o<shared_ptr<BluetoothGattCharacteristic>> c = service->findCharacteristic(soil_moisture_characteristic);

    if (!c) {
        throw runtime_error("The device is missing the soil moisture characteristic");
    }

    auto temperature = service->findCharacteristic(temperature_characteristic);

    auto light = service->findCharacteristic(light_characteristic);

    return SoilMoisture(gatt, service, c.operator*(), temperature, light);
}

SoilMoisture::SoilMoisture(const shared_ptr<BluetoothGatt> &gatt,
                           const shared_ptr<BluetoothGattService> &s,
                           const BluetoothGattCharacteristicPtr &soilMoistureCharacteristic,
                           const o<BluetoothGattCharacteristicPtr> temperatureCharacteristic,
                           const o<BluetoothGattCharacteristicPtr> lightCharacteristic)
    : gatt(gatt), s(s), soilMoistureCharacteristic(soilMoistureCharacteristic),
      temperatureCharacteristic(temperatureCharacteristic), lightCharacteristic(lightCharacteristic) { }

ByteBuffer SoilMoisture::writeAndRead(const BluetoothGattCharacteristicPtr &c, ByteBuffer &requestBytes) {
    requestBytes.setCursor(0);

    uint8_t expectedCode = requestBytes.get8(0);

    gatt->writeValue(c, requestBytes);

    auto responseBytes = gatt->readValue(c);

    if (responseBytes.getSize() < 1) {
        throw runtime_error("Unexpected number of bytes read: " + to_string(responseBytes.getSize()));
    }

    uint8_t actualCode = responseBytes.read8();
    if (actualCode != expectedCode) {
        throw runtime_error("Unexpected response code: " + to_string(actualCode) + ", expected " +
                            to_string(expectedCode));
    }

    return responseBytes;
}

uint8_t SoilMoisture::getSensorCount() {
    auto buffer = createGetSensorCount();
    return writeAndRead(soilMoistureCharacteristic, buffer).read8();
}

uint16_t SoilMoisture::getValue(uint8_t sensor) {
    auto req = createGetValue(sensor);

    auto buffer = writeAndRead(soilMoistureCharacteristic, req);
    buffer.read8(); // sensor index
    return buffer.read16le();
}

string SoilMoisture::getName(uint8_t sensor) {
    auto req = createGetSensorName(sensor);

    auto buffer = writeAndRead(soilMoistureCharacteristic, req);
    size_t bytesLeft = buffer.getBytesLeft();
    uint8_t bytes[bytesLeft];
    buffer.copy(bytes, bytesLeft);

    if (bytesLeft == 0 || bytesLeft < (bytes[0] + 1)) {
        throw runtime_error("Bad response from device. buffer size: " + to_string(bytesLeft) + ", buffer[0]=" +
                            to_string((int) bytes[0]));
    }

    return string((const char *) &bytes[1], bytes[0]);
}

bool SoilMoisture::hasTemperatureSensor() {
    return (bool) temperatureCharacteristic;
}

/**
 * https://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.intermediate_temperature.xml
 */
o<double> SoilMoisture::readTemperature() {
    if (!temperatureCharacteristic) {
        return o<double>();
    }

    auto responseBytes = gatt->readValue(*temperatureCharacteristic);

    if (responseBytes.getSize() < 2) {
        throw runtime_error("Unexpected number of bytes read: " + to_string(responseBytes.getSize()));
    }

    bitset<8> b0 = responseBytes.read8();

    bool is_fahrenheit = b0.test(0);
    bool has_timestamp = b0.test(1);
    bool has_type = b0.test(2);

    double temperature = responseBytes.readFLOAT();

    return o<double>(temperature);
}

void SoilMoisture::setLight(uint8_t light, uint8_t value) {
    if (!lightCharacteristic) {
        return;
    }

    auto responseBytes = gatt->readValue(*lightCharacteristic);

    if (responseBytes.getSize() < 2) {
        throw runtime_error("Unexpected number of bytes read: " + to_string(responseBytes.getSize()));
    }

    bitset<8> b0 = responseBytes.read8();
}

}
}