//  ************************************************************************************************
//
//  BornAgain: simulate and fit reflection and scattering
//
//! @file      GUI/coregui/DataLoaders/QREDataLoader.cpp
//! @brief     Implements class QREDataLoader
//!
//! @homepage  http://www.bornagainproject.org
//! @license   GNU General Public License v3 or higher (see COPYING)
//! @copyright Forschungszentrum Jülich GmbH 2021
//! @authors   Scientific Computing Group at MLZ (see CITATION, AUTHORS)
//
//  ************************************************************************************************

#include "GUI/coregui/DataLoaders/QREDataLoader.h"
#include "Base/Axis/PointwiseAxis.h"
#include "Device/Histo/IntensityDataIOFactory.h"
#include "Device/InputOutput/DataFormatUtils.h"
#include "Device/Unit/AxisNames.h"
#include "GUI/coregui/DataLoaders/QREDataLoaderProperties.h"
#include "GUI/coregui/DataLoaders/QREDataLoaderResultModel.h"
#include "GUI/coregui/Models/DataItem.h"
#include "GUI/coregui/Models/JobItemUtils.h"
#include "GUI/coregui/Models/RealDataItem.h"
#include "GUI/coregui/Models/SpecularDataItem.h"
#include "GUI/coregui/utils/DeserializationException.h"
#include "GUI/coregui/utils/ImportDataInfo.h"
#include "qcustomplot.h"
#include "ui_QREDataLoaderProperties.h"
#include <QFile>
#include <QString>
#include <QTextStream>

namespace {

//! For emitting a signal on method exit
class AutoEmit {
public:
    AutoEmit(std::function<void()> fn) : m_fn(fn) {}
    ~AutoEmit() { m_fn(); }

private:
    std::function<void()> m_fn;
};

QVector<QPair<int, int>> expandLineNumberPattern(const QString& pattern, bool* ok = nullptr)
{
    QVector<QPair<int, int>> result;

    // splitting "1, 2-3" first on comma-separated tokens
    for (const auto& token : pattern.split(",")) {
        const auto parts = token.split("-");
        // splitting on dash-separated tokens
        if (!parts.empty()) {
            // if no "-" is present, make from "1" a pair {1, 1}
            // if "-" is present, make from "1-2" a pair {1,2}
            bool ok2 = true;
            const auto conv0 = parts[0].toInt(&ok2);
            if (ok2) {
                const auto conv1 = parts.size() > 1 ? parts[1].toInt(&ok2) : conv0;
                if (ok2) {
                    result.push_back({conv0, conv1});
                } else {
                    if (ok != nullptr) {
                        *ok = false;
                    }
                    return {};
                }
            }
        }
    }

    return result;
}

} // namespace

QREDataLoader::QREDataLoader() : m_propertiesWidget(nullptr)
{
    initWithDefaultImportSettings();
}

QString QREDataLoader::name() const
{
    return "CSV file (Reflectometry - Q/R/sigma_R)";
}

QString QREDataLoader::persistentClassName() const
{
    return "QREDataLoader";
}

void QREDataLoader::populateImportSettingsWidget(QWidget* parent)
{
    if (m_propertiesWidget == nullptr)
        m_propertiesWidget = new QREDataLoaderProperties;

    QHBoxLayout* l = new QHBoxLayout(parent);
    l->setContentsMargins(0, 0, 0, 0);
    parent->setLayout(l);
    l->addWidget(m_propertiesWidget);

    if (m_importSettings.separator == " ")
        m_propertiesWidget->m_ui->separatorCombo->setCurrentText("<SPACE>");
    else if (m_importSettings.separator == "\t")
        m_propertiesWidget->m_ui->separatorCombo->setCurrentText("<TAB>");
    else
        m_propertiesWidget->m_ui->separatorCombo->setCurrentText(m_importSettings.separator);

    m_propertiesWidget->m_ui->headerPrefixEdit->setText(m_importSettings.headerPrefix);
    m_propertiesWidget->m_ui->linesToSkipEdit->setText(m_importSettings.linesToSkip);

    for (const auto dataType : {DataType::Q, DataType::R, DataType::dR}) {
        m_propertiesWidget->columnSpinBox((int)dataType)
            ->setValue(m_importSettings.columnDefinitions[dataType].column + 1); // view is 1-based

        m_propertiesWidget->factorSpinBox((int)dataType)
            ->setValue(m_importSettings.columnDefinitions[dataType].factor);
    }

    m_propertiesWidget->m_ui->enableErrorCheckBox->setChecked(
        m_importSettings.columnDefinitions[DataType::dR].enabled);

    if (m_importSettings.columnDefinitions[DataType::Q].unit == UnitInFile::perAngstrom)
        m_propertiesWidget->m_ui->qUnitCombo->setCurrentIndex(1);
    else
        m_propertiesWidget->m_ui->qUnitCombo->setCurrentIndex(0); // 1/nm

    QObject::connect(m_propertiesWidget, &QREDataLoaderProperties::propertiesChanged, [this]() {
        applyImportSettings();
        emit importSettingsChanged();
    });
}

