#include <QDebug>
#include <QMutexLocker>
#include <QRegularExpression>

#include "DaemonThreadManager.h"
#include "MathChannel.h"
#include "MathProvider.h"
#include "OriginTimestamp.h"
#include "TDeviceManager.h"
#include "chip.h"

static const QRegularExpression VARIABLE_REGEX("\\[(([0-9a-zA-Z]{6}):\\d{2})(_nc)?\\]");

#define VARIABLE_REGEX_CHANNEL_ID 1
#define VARIABLE_REGEX_SERIAL_NUMBER 2
#define VARIABLE_REGEX_NO_CALIBRATION 3

static const QString NAME_PREFIX = QString("MATH");

static const Version VERSION = {0, 0};

static const QString DESCRIPTION = chip_description(USBTENKI_CHIP_MATH);

static const QString SHORT_DESCRIPTION = chip_description_short(USBTENKI_CHIP_MATH);

#define EVENT_CODE_EXPRESSION_CHANGED -1

MathChannel::MathChannel(int index) : index(index), name(TChannel::buildID(NAME_PREFIX, index)), table(nullptr) {

    queue_init(&events);
    queue_grow(&events, 64);

    mathDevice = new TDevice(&MathProvider::instance(), name, NAME_PREFIX, VERSION);
    mathDevice->port = index;
    mathDevice->flags = DEVICE_FLAG_MATH;
    mathDevice->timestamp = OriginTimestamp::instance().value();

    mathDevice->channels.resize(1);
    mathDevice->channels[0] = new TChannel(mathDevice, index, USBTENKI_CHIP_MATH, name);

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

    DaemonThreadManager::instance().addThread(this);
}

MathChannel::~MathChannel() {

    delete mathDevice;

    mutex.lock();

    Event *event;

    while ((event = (Event *)queue_remove(&events))) {
        if (event->code == EVENT_CODE_EXPRESSION_CHANGED) {
            delete ((QString *)event->data);
        } else {
            delete ((TDevice *)event->data);
        }
        delete event;
    }

    mutex.unlock();
}

const QString &MathChannel::getExpression() {

    QMutexLocker locker(&mutex);

    return expression;
}

unit_t MathChannel::getUnit() {

    QMutexLocker locker(&mutex);

    return mathDevice->channels[0]->quantity.unit;
}

TDevice *MathChannel::getDevice() {

    QMutexLocker locker(&mutex);

    return mathDevice;
}

void MathChannel::setExpression(const QString &s) { addEvent(EVENT_CODE_EXPRESSION_CHANGED, new QString(s)); }

