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

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlComponent>
#include <QQuick3DObject>
#include <QVector3D>
#include <QCommandLineParser>
#include <QQuickWindow>
#include <private/qquick3dxrview_p.h>
#include <private/qquick3dsceneenvironment_p.h>
#include <rhi/qrhi.h>

#include <QtCore/qloggingcategory.h>

#ifdef Q_OS_WIN
#  include <fcntl.h>
#  include <io.h>
#endif // Q_OS_WIN

// A valid screen grab requires the scene to not change
// for SCENE_STABLE_TIME ms
#define SCENE_STABLE_TIME 200

static int getLancelotSceneTimeout()
{
    static int sceneTimeout = qEnvironmentVariableIntValue("LANCELOT_SCENE_TIMEOUT");
    if (!sceneTimeout)
        sceneTimeout = 6000; // Default value in ms
    return sceneTimeout;
}

static bool writeOutGrabbedFrame(QImage lastGrab, QSize outputSize, const QString &ofile)
{
    if (outputSize.isValid())
        lastGrab = lastGrab.scaled(outputSize, Qt::KeepAspectRatio);

    if (ofile == "-") {   // Write to stdout
        QFile of;
#ifdef Q_OS_WIN
        // Make sure write to stdout doesn't do LF->CRLF
        _setmode(_fileno(stdout), _O_BINARY);
#endif // Q_OS_WIN
        if (!of.open(1, QIODevice::WriteOnly) || !lastGrab.save(&of, "ppm"))
            qWarning() << "Error: failed to write grabbed image to stdout.";
    } else {
        if (!lastGrab.save(ofile))
            qWarning() << "Error: failed to store grabbed image to" << ofile;
    }

    return true;
}

enum class MultiviewOutputStrategy {
    Single,
    Left,
    Right,
    Separate,
    Merged
};

template <MultiviewOutputStrategy Strategy>
static QString generateOfName(int frame)
{
    if constexpr (Strategy == MultiviewOutputStrategy::Left)
        return QString::asprintf("frame_left_%d.png", frame);
    if constexpr (Strategy == MultiviewOutputStrategy::Right)
        return QString::asprintf("frame_right_%d.png", frame);
    if constexpr (Strategy == MultiviewOutputStrategy::Merged)
        return QString::asprintf("frame_multiview_%d.png", frame);

    return QString::asprintf("frame_%d.png", frame);
}

template <MultiviewOutputStrategy Strategy>
static bool writeOutMultiview(const QImage &left, const QImage &right, QSize outputSize, int frame, QString ofile)
{
    // Assumes the images are of the same size
    if (!outputSize.isValid())
        outputSize = left.size();
    if (!outputSize.isValid())
        outputSize = right.size();

    if constexpr (Strategy != MultiviewOutputStrategy::Separate) {
        if (ofile.isEmpty())
            ofile = generateOfName<Strategy>(frame);
    }

    if constexpr (Strategy == MultiviewOutputStrategy::Left) {
        Q_UNUSED(right);
        return writeOutGrabbedFrame(left, outputSize, ofile);
    }

    if constexpr (Strategy == MultiviewOutputStrategy::Right) {
        Q_UNUSED(left);
        return writeOutGrabbedFrame(right, outputSize, ofile);
    }

    if constexpr (Strategy == MultiviewOutputStrategy::Separate) {
        if (ofile.size() == 1) {
            qWarning() << "Error: separate output strategy requires two output files";
            return false;
        }

        QString ofileLeft;
        QString ofileRight;

        if (ofile.isEmpty()) {
            ofileLeft = generateOfName<MultiviewOutputStrategy::Left>(frame);
            ofileRight = generateOfName<MultiviewOutputStrategy::Right>(frame);
        } else {
            ofileLeft = QStringLiteral("left_") + ofile;
            ofileRight = QStringLiteral("right_") + ofile;
        }

        if (!writeOutGrabbedFrame(left, outputSize, ofileLeft))
            return false;
        return writeOutGrabbedFrame(right, outputSize, ofileRight);
    }

    if constexpr (Strategy == MultiviewOutputStrategy::Merged) {
        QImage mergedImage(left.width() + right.width(), qMax(left.height(), right.height()), QImage::Format_ARGB32);
        QPainter painter(&mergedImage);
        painter.drawImage(0, 0, left);
        painter.drawImage(left.width(), 0, right);
        painter.end();
        return writeOutGrabbedFrame(mergedImage, outputSize, ofile);
    }
}