void QREDataLoader::initWithDefaultImportSettings()
{
    m_importSettings.separator = ";";
    m_importSettings.headerPrefix = "#,//";
    m_importSettings.linesToSkip = "";

    for (const auto dataType : {DataType::Q, DataType::R, DataType::dR}) {
        m_importSettings.columnDefinitions[dataType].enabled = true;
        m_importSettings.columnDefinitions[dataType].column = (int)dataType;
        m_importSettings.columnDefinitions[dataType].unit = UnitInFile::none;
        m_importSettings.columnDefinitions[dataType].factor = 1.0;
    }

    m_importSettings.columnDefinitions[DataType::Q].unit = UnitInFile::perNanoMeter;
}

QByteArray QREDataLoader::serialize() const
{
    // changed in version 2: no file hash anymore (1 was only a developer version)
    // changed in version 3: refactored lists/vectors (2 was only a developer version)
    // changed in version 4: refactored lists/vectors (3 was only a developer version)

    QByteArray a;
    QDataStream s(&a, QIODevice::WriteOnly);
    s.setVersion(QDataStream::Qt_5_12);

    s << (quint8)4; // version
    s << m_fileContent;
    s << m_importSettings;
    s << m_importResult.lines;
    s << m_importResult.rawValues;
    s << m_importResult.qValues;
    s << m_importResult.rValues;
    s << m_importResult.eValues;
    s << m_importResult.validCalculatedLines;
    s << m_importResult.maxColumnCount;
    s << m_importResult.calculationErrors;
    s << m_importResult.error;
    s << m_importResult.importSettings;

    return a;
}

void QREDataLoader::deserialize(const QByteArray& data)
{
    m_importSettings.columnDefinitions.clear(); // sufficient
    m_importResult.clear();

    QDataStream s(data);
    s.setVersion(QDataStream::Qt_5_12);

    quint8 version;
    s >> version;

    if (version < 4) // anything smaller 4 was internal developer version => no backwards
                     // compatibility necessary
        throw DeserializationException::tooOld();

    if (version == 4) {
        s >> m_fileContent;
        s >> m_importSettings;
        s >> m_importResult.lines;
        s >> m_importResult.rawValues;
        s >> m_importResult.qValues;
        s >> m_importResult.rValues;
        s >> m_importResult.eValues;
        s >> m_importResult.validCalculatedLines;
        s >> m_importResult.maxColumnCount;
        s >> m_importResult.calculationErrors;
        s >> m_importResult.error;
        s >> m_importResult.importSettings;
    } else
        throw DeserializationException::tooNew();

    if (s.status() != QDataStream::Ok)
        throw DeserializationException::streamError();
}

AbstractDataLoader* QREDataLoader::clone() const
{
    auto loader = new QREDataLoader();
    loader->deserialize(serialize());
    return loader;
}

