/*
    SPDX-FileCopyrightText: 2012 Alex Richardson <alex.richardson@gmx.de>

    SPDX-License-Identifier: LGPL-2.1-or-later
*/

// sut
#include <primitivearraydata.hpp>
#include <arraydatainformation.hpp>
#include <topleveldatainformation.hpp>
#include <primitivetemplateinfo.hpp>
#include <primitivefactory.hpp>
#include <scriptengineinitializer.hpp>
// Okteta core
#include <Okteta/ByteArrayModel>
// Qt
#include <QTest>
#include <QScriptEngine>
#include <QRandomGenerator>
// Std
#include <memory>
#include <limits>
#include <utility>

class PrimitiveArrayTest : public QObject
{
    Q_OBJECT

private:
    /** Tests user defined overrides of byteOrder, typeName, and toStringFunc. */
    template <PrimitiveDataType primType, typename T> void testReadCustomizedPrimitiveInternal();
    template <PrimitiveDataType primType, typename T> void testReadPrimitiveInternal();
    template <PrimitiveDataType primType> void testReadPrimitive();
    template <typename T> bool compareItems(T first, T second, uint index);

private Q_SLOTS:
    void initTestCase();
    void testReadFloat();
    void testReadDouble();
    void testReadChar();
    void testReadInt8();
    void testReadInt16();
    void testReadInt32();
    void testReadInt64();
    void testReadUInt8();
    void testReadUInt16();
    void testReadUInt32();
    void testReadUInt64();
    void testReadBool8();
    void testReadBool16();
    void testReadBool32();
    void testReadBool64();

private:
    std::unique_ptr<Okteta::Byte[]> data;
    std::unique_ptr<Okteta::ByteArrayModel> model;
    std::unique_ptr<Okteta::Byte[]> endianData;
    std::unique_ptr<Okteta::ByteArrayModel> endianModel;
};

#if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
#define CURRENT_BYTE_ORDER (DataInformation::DataInformationEndianness::EndiannessLittle)
#define NON_CURRENT_BYTE_ORDER (DataInformation::DataInformationEndianness::EndiannessBig)
#elif Q_BYTE_ORDER == Q_BIG_ENDIAN
#define CURRENT_BYTE_ORDER (DataInformation::DataInformationEndianness::EndiannessBig)
#define NON_CURRENT_BYTE_ORDER (DataInformation::DataInformationEndianness::EndiannessLittle)
#else
#error unknown byte order
#endif

static constexpr uint SIZE = 8192;
static constexpr uint ENDIAN_SIZE = 16;

void PrimitiveArrayTest::initTestCase()
{
    data = std::unique_ptr<Okteta::Byte[]>(new Okteta::Byte[SIZE]);
    // ensure that we have at least one NaN (quiet + signalling)
    AllPrimitiveTypes quietDouble(std::numeric_limits<double>::quiet_NaN());
    AllPrimitiveTypes signallingDouble(std::numeric_limits<double>::signaling_NaN());
    for (int i = 0; i < 8; ++i) {
        data[i] = quietDouble.allBytes[i];
        data[8 + i] = signallingDouble.allBytes[i];
    }

    AllPrimitiveTypes quietFloat(std::numeric_limits<float>::quiet_NaN());
    AllPrimitiveTypes signallingFloat(std::numeric_limits<float>::signaling_NaN());
    for (int i = 0; i < 4; ++i) {
        data[16 + i] = quietFloat.allBytes[i];
        data[20 + i] = signallingFloat.allBytes[i];
    }

    auto* const randomGenerator = QRandomGenerator::global();
    for (uint i = 24; i < SIZE; ++i) {
        data[i] = static_cast<Okteta::Byte>(randomGenerator->bounded(256));
    }

    auto copy = std::unique_ptr<Okteta::Byte[]>(new Okteta::Byte[SIZE]); // no make_unique, no need for initialization, TODO C++20: use std::make_unique_for_overwrite<Okteta::Byte[]>(SIZE)
    memcpy(copy.get(), data.get(), SIZE);
    model = std::make_unique<Okteta::ByteArrayModel>(std::move(copy), SIZE);
    QCOMPARE(model->size(), Okteta::Size(SIZE));

    endianData = std::unique_ptr<Okteta::Byte[]>(new Okteta::Byte[ENDIAN_SIZE]);
    for (uint i = 0; i < ENDIAN_SIZE; ++i) {
        endianData[i] = i;
    }

    auto endianCopy = std::unique_ptr<Okteta::Byte[]>(new Okteta::Byte[ENDIAN_SIZE]); // no make_unique, no need for initialization, TODO C++20: use std::make_unique_for_overwrite<Okteta::Byte[]>(ENDIAN_SIZE)
    memcpy(endianCopy.get(), endianData.get(), ENDIAN_SIZE);
    endianModel = std::make_unique<Okteta::ByteArrayModel>(std::move(endianCopy), ENDIAN_SIZE);
    QCOMPARE(endianModel->size(), Okteta::Size(ENDIAN_SIZE));
}

