#include <QDebug>
#include <QEventLoop>
#include <QMutexLocker>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrl>

#include <chrono>
#include <cmath>
#include <thread>

#include <dracal/common/log.hpp>

#include "Config.h"
#include "OriginTimestamp.h"
#include "TDeviceManager.h"
#include "TUSBProvider.h"
#include "USBThread.h"
#include "chip.h"
#include "usbtenki.h"
#include "usbtenki_cmds.h"
#include "usbtenki_provider.h"

static void shift_f(const double coeffs[3], double x0, double x1,
                    double s, double level, double new_coeffs[3])
{
    double a0 = coeffs[0], a1 = coeffs[1], a2 = coeffs[2];
    double delta = x1 - x0;
    new_coeffs[2] = s * a2;
    new_coeffs[1] = s * (a1 - 2.0 * a2 * delta);
    new_coeffs[0] = s * (a2 * delta * delta - a1 * delta + a0) + (1.0 - s) * level;
}

static const Version TICKER_VERSION = {.major = 0, .minor = 0};

static const Quantity TICKER_QUANTITY = {UNIT_UNKNOWN, QUANTITY_TYPE_ERROR, CHIP_ERROR_TICKER_DEVICE};

TUSBProvider::TUSBProvider() : TProvider("USB Devices") {

    USBThread::instance().setPollInterval(pollInterval);
    USBThread::instance().setVirtualOptions(virtualOptions.flags, virtualOptions.standard_sea_level_pressure);

    tickerDevice = new TDevice(this, TDevice::TICKER_SERIAL_NUMBER, TDevice::TICKER_SERIAL_NUMBER, TICKER_VERSION);

    tickerDevice->timestamp = OriginTimestamp::instance().value();
    tickerDevice->flags = (DEVICE_FLAG_ALIVE | DEVICE_FLAG_TICKER);

    tickerDevice->channels.resize(1);
    tickerDevice->channels[0] = new TChannel(tickerDevice, 0, USBTENKI_CHIP_TICKER, TDevice::TICKER_CHANNEL_ID);
    tickerDevice->channels[0]->quantity = TICKER_QUANTITY;
    tickerDevice->channels[0]->calibratedQuantity = TICKER_QUANTITY;

    TDeviceManager::instance().addDevice(tickerDevice);
    TDeviceManager::instance().connectDevice(tickerDevice);
}

TUSBProvider::~TUSBProvider() {
    // Singleton lives forever
}

int TUSBProvider::start() {

    USBThread &thread = USBThread::instance();

    if (!thread.isRunning()) {
        thread.start();
    }

    return 0;
}

void TUSBProvider::shutdown() { USBThread::instance().shutdown(); }

int TUSBProvider::getDeviceCount() {

    QMutexLocker locker(&mutex);

    return devicesBySerialNumber.size();
}

QVector<TDevice *> TUSBProvider::getDevices() {

    QMutexLocker locker(&mutex);

    QVector<TDevice *> results(devicesBySerialNumber.size());
    int i = 0;

    QMapIterator<QString, TDevice *> it(devicesBySerialNumber);
    while (it.hasNext()) {
        it.next();
        results[i++] = it.value();
    }

    return results;
}

TDevice *TUSBProvider::findDevice(const QString &serial) {

    QMutexLocker locker(&mutex);

    return devicesBySerialNumber.value(serial);
}

int TUSBProvider::set_DXC120_ASC(const QString &serialNumber, int value) {

    QMutexLocker locker(&(USBThread::instance()._mutex));

    QByteArray serialNumberByteArray = serialNumber.toLocal8Bit();

    USBTenki_dev_handle handle = usbtenki_provider_find_handle(serialNumberByteArray.data());

    if (!handle) {
        return -1;
    }

    int res = usbtenki_set_dxc120_asc(handle, value);

    usbtenki_closeDevice(handle);
    return res;
}

int TUSBProvider::get_DXC120_ASC(const QString &serialNumber) {

    QMutexLocker locker(&(USBThread::instance()._mutex));

    QByteArray serialNumberByteArray = serialNumber.toLocal8Bit();

    USBTenki_dev_handle handle = usbtenki_provider_find_handle(serialNumberByteArray.data());

    if (!handle) {
        return -1;
    }

    int res = usbtenki_get_dxc120_asc(handle);

    usbtenki_closeDevice(handle);
    return res;
}

int TUSBProvider::set_DXC120_FRC(const QString &serialNumber, int value) {

    QMutexLocker locker(&(USBThread::instance()._mutex));

    QByteArray serialNumberByteArray = serialNumber.toLocal8Bit();

    USBTenki_dev_handle handle = usbtenki_provider_find_handle(serialNumberByteArray.data());

    if (!handle) {
        return -1;
    }

    int res = usbtenki_set_dxc120_frc(handle, value);

    usbtenki_closeDevice(handle);
    return res;
}

