// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

//TESTED_COMPONENT=plugins/declarative/multimedia

#include <QtTest/QtTest>

#include <QtCore/qobject.h>
#include <QtMultimedia/qmediaplayer.h>
#include <QtMultimedia/qvideoframe.h>
#include <QtMultimedia/qvideoframeformat.h>
#include <QtMultimedia/qvideosink.h>
#include <QtMultimediaQuick/private/qquickvideooutput_p.h>
#include <QtQml/qqmlcomponent.h>
#include <QtQml/qqmlengine.h>
#include <QtQuick/qquickview.h>
#include <QtQuick/private/qsgrenderloop_p.h>
#include <QtQuick/private/qsgthreadedrenderloop_p.h>

void presentDummyFrame(QVideoSink *sink, const QSize &size)
{
    if (sink) {
        QVideoFrameFormat format(size, QVideoFrameFormat::Format_ARGB8888_Premultiplied);
        QVideoFrame frame(format);

        sink->setVideoFrame(frame);

        // Have to spin an event loop or two for the surfaceFormatChanged() signal
        qApp->processEvents();
    }
}

class tst_QQuickVideoOutput : public QObject
{
    Q_OBJECT
public:
    tst_QQuickVideoOutput();

    ~tst_QQuickVideoOutput() override
    {
        delete m_mappingOutput;
        delete m_mappingComponent;
    }

public slots:
    void initTestCase();
    void cleanup();

private slots:
    void fillMode();
    void orientation();
    void surfaceSource();
    void paintSurface();
    void sourceRect();

    void contentRect();
    void contentRect_data();

    void threadedSignalEmission_shouldNotCrash();

private:
    QQmlEngine m_engine;

    // Variables used for the mapping test
    QQmlComponent *m_mappingComponent = nullptr;
    QQuickVideoOutput *m_mappingOutput = nullptr;

    void updateOutputGeometry(QObject *output);
};

tst_QQuickVideoOutput::tst_QQuickVideoOutput() = default;

void tst_QQuickVideoOutput::initTestCase()
{
    // We initialize the mapping vars here
    m_mappingComponent = new QQmlComponent(&m_engine);
    m_mappingComponent->loadUrl(QUrl("qrc:/main.qml"));

    auto *component = m_mappingComponent->create();
    QVERIFY(component != nullptr);

    m_mappingOutput = qobject_cast<QQuickVideoOutput *>(component);
    QVERIFY(m_mappingOutput);

    presentDummyFrame(m_mappingOutput->videoSink(), QSize(200,100));
    updateOutputGeometry(m_mappingOutput);
    // First make sure the component has processed the frame
    QCOMPARE(m_mappingOutput->sourceRect(), QRectF(0, 0, 200,100));
}

void tst_QQuickVideoOutput::cleanup()
{
    QQuickVideoOutput::setSignalBackoff(std::nullopt);
}

void tst_QQuickVideoOutput::fillMode()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != nullptr);

    QSignalSpy propSpy(videoOutput, SIGNAL(fillModeChanged(QQuickVideoOutput::FillMode)));

    // Default is preserveaspectfit
    QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::PreserveAspectFit);
    QCOMPARE(propSpy.size(), 0);

    videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::PreserveAspectCrop)));
    QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::PreserveAspectCrop);
    QCOMPARE(propSpy.size(), 1);

    videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch)));
    QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::Stretch);
    QCOMPARE(propSpy.size(), 2);

    videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch)));
    QCOMPARE(videoOutput->property("fillMode").value<QQuickVideoOutput::FillMode>(), QQuickVideoOutput::Stretch);
    QCOMPARE(propSpy.size(), 2);

    delete videoOutput;
}

void tst_QQuickVideoOutput::orientation()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != nullptr);

    QSignalSpy propSpy(videoOutput, SIGNAL(orientationChanged()));

    // Default orientation is 0
    QCOMPARE(videoOutput->property("orientation").toInt(), 0);
    QCOMPARE(propSpy.size(), 0);

    videoOutput->setProperty("orientation", QVariant(90));
    QCOMPARE(videoOutput->property("orientation").toInt(), 90);
    QCOMPARE(propSpy.size(), 1);

    videoOutput->setProperty("orientation", QVariant(180));
    QCOMPARE(videoOutput->property("orientation").toInt(), 180);
    QCOMPARE(propSpy.size(), 2);

    videoOutput->setProperty("orientation", QVariant(270));
    QCOMPARE(videoOutput->property("orientation").toInt(), 270);
    QCOMPARE(propSpy.size(), 3);

    videoOutput->setProperty("orientation", QVariant(360));
    QCOMPARE(videoOutput->property("orientation").toInt(), 360);
    QCOMPARE(propSpy.size(), 4);

    // More than 360 should be fine
    videoOutput->setProperty("orientation", QVariant(540));
    QCOMPARE(videoOutput->property("orientation").toInt(), 540);
    QCOMPARE(propSpy.size(), 5);

    // Negative should be fine
    videoOutput->setProperty("orientation", QVariant(-180));
    QCOMPARE(videoOutput->property("orientation").toInt(), -180);
    QCOMPARE(propSpy.size(), 6);

    // Same value should not reemit
    videoOutput->setProperty("orientation", QVariant(-180));
    QCOMPARE(videoOutput->property("orientation").toInt(), -180);
    QCOMPARE(propSpy.size(), 6);

    // Non multiples of 90 should not work
    videoOutput->setProperty("orientation", QVariant(-1));
    QCOMPARE(videoOutput->property("orientation").toInt(), -180);
    QCOMPARE(propSpy.size(), 6);

    delete videoOutput;
}