template <typename T>
bool PrimitiveArrayTest::compareItems(T first, T second, uint index)
{
    Q_UNUSED(index)
    return first == second;
}

template <>
bool PrimitiveArrayTest::compareItems(float first, float second, uint index)
{
    if (first != first) {
        qDebug() << "first was NaN at index" << index;
        // NaN
        if (second != second) {
            // second is NaN too;
            union
            {
                float floating;
                QIntegerForSizeof<float>::Unsigned integer;
            } firstUn, secondUn;
            firstUn.floating = first;
            secondUn.floating = second;
            return firstUn.integer == secondUn.integer;
        }

        return false;
    }
    return first == second;
}

template <>
bool PrimitiveArrayTest::compareItems(double first, double second, uint index)
{
    if (first != first) {
        qDebug() << "first was NaN at index" << index;
        // NaN
        if (second != second) {
            // second is NaN too;
            union
            {
                double floating;
                QIntegerForSizeof<double>::Unsigned integer;
            } firstUn, secondUn;
            firstUn.floating = first;
            secondUn.floating = second;
            return firstUn.integer == secondUn.integer;
        }

        return false;
    }
    return first == second;
}

template <PrimitiveDataType primType>
inline void PrimitiveArrayTest::testReadPrimitive()
{
    testReadPrimitiveInternal<primType, typename PrimitiveInfo<primType>::valueType>();
    testReadCustomizedPrimitiveInternal<primType, typename PrimitiveInfo<primType>::valueType>();
}

QScriptValue customToStringFunc(QScriptContext* context, QScriptEngine* engine)
{
    Q_UNUSED(context)
    Q_UNUSED(engine)
    return QStringLiteral("myvalue");
}

template <PrimitiveDataType primType, typename T>
void PrimitiveArrayTest::testReadCustomizedPrimitiveInternal()
{
    LoggerWithContext lwc(nullptr, QString());
    std::unique_ptr<QScriptEngine> engine = ScriptEngineInitializer::newEngine();

    auto primInfo = PrimitiveFactory::newInstance(QStringLiteral("value"), primType, lwc);
    primInfo->setByteOrder(NON_CURRENT_BYTE_ORDER);
    primInfo->setCustomTypeName(QStringLiteral("mytype"));
    primInfo->setToStringFunction(engine->newFunction(customToStringFunc));

    auto managedDataInf = std::make_unique<ArrayDataInformation>(QStringLiteral("values"),
                                                                 endianModel->size() / sizeof(T),
                                                                 std::move(primInfo),
                                                                 lwc);
    ArrayDataInformation* const dataInf = managedDataInf.get();
    const auto top = std::make_unique<TopLevelDataInformation>(std::move(managedDataInf), nullptr, std::move(engine));

    QCOMPARE(dataInf->childCount(), uint(ENDIAN_SIZE / sizeof(T)));
    quint8 bitOffs = 0;
    qint64 result = dataInf->readData(endianModel.get(), 0, endianModel->size() * 8, &bitOffs);
    QCOMPARE(Okteta::Size(result), endianModel->size() * 8);
    T* const dataAsT = reinterpret_cast<T*>(endianData.get());
    QVERIFY(!dataInf->mData->isComplex());
    auto* const arrayData = static_cast<PrimitiveArrayData<primType>*>(dataInf->mData.get());

    // Verify byteOrder of values. The data is set up without palindromes.
    if (sizeof(T) > 1) {
        for (uint i = 0; i < dataInf->childCount(); ++i) {
            AllPrimitiveTypes childDataAll = arrayData->valueAt(i);
            T childData = childDataAll.value<T>();
            T expected = dataAsT[i];
            // TODO comparison for float and double: nan != nan
            if (compareItems<T>(childData, expected, i)) {
                QByteArray desc = "i=" + QByteArray::number(i) + ", model[i]="
                                  + QByteArray::number(childData)
                                  + ", data[i]=" + QByteArray::number(expected);
                QVERIFY2(!compareItems<T>(childData, expected, i), desc.constData());
            }
        }
    }
    // Verify typeName as the user will see it.
    QCOMPARE(arrayData->dataAt(0, DataInformation::ColumnType, Qt::DisplayRole).toString(),
             QStringLiteral("mytype"));
    // Verify value string as the user will see it.
    QCOMPARE(arrayData->dataAt(0, DataInformation::ColumnValue, Qt::DisplayRole).toString(),
             QStringLiteral("myvalue"));
}

