// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include <QMainWindow>
#include <QApplication>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMenuBar>
#include <QStatusBar>
#include <QFileDialog>
#include <QMessageBox>
#include <QLabel>
#include <QScrollArea>
#include <QGridLayout>
#include <QPushButton>
#include <QSlider>
#include <QCheckBox>
#include <QScrollBar>
#include <QListWidget>
#include <QPainter>

#include <QQuickWindow>
#include <QQuickRenderControl>
#include <QQuickRenderTarget>
#include <QQuickGraphicsDevice>
#include <QQuickGraphicsConfiguration>
#include <QQuickItem>
#include <QQmlEngine>
#include <QQmlComponent>

#include <QAnimationDriver>
#include <QElapsedTimer>

#include <rhi/qrhi.h>

//! [anim-driver]
class AnimationDriver : public QAnimationDriver
{
public:
    AnimationDriver(QObject *parent = nullptr)
        : QAnimationDriver(parent),
          m_step(16)
    {
    }

    void setStep(int milliseconds)
    {
        m_step = milliseconds;
    }

    void advance() override
    {
        m_elapsed += m_step;
        advanceAnimation();
    }

    qint64 elapsed() const override
    {
        return m_elapsed;
    }

private:
    int m_step;
    qint64 m_elapsed = 0;
};
//! [anim-driver]

class ImageLabel;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();
    ~MainWindow();

    void load(const QString &filename);
    void render();

private slots:
    void openRequested();

private:
    void reset();
    void stepAnimations();

    AnimationDriver *m_animationDriver = nullptr;

    std::unique_ptr<QQuickRenderControl> m_renderControl;
    std::unique_ptr<QQuickWindow> m_scene;
    std::unique_ptr<QQmlEngine> m_qmlEngine;
    std::unique_ptr<QQmlComponent> m_qmlComponent;

    std::unique_ptr<QRhiTexture> m_texture;
    std::unique_ptr<QRhiRenderBuffer> m_ds;
    std::unique_ptr<QRhiTextureRenderTarget> m_rt;
    std::unique_ptr<QRhiRenderPassDescriptor> m_rpDesc;

    qint64 m_frameCount = 0;
    QSize m_thumbnailSize;
    QVector<double> m_cpuTimes;
    QVector<double> m_gpuTimes;

    QLabel *m_apiMsg;
    QLabel *m_statusMsg;
    QLabel *m_avgCpuMsg;
    QLabel *m_avgGpuMsg;
    QLabel *m_driverInfoMsg;

    QScrollArea *m_scrollArea;
    QGridLayout *m_grid;
    QVector<QWidget *> m_gridWidgets;

public:
#if QT_CONFIG(vulkan)
    QVulkanInstance *m_vulkanInstance = nullptr;
#endif

    ImageLabel *m_focus = nullptr;
    QLabel *m_fullSizeViewerWindow = nullptr;
    QVector<QImage> m_frames;

    bool m_mirrorVertically = false;
};