void QREDataLoader::processContents()
{
    AutoEmit emitter([this] { contentsProcessed(); }); // automatic signal calling in any return

    // Important: If the current options match the ones in m_parsingResult, then nothing should be
    // performed. Otherwise e.g. a linked instrument may have to be re-linked

    m_importResult.error.clear();

    ASSERT(m_item != nullptr);
    ASSERT(m_item->isSpecularData());

    const auto invalidateItemData = [this]() {
        m_item->removeNativeData();
        m_item->specularDataItem()->setOutputData(nullptr);
    };

    const bool parsingSettingsChanged =
        m_importResult.importSettings.headerPrefix != m_importSettings.headerPrefix
        || m_importResult.importSettings.linesToSkip != m_importSettings.linesToSkip
        || m_importResult.importSettings.separator != m_importSettings.separator;
    const bool calculationSettingsChanged =
        m_importResult.importSettings.columnDefinitions != m_importSettings.columnDefinitions;
    const bool calculationIsNecessary = (parsingSettingsChanged || calculationSettingsChanged);
    const bool creationOfOutputDataIsNecessary = calculationIsNecessary;

    if (parsingSettingsChanged)
        // everything has to be re-parsed
        parseFileContent();

    if (calculationIsNecessary)
        calculateFromParseResult();

    m_importResult.importSettings = m_importSettings;

    // -- make a few checks (mainly for fulfilling PointwiseAxis::sanityCheck())
    if (m_importResult.validCalculatedLines < 2)
        m_importResult.error = "At least two full rows must exist";

    if (!m_importResult.error.isEmpty()) {
        invalidateItemData();
        return;
    }

    if (creationOfOutputDataIsNecessary) {

        try {
            createOutputDataFromParsingResult(m_item);
        } catch (...) {
            m_importResult.error = "Import not successful - caught an exception.";
            invalidateItemData();
        }
    }
}

int QREDataLoader::numErrors() const
{
    return (m_importResult.error.isEmpty() ? 0 : 1) + m_importResult.calculationErrors.size();
}

int QREDataLoader::numLineRelatedErrors() const
{
    return m_importResult.calculationErrors.size();
}

QStringList QREDataLoader::lineUnrelatedErrors() const
{
    if (!m_importResult.error.isEmpty())
        return {m_importResult.error};

    return {};
}

AbstractDataLoaderResultModel* QREDataLoader::createResultModel() const
{
    return new QREDataLoaderResultModel(&m_importResult);
}

QByteArray QREDataLoader::fileContent() const
{
    return m_fileContent;
}

void QREDataLoader::guessSettings()
{
    // #baimport - move to utils; create unit test
    // search for lines which start with a number, then try the separators

    const auto isFirstNumberChar = [](const QChar& c) {
        return c.isNumber() || c == '.' || c == '+' || c == '-';
    };

    const auto belongsToNumber = [](const QChar& c) {
        return c.isNumber() || c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-';
    };

    QTextStream in(m_fileContent);
    int lineNr = 0;
    const int maxLinesToExamine = 100;
    while (!in.atEnd() && lineNr < maxLinesToExamine) {
        lineNr++;
        QString line = in.readLine().trimmed();
        if (line.isEmpty())
            continue;
        if (!isFirstNumberChar(line[0]))
            continue;

        // line starts with a number => search gap after it
        int startOfGap = 1;
        while (startOfGap < line.size() && belongsToNumber(line[startOfGap]))
            startOfGap++;

        if (startOfGap == line.size())
            continue;

        int endOfGap = startOfGap;
        while (endOfGap < line.size() && !belongsToNumber(line[endOfGap])) {
            endOfGap++;
        }

        QStringRef gapContent(&line, startOfGap, endOfGap - startOfGap);
        if (gapContent.isEmpty())
            continue;

        if (gapContent.contains("\t")) {
            m_importSettings.separator = "\t";
            return;
        }

        gapContent = gapContent.trimmed();
        if (gapContent.isEmpty()) {
            m_importSettings.separator = " ";
            return;
        }

        if (gapContent.contains(";")) {
            m_importSettings.separator = ";";
            return;
        }

        if (gapContent.contains(",")) {
            m_importSettings.separator = ",";
            return;
        }
    }
}

void QREDataLoader::setFileContents(const QByteArray& fileContent)
{
    m_fileContent = fileContent;
}