void tst_QQuickVideoOutput::surfaceSource()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != nullptr);

    QMediaPlayer holder(this);

    QCOMPARE(holder.videoOutput(), nullptr);

    holder.setVideoOutput(videoOutput);

    QVERIFY(holder.videoOutput() != nullptr);
    QVERIFY(holder.videoSink() != nullptr);

    delete videoOutput;

    // This should clear the surface
    QVERIFY(holder.videoOutput() == nullptr);
    QVERIFY(holder.videoSink() == nullptr);

    // Also, creating two sources, setting them in order, and destroying the first
    // should not zero holder.videoSink()
    videoOutput = component.create();
    holder.setVideoOutput(videoOutput);

    QObject *surface = holder.videoOutput();
    QVERIFY(surface != nullptr);

    QObject *videoOutput2 = component.create();
    QVERIFY(videoOutput2);
    holder.setVideoOutput(videoOutput2);
    QVERIFY(holder.videoOutput() != nullptr);
    QVERIFY(holder.videoOutput() != surface); // Surface should have changed
    surface = holder.videoOutput();
    QVERIFY(surface == videoOutput2);

    // Now delete first one
    delete videoOutput;
    QVERIFY(holder.videoOutput() == surface); // Should not have changed surface

    // Now create a second surface and assign it as the source
    // The old surface holder should be zeroed
    QMediaPlayer holder2(this);
    holder2.setVideoOutput(videoOutput2);

    QVERIFY(holder.videoOutput() == nullptr);
    QVERIFY(holder2.videoOutput() != nullptr);

    // Finally a combination - set the same source to two things, then assign a new source
    // to the first output - should not reset the first source
    videoOutput = component.create();
    holder2.setVideoOutput(videoOutput);

    // Both vo and vo2 were pointed to holder2 - setting vo2 should not clear holder2
    QVERIFY(holder2.videoOutput() != nullptr);
    QVERIFY(holder.videoOutput() == nullptr);
    holder.setVideoOutput(videoOutput2);
    QVERIFY(holder2.videoOutput() != nullptr);
    QVERIFY(holder.videoOutput() != nullptr);

    // They should also be independent
    QVERIFY(holder.videoOutput() != holder2.videoOutput());

    delete videoOutput;
    delete videoOutput2;
}

static const uchar rgb32ImageData[] =
{//  B     G     R     A
    0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff,
    0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff,
    0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff,
    0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff,
    0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff,
    0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff,
    0x00, 0x01, 0x02, 0xff, 0x03, 0x04, 0x05, 0xff,
    0x06, 0x07, 0x08, 0xff, 0x09, 0x0a, 0x0b, 0xff
};

static QVideoFrame makeRGB32Frame()
{
    QVideoFrame frame(QVideoFrameFormat(QSize(4, 4), QVideoFrameFormat::Format_ARGB8888));
    frame.map(QVideoFrame::ReadWrite);
    QTEST_ASSERT(frame.mappedBytes(0) == 64);
    memcpy(frame.bits(0), rgb32ImageData, 64);
    frame.unmap();
    return frame;
}

void tst_QQuickVideoOutput::paintSurface()
{
    QQuickView window;
    window.setSource(QUrl("qrc:/main.qml"));
    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));

    auto videoOutput = qobject_cast<QQuickVideoOutput *>(window.rootObject());
    QVERIFY(videoOutput);

    auto surface = videoOutput->videoSink();
    QVERIFY(surface);
    videoOutput->setSize(QSize(2, 2));

    surface->setVideoFrame(makeRGB32Frame());
}