int TUSBProvider::set_DXC220_KZ(const QString &serialNumber, int value) {

    QMutexLocker locker(&(USBThread::instance()._mutex));

    QByteArray serialNumberByteArray = serialNumber.toLocal8Bit();

    USBTenki_dev_handle handle = usbtenki_provider_find_handle(serialNumberByteArray.data());

    if (!handle) {
        return -1;
    }

    int res = usbtenki_set_dxc220_kz(handle, value);

    usbtenki_closeDevice(handle);
    return res >= 0;
}

int TUSBProvider::set_DXC300_KS(const QString &serialNumber, int value, int offset) {

    QMutexLocker locker(&(USBThread::instance()._mutex));

    QByteArray serialNumberByteArray = serialNumber.toLocal8Bit();

    USBTenki_dev_handle handle = usbtenki_provider_find_handle(serialNumberByteArray.data());

    if (!handle) {
        dracal_warn("set_DXC300_KS sn={} value={} offset={} res=-1 (no handle)", serialNumber.toStdString(), value, offset);
        return -1;
    }

    int res;
    unsigned char repBuf[32];

    float pre_co2[1];
    float pre_p[4];
    float pre_t[4];
    float pre_rh[4];

    res = 0;
    res = res < 0 ? res : usbtenki_getCalibration(handle, 0, (uint8_t *)pre_co2, sizeof(pre_co2));
    res = res < 0 ? res : usbtenki_getCalibration(handle, 7, (uint8_t *)pre_p, sizeof(pre_p));
    res = res < 0 ? res : usbtenki_getCalibration(handle, 8, (uint8_t *)pre_t, sizeof(pre_t));
    res = res < 0 ? res : usbtenki_getCalibration(handle, 9, (uint8_t *)pre_rh, sizeof(pre_rh));

    if (res < 0) {
        usbtenki_closeDevice(handle);
        dracal_warn("set_DXC300_KS sn={} value={} offset={} res=-1 (pre getCalibration failed)", serialNumber.toStdString(), value, offset);
        return -1;
    }

    uint16_t offset_16 = offset / 10;
    res = usbtenki_setCalibration(handle, 4, (uint8_t *)&offset_16, sizeof(offset_16));
    if (res < 0) {
        usbtenki_command(handle, USBTENKI_ROLLBACK, 0, repBuf, sizeof(repBuf));
        usbtenki_closeDevice(handle);
        dracal_warn("set_DXC300_KS sn={} value={} offset={} res=-2 (setCalibration offset failed)", serialNumber.toStdString(), value, offset);
        return -2;
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    float post_co2[1];
    float post_p[4];
    float post_t[4];
    float post_rh[4];

    res = usbtenki_set_dxc220_kz(handle, value);
    if (res < 0) {
        usbtenki_closeDevice(handle);
        dracal_warn("set_DXC300_KS sn={} value={} offset={} res=-2 (set_dxc220_kz failed)", serialNumber.toStdString(), value, offset);
        return -2;
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(1500));

    res = res < 0 ? res : usbtenki_getCalibration(handle, 0, (uint8_t *)post_co2, sizeof(post_co2));
    res = res < 0 ? res : usbtenki_getCalibration(handle, 7, (uint8_t *)post_p, sizeof(post_p));
    res = res < 0 ? res : usbtenki_getCalibration(handle, 8, (uint8_t *)post_t, sizeof(post_t));
    res = res < 0 ? res : usbtenki_getCalibration(handle, 9, (uint8_t *)post_rh, sizeof(post_rh));

    if (res < 0) {
        usbtenki_closeDevice(handle);
        dracal_warn("set_DXC300_KS sn={} value={} offset={} res=-3 (post getCalibration failed)", serialNumber.toStdString(), value, offset);
        return -3;
    }

    double coeffs[3], new_coeffs[3];
    int n_valid;

    n_valid = 0;
    for (int i = 0; i < 3; i++) {
        coeffs[i] = (double)pre_p[i + 1];
        n_valid += std::isfinite(pre_p[i + 1]) ? 1 : 0;
    }
    if (n_valid == 3) {
        shift_f(coeffs, (double)pre_p[0], (double)post_p[0], 1.0, 1.0, new_coeffs);
        for (int i = 0; i < 3; i++) post_p[i + 1] = (float)new_coeffs[i];
        res = res < 0 ? res : usbtenki_setCalibration(handle, 7, (uint8_t *)post_p, sizeof(post_p));
    }

    n_valid = 0;
    for (int i = 0; i < 3; i++) {
        coeffs[i] = (double)pre_t[i + 1];
        n_valid += std::isfinite(pre_t[i + 1]) ? 1 : 0;
    }
    if (n_valid == 3) {
        shift_f(coeffs, (double)pre_t[0], (double)post_t[0], 1.0, 1.0, new_coeffs);
        for (int i = 0; i < 3; i++) post_t[i + 1] = (float)new_coeffs[i];
        res = res < 0 ? res : usbtenki_setCalibration(handle, 8, (uint8_t *)post_t, sizeof(post_t));
    }

    n_valid = 0;
    for (int i = 0; i < 3; i++) {
        coeffs[i] = (double)pre_rh[i + 1];
        n_valid += std::isfinite(pre_rh[i + 1]) ? 1 : 0;
    }
    if (n_valid == 3) {
        shift_f(coeffs, (double)pre_rh[0], (double)post_rh[0], 1.0, 1.0, new_coeffs);
        for (int i = 0; i < 3; i++) post_rh[i + 1] = (float)new_coeffs[i];
        res = res < 0 ? res : usbtenki_setCalibration(handle, 9, (uint8_t *)post_rh, sizeof(post_rh));
    }

    if (res < 0) {
        usbtenki_command(handle, USBTENKI_ROLLBACK, 0, repBuf, sizeof(repBuf));
        usbtenki_closeDevice(handle);
        dracal_warn("set_DXC300_KS sn={} value={} offset={} res=-4 (setCalibration coeffs failed)", serialNumber.toStdString(), value, offset);
        return -4;
    }

    res = usbtenki_command(handle, USBTENKI_COMMIT, 0, repBuf, sizeof(repBuf));
    usbtenki_closeDevice(handle);
    dracal_info("set_DXC300_KS sn={} value={} offset={} res={}", serialNumber.toStdString(), value, offset, res);
    return res;
}

bool TUSBProvider::isServiceRunning() {
    static const bool running = []() -> bool {
        return false;
        // QNetworkAccessManager manager;
        // const QString port = loadDracalUsbServicePort();
        // const QString url = QString("http://localhost:%1/dracal-service-info").arg(port);
        // QNetworkRequest request{QUrl(url)};

        // QNetworkReply *reply = manager.get(request);

        // // Block until the request is finished
        // QEventLoop loop;
        // QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
        // loop.exec();

        // bool result = (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200);

        // reply->deleteLater();
        // return result;
    }();

    return running;
}

void TUSBProvider::setPollInterval(int ms) { USBThread::instance().setPollInterval(ms); }

void TUSBProvider::setVirtualOptions(unsigned int flags, double slp) {

    USBThread::instance().setVirtualOptions(flags, slp);
}

void TUSBProvider::setUserCalibration(const QString &serialNumber, int channelIndex, bool on) {

    USBThread::instance().setUserCalibration(serialNumber, channelIndex, on);
}

int TUSBProvider::setUserCalibrationPoint(TChannel *channel, int id, double x, double y) {
    QMutexLocker locker(&(USBThread::instance()._mutex));

    TDevice *device = channel->device;

    QByteArray serialNumberByteArray = device->serialNumber.toLocal8Bit();

    USBTenki_dev_handle handle = usbtenki_provider_find_handle(serialNumberByteArray.data());

    if (!handle) {
        return -1;
    }

    Point point = {x, y};
    int res = usbtenki_setUserCalibrationPoint(handle, channel->index, id, &point);

    usbtenki_closeDevice(handle);
    return res;
}

void TUSBProvider::tick(int64_t timestamp) {

    tickerDevice->timestamp = timestamp;

    TDeviceManager::instance().updateDevice(tickerDevice);
}

void TUSBProvider::addSource(Source *source) {

    QString serialNumber;
    TDevice *device;

    {
        QMutexLocker locker(&mutex);

        serialNumber = source->device->serial_number;
        device = devicesBySerialNumber.value(serialNumber);

        if (device) {
            return;
        }

        device = new TDevice(this, source);
        devicesBySerialNumber.insert(serialNumber, device);
    }

    TDeviceManager::instance().addDevice(device);

    if (device->isAlive()) {
        TDeviceManager::instance().connectDevice(device);
    } else {
        disconnectedSerialNumbers.insert(serialNumber);
    }
}

void TUSBProvider::updateSource(Source *source) {

    TDevice *device;
    QString serialNumber;

    {
        QMutexLocker locker(&mutex);

        serialNumber = source->device->serial_number;
        device = devicesBySerialNumber.value(serialNumber);

        if (!device) {
            return; // should never happen
        }

        device->update(source);
    }

    if (device->isAlive()) {
        if (disconnectedSerialNumbers.remove(serialNumber)) {
            TDeviceManager::instance().connectDevice(device);
        }
        TDeviceManager::instance().updateDevice(device);
    } else if (!disconnectedSerialNumbers.contains(serialNumber)) {
        disconnectedSerialNumbers.insert(serialNumber);
        TDeviceManager::instance().disconnectDevice(device);
    }
}