void QREDataLoader::parseFileContent() const
{
    m_importResult.clear();

    const QStringList headerPrefixes = (m_importSettings.headerPrefix.trimmed().isEmpty())
                                           ? QStringList()
                                           : m_importSettings.headerPrefix.split(",");

    const auto lineIsHeader = [headerPrefixes](const QString& line) {
        for (const auto& prefix : headerPrefixes) {
            if (line.startsWith(prefix.trimmed()))
                return true;
        }

        return false;
    };

    const auto skippedLines = expandLineNumberPattern(m_importSettings.linesToSkip);
    const auto lineShouldBeSkipped = [skippedLines](int lineNr) {
        for (const auto pair : skippedLines) {
            if (lineNr >= pair.first && lineNr <= pair.second)
                return true;
        }
        return false;
    };

    QTextStream in(m_fileContent);
    int lineNr = 0;
    // if separator is SPACE: e.g. three consecutive SPACEs do not represent 3 columns => delete
    // empty parts
    QString::SplitBehavior splitBehavior =
        m_importSettings.separator == " " ? QString::SkipEmptyParts : QString::KeepEmptyParts;

    while (!in.atEnd()) {
        lineNr++;

        QString line = in.readLine();

        const bool skip =
            lineIsHeader(line) || lineShouldBeSkipped(lineNr) || line.trimmed().isEmpty();

        m_importResult.lines << qMakePair(skip, line);
    }

    m_importResult.rawValues.resize(m_importResult.lines.size());

    for (int lineNr = 0; lineNr < m_importResult.lines.size(); lineNr++) {

        const bool skip = m_importResult.lines[lineNr].first;
        if (skip)
            continue;

        const QStringList lineEntries =
            m_importResult.lines[lineNr].second.split(m_importSettings.separator, splitBehavior);

        m_importResult.maxColumnCount =
            std::max(m_importResult.maxColumnCount, lineEntries.count());

        QVector<double> rowEntriesAsDouble;

        for (int col = 0; col < lineEntries.count(); col++) {
            bool ok = false;
            double val = lineEntries[col].toDouble(&ok);
            if (!ok)
                val = std::numeric_limits<double>::quiet_NaN();

            rowEntriesAsDouble << val;
        }

        m_importResult.rawValues[lineNr] = rowEntriesAsDouble;
    }
}

void QREDataLoader::calculateFromParseResult() const
{
    m_importResult.clearCalculatedValues();
    m_importResult.qValues.resize(m_importResult.lines.size());
    m_importResult.rValues.resize(m_importResult.lines.size());
    m_importResult.eValues.resize(m_importResult.lines.size());

    // -- calculate the Q/R/E values (take from specified column, use factor)
    const auto& c = m_importSettings.columnDefinitions; // easier access
    const bool errorColumnIsEnabled = c[DataType::dR].enabled;
    const double unitFac = (c[DataType::Q].unit == UnitInFile::perAngstrom) ? 10.0 : 1.0;
    const double qFactor = c[DataType::Q].factor * unitFac;
    const double rFactor = c[DataType::R].factor;
    const double eFactor = c[DataType::dR].factor;

    const int qCol = c[DataType::Q].column;
    const int rCol = c[DataType::R].column;
    const int eCol = c[DataType::dR].column;

    QSet<double> foundQValues;
    double lastFoundQ = std::numeric_limits<double>::quiet_NaN();

    for (int lineNr = 0; lineNr < m_importResult.lines.size(); lineNr++) {
        const bool skipLine = m_importResult.lines[lineNr].first;
        if (skipLine)
            continue;

        const auto& rawValues = m_importResult.rawValues[lineNr];

        const bool qColIsValid = qCol >= 0 && qCol < rawValues.size();
        const bool rColIsValid = rCol >= 0 && rCol < rawValues.size();
        const bool eColIsValid = eCol >= 0 && eCol < rawValues.size();

        const double q =
            qColIsValid ? rawValues[qCol] * qFactor : std::numeric_limits<double>::quiet_NaN();
        const double r =
            rColIsValid ? rawValues[rCol] * rFactor : std::numeric_limits<double>::quiet_NaN();
        const double e =
            eColIsValid ? rawValues[eCol] * eFactor : std::numeric_limits<double>::quiet_NaN();

        if (std::isnan(q)) {
            m_importResult.addError(lineNr, ErrorDefinition::columnDoesNotContainValidNumber,
                                    qCol + 1);
            continue;
        }

        if (std::isnan(r)) {
            m_importResult.addError(lineNr, ErrorDefinition::columnDoesNotContainValidNumber,
                                    rCol + 1);
            continue;
        }

        if (std::isnan(e) && errorColumnIsEnabled) {
            m_importResult.addError(lineNr, ErrorDefinition::columnDoesNotContainValidNumber,
                                    eCol + 1);
            continue;
        }

        if (foundQValues.contains(q)) {
            m_importResult.addError(lineNr, ErrorDefinition::duplicateQ, q);
            continue;
        }

        if (!std::isnan(lastFoundQ) && q <= lastFoundQ) {
            m_importResult.addError(lineNr, ErrorDefinition::wrongQOrder);
            continue;
        }

        if (r > 1.0) {
            m_importResult.addError(lineNr, ErrorDefinition::RGreaterOne, r);
            continue;
        }

        if (r < 0.0) {
            m_importResult.addError(lineNr, ErrorDefinition::RLessZero, r);
            continue;
        }

        m_importResult.qValues[lineNr] = q;
        m_importResult.rValues[lineNr] = r;
        m_importResult.eValues[lineNr] = e;
        m_importResult.validCalculatedLines++;
        foundQValues << q;
        lastFoundQ = q;
    }
}