int main(int argc, char *argv[])
{
    QHashSeed::setDeterministicGlobalSeed();

    QCoreApplication::setAttribute(Qt::AA_DisableShaderDiskCache);
    QGuiApplication app(argc, argv);

    QUnifiedTimer::instance()->setConsistentTiming(true);

    QElapsedTimer elapsedTimer;

    QCommandLineParser cmdLineParser;
    // NOTE: Keep the same behavior as the non-XR scenegrabber which uses single dash word options.
    cmdLineParser.setSingleDashWordOptionMode(QCommandLineParser::SingleDashWordOptionMode::ParseAsLongOptions);
    const QString appDesc = QString::asprintf("qmlxr %s", qVersion());
    cmdLineParser.setApplicationDescription(appDesc);
    app.setApplicationVersion(QStringLiteral(QT_VERSION_STR));
    cmdLineParser.addHelpOption();
    cmdLineParser.addPositionalArgument(QStringLiteral("file"),
                                        QStringLiteral(".qml file with a Node root"),
                                        QStringLiteral("file"));

    QCommandLineOption disableMultiviewOption("disable-multiview", QStringLiteral("Disable multiview rendering (Enabled by default if supported)"));
    cmdLineParser.addOption(disableMultiviewOption);

    QCommandLineOption multiviewStrategyOption("output-strategy", QStringLiteral("Output <strategy> for captured multiview frames are outputed.\n <strategy> should be one of: left, right, separate, or merged (default)"),
                                               QStringLiteral("strategy"),
                                               QStringLiteral("merged"));
    cmdLineParser.addOption(multiviewStrategyOption);

    QCommandLineOption outputSizeOption({ "s", "size" }, QStringLiteral("Output size for the grabbed frame"), QStringLiteral("widthxheight"));
    cmdLineParser.addOption(outputSizeOption);

    QCommandLineOption shaderDebugOption("print-shaders", QStringLiteral("Print generated material shaders"));
    cmdLineParser.addOption(shaderDebugOption);

    QCommandLineOption captureOption({ "c", "capture" }, QStringLiteral("Take RenderDoc frame captures (qtbase must be built with RenderDoc support)"));
    cmdLineParser.addOption(captureOption);


    QCommandLineOption viewonlyOption({ "v", "viewonly"}, QStringLiteral("Only view the scene, do not grab frames"));
    cmdLineParser.addOption(viewonlyOption);

    QCommandLineOption outputOption({ "o", "output" }, QStringLiteral("Output file for the grabbed frame"), QStringLiteral("file"));
    cmdLineParser.addOption(outputOption);

    QCommandLineOption exitOption({ "x", "exit-after" }, QStringLiteral("Exit after <num_frames> frames"), QStringLiteral("num_frames"), QStringLiteral("1"));
    cmdLineParser.addOption(exitOption);

    QCommandLineOption debugOption({ "l", "validate" }, QStringLiteral("Enable D3D12/Vulkan/OpenXR debug or validation layer, if available. Also enables QRhi leak checking."));
    cmdLineParser.addOption(debugOption);

    QCommandLineOption msaaOption({ "a", "msaa" }, QStringLiteral("Request MSAA with <num_samples> samples. <num_samples> is 2 or 4."), QStringLiteral("num_samples"));
    cmdLineParser.addOption(msaaOption);

    QCommandLineOption showControllersOptions("show-controllers", QStringLiteral("Show controllers in the scene"));
    cmdLineParser.addOption(showControllersOptions);

    QCommandLineOption showRuntimeDialogOption("show-runtime-dialog", QStringLiteral("Show runtime dialog"));
    cmdLineParser.addOption(showRuntimeDialogOption);

    QCommandLineOption submitDepthOption({ "e", "submit-depth" }, QStringLiteral("Forces submitting the depth buffer (XR_KHR_composition_layer_depth), if supported."));
    cmdLineParser.addOption(submitDepthOption);

    QCommandLineOption nullOption("backend-null", QStringLiteral("Use QRhi backend Null"));
    cmdLineParser.addOption(nullOption);
    QCommandLineOption glOption("backend-opengl", QStringLiteral("Use QRhi backend OpenGL (ES)"));
    cmdLineParser.addOption(glOption);
    QCommandLineOption vkOption("backend-vulkan", QStringLiteral("Use QRhi backend Vulkan [default on Android]"));
    cmdLineParser.addOption(vkOption);
    QCommandLineOption d3d11Option("backend-d3d11", QStringLiteral("Use QRhi backend Direct3D 11"));
    cmdLineParser.addOption(d3d11Option);
    QCommandLineOption d3d12Option("backend-d3d12", QStringLiteral("Use QRhi backend Direct3D 12 [default on Windows]"));
    cmdLineParser.addOption(d3d12Option);
    QCommandLineOption mtlOption("backend-metal", QStringLiteral("Use QRhi backend Metal"));
    cmdLineParser.addOption(mtlOption);

    QCommandLineOption devOpt("q", QStringLiteral("Enable developer settings and logging"));
    cmdLineParser.addOption(devOpt);

    cmdLineParser.process(app);

    MultiviewOutputStrategy multiviewStrategy = MultiviewOutputStrategy::Merged;

    if (cmdLineParser.isSet(multiviewStrategyOption)) {
        const QString strategy = cmdLineParser.value(multiviewStrategyOption);
        if (strategy == QStringLiteral("left"))
            multiviewStrategy = MultiviewOutputStrategy::Left;
        else if (strategy == QStringLiteral("right"))
            multiviewStrategy = MultiviewOutputStrategy::Right;
        else if (strategy == QStringLiteral("separate"))
            multiviewStrategy = MultiviewOutputStrategy::Separate;
        else if (strategy == QStringLiteral("merged"))
            multiviewStrategy = MultiviewOutputStrategy::Merged;
        else {
            qWarning() << "Error: unknown output strategy" << strategy;
            return 1;
        }
    }

    QString ofile;

    if (cmdLineParser.isSet(outputOption)) {
        ofile = cmdLineParser.value(outputOption);
        if (ofile.isEmpty()) {
            qWarning() << "Error: output file is required in combination with the output option";
            return 1;
        }
    }

    QSize outputSize;
    if (cmdLineParser.isSet(outputSizeOption)) {
        const QStringList size = cmdLineParser.value(outputSizeOption).split('x');
        if (size.size() != 2) {
            qWarning() << "Error: output size must be in the format widthxheight";
            return 1;
        }
        outputSize = QSize(size[0].toInt(), size[1].toInt());
    }

    const auto positionalArguments = cmdLineParser.positionalArguments();
    if (positionalArguments.isEmpty()) {
        cmdLineParser.showHelp();
        return 0;
    }

    const QString filename = positionalArguments.first();

    const bool devOptSet = cmdLineParser.isSet(devOpt);

    if (devOptSet) {
        qputenv("QSG_INFO", "1");
        qputenv("QSG_RHI_PROFILE", "1");
        QLoggingCategory::setFilterRules(QStringLiteral("qt.quick3d.xr=true"));
    }

#ifdef Q_OS_WIN
    QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12);