MainWindow::MainWindow()
{
    QWidget *centralWidget = new QWidget(this);
    QVBoxLayout *vlayout = new QVBoxLayout(centralWidget);
    QHBoxLayout *controlLayout = new QHBoxLayout;
    vlayout->addLayout(controlLayout);

    QPushButton *btn = new QPushButton(tr("Next frame"));
    QObject::connect(btn, &QPushButton::clicked, btn, [this] {
        render();
    });
    controlLayout->addWidget(btn);

    QPushButton *btnMulti = new QPushButton(tr("Next 10 frames"));
    QObject::connect(btnMulti, &QPushButton::clicked, btn, [this] {
        for (int i = 0; i < 10; ++i)
            QMetaObject::invokeMethod(this, &MainWindow::render, Qt::QueuedConnection);
    });
    controlLayout->addWidget(btnMulti);

//! [anim-slider]
    QSlider *animSlider = new QSlider;
    animSlider->setOrientation(Qt::Horizontal);
    animSlider->setMinimum(1);
    animSlider->setMaximum(1000);
    QLabel *animLabel = new QLabel;
    QObject::connect(animSlider, &QSlider::valueChanged, animSlider, [this, animLabel, animSlider] {
        if (m_animationDriver)
            m_animationDriver->setStep(animSlider->value());
        animLabel->setText(tr("Simulated elapsed time per frame: %1 ms").arg(animSlider->value()));
    });
    animSlider->setValue(16);
    QCheckBox *animCheckBox = new QCheckBox(tr("Custom animation driver"));
    animCheckBox->setToolTip(tr("Note: Installing the custom animation driver makes widget drawing unreliable, depending on the platform.\n"
                                "This is due to widgets themselves relying on QPropertyAnimation and similar, which are driven by the same QAnimationDriver.\n"
                                "In any case, the functionality of the widgets are not affected, just the rendering may lag behind.\n"
                                "When not checked, Qt Quick animations advance based on the system time, i.e. the time elapsed since the last press of the Next button."));
    QObject::connect(animCheckBox, &QCheckBox::checkStateChanged, animCheckBox, [this, animCheckBox, animSlider, animLabel] {
        if (animCheckBox->isChecked()) {
            animSlider->setEnabled(true);
            animLabel->setEnabled(true);
            m_animationDriver = new AnimationDriver(this);
            m_animationDriver->install();
            m_animationDriver->setStep(animSlider->value());
        } else {
            animSlider->setEnabled(false);
            animLabel->setEnabled(false);
            delete m_animationDriver;
            m_animationDriver = nullptr;
        }
    });
    animSlider->setEnabled(false);
    animLabel->setEnabled(false);
    controlLayout->addWidget(animCheckBox);
    controlLayout->addWidget(animLabel);
    controlLayout->addWidget(animSlider);
//! [anim-slider]

    QCheckBox *mirrorCheckBox = new QCheckBox(tr("Mirror vertically"));
    QObject::connect(mirrorCheckBox, &QCheckBox::checkStateChanged, mirrorCheckBox, [this, mirrorCheckBox] {
        m_mirrorVertically = mirrorCheckBox->isChecked();
    });
    controlLayout->addWidget(mirrorCheckBox);

    QGridLayout *gridLayout = new QGridLayout;
    vlayout->addLayout(gridLayout);

    QWidget *viewport = new QWidget;
    m_grid = new QGridLayout(viewport);
    m_scrollArea = new QScrollArea;
    m_scrollArea->setWidgetResizable(true);
    m_scrollArea->setWidget(viewport);
    QObject::connect(m_scrollArea->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this] {
        m_scrollArea->verticalScrollBar()->setSliderPosition(m_scrollArea->verticalScrollBar()->maximum());
    });

    gridLayout->addWidget(m_scrollArea);
    setCentralWidget(centralWidget);

    QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(tr("&Open"), this, &MainWindow::openRequested);
    fileMenu->addAction(tr("E&xit"), qApp, &QCoreApplication::quit);

    m_statusMsg = new QLabel;
    statusBar()->addWidget(m_statusMsg);
    m_avgCpuMsg = new QLabel;
    statusBar()->addWidget(m_avgCpuMsg);
    m_avgGpuMsg = new QLabel;
    statusBar()->addWidget(m_avgGpuMsg);
    m_apiMsg = new QLabel;
    statusBar()->addWidget(m_apiMsg);
    m_driverInfoMsg = new QLabel;
    statusBar()->addWidget(m_driverInfoMsg);
}

MainWindow::~MainWindow()
{
    delete m_fullSizeViewerWindow;
}

void MainWindow::openRequested()
{
    QString filename = QFileDialog::getOpenFileName(this, tr("Open file"), QString(), tr("QML files (*.qml);;All files (*.*)"));
    if (!filename.isEmpty())
        load(filename);
}

void MainWindow::reset()
{
    m_rpDesc.reset();
    m_rt.reset();
    m_ds.reset();
    m_texture.reset();

    m_qmlComponent.reset();
    m_qmlEngine.reset();
    m_scene.reset();
    m_renderControl.reset();

    m_apiMsg->setText(QString());
    m_statusMsg->setText(QString());
    m_avgCpuMsg->setText(QString());
    m_avgGpuMsg->setText(QString());
    m_driverInfoMsg->setText(QString());

    m_frameCount = 0;

    qDeleteAll(m_gridWidgets);
    m_gridWidgets.clear();

    delete m_fullSizeViewerWindow;
    m_fullSizeViewerWindow = nullptr;
    m_focus = nullptr;
}

