#include <algorithm>
#include <stdio.h>
#include <string.h>

#include <QDebug>
#include <QMutexLocker>
#include <QSettings>

#include <dracal/common/chrono.hpp>
#include <dracal/common/fs.hpp>
#include <dracal/common/platform.hpp>
#include <dracal/common/log.hpp>
#include <dracal/logging/logger/interval.hpp>

#include "Config.h"
#include "ConfigPanel.h"
#include "DaemonThreadManager.h"
#include "LoggingThread.h"
#include "MathProvider.h"
#include "OriginTimestamp.h"
#include "TDeviceManager.h"
#include "chip.h"
#include "usbtenki_version.h"

#ifdef Q_OS_WIN
    #include <windows.h>
#endif

using namespace dracal::common;
using namespace dracal::logging;

static const char FILE_MODE_WRITE[] = "w";
static const char FILE_MODE_APPEND[] = "a";

static char *new_char_array(const QString &s) {

    QByteArray bytes = s.toUtf8();
    unsigned int len = qstrlen(bytes);
    char *result = new char[len + 1];
    memcpy(result, bytes.constData(), len);
    result[len] = '\0';

    return result;
}

LoggingThread::LoggingThread(const QVector<QString> &channelIDs, const QString &path, const QString &comment,
                             int interval, bool append)
    : path(path), channelCount(channelIDs.size()), csvopt(ConfigPanel::instance().getCSVOptions()),
      fileMode(append ? FILE_MODE_APPEND : FILE_MODE_WRITE), origin(OriginTimestamp::instance().value()),
      interval_us(interval * 1000), // ms -> us
      lineCounter(0) {
    dracal_trace("logging thread start");
    TDeviceManager &dm = TDeviceManager::instance();

    csvopt.file = nullptr;
    csvopt.datalog = &datalog;

    // Add ticker device
    QVector<QString> channelIDsWithTicker(channelIDs);
    channelIDsWithTicker.append(TDevice::TICKER_CHANNEL_ID);
    TDeviceSignal *tickerSignal = dm.getDeviceSignal(TDevice::TICKER_SERIAL_NUMBER);
    connect(tickerSignal, &TDeviceSignal::deviceUpdated, this, &LoggingThread::onDeviceUpdated);

    // Initialize channel headers
    list_init(&channelHeaders);
    list_grow(&channelHeaders, channelIDs.size());

    dracal_trace("logging thread init data table");

    // Create DataTable
    table = new DataTable(channelIDsWithTicker, false);
    table->connect(0, TDevice::TICKER_CHANNEL_ID);

    datalog.interval = interval_us;
    datalog.comment = new_char_array(comment);

    list_init(&datalog.devices);
    list_init(&datalog.channels);
    list_grow(&datalog.channels, channelIDs.size());

    dracal_trace("logging thread init channels");

    for (int i = 0; i < channelIDs.size(); i++) {

        QString id = channelIDs[i];

        TChannel *inputChannel = dm.getChannel(id);
        if (!inputChannel) {
            continue;
        }

        TDevice *inputDevice = inputChannel->device;
        if (inputDevice->isAlive()) {
            table->connect(inputDevice->timestamp, i);
            table->insert(inputDevice->timestamp, i, inputChannel->calibratedQuantity);
        }

        TDeviceSignal *signal = deviceSignalBySerial.value(inputDevice->serialNumber);
        if (!signal) {
            signal = dm.getDeviceSignal(inputDevice->serialNumber);
            if (!signal) {
                continue;
            }
            deviceSignalBySerial.insert(inputDevice->serialNumber, signal);
            connect(signal, &TDeviceSignal::deviceConnected, this, &LoggingThread::onDeviceConnected);
            connect(signal, &TDeviceSignal::deviceDisconnected, this, &LoggingThread::onDeviceDisconnected);
            connect(signal, &TDeviceSignal::deviceUpdated, this, &LoggingThread::onDeviceUpdated);
        }

        Channel *channel = new Channel();
        channel->chip_id = inputChannel->chip_id;
        channel->quantity = inputChannel->calibratedQuantity;

        list_add(&datalog.channels, channel);

        // Custom channel header

        CSV_Channel_Header *ch = new CSV_Channel_Header();
        ch->name = new_char_array(inputDevice->productName);
        ch->id = new_char_array(inputChannel->id);

        ch->description = new_char_array(dm.getAlias(inputChannel->id));

        if (inputDevice->isMath()) {
            ch->unit = MathProvider::instance().getUnit(inputChannel->index);
        } else {
            unit_t nativeUnit = inputChannel->getNativeUnit();
            unit_t prefUnit = csvopt.units[unit_category(nativeUnit)];
            if (prefUnit == UNIT_SENSOR_DEFAULT) {
                prefUnit = nativeUnit;
            }
            ch->unit = prefUnit;
        }

        list_add(&channelHeaders, ch);
    }

    dracal_trace("logging thread init events init");

    // Initialize event queue
    queue_init(&events);
    queue_grow(&events, 64);

    // Initialize interval_logger
    std::vector<
        std::tuple<interval_logger::serial, interval_logger::channel_id, interval_logger::product_name,
                   interval_logger::channel_name, interval_logger::channel_description, interval_logger::unit_symbol>>
        infos;

    // Populate channel information for interval_logger
    for (int i = 0; i < channelIDs.size(); i++) {
        QString id = channelIDs[i];
        TChannel *inputChannel = dm.getChannel(id);
        if (!inputChannel) {
            continue;
        }

        TDevice *inputDevice = inputChannel->device;

        // Get unit symbol
        unit_t nativeUnit = inputChannel->getNativeUnit();
        unit_t prefUnit = csvopt.units[unit_category(nativeUnit)];
        if (prefUnit == UNIT_SENSOR_DEFAULT) {
            prefUnit = nativeUnit;
        }

        infos.emplace_back(inputDevice->serialNumber.toStdString(),     // serial
                           inputChannel->id.toStdString(),              // channel_id
                           inputDevice->productName.toStdString(),      // product_name
                           inputChannel->id.toStdString(),              // channel_name (using ID)
                           dm.getAlias(inputChannel->id).toStdString(), // channel_description
                           std::string(unit_to_string(prefUnit, csvopt.flags & CSV_FLAG_ASCII)) // unit_symbol
        );
    }

    dracal_trace("logging thread init channel informations");

    // Disable unit conversion in csv module since input quantities already have the correct unit
    memset(csvopt.units, UNIT_SENSOR_DEFAULT, sizeof(csvopt.units));

    std::vector<std::string> timestamp_column_names;
    std::function<std::vector<std::string>(const chrono::time_point<chrono::system_clock>)> timestamp_cb;

    switch (csvopt.time_format) {
    case CSV_TIME_NONE:
        timestamp_column_names = {};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock>) -> std::vector<std::string> { return {}; };
        break;
    case CSV_TIME_SYSTEM_DEFAULT:
        timestamp_column_names = {"DateTime"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            return {dracal::common::format_timestamp_local_system_default(ts)};
        };
        break;
    case CSV_TIME_ISO_8601_LONG:
        timestamp_column_names = {"DateTime"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            return {dracal::common::format_timestamp_local_YYYYMMDDTHHMMSS(ts)};
        };
        break;
    case CSV_TIME_ISO_8601_LONG_DUAL:
        timestamp_column_names = {"Date", "Time"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            const auto res = dracal::common::format_timestamp_local_YYYYMMDD_HHMMSS(ts);
            return {res[0], res[1]};
        };
        break;
    case CSV_TIME_ISO_8601_SHORT:
        timestamp_column_names = {"Time"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            return {dracal::common::format_timestamp_local_HHMMSS(ts)};
        };
        break;
    case CSV_TIME_ISO_8601_LONG_MS:
        timestamp_column_names = {"DateTime"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            return {dracal::common::format_timestamp_local_YYYYMMDDTHHMMSSmmm(ts)};
        };
        break;
    case CSV_TIME_ISO_8601_LONG_MS_DUAL:
        timestamp_column_names = {"Date", "Time"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            const auto res = dracal::common::format_timestamp_local_YYYYMMDD_HHMMSSmmm(ts);
            return {res[0], res[1]};
        };
        break;
    case CSV_TIME_ISO_8601_SHORT_MS:
        timestamp_column_names = {"Time"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            return {dracal::common::format_timestamp_local_HHMMSSmmm(ts)};
        };
        break;
    case CSV_TIME_EPOCH_MS:
        timestamp_column_names = {"Timestamp"};
        timestamp_cb = [](const chrono::time_point<chrono::system_clock> ts) -> std::vector<std::string> {
            return {dracal::common::format_timestamp_milliseconds_from_epoch(ts)};
        };
        break;
    }

    dracal_trace("logging thread init ts callbacks");

    dracal::logging::interval_logger::error_policy e_policy =
        dracal::logging::interval_logger::error_policy::write_empty;

    if (csvopt.error_str == nullptr) {
        e_policy = dracal::logging::interval_logger::error_policy::write_previous;
    } else {
        const std::string raw_error_policy = csvopt.error_str;

        if (raw_error_policy == "") {
            e_policy = dracal::logging::interval_logger::error_policy::write_empty;
        } else if (raw_error_policy == "0") {
            e_policy = dracal::logging::interval_logger::error_policy::write_zero;
        } else if (raw_error_policy == "-1") {
            e_policy = dracal::logging::interval_logger::error_policy::write_negative_1;
        } else if (raw_error_policy == "error") {
            e_policy = dracal::logging::interval_logger::error_policy::write_error;
        }
    }

    dracal_trace("logging thread init error handling callbacks");

    _interval_logger = std::make_unique<dracal::logging::interval_logger>(
        fs::path(path.toStdString()),                                                            // file path
        chrono::milliseconds(interval),                                                          // interval
        std::string(csvopt.separator_str),                                                       // separator
        e_policy,                                                                                // error policy
        append ? dracal::logging::file_policy::append : dracal::logging::file_policy::overwrite, // file policy
        comment.toStdString(),                                                                   // comment
        timestamp_column_names,    // timestamp column names
        timestamp_cb,              // timestamp convert function
        chrono::milliseconds(300), // grace duration
        chrono::milliseconds(1),   // sleep duration
        10,                        // memory max lines
        infos                      // channel infos
    );

    dracal_trace("logging thread logger created");

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