#endif
#ifdef Q_OS_ANDROID
    QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);
#endif

    if (cmdLineParser.isSet(nullOption))
        QQuickWindow::setGraphicsApi(QSGRendererInterface::Null);
    else if (cmdLineParser.isSet(glOption))
        QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
    else if (cmdLineParser.isSet(vkOption))
        QQuickWindow::setGraphicsApi(QSGRendererInterface::Vulkan);
    else if (cmdLineParser.isSet(d3d11Option))
        QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11);
    else if (cmdLineParser.isSet(d3d12Option))
        QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12);
    else if (cmdLineParser.isSet(mtlOption))
        QQuickWindow::setGraphicsApi(QSGRendererInterface::Metal);

    qputenv("QT_QUICK_CONTROLS_STYLE", "Basic");

    if (cmdLineParser.isSet(disableMultiviewOption))
        qputenv("QT_QUICK3D_XR_DISABLE_MULTIVIEW", "0");

    if (cmdLineParser.isSet(shaderDebugOption))
        qputenv("QT_RHI_SHADER_DEBUG", "1");

    if (cmdLineParser.isSet(captureOption)) {
        qputenv("QT_QUICK3D_SHADER_DEBUG_INFO", "1");
        qputenv("QT_QUICK3D_XR_FRAME_CAPTURE", "1");
    }

    if (cmdLineParser.isSet(debugOption)) {
        qputenv("QSG_RHI_DEBUG_LAYER", "1");
        qputenv("QT_RHI_LEAK_CHECK", "1");
    }

    if (cmdLineParser.isSet(submitDepthOption))
        qputenv("QT_QUICK3D_XR_SUBMIT_DEPTH", "1");

    const bool viewOnly = cmdLineParser.isSet(viewonlyOption);

    QCoreApplication::setApplicationName("Qt XR Runner");
    QCoreApplication::setOrganizationName("The Qt Company");
    QCoreApplication::setApplicationVersion(QT_VERSION_STR);

    QQmlApplicationEngine engine;
    const QUrl url(QStringLiteral("qrc:/QtQuick3D/Lancelot/Xr/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url, viewOnly](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl) {
            qWarning() << "Error: failed to load" << url;
            QCoreApplication::exit(-1);
        } else if (!viewOnly) {
            QTimer::singleShot(getLancelotSceneTimeout(), qApp, [] {
                qWarning() << "Error: timed out waiting for scene to stabilize!";
                QCoreApplication::exit(3);
            });
        }
    }, Qt::QueuedConnection);
    engine.load(url);

    const QList<QObject *> rootObjects = engine.rootObjects();
    std::unique_ptr<QQmlComponent> component;
    int currentSingleViewEye = 0;
    int currentFrame = 1;
    int exitAfter = 1;
    if (cmdLineParser.isSet(exitOption))
        exitAfter = qMax(0, cmdLineParser.value(exitOption).toInt());

    if (!rootObjects.isEmpty()) {
        QQuick3DXrView *xrView = qobject_cast<QQuick3DXrView *>(rootObjects.first());

        if (cmdLineParser.isSet(showControllersOptions))
            xrView->setProperty("showControllers", true);

        if (cmdLineParser.isSet(showRuntimeDialogOption))
            xrView->setProperty("showRuntimeDialog", true);

        component.reset(new QQmlComponent(&engine, QUrl::fromLocalFile(filename)));
        if (component->isError()) {
            for (const QQmlError &error : component->errors()) {
                qWarning() << error.url() << error.line() << error;
                return 1;
            }
        }
        QObject *subRoot = component->create();
        if (component->isError()) {
            for (const QQmlError &error : component->errors()) {
                qWarning() << error.url() << error.line() << error;
                return 1;
            }
        }

        if (QQuick3DObject *obj = qobject_cast<QQuick3DObject *>(subRoot)) {
            obj->setParentItem(xrView);
            QObject *origin = xrView->findChild<QObject *>("xrorigin");
            QVariant posProp = obj->property("qmlxr_originPosition");
            if (!posProp.isNull()) {
                QVector3D camPos = posProp.value<QVector3D>();
                if (devOptSet)
                    qDebug() << "Setting origin position to" << camPos;
                origin->setProperty("position", camPos);
            }
            QVariant rotProp = obj->property("qmlxr_originRotation");
            if (!rotProp.isNull()) {
                QVector3D camRot = rotProp.value<QVector3D>();
                if (devOptSet)
                    qDebug() << "Setting origin rotation to" << camRot;
                origin->setProperty("eulerRotation", camRot);
            }
            QVariant envProp = obj->property("qmlxr_environment");
            if (!envProp.isNull()) {
                QQuick3DSceneEnvironment *env = envProp.value<QQuick3DSceneEnvironment *>();
                if (devOptSet)
                    qDebug() << "Setting SceneEnvironment to" << env;
                xrView->setEnvironment(env);
            }
        } else {
            qWarning() << subRoot << "is not a QQuick3DObject";
            return 1;
        }

        if (cmdLineParser.isSet(msaaOption)) {
            const int samples = qBound(1, cmdLineParser.value(msaaOption).toInt(), 8);
            if (samples > 1) {
                QQuick3DSceneEnvironment *sceneEnv = xrView->environment();
                if (devOptSet)
                    qDebug() << "Requesting MSAA with sample count" << samples << "on" << sceneEnv;
                sceneEnv->setAntialiasingMode(QQuick3DSceneEnvironment::MSAA);
                if (samples == 2)
                    sceneEnv->setAntialiasingQuality(QQuick3DSceneEnvironment::Medium);
                else if (samples == 4)
                    sceneEnv->setAntialiasingQuality(QQuick3DSceneEnvironment::High);
                else
                    sceneEnv->setAntialiasingQuality(QQuick3DSceneEnvironment::VeryHigh);
            }
        }

        const bool grab = !cmdLineParser.isSet(viewonlyOption) && !cmdLineParser.isSet(captureOption);

        if (grab) {
            QObject::connect(xrView, &QQuick3DXrView::frameReady, xrView, [&]() {
                if (!elapsedTimer.isValid())
                    elapsedTimer.start();
                if (elapsedTimer.elapsed() < SCENE_STABLE_TIME)
                    return;

                QRhiTexture *colorBuffer = nullptr;
                auto *view = QQuick3DXrViewPrivate::getView3d(xrView);
                if (view && view->window()) {
                    QQuickWindow *quickWindow = view->window();
                    QRhiRenderTarget *rt = QQuickWindowPrivate::get(quickWindow)->activeCustomRhiRenderTarget();
                    if (rt && rt->resourceType() == QRhiResource::TextureRenderTarget)
                        colorBuffer = static_cast<QRhiTextureRenderTarget *>(rt)->description().colorAttachmentAt(0)->texture();
                }
                if (!colorBuffer)
                    return;

                const int viewCount = qMax(1, colorBuffer->arraySize());

                if (!cmdLineParser.isSet(viewonlyOption)) {
                    QRhi *rhi = colorBuffer->rhi();
                    QRhiCommandBuffer *cb = nullptr;
                    if (rhi->beginOffscreenFrame(&cb) != QRhi::FrameOpSuccess)
                        return;
                    QRhiResourceUpdateBatch *resourceUpdates = rhi->nextResourceUpdateBatch();
                    QVarLengthArray<QImage, 2> images(viewCount);
                    QVarLengthArray<QRhiReadbackResult, 2> results(viewCount);
                    QImage::Format imageFormat = QImage::Format_ARGB32;
                    for (int layer = 0; layer < viewCount; ++layer) {
                        results[layer].completed = [&results, &imageFormat, &images, rhi, layer] {
                            const uchar *p = reinterpret_cast<const uchar *>(results[layer].data.constData());
                            imageFormat = results[layer].format == QRhiTexture::BGRA8 ? QImage::Format_ARGB32 : QImage::Format_RGBA8888;
                            const QImage img(p, results[layer].pixelSize.width(), results[layer].pixelSize.height(), imageFormat);
                            // the order of the callbacks is undefined, hence must rely on the layer index
                            if (rhi->isYUpInFramebuffer())
                                images[layer] = img.flipped();
                            else
                                images[layer] = img.copy();
                        };
                        QRhiReadbackDescription readbackDesc(colorBuffer);
                        readbackDesc.setLayer(layer);
                        resourceUpdates->readBackTexture(readbackDesc, &results[layer]);
                    }
                    cb->resourceUpdate(resourceUpdates);
                    rhi->endOffscreenFrame();

                    if (images.count() >= 2) {
                        // multiview, this is the complete frame (2D texture array)
                        const auto &left = images.at(0);
                        const auto &right = images.at(1);

                        switch (multiviewStrategy) {
                        case MultiviewOutputStrategy::Single:
                            Q_FALLTHROUGH();
                        case MultiviewOutputStrategy::Left:
                            writeOutMultiview<MultiviewOutputStrategy::Left>(left, {}, outputSize, currentFrame, ofile);
                            break;
                        case MultiviewOutputStrategy::Right:
                            writeOutMultiview<MultiviewOutputStrategy::Right>({}, right, outputSize, currentFrame, ofile);
                            break;
                        case MultiviewOutputStrategy::Separate:
                            writeOutMultiview<MultiviewOutputStrategy::Separate>(left, right, outputSize, currentFrame, ofile);
                            break;
                        case MultiviewOutputStrategy::Merged:
                            writeOutMultiview<MultiviewOutputStrategy::Merged>(left, right, outputSize, currentFrame, ofile);
                            break;
                        }
                    } else if (images.count() == 1) {
                        // this is either the left or right eye content (2D texture)
                        if (ofile.isEmpty())
                            ofile = generateOfName<MultiviewOutputStrategy::Single>(currentFrame);
                        writeOutGrabbedFrame(images[0], outputSize, ofile);
                    }
                }

                if (viewCount >= 2) {
                    currentFrame += 1;
                } else {
                    currentSingleViewEye = (currentSingleViewEye + 1) % 2;
                    if (currentSingleViewEye == 0)
                        currentFrame += 1;
                }

                if (exitAfter > 0 && currentFrame > exitAfter)
                    QCoreApplication::exit();

            }, Qt::DirectConnection);
        }
    }

    return app.exec();
}