//! [load-1]
void MainWindow::load(const QString &filename)
{
    reset();

    m_renderControl.reset(new QQuickRenderControl);
    m_scene.reset(new QQuickWindow(m_renderControl.get()));

    // enable lastCompletedGpuTime() on QRhiCommandBuffer, if supported by the underlying 3D API
    QQuickGraphicsConfiguration config;
    config.setTimestamps(true);
    m_scene->setGraphicsConfiguration(config);

#if QT_CONFIG(vulkan)
    if (m_scene->graphicsApi() == QSGRendererInterface::Vulkan)
        m_scene->setVulkanInstance(m_vulkanInstance);
#endif

    m_qmlEngine.reset(new QQmlEngine);
    m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.get(), QUrl::fromLocalFile(filename)));
    if (m_qmlComponent->isError()) {
        for (const QQmlError &error : m_qmlComponent->errors())
            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to load %1").arg(filename));
        reset();
        return;
    }
//! [load-1]

//! [load-instantiate]
    QObject *rootObject = m_qmlComponent->create();
    if (m_qmlComponent->isError()) {
        for (const QQmlError &error : m_qmlComponent->errors())
            qWarning() << error.url() << error.line() << error;
        QMessageBox::critical(this, tr("Cannot load QML scene"), tr("Failed to create component"));
        reset();
        return;
    }

    QQuickItem *rootItem = qobject_cast<QQuickItem *>(rootObject);
    if (!rootItem) {
        // Get rid of the on-screen window, if the root object was a Window
        if (QQuickWindow *w = qobject_cast<QQuickWindow *>(rootObject))
            delete w;
        QMessageBox::critical(this,
                              tr("Invalid root item in QML scene"),
                              tr("Root object is not a QQuickItem. If this is a scene with Window in it, note that such scenes are not supported."));
        reset();
        return;
    }

    if (rootItem->size().width() < 16)
        rootItem->setSize(QSizeF(640, 360));

    m_scene->contentItem()->setSize(rootItem->size());
    m_scene->setGeometry(0, 0, rootItem->width(), rootItem->height());

    rootItem->setParentItem(m_scene->contentItem());

    m_statusMsg->setText(tr("QML scene loaded"));
//! [load-instantiate]

//! [load-graphicsinit]
    const bool initSuccess = m_renderControl->initialize();
    if (!initSuccess) {
        QMessageBox::critical(this, tr("Cannot initialize renderer"), tr("QQuickRenderControl::initialize() failed"));
        reset();
        return;
    }

    const QSGRendererInterface::GraphicsApi api = m_scene->rendererInterface()->graphicsApi();
    switch (api) {
    case QSGRendererInterface::OpenGL:
        m_apiMsg->setText(tr("OpenGL"));
        break;
    case QSGRendererInterface::Direct3D11:
        m_apiMsg->setText(tr("D3D11"));
        break;
    case QSGRendererInterface::Direct3D12:
        m_apiMsg->setText(tr("D3D12"));
        break;
    case QSGRendererInterface::Vulkan:
        m_apiMsg->setText(tr("Vulkan"));
        break;
    case QSGRendererInterface::Metal:
        m_apiMsg->setText(tr("Metal"));
        break;
    default:
        m_apiMsg->setText(tr("Unknown 3D API"));
        break;
    }

    QRhi *rhi = m_renderControl->rhi();
    if (!rhi) {
        QMessageBox::critical(this, tr("Cannot render"), tr("No QRhi from QQuickRenderControl"));
        reset();
        return;
    }

    m_driverInfoMsg->setText(QString::fromUtf8(rhi->driverInfo().deviceName));
//! [load-graphicsinit]

//! [texture-setup]
    const QSize pixelSize = rootItem->size().toSize(); // no scaling, i.e. the item size is in pixels

    m_texture.reset(rhi->newTexture(QRhiTexture::RGBA8, pixelSize, 1,
                                    QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource));
    if (!m_texture->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create texture object"));
        reset();
        return;
    }

    m_ds.reset(rhi->newRenderBuffer(QRhiRenderBuffer::DepthStencil, pixelSize, 1));
    if (!m_ds->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create depth-stencil buffer"));
        reset();
        return;
    }

    QRhiTextureRenderTargetDescription rtDesc(QRhiColorAttachment(m_texture.get()));
    rtDesc.setDepthStencilBuffer(m_ds.get());
    m_rt.reset(rhi->newTextureRenderTarget(rtDesc));
    m_rpDesc.reset(m_rt->newCompatibleRenderPassDescriptor());
    m_rt->setRenderPassDescriptor(m_rpDesc.get());
    if (!m_rt->create()) {
        QMessageBox::critical(this, tr("Cannot render"), tr("Cannot create render target"));
        reset();
        return;
    }

    m_scene->setRenderTarget(QQuickRenderTarget::fromRhiRenderTarget(m_rt.get()));