LoggingThread::~LoggingThread() {

    DaemonThreadManager::instance().removeThread(this);

    mutex.lock();

    TDevice *device;
    while ((device = (TDevice *)queue_remove(&events))) {
        delete device;
    }
    queue_clear(&events);

    mutex.unlock();

    LIST_FOR(&channelHeaders) {
        CSV_Channel_Header *ch = LIST_CUR(CSV_Channel_Header);
        delete[] ch->name;
        delete[] ch->id;
        delete[] ch->description;
        delete ch;
    }

    list_clear(&channelHeaders);

    delete table;
    delete[] datalog.comment;

    LIST_FOR(&datalog.devices) {
        Device *device = LIST_CUR(Device);
        device_delete(device);
    }

    list_clear(&datalog.devices);
    list_clear(&datalog.channels);
}

void LoggingThread::run() {
    dracal_trace("logging thread run started");
    const bool preventSleep = loadEnablePreventSleepDuringLogging();

    if (preventSleep) {
#if defined(Q_OS_WIN)
        dracal_info("no sleep for the logs");
        SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED);
#elif defined(Q_OS_MAC)
        dracal_info("no sleep for the logs");
        sleep.inhibit("logging in progress");
#endif
    }

    dracal_trace("logging thread sleep disabled");

    // QByteArray pathByteArray = path.toLocal8Bit();

    // csvopt.file = fopen(pathByteArray.data(), fileMode);
    // if (!csvopt.file) {
    //     emit error(ERROR_OPEN_OUTPUT);
    //     return;
    // }

    datalog.creation_timestamp = timestamp_now();

    dracal_trace("logging timestamped now");

    if (!_interval_logger) {
        emit error(ERROR_OPEN_OUTPUT);
        return;
    }

    const bool ok = _interval_logger->start(chrono::system_clock::now());

    if (!ok) {
        emit error(ERROR_OPEN_OUTPUT);
        return;
    }

    dracal_trace("logger started");

    QSettings settings;
    decimal_point_index = settings.value("logger/decimal_point", 0).toInt();

    // csv = csv_init(&csvopt);
    // csv_write_header_custom(csv, &channelHeaders);

    while (processEvents())
        ;

    dracal_trace("logging process events done");

    _interval_logger->stop();

    dracal_trace("loggger stopped");

    // csv_exit(csv);
    // fclose(csvopt.file);

    // Disconnect signals

    TDeviceManager &dm = TDeviceManager::instance();
    TDeviceSignal *signal = dm.getDeviceSignal(TDevice::TICKER_SERIAL_NUMBER);
    signal->disconnect(this);

    QMapIterator<QString, TDeviceSignal *> it(deviceSignalBySerial);

    while (it.hasNext()) {
        it.next();
        signal = it.value();
        signal->disconnect(this);
    }

    dracal_trace("logging signal disconnected");