void QREDataLoader::createOutputDataFromParsingResult(RealDataItem* item) const
{
    // -- create OutputData
    std::vector<double> qVec;
    std::vector<double> rVec;

    for (int lineNr = 0; lineNr < m_importResult.lines.size(); lineNr++) {
        const bool skipLine = m_importResult.lines[lineNr].first;
        const bool lineHasError = m_importResult.calculationErrors.contains(lineNr);
        if (skipLine || lineHasError)
            continue;

        qVec.push_back(m_importResult.qValues[lineNr]);
        rVec.push_back(m_importResult.rValues[lineNr]);
    }

    OutputData<double>* oData = new OutputData<double>();
    oData->addAxis(PointwiseAxis("qVector", qVec));
    oData->setRawDataVector(rVec);

    // -- Replacement of item->setImportData(std::move(data));
    item->initNativeData();

    QString units_name = JobItemUtils::nameFromAxesUnits(Axes::Units::QSPACE);

    // -- Replacement of specularItem->reset(std::move(data));
    SpecularDataItem* specularItem = item->specularDataItem();
    ComboProperty combo = ComboProperty() << units_name;

    specularItem->setItemValue(SpecularDataItem::P_AXES_UNITS, combo.variant());
    specularItem->getItem(SpecularDataItem::P_AXES_UNITS)->setVisible(true);

    auto label_map = AxisNames::InitSpecAxis();
    const auto xAxisTitle = QString::fromStdString(label_map[Axes::Units::QSPACE]);
    const auto yAxisTitle = "Signal [a.u.]"; // taken from ImportDataInfo::axisLabel

    specularItem->setXaxisTitle(xAxisTitle);
    specularItem->setYaxisTitle(yAxisTitle);
    specularItem->setOutputData(oData); // takes ownership of odata
    specularItem->setAxesRangeToData();

    item->setNativeDataUnits(units_name);
    item->setNativeOutputData(oData->clone()); // takes ownership of odata
}

QDataStream& operator<<(QDataStream& stream, const QREDataLoader::ImportSettings& s)
{
    stream << s.serialize();
    return stream;
}

QDataStream& operator>>(QDataStream& stream, QREDataLoader::ImportSettings& s)
{
    QByteArray b;
    stream >> b;
    s.deserialize(b);
    return stream;
}

void QREDataLoader::applyImportSettings()
{
    if (!m_propertiesWidget)
        return;

    const auto ui = m_propertiesWidget->m_ui;

    m_importSettings.separator = ui->separatorCombo->currentText();
    if (m_importSettings.separator == "<TAB>")
        m_importSettings.separator = "\t";
    if (m_importSettings.separator == "<SPACE>")
        m_importSettings.separator = " ";

    m_importSettings.headerPrefix = ui->headerPrefixEdit->text();
    m_importSettings.linesToSkip = ui->linesToSkipEdit->text();

    for (const auto dataType : m_importSettings.columnDefinitions.keys()) {
        auto& colDef = m_importSettings.columnDefinitions[dataType];

        colDef.column = m_propertiesWidget->columnSpinBox((int)dataType)->value() - 1;
        colDef.factor = m_propertiesWidget->factor((int)dataType);
    }

    m_importSettings.columnDefinitions[DataType::Q].unit =
        m_propertiesWidget->m_ui->qUnitCombo->currentIndex() == 0 ? UnitInFile::perNanoMeter
                                                                  : UnitInFile::perAngstrom;

    m_importSettings.columnDefinitions[DataType::dR].enabled =
        m_propertiesWidget->m_ui->enableErrorCheckBox->isChecked();
}

void QREDataLoader::ImportResult::clear()
{
    lines.clear();
    rawValues.clear();
    qValues.clear();
    rValues.clear();
    eValues.clear();
    validCalculatedLines = 0;
    maxColumnCount = 0;
    calculationErrors.clear();
    error.clear();
    importSettings.columnDefinitions.clear(); // sufficient
}