//! [texture-setup]

    render();
}

class ImageLabel : public QLabel
{
public:
    ImageLabel(MainWindow *mw, int frameIndex)
        : m_mainWindow(mw),
          m_frameIndex(frameIndex)
    {
    }

    void mousePressEvent(QMouseEvent *) override {
        if (m_mainWindow->m_focus != this) {
            ImageLabel *oldFocus = m_mainWindow->m_focus;
            m_mainWindow->m_focus = this;
            if (oldFocus)
                oldFocus->update();
            update();
        }
    }

    void paintEvent(QPaintEvent *e) override {

        if (m_mainWindow->m_focus == this) {
            QPainter p(this);
            p.fillRect(0, 0, width(), height(), Qt::blue);
            p.end();
            QLabel::paintEvent(e);
            p.begin(this);
            p.setOpacity(0.5);
            p.fillRect(0, 0, width(), height(), Qt::blue);
        } else {
            QLabel::paintEvent(e);
        }
    }

    void mouseDoubleClickEvent(QMouseEvent *) override {
        QLabel *&w(m_mainWindow->m_fullSizeViewerWindow);
        if (!w)
            w = new QLabel;
        const QImage &image(m_mainWindow->m_frames[m_frameIndex]);
        w->resize(image.width(), image.height());
        w->setPixmap(QPixmap::fromImage(image));
        w->show();
        w->activateWindow();
    }

    MainWindow *m_mainWindow;
    int m_frameIndex;
};

void MainWindow::render()
{
    if (!m_renderControl)
        return;

    if (m_frameCount > 0)
        stepAnimations();

    // this is only here to communicate the possibly changed mirrorVertically flag
    QQuickRenderTarget quickRt = QQuickRenderTarget::fromRhiRenderTarget(m_rt.get());
    quickRt.setMirrorVertically(m_mirrorVertically);
    m_scene->setRenderTarget(quickRt);

//! [render-core]
    QElapsedTimer cpuTimer;
    cpuTimer.start();

    m_renderControl->polishItems();

    m_renderControl->beginFrame();

    m_renderControl->sync();
    m_renderControl->render();

    QRhi *rhi = m_renderControl->rhi();
    QRhiReadbackResult readResult;
    QRhiResourceUpdateBatch *readbackBatch = rhi->nextResourceUpdateBatch();
    readbackBatch->readBackTexture(m_texture.get(), &readResult);
    m_renderControl->commandBuffer()->resourceUpdate(readbackBatch);

    m_renderControl->endFrame();

    const double gpuRenderTimeMs = m_renderControl->commandBuffer()->lastCompletedGpuTime() * 1000.0;
    const double cpuRenderTimeMs = cpuTimer.nsecsElapsed() / 1000000.0;

    // m_renderControl->begin/endFrame() is based on QRhi's
    // begin/endOffscreenFrame() under the hood, meaning it does not do
    // pipelining, unlike swapchain-based frames, and therefore the readback is
    // guaranteed to complete once endFrame() returns.
    QImage wrapperImage(reinterpret_cast<const uchar *>(readResult.data.constData()),
                    readResult.pixelSize.width(), readResult.pixelSize.height(),
                    QImage::Format_RGBA8888_Premultiplied);
    QImage result;
    if (rhi->isYUpInFramebuffer())
        result = wrapperImage.mirrored();
    else
        result = wrapperImage.copy();

//! [render-core]

    m_gpuTimes.append(gpuRenderTimeMs);
    m_cpuTimes.append(cpuRenderTimeMs);

    m_frames.append(result);

    if (m_thumbnailSize.isEmpty())
        m_thumbnailSize = size() / 4;

    const QImage thumbnail = result.scaled(m_thumbnailSize, Qt::KeepAspectRatio);

    ImageLabel *image = new ImageLabel(this, m_frames.count() - 1);
    image->setPixmap(QPixmap::fromImage(thumbnail));
    image->setAlignment(Qt::AlignCenter);

    QLabel *label = new QLabel(tr("Frame %1\nCPU: %2 ms GPU: %3 ms").arg(m_frameCount).arg(cpuRenderTimeMs, 0, 'f', 4).arg(gpuRenderTimeMs, 0, 'f', 4));
    label->setAlignment(Qt::AlignCenter);
    QWidget *container = new QWidget;
    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(image);
    layout->addWidget(label);
    container->setLayout(layout);
    m_grid->addWidget(container, m_frameCount / 3, m_frameCount % 3);
    m_gridWidgets.append(container);

    m_scrollArea->verticalScrollBar()->setSliderPosition(m_scrollArea->verticalScrollBar()->maximum());

    m_frameCount += 1;

    double v = 0;
    for (double t : std::as_const(m_cpuTimes))
        v += t;
    v /= m_cpuTimes.count();
    m_avgCpuMsg->setText(tr("Avg. CPU render time: %1 ms").arg(v, 0, 'f', 4));
    if (m_cpuTimes.count() > 64) {
        m_cpuTimes.clear();
        m_cpuTimes.append(v);
    }
    v = 0;
    for (double t : std::as_const(m_gpuTimes))
        v += t;
    v /= m_gpuTimes.count();
    m_avgGpuMsg->setText(tr("Avg. GPU render time: %1 ms").arg(v, 0, 'f', 4));
    if (m_gpuTimes.count() > 64) {
        m_gpuTimes.clear();
        m_gpuTimes.append(v);
    }
}