#if defined(Q_OS_WIN)
    SetThreadExecutionState(ES_CONTINUOUS);
    dracal_info("sleep prevention turned off");
#elif defined(Q_OS_MAC)
    sleep.uninhibit();
    dracal_info("sleep prevention turned off");
#endif

    dracal_trace("logging run done");
}

void LoggingThread::shutdown() {

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

bool LoggingThread::processEvents() {

    State currentState;
    TDevice *device;

    const auto now = chrono::steady_clock::now();

    if (now > _last_check + chrono::milliseconds(300)) {
        _last_check = now;
        emit written(_interval_logger->number_lines_written());
    }

    {
        QMutexLocker locker(&mutex);

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

        currentState = state;
        device = (TDevice *)queue_remove(&events);
    }

    if (device) {

        QVector<TChannel *> &channels = device->channels;
        int64_t timestamp = device->timestamp;

        switch (device->event) {

        case TDevice::UPDATE:
            for (int i = 0; i < channels.size(); i++) {
                if (channels[i]->id == "TICKER:00") {
                    continue;
                }

                table->insert(timestamp, channels[i]->id, channels[i]->calibratedQuantity);

                const auto ticker_timestamp =
                    std::chrono::time_point<std::chrono::system_clock>(std::chrono::microseconds(timestamp));
                Quantity to_write = channels[i]->calibratedQuantity;
                const unit_category_t cat = unit_category(to_write.unit);
                quantity_convert_to_unit(&to_write, csvopt.units[cat]);

                std::string value;

                switch (to_write.type) {
                case QUANTITY_TYPE_FLOAT:
                    switch (decimal_point_index) {
                    case 0: // system default
                    {
                        std::locale loc(""); // Get user's default locale
                        value = fmt::format(loc, "{:.{}Lf}", to_write.value_float, csvopt.frac_digits);
                        break;
                    }
                    case 1: // dot
                    {
                        value = fmt::format("{:.{}f}", to_write.value_float, csvopt.frac_digits);
                        break;
                    }
                    case 2: // comma
                    {
                        static const std::locale comma_locale = []() {
                            struct comma_numpunct : std::numpunct<char> {
                                char do_decimal_point() const override { return ','; }
                            };
                            return std::locale(std::locale::classic(), new comma_numpunct);
                        }();

                        value = fmt::format(comma_locale, "{:.{}Lf}", to_write.value_float, csvopt.frac_digits);
                        break;
                    }
                    }
                    break;
                case QUANTITY_TYPE_UINT32:
                    value = fmt::format("{}", to_write.value_uint32);
                    break;
                case QUANTITY_TYPE_INT32:
                    value = fmt::format("{}", to_write.value_int32);
                    break;
                case QUANTITY_TYPE_ERROR:
                    // nothing to do, the error handling write previous and/or other error strings is handled inside the
                    // interval_logger.
                    continue;
                }

                _interval_logger->update(channels[i]->id.toStdString(), ticker_timestamp, value);
            }
            break;

        case TDevice::CONNECT:
            for (int i = 0; i < channels.size(); i++) {
                table->connect(timestamp, channels[i]->id);
            }
            break;

        case TDevice::DISCONNECT:
            for (int i = 0; i < channels.size(); i++) {
                table->disconnect(timestamp, channels[i]->id);
            }
            break;
        }

        delete device;
    }

    writeRows();

    return currentState != SHUTDOWN;
}

void LoggingThread::writeRows() {

    if (!table->hasCompleteRow()) {
        return;
    }

    unsigned int n = 0;

    do {

        const DataRow *row = table->nextCompleteRow();

        datalog.timestamp = row->getTimestamp();

        for (int c = 0; c < channelCount; c++) {
            Channel *channel = LIST_GET(&datalog.channels, c, Channel);
            channel->quantity = row->getQuantity(c);
        }

        // csv_write_row(csv);
        n++;

    } while (table->hasCompleteRow());

    // fflush(csvopt.file);

    lineCounter += n;
    emit written(lineCounter);
}

void LoggingThread::onDeviceConnected(const TDevice *inputDevice) { addEvent(inputDevice); }

void LoggingThread::onDeviceDisconnected(const TDevice *inputDevice) { addEvent(inputDevice); }

void LoggingThread::onDeviceUpdated(const TDevice *inputDevice) {

    int64_t dt = inputDevice->timestamp - origin;

    if (dt % interval_us == 0) {
        addEvent(inputDevice);
    }
}

void LoggingThread::addEvent(const TDevice *device) {

    TDevice *copy = new TDevice(*device);

    mutex.lock();
    queue_add(&events, copy);
    condition.wakeOne();
    mutex.unlock();
}