template <PrimitiveDataType primType, typename T>
void PrimitiveArrayTest::testReadPrimitiveInternal()
{
    LoggerWithContext lwc(nullptr, QString());
    auto managedDataInf = std::make_unique<ArrayDataInformation>(QStringLiteral("values"),
                                                                 model->size() / sizeof(T),
                                                                 PrimitiveFactory::newInstance(QStringLiteral("value"), primType, lwc),
                                                                 lwc);
    managedDataInf->setByteOrder(CURRENT_BYTE_ORDER);
    ArrayDataInformation* const dataInf = managedDataInf.get();
    const auto top = std::make_unique<TopLevelDataInformation>(std::move(managedDataInf), nullptr, ScriptEngineInitializer::newEngine());
    QCOMPARE(dataInf->childCount(), uint(SIZE / sizeof(T)));
    quint8 bitOffs = 0;
    qint64 result = dataInf->readData(model.get(), 0, model->size() * 8, &bitOffs);
    QCOMPARE(Okteta::Size(result), model->size() * 8);
    T* const dataAsT = reinterpret_cast<T*>(data.get());
    QVERIFY(!dataInf->mData->isComplex());
    auto* const arrayData = static_cast<PrimitiveArrayData<primType>*>(dataInf->mData.get());
    for (uint i = 0; i < dataInf->childCount(); ++i) {
        AllPrimitiveTypes childDataAll = arrayData->valueAt(i);
        const auto childData = childDataAll.value<T>();
        const auto expected = dataAsT[i];
        // TODO comparison for float and double: nan != nan
        if (!compareItems<T>(childData, expected, i)) {
            QByteArray desc = "i=" + QByteArray::number(i) + ", model[i]="
                              + QByteArray::number(childData)
                              + ", data[i]=" + QByteArray::number(expected);
            QVERIFY2(compareItems<T>(childData, expected, i), desc.constData());
        }
    }
}

void PrimitiveArrayTest::testReadFloat()
{
    testReadPrimitive<PrimitiveDataType::Float>();
}

void PrimitiveArrayTest::testReadDouble()
{
    testReadPrimitive<PrimitiveDataType::Double>();
}

void PrimitiveArrayTest::testReadChar()
{
    testReadPrimitive<PrimitiveDataType::Char>();
}

void PrimitiveArrayTest::testReadInt8()
{
    testReadPrimitive<PrimitiveDataType::Int8>();
}

void PrimitiveArrayTest::testReadInt16()
{
    testReadPrimitive<PrimitiveDataType::Int16>();
}

void PrimitiveArrayTest::testReadInt32()
{
    testReadPrimitive<PrimitiveDataType::Int32>();
}

void PrimitiveArrayTest::testReadInt64()
{
    testReadPrimitive<PrimitiveDataType::Int64>();
}

void PrimitiveArrayTest::testReadUInt8()
{
    testReadPrimitive<PrimitiveDataType::UInt8>();
}

void PrimitiveArrayTest::testReadUInt16()
{
    testReadPrimitive<PrimitiveDataType::UInt16>();
}

void PrimitiveArrayTest::testReadUInt32()
{
    testReadPrimitive<PrimitiveDataType::UInt32>();
}

void PrimitiveArrayTest::testReadUInt64()
{
    testReadPrimitive<PrimitiveDataType::UInt64>();
}

void PrimitiveArrayTest::testReadBool8()
{
    testReadPrimitive<PrimitiveDataType::Bool8>();
}

void PrimitiveArrayTest::testReadBool16()
{
    testReadPrimitive<PrimitiveDataType::Bool16>();
}

void PrimitiveArrayTest::testReadBool32()
{
    testReadPrimitive<PrimitiveDataType::Bool32>();
}

void PrimitiveArrayTest::testReadBool64()
{
    testReadPrimitive<PrimitiveDataType::Bool64>();
}

QTEST_GUILESS_MAIN(PrimitiveArrayTest)

#include "primitivearraytest.moc"