//! [anim-step]
void MainWindow::stepAnimations()
{
    if (m_animationDriver) {
        // Now the Qt Quick scene will think that <slider value> milliseconds have
        // elapsed and update animations accordingly when doing the next frame.
        m_animationDriver->advance();
    }
}
//! [anim-step]

int main(int argc, char **argv)
{
    QApplication app(argc, argv);

#if QT_CONFIG(vulkan)
    QVulkanInstance vulkanInstance;
#endif

    MainWindow mainWindow;

//! [apiselect]
    QDialog apiSelect;
    QVBoxLayout *selLayout = new QVBoxLayout;
    selLayout->addWidget(new QLabel(QObject::tr("Select graphics API to use")));
    QListWidget *apiList = new QListWidget;
    QVarLengthArray<QSGRendererInterface::GraphicsApi, 5> apiValues;
#ifdef Q_OS_WIN
    apiList->addItem("Direct3D 11");
    apiValues.append(QSGRendererInterface::Direct3D11);
    apiList->addItem("Direct3D 12");
    apiValues.append(QSGRendererInterface::Direct3D12);
#endif
#if QT_CONFIG(metal)
    apiList->addItem("Metal");
    apiValues.append(QSGRendererInterface::Metal);
#endif
#if QT_CONFIG(vulkan)
    apiList->addItem("Vulkan");
    apiValues.append(QSGRendererInterface::Vulkan);
#endif
#if QT_CONFIG(opengl)
    apiList->addItem("OpenGL / OpenGL ES");
    apiValues.append(QSGRendererInterface::OpenGL);
#endif
    if (apiValues.isEmpty()) {
        QMessageBox::critical(nullptr, QObject::tr("No 3D graphics API"), QObject::tr("No 3D graphics APIs are supported in this Qt build"));
        return 1;
    }
//! [apiselect]
    apiList->setCurrentRow(0);
    selLayout->addWidget(apiList);
    QPushButton *okBtn = new QPushButton("Ok");
    okBtn->setDefault(true);
    selLayout->addWidget(okBtn);
    apiSelect.setLayout(selLayout);
    apiSelect.resize(320, 200);
    apiSelect.show();

    QObject::connect(okBtn, &QPushButton::clicked, okBtn, [apiList, &apiSelect, &apiValues, &mainWindow
#if QT_CONFIG(vulkan)
                                                           , &vulkanInstance
#endif
    ] {
        const QSGRendererInterface::GraphicsApi api = apiValues[apiList->currentRow()];
        QQuickWindow::setGraphicsApi(api);

#if QT_CONFIG(vulkan)
        if (api == QSGRendererInterface::Vulkan) {
            vulkanInstance.setExtensions(QQuickGraphicsConfiguration::preferredInstanceExtensions());
            if (!vulkanInstance.create()) {
                QMessageBox::critical(nullptr, QObject::tr("Cannot initialize Vulkan"), QObject::tr("Failed to create VkInstance"));
                return;
            }
            mainWindow.m_vulkanInstance = &vulkanInstance;
        }
#endif

        mainWindow.resize(1280, 720);
        mainWindow.show();
        mainWindow.load(QLatin1String(":/demo.qml"));
        // load() renders one frame, add 19 more
        for (int i = 1; i <= 19; ++i) {
            mainWindow.render();
            // have to process events, e.g. to get queued metacalls delivered
            QCoreApplication::processEvents();
        }

        apiSelect.close();
    });

    return app.exec();
}

#include "main.moc"