void QREDataLoader::ImportResult::clearCalculatedValues()
{
    qValues.clear();
    rValues.clear();
    eValues.clear();
    calculationErrors.clear();
    validCalculatedLines = 0;
}

void QREDataLoader::ImportResult::addError(int line, ErrorDefinition::Type type, int data)
{
    calculationErrors[line] = ErrorDefinition(type, data);
}

void QREDataLoader::ImportResult::addError(int line, ErrorDefinition::Type type, double data)
{
    calculationErrors[line] = ErrorDefinition(type, data);
}

QString QREDataLoader::ImportResult::errorText(int line) const
{
    auto error = calculationErrors.value(line, ErrorDefinition());
    return error.type == ErrorDefinition::none ? QString() : error.toString();
}

bool QREDataLoader::ImportSettings::operator!=(const ImportSettings& other) const
{
    return serialize() != other.serialize();
}

QByteArray QREDataLoader::ImportSettings::serialize() const
{
    QByteArray a;
    QDataStream s(&a, QIODevice::WriteOnly);
    s.setVersion(QDataStream::Qt_5_12);

    s << (quint8)1; // version
    s << separator;
    s << headerPrefix;
    s << linesToSkip;

    s << (quint8)columnDefinitions.count();
    for (const auto dataType : columnDefinitions.keys()) {
        s << (quint8)dataType;
        s << columnDefinitions[dataType].enabled;
        s << columnDefinitions[dataType].column;
        s << (quint8)columnDefinitions[dataType].unit;
        s << columnDefinitions[dataType].factor;
    }

    return a;
}

void QREDataLoader::ImportSettings::deserialize(const QByteArray& data)
{
    columnDefinitions.clear();

    QDataStream s(data);
    s.setVersion(QDataStream::Qt_5_12);

    quint8 version;
    s >> version;

    if (version == 1) {
        s >> separator;
        s >> headerPrefix;
        s >> linesToSkip;

        quint8 nDefs;
        s >> nDefs;
        for (int i = 0; i < nDefs; i++) {
            quint8 dataType;
            s >> dataType;
            auto& colDef = columnDefinitions[(DataType)dataType];
            s >> colDef.enabled;
            s >> colDef.column;
            quint8 unit;
            s >> unit;
            colDef.unit = UnitInFile(unit);
            s >> colDef.factor;
        }
    } else
        throw DeserializationException::tooNew();

    if (s.status() != QDataStream::Ok)
        throw DeserializationException::streamError();
}

bool QREDataLoader::ColumnDefinition::operator==(const ColumnDefinition& other) const
{
    return enabled == other.enabled && column == other.column && unit == other.unit
           && factor == other.factor;
}

QREDataLoader::ErrorDefinition::ErrorDefinition(Type t, int d) : type(t), data(d) {}
QREDataLoader::ErrorDefinition::ErrorDefinition(Type t, double d) : type(t), data(d) {}

QString QREDataLoader::ErrorDefinition::toString() const
{
    switch (type) {
    case columnDoesNotContainValidNumber:
        return QString("Raw column %1 does not contain a valid number - line is discarded")
            .arg(std::get<int>(data));

    case duplicateQ:
        return QString("The value %1 for Q is duplicate - line is discarded")
            .arg(std::get<double>(data));

    case wrongQOrder:
        return QString("Q coordinates must be sorted in ascending order - line is discarded");

    case RGreaterOne:
        return QString("The value %1 for R is greater than 1.0 - line is discarded")
            .arg(std::get<double>(data));

    case RLessZero:
        return QString("The value %1 for R is less than 0 - line is discarded")
            .arg(std::get<double>(data));
    }

    return "Unspecified error";
}

QDataStream& operator<<(QDataStream& stream, const QREDataLoader::ErrorDefinition& s)
{
    stream << (quint8)s.type;
    if (std::holds_alternative<int>(s.data)) {
        stream << true;
        stream << quint32(std::get<int>(s.data));
    } else {
        stream << false;
        stream << qreal(std::get<double>(s.data));
    }

    return stream;
}

QDataStream& operator>>(QDataStream& stream, QREDataLoader::ErrorDefinition& s)
{
    quint8 t;
    stream >> t;
    s.type = QREDataLoader::ErrorDefinition::Type(t);
    bool isInt = false;
    stream >> isInt;
    if (isInt) {
        quint32 d;
        stream >> d;
        s.data = int(d);
    } else {
        qreal d;
        stream >> d;
        s.data = double(d);
    }

    return stream;
}