void MathChannel::setUnit(unit_t unit) {

    mutex.lock();
    mathDevice->channels[0]->quantity.unit = unit;
    mathDevice->channels[0]->calibratedQuantity.unit = unit;
    mutex.unlock();

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

void MathChannel::run() {

    TDeviceManager &dm = TDeviceManager::instance();
    connect(&dm, &TDeviceManager::deviceAdded, this, &MathChannel::onDeviceAdded);

    while (processEvents())
        ;

    dm.disconnect(this);
    clear();
}

void MathChannel::shutdown() {

    mutex.lock();
    state = SHUTDOWN;
    condition.wakeOne();
    mutex.unlock();
}

bool MathChannel::processEvents() {

    State currentState;
    Event *event;

    {
        QMutexLocker locker(&mutex);

        while (queue_size(&events) == 0 && state == READY) {
            condition.wait(&mutex);
        }

        currentState = state;
        event = (Event *)queue_remove(&events);
    }

    if (event) {

        switch (event->code) {

        case TDevice::UPDATE: {
            TDevice *device = (TDevice *)event->data;
            processDeviceUpdate(device);
            delete device;
        } break;

        case TDevice::ADD: {
            TDevice *device = (TDevice *)event->data;
            processDeviceAdd(device);
            delete device;
        } break;

        case TDevice::CONNECT: {
            TDevice *device = (TDevice *)event->data;
            processDeviceConnect(device);
            delete device;
        } break;

        case TDevice::DISCONNECT: {
            TDevice *device = (TDevice *)event->data;
            processDeviceDisconnect(device);
            delete device;
        } break;

        case EVENT_CODE_EXPRESSION_CHANGED: {
            expression = *((QString *)event->data);
            delete ((QString *)event->data);
            processExpressionChange();
        } break;

        default:
            // Unknown code. Exit thread (fail-fast) so that hopefully we'll notice something is wrong.
            return false;
        }

        delete event;
    }

    return currentState != SHUTDOWN;
}

void MathChannel::processDeviceAdd(const TDevice *eventDevice) {

    const QString &serialNumber = eventDevice->serialNumber;

    if (!serialNumbers.value(serialNumber)) {
        return;
    }

    TDevice *device = devicesBySerialNumber.value(serialNumber);

    if (device) {
        device->update(eventDevice);
    } else {
        device = addDevice(eventDevice);
        if (!device) {
            return;
        }
    }
}

void MathChannel::processDeviceConnect(const TDevice *eventDevice) {

    const QString &serialNumber = eventDevice->serialNumber;
    TDevice *device = devicesBySerialNumber.value(serialNumber);

    if (device) {
        device->update(eventDevice);
    } else {
        device = addDevice(eventDevice);
        if (!device) {
            return;
        }
    }

    QVector<TChannel *> &channels = device->channels;
    for (int i = 0; i < channels.size(); i++) {
        table->connect(device->timestamp, channels[i]->id);
    }

    if (table->getDisconnectionCount() == 0) {
        setActive();
        update();
    }
}

void MathChannel::processDeviceDisconnect(const TDevice *eventDevice) {

    const QString &serialNumber = eventDevice->serialNumber;
    TDevice *device = devicesBySerialNumber.value(serialNumber);

    if (device) {
        device->update(eventDevice);
    } else {
        device = addDevice(eventDevice);
        if (!device) {
            return;
        }
    }

    QVector<TChannel *> &channels = device->channels;
    for (int i = 0; i < channels.size(); i++) {
        table->disconnect(device->timestamp, channels[i]->id);
    }

    update();
    setInactive();
}

void MathChannel::processDeviceUpdate(const TDevice *eventDevice) {

    const QString &serialNumber = eventDevice->serialNumber;
    TDevice *device = devicesBySerialNumber.value(serialNumber);

    if (device) {
        device->update(eventDevice);
    } else {
        device = addDevice(eventDevice);
        if (!device) {
            return;
        }
    }

    QVector<TChannel *> &channels = device->channels;
    for (int i = 0; i < channels.size(); i++) {
        bool noCalibration = channelIDs[channels[i]->id];
        const Quantity &quantity = noCalibration ? channels[i]->quantity : channels[i]->calibratedQuantity;
        table->insert(device->timestamp, channels[i]->id, quantity);
    }

    update();
}

void MathChannel::processExpressionChange() {

    clear();

    // Always reference the ticker device, or the math device will hang on constant expressions

    serialNumbers.insert(TDevice::TICKER_SERIAL_NUMBER, true);
    channelIDs.insert(TDevice::TICKER_CHANNEL_ID, true);

    // Identify each channel (and device) referenced by the expression

    QRegularExpressionMatchIterator itRegex = VARIABLE_REGEX.globalMatch(expression);

    while (itRegex.hasNext()) {

        QRegularExpressionMatch m = itRegex.next();

        QString serial = m.captured(VARIABLE_REGEX_SERIAL_NUMBER);

        // Disallow referencing math channels because it can trigger an infinite feedback loop!
        if (serial == NAME_PREFIX) {
            continue;
        }

        serialNumbers.insert(serial, true);

        bool noCalibration = m.capturedLength(VARIABLE_REGEX_NO_CALIBRATION) > 0;
        QString channelID = m.captured(VARIABLE_REGEX_CHANNEL_ID);

        channelIDs.insert(channelID, noCalibration);
    }

    // Add channels to DataTable

    QVector<QString> channelIDsVector(channelIDs.size());
    int i = 0;

    QMapIterator<QString, bool> itChannelID(channelIDs);
    while (itChannelID.hasNext()) {
        itChannelID.next();
        QString channelID = itChannelID.key();
        channelIDsVector[i] = channelID;
        i++;
    }

    table = new DataTable(channelIDsVector, false);

    // Add devices

    QMapIterator<QString, bool> itSerialNumbers(serialNumbers);
    while (itSerialNumbers.hasNext()) {
        itSerialNumbers.next();
        QString serialNumber = itSerialNumbers.key();
        addDevice(serialNumber);
    }

    if (table->getDisconnectionCount() > 0) {
        setInactive();
    } else {
        setActive();
        update();
    }
}

TDevice *MathChannel::addDevice(const TDevice *inputDevice) {

    const QString &serialNumber = inputDevice->serialNumber;

    TDeviceSignal *signal = TDeviceManager::instance().getDeviceSignal(serialNumber);
    if (!signal) {
        return nullptr;
    }

    connect(signal, &TDeviceSignal::deviceConnected, this, &MathChannel::onDeviceConnected);
    connect(signal, &TDeviceSignal::deviceDisconnected, this, &MathChannel::onDeviceDisconnected);
    connect(signal, &TDeviceSignal::deviceUpdated, this, &MathChannel::onDeviceUpdated);

    TDevice *device = new TDevice(*inputDevice);
    bool connected = device->isAlive();

    devicesBySerialNumber.insert(serialNumber, device);
    int64_t t = device->timestamp;

    for (int i = 0; i < device->channels.size(); i++) {
        TChannel *channel = device->channels[i];
        channelsByID.insert(channel->id, channel);
        if (connected) {
            table->connect(t, channel->id);
            bool noCalibration = channelIDs.value(channel->id);
            const Quantity &quantity = noCalibration ? channel->quantity : channel->calibratedQuantity;
            table->insert(device->timestamp, channel->id, quantity);
        }
    }

    return device;
}

TDevice *MathChannel::addDevice(const QString &serialNumber) {

    TDeviceManager &dm = TDeviceManager::instance();
    TDevice *device = dm.getDevice(serialNumber);

    if (!device) {
        return nullptr;
    }

    return addDevice(device);
}

void MathChannel::clear() {

    QMapIterator<QString, TDevice *> it(devicesBySerialNumber);

    while (it.hasNext()) {
        it.next();
        const QString &serial = it.key();
        TDevice *device = it.value();
        TDeviceSignal *signal = TDeviceManager::instance().getDeviceSignal(serial);
        if (signal) {
            signal->disconnect(this);
        }
        delete device;
    }

    serialNumbers.clear();
    channelIDs.clear();

    devicesBySerialNumber.clear();
    channelsByID.clear();

    if (table) {
        delete table;
        table = nullptr;
    }
}

void MathChannel::setActive() {

    {
        QMutexLocker locker(&mutex);

        if (mathDevice->isAlive()) {
            return;
        }

        mathDevice->setAlive(true);
    }

    TDeviceManager::instance().connectDevice(mathDevice);
}

void MathChannel::setInactive() {

    {
        QMutexLocker locker(&mutex);

        if (!mathDevice->isAlive()) {
            return;
        }

        mathDevice->setAlive(false);
        mathDevice->channels[0]->invalidate();
    }

    TDeviceManager::instance().disconnectDevice(mathDevice);
}

bool MathChannel::replaceVariables(QString &output, const DataRow *row) {

    QRegularExpressionMatchIterator it = VARIABLE_REGEX.globalMatch(expression);

    int start = 0;

    while (it.hasNext()) {

        QRegularExpressionMatch m = it.next();
        QString channelID = m.captured(VARIABLE_REGEX_CHANNEL_ID);
        TChannel *channel = channelsByID.value(channelID);

        if (!channel) {
            output = "Error";
            return false;
        }

        const Quantity &qty = row->getQuantity(channel->id);
        QString value = QString::number(quantity_value_as_double(&qty), 'f', 10);

        output += QStringRef(&expression, start, m.capturedStart(0) - start);
        output += value;
        start = m.capturedEnd(0);
    }

    output += QStringRef(&expression, start, expression.length() - start);

    return true;
}

void MathChannel::update() {

    while (table->hasCompleteRow()) {
        const DataRow *row = table->nextCompleteRow();
        update(row);
    }
}

void MathChannel::update(const DataRow *row) {

    Quantity quantity;
    QString resolvedExpression;

    if (replaceVariables(resolvedExpression, row)) {

        engine.clearExceptions();
        double result = engine.evaluate(resolvedExpression).toNumber();

        if (engine.hasUncaughtException()) {
            quantity.value_error = CHIP_ERROR_INVALID_DATA;
            quantity.type = QUANTITY_TYPE_ERROR;
        } else if (qIsNaN(result)) {
            quantity.value_error = CHIP_ERROR_NO_DATA;
            quantity.type = QUANTITY_TYPE_ERROR;
        } else {
            quantity.value_float = (float)result;
            quantity.type = QUANTITY_TYPE_FLOAT;
        }

    } else {
        quantity.value_error = CHIP_ERROR_INVALID_DATA;
        quantity.type = QUANTITY_TYPE_ERROR;
    }

    mutex.lock();
    mathDevice->channels[0]->quantity = quantity;
    mathDevice->channels[0]->calibratedQuantity = quantity;
    mathDevice->timestamp = row->getTimestamp();
    mutex.unlock();

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

void MathChannel::onDeviceAdded(const TDevice *inputDevice) { addEvent(TDevice::ADD, new TDevice(*inputDevice)); }

void MathChannel::onDeviceConnected(const TDevice *inputDevice) {

    addEvent(TDevice::CONNECT, new TDevice(*inputDevice));
}

void MathChannel::onDeviceDisconnected(const TDevice *inputDevice) {

    addEvent(TDevice::DISCONNECT, new TDevice(*inputDevice));
}

void MathChannel::onDeviceUpdated(const TDevice *inputDevice) { addEvent(TDevice::UPDATE, new TDevice(*inputDevice)); }

void MathChannel::addEvent(int code, void *data) {

    mutex.lock();
    queue_add(&events, new Event(code, data));
    condition.wakeOne();
    mutex.unlock();
}