void tst_QQuickVideoOutput::sourceRect()
{
    QQmlComponent component(&m_engine);
    component.loadUrl(QUrl("qrc:/main.qml"));

    QObject *videoOutput = component.create();
    QVERIFY(videoOutput != nullptr);

    QMediaPlayer holder(this);

    QSignalSpy propSpy(videoOutput, SIGNAL(sourceRectChanged()));

    holder.setVideoOutput(videoOutput);

    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF());

    presentDummyFrame(holder.videoSink(), QSize(200,100));

    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));
    QCOMPARE(propSpy.size(), 1);

    // Another frame shouldn't cause a source rect change
    presentDummyFrame(holder.videoSink(), QSize(200,100));
    QCOMPARE(propSpy.size(), 1);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    // Changing orientation and stretch modes should not affect this
    videoOutput->setProperty("orientation", QVariant(90));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("orientation", QVariant(180));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("orientation", QVariant(270));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("orientation", QVariant(-90));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::PreserveAspectCrop)));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch)));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    videoOutput->setProperty("fillMode", QVariant(int(QQuickVideoOutput::Stretch)));
    updateOutputGeometry(videoOutput);
    QCOMPARE(videoOutput->property("sourceRect").toRectF(), QRectF(0, 0, 200, 100));

    delete videoOutput;
}

void tst_QQuickVideoOutput::updateOutputGeometry(QObject *output)
{
    // Since the object isn't visible, update() doesn't do anything
    // so we manually force this
    QMetaObject::invokeMethod(output, "_q_updateGeometry");
}

void tst_QQuickVideoOutput::contentRect()
{
    QFETCH(int, orientation);
    QFETCH(QQuickVideoOutput::FillMode, fillMode);
    QFETCH(QRectF, expected);

    QVERIFY(m_mappingOutput);
    m_mappingOutput->setProperty("orientation", QVariant(orientation));
    m_mappingOutput->setProperty("fillMode", QVariant::fromValue(fillMode));

    updateOutputGeometry(m_mappingOutput);

    QRectF output = m_mappingOutput->property("contentRect").toRectF();
    QCOMPARE(output, expected);
}

void tst_QQuickVideoOutput::contentRect_data()
{
    QTest::addColumn<int>("orientation");
    QTest::addColumn<QQuickVideoOutput::FillMode>("fillMode");
    QTest::addColumn<QRectF>("expected");

    QQuickVideoOutput::FillMode stretch = QQuickVideoOutput::Stretch;
    QQuickVideoOutput::FillMode fit = QQuickVideoOutput::PreserveAspectFit;
    QQuickVideoOutput::FillMode crop = QQuickVideoOutput::PreserveAspectCrop;

    // Stretch just keeps the full render rect regardless of orientation
    QTest::newRow("s0") << 0 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s90") << 90 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s180") << 180 << stretch << QRectF(0,0,150,100);
    QTest::newRow("s270") << 270 << stretch << QRectF(0,0,150,100);

    // Fit depends on orientation
    // Source is 200x100, fitting in 150x100 -> 150x75
    // or 100x200 -> 50x100
    QTest::newRow("f0") << 0 << fit << QRectF(0,12.5f,150,75);
    QTest::newRow("f90") << 90 << fit << QRectF(50,0,50,100);
    QTest::newRow("f180") << 180 << fit << QRectF(0,12.5,150,75);
    QTest::newRow("f270") << 270 << fit << QRectF(50,0,50,100);

    // Crop also depends on orientation, may go outside render rect
    // 200x100 -> -25,0 200x100
    // 100x200 -> 0,-100 150x300
    QTest::newRow("c0") << 0 << crop << QRectF(-25,0,200,100);
    QTest::newRow("c90") << 90 << crop << QRectF(0,-100,150,300);
    QTest::newRow("c180") << 180 << crop << QRectF(-25,0,200,100);
    QTest::newRow("c270") << 270 << crop << QRectF(0,-100,150,300);
}

void tst_QQuickVideoOutput::threadedSignalEmission_shouldNotCrash()
{
    using namespace std::chrono_literals;
    using namespace Qt::Literals;
    // artificially defer signal emission on render thread force race condition
    QQuickVideoOutput::setSignalBackoff(100ms);

    for (int i = 0; i != 5; ++i) {
        QQuickView window;
        if (!QLatin1String(QSGRenderLoop::instance()->metaObject()->className())
                     .contains("Threaded"_L1))
            QSKIP("tst_QQuickVideoOutput::render only relevant for threaded rendering");

        window.setSource(QUrl("qrc:/main.qml"));
        window.show();
        QVERIFY(QTest::qWaitForWindowExposed(&window));

        auto videoOutput = qobject_cast<QQuickVideoOutput *>(window.rootObject());
        QVERIFY(videoOutput);

        auto surface = videoOutput->videoSink();
        QVERIFY(surface);
        videoOutput->setSize(QSize(2, 2));

        // set video frame to force QQuickWindow::afterFrameEnd
        surface->setVideoFrame(makeRGB32Frame());

        // delete video output
        QTest::qWait(10ms);
        delete videoOutput;

        QTest::qWait(200ms);
    }
}

QTEST_MAIN(tst_QQuickVideoOutput)

#include "tst_qquickvideooutput.moc"
