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

#include "qvideowindow_p.h"
#include <QPlatformSurfaceEvent>
#include <qfile.h>
#include <qpainter.h>
#include <private/qguiapplication_p.h>
#include <private/qmemoryvideobuffer_p.h>
#include <private/qhwvideobuffer_p.h>
#include <private/qmultimediautils_p.h>
#include <private/qvideoframe_p.h>
#include <qpa/qplatformintegration.h>

QT_BEGIN_NAMESPACE

static QSurface::SurfaceType platformSurfaceType()
{
#if defined(Q_OS_DARWIN)
    return QSurface::MetalSurface;
#elif defined (Q_OS_WIN)
    return QSurface::Direct3DSurface;
#endif

    auto *integration = QGuiApplicationPrivate::platformIntegration();

    if (!integration->hasCapability(QPlatformIntegration::OpenGL))
        return QSurface::RasterSurface;

    if (QCoreApplication::testAttribute(Qt::AA_ForceRasterWidgets))
        return QSurface::RasterSurface;

    return QSurface::OpenGLSurface;
}

QVideoWindowPrivate::QVideoWindowPrivate(QVideoWindow *q)
    : q(q),
    m_sink(new QVideoSink)
{
    Q_ASSERT(q);

    if (QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::RhiBasedRendering)) {
        auto surfaceType = ::platformSurfaceType();
        q->setSurfaceType(surfaceType);
        switch (surfaceType) {
        case QSurface::RasterSurface:
        case QSurface::OpenVGSurface:
        default:
            // can't use those surfaces, need to render in SW
            m_graphicsApi = QRhi::Null;
            break;
        case QSurface::OpenGLSurface:
            m_graphicsApi = QRhi::OpenGLES2;
            break;
        case QSurface::VulkanSurface:
            m_graphicsApi = QRhi::Vulkan;
            break;
        case QSurface::MetalSurface:
            m_graphicsApi = QRhi::Metal;
            break;
        case QSurface::Direct3DSurface:
            m_graphicsApi = QRhi::D3D11;
            break;
        }
    }

    QObject::connect(m_sink.get(), &QVideoSink::videoFrameChanged, q, &QVideoWindow::setVideoFrame);
}

QVideoWindowPrivate::~QVideoWindowPrivate()
{
    QObject::disconnect(m_sink.get(), &QVideoSink::videoFrameChanged,
            q, &QVideoWindow::setVideoFrame);
}

static const float g_vw_quad[] = {
    // 4 clockwise rotation of texture vertexes (the second pair)
    // Rotation 0
    -1.f, -1.f,   0.f, 0.f,
    -1.f, 1.f,    0.f, 1.f,
    1.f, -1.f,    1.f, 0.f,
    1.f, 1.f,     1.f, 1.f,
    // Rotation 90
    -1.f, -1.f,   0.f, 1.f,
    -1.f, 1.f,    1.f, 1.f,
    1.f, -1.f,    0.f, 0.f,
    1.f, 1.f,     1.f, 0.f,

    // Rotation 180
    -1.f, -1.f,   1.f, 1.f,
    -1.f, 1.f,    1.f, 0.f,
    1.f, -1.f,    0.f, 1.f,
    1.f, 1.f,     0.f, 0.f,
    // Rotation 270
    -1.f, -1.f,   1.f, 0.f,
    -1.f, 1.f,    0.f, 0.f,
    1.f, -1.f,    1.f, 1.f,
    1.f, 1.f,     0.f, 1.f
};

static QShader vwGetShader(const QString &name)
{
    QFile f(name);
    if (f.open(QIODevice::ReadOnly))
        return QShader::fromSerialized(f.readAll());

    return QShader();
}

void QVideoWindowPrivate::initRhi()
{
    if (m_graphicsApi == QRhi::Null)
        return;

    [[maybe_unused]] QRhi::Flags rhiFlags = {};//QRhi::EnableDebugMarkers | QRhi::EnableProfiling;

#if QT_CONFIG(opengl)
    if (m_graphicsApi == QRhi::OpenGLES2) {
        m_fallbackSurface.reset(QRhiGles2InitParams::newFallbackSurface(q->format()));
        QRhiGles2InitParams params;
        params.fallbackSurface = m_fallbackSurface.get();
        params.window = q;
        params.format = q->format();
        m_rhi.reset(QRhi::create(QRhi::OpenGLES2, &params, rhiFlags));
    }
#endif

#if QT_CONFIG(vulkan)
    if (m_graphicsApi == QRhi::Vulkan) {
        QRhiVulkanInitParams params;
        params.inst = q->vulkanInstance();
        params.window = q;
        m_rhi.reset(QRhi::create(QRhi::Vulkan, &params, rhiFlags));
    }
#endif

#ifdef Q_OS_WIN
    if (m_graphicsApi == QRhi::D3D11) {
        QRhiD3D11InitParams params;
        params.enableDebugLayer = true;
        m_rhi.reset(QRhi::create(QRhi::D3D11, &params, rhiFlags));
    }
#endif

#if QT_CONFIG(metal)
    if (m_graphicsApi == QRhi::Metal) {
        QRhiMetalInitParams params;
        m_rhi.reset(QRhi::create(QRhi::Metal, &params, rhiFlags));
    }
#endif
    if (!m_rhi)
        return;

    m_swapChain.reset(m_rhi->newSwapChain());
    m_swapChain->setWindow(q);
    m_renderPass.reset(m_swapChain->newCompatibleRenderPassDescriptor());
    m_swapChain->setRenderPassDescriptor(m_renderPass.get());

    m_vertexBuf.reset(m_rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(g_vw_quad)));
    m_vertexBuf->create();
    m_vertexBufReady = false;

    m_uniformBuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(QVideoTextureHelper::UniformData)));
    m_uniformBuf->create();

    m_textureSampler.reset(m_rhi->newSampler(QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
                                             QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge));
    m_textureSampler->create();

    m_shaderResourceBindings.reset(m_rhi->newShaderResourceBindings());
    m_subtitleResourceBindings.reset(m_rhi->newShaderResourceBindings());

    m_subtitleUniformBuf.reset(m_rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, sizeof(QVideoTextureHelper::UniformData)));
    m_subtitleUniformBuf->create();
}

void QVideoWindowPrivate::setupGraphicsPipeline(QRhiGraphicsPipeline *pipeline, QRhiShaderResourceBindings *bindings, const QVideoFrameFormat &fmt)
{

    pipeline->setTopology(QRhiGraphicsPipeline::TriangleStrip);
    QShader vs = vwGetShader(QVideoTextureHelper::vertexShaderFileName(fmt));
    Q_ASSERT(vs.isValid());
    QShader fs = vwGetShader(QVideoTextureHelper::fragmentShaderFileName(
            fmt, m_rhi.get(), m_swapChain->format()));
    Q_ASSERT(fs.isValid());
    pipeline->setShaderStages({
        { QRhiShaderStage::Vertex, vs },
        { QRhiShaderStage::Fragment, fs }
    });
    QRhiVertexInputLayout inputLayout;
    inputLayout.setBindings({
        { 4 * sizeof(float) }
    });
    inputLayout.setAttributes({
        { 0, 0, QRhiVertexInputAttribute::Float2, 0 },
        { 0, 1, QRhiVertexInputAttribute::Float2, 2 * sizeof(float) }
    });
    pipeline->setVertexInputLayout(inputLayout);
    pipeline->setShaderResourceBindings(bindings);
    pipeline->setRenderPassDescriptor(m_renderPass.get());
    pipeline->create();
}

void QVideoWindowPrivate::updateTextures(QRhiResourceUpdateBatch *rub)
{
    // We render a 1x1 black pixel when we don't have a video
    if (!m_texturePool.currentFrame().isValid())
        m_texturePool.setCurrentFrame(QVideoFramePrivate::createFrame(
                std::make_unique<QMemoryVideoBuffer>(QByteArray{ 4, 0 }, 4),
                QVideoFrameFormat(QSize(1, 1), QVideoFrameFormat::Format_RGBA8888)));

    if (!m_texturePool.texturesDirty())
        return;

    QVideoFrameTextures *textures = m_texturePool.updateTextures(*m_rhi, *rub);
    if (!textures)
        return;

    QRhiShaderResourceBinding bindings[4];
    auto *b = bindings;
    *(b++) = QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
                                                   m_uniformBuf.get());

    auto fmt = m_texturePool.currentFrame().surfaceFormat();
    auto textureDesc = QVideoTextureHelper::textureDescription(fmt.pixelFormat());

    for (int i = 0; i < textureDesc->nplanes; ++i)
        (*b++) = QRhiShaderResourceBinding::sampledTexture(
                i + 1, QRhiShaderResourceBinding::FragmentStage, textures->texture(i),
                m_textureSampler.get());
    m_shaderResourceBindings->setBindings(bindings, b);
    m_shaderResourceBindings->create();

    if (fmt != format) {
        format = fmt;
        if (!m_graphicsPipeline)
            m_graphicsPipeline.reset(m_rhi->newGraphicsPipeline());

        setupGraphicsPipeline(m_graphicsPipeline.get(), m_shaderResourceBindings.get(), format);
    }
}

void QVideoWindowPrivate::updateSubtitle(QRhiResourceUpdateBatch *rub, const QSize &frameSize)
{
    m_subtitleDirty = false;
    m_hasSubtitle = !m_texturePool.currentFrame().subtitleText().isEmpty();
    if (!m_hasSubtitle)
        return;

    m_subtitleLayout.update(frameSize, m_texturePool.currentFrame().subtitleText());
    QSize size = m_subtitleLayout.bounds.size().toSize();

    QImage img = m_subtitleLayout.toImage();

    m_subtitleTexture.reset(m_rhi->newTexture(QRhiTexture::RGBA8, size));
    m_subtitleTexture->create();
    rub->uploadTexture(m_subtitleTexture.get(), img);

    QRhiShaderResourceBinding bindings[2];

    bindings[0] = QRhiShaderResourceBinding::uniformBuffer(0, QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
                                                           m_subtitleUniformBuf.get());

    bindings[1] = QRhiShaderResourceBinding::sampledTexture(1, QRhiShaderResourceBinding::FragmentStage,
                                                            m_subtitleTexture.get(), m_textureSampler.get());
    m_subtitleResourceBindings->setBindings(bindings, bindings + 2);
    m_subtitleResourceBindings->create();

    if (!m_subtitlePipeline) {
        m_subtitlePipeline.reset(m_rhi->newGraphicsPipeline());

        QRhiGraphicsPipeline::TargetBlend blend;
        blend.enable = true;
        m_subtitlePipeline->setTargetBlends({ blend });
        setupGraphicsPipeline(m_subtitlePipeline.get(), m_subtitleResourceBindings.get(), QVideoFrameFormat(QSize(1, 1), QVideoFrameFormat::Format_RGBA8888));
    }
}

void QVideoWindowPrivate::init()
{
    if (initialized)
        return;
    initialized = true;

    initRhi();

    if (!m_rhi)
        backingStore = new QBackingStore(q);
    else
        m_sink->setRhi(m_rhi.get());
}

void QVideoWindowPrivate::resizeSwapChain()
{
    m_hasSwapChain = m_swapChain->createOrResize();
}

void QVideoWindowPrivate::releaseSwapChain()
{
    if (m_hasSwapChain) {
        m_hasSwapChain = false;
        m_swapChain->destroy();
    }
}

void QVideoWindowPrivate::render()
{
    if (!initialized)
        init();

    if (!q->isExposed() || !isExposed)
        return;

    QRect rect(0, 0, q->width(), q->height());

    if (backingStore) {
        if (backingStore->size() != q->size())
            backingStore->resize(q->size());

        backingStore->beginPaint(rect);

        QPaintDevice *device = backingStore->paintDevice();
        if (!device)
            return;
        QPainter painter(device);

        QVideoFrame frame = m_texturePool.currentFrame();
        frame.paint(&painter, rect, { Qt::black, aspectRatioMode });
        painter.end();

        backingStore->endPaint();
        backingStore->flush(rect);
        return;
    }

    const VideoTransformation frameTransformation =
            qNormalizedFrameTransformation(m_texturePool.currentFrame().surfaceFormat());
    const QSize frameSize = qRotatedFramePresentationSize(m_texturePool.currentFrame());
    const QSize scaled = frameSize.scaled(rect.size(), aspectRatioMode);
    QRect videoRect = QRect(QPoint(0, 0), scaled);
    videoRect.moveCenter(rect.center());
    QRect subtitleRect = videoRect.intersected(rect);

    if (!m_hasSwapChain || (m_swapChain->currentPixelSize() != m_swapChain->surfacePixelSize()))
        resizeSwapChain();

    const auto requiredSwapChainFormat =
            qGetRequiredSwapChainFormat(m_texturePool.currentFrame().surfaceFormat());
    if (qShouldUpdateSwapChainFormat(m_swapChain.get(), requiredSwapChainFormat)) {
        releaseSwapChain();
        m_swapChain->setFormat(requiredSwapChainFormat);
        resizeSwapChain();
    }

    if (!m_hasSwapChain)
        return;

    QRhi::FrameOpResult r = m_rhi->beginFrame(m_swapChain.get());

    if (r == QRhi::FrameOpSwapChainOutOfDate) {
        resizeSwapChain();
        if (!m_hasSwapChain)
            return;
        r = m_rhi->beginFrame(m_swapChain.get());
    }
    if (r != QRhi::FrameOpSuccess) {
        qWarning("beginFrame failed with %d, retry", r);
        q->requestUpdate();
        return;
    }

    QRhiResourceUpdateBatch *rub = m_rhi->nextResourceUpdateBatch();

    if (!m_vertexBufReady) {
        m_vertexBufReady = true;
        rub->uploadStaticBuffer(m_vertexBuf.get(), g_vw_quad);
    }

    updateTextures(rub);

    if (m_subtitleDirty || m_subtitleLayout.videoSize != subtitleRect.size())
        updateSubtitle(rub, subtitleRect.size());

    const float mirrorFrame = frameTransformation.mirroredHorizontallyAfterRotation ? -1.f : 1.f;
    const float xscale = mirrorFrame * float(videoRect.width()) / float(rect.width());
    const float yscale = -1.f * float(videoRect.height()) / float(rect.height());

    QMatrix4x4 transform;
    transform.scale(xscale, yscale);

    float maxNits = 100;
    if (m_swapChain->format() == QRhiSwapChain::HDRExtendedSrgbLinear) {
        auto info = m_swapChain->hdrInfo();
        if (info.limitsType == QRhiSwapChainHdrInfo::ColorComponentValue)
            maxNits = 100 * info.limits.colorComponentValue.maxColorComponentValue;
        else
            maxNits = info.limits.luminanceInNits.maxLuminance;
    }

    QByteArray uniformData;
    QVideoTextureHelper::updateUniformData(&uniformData, m_rhi.get(),
                                           m_texturePool.currentFrame().surfaceFormat(),
                                           m_texturePool.currentFrame(), transform, 1.f, maxNits);
    rub->updateDynamicBuffer(m_uniformBuf.get(), 0, uniformData.size(), uniformData.constData());

    if (m_hasSubtitle) {
        QMatrix4x4 st;
        st.translate(0, -2.f * (float(m_subtitleLayout.bounds.center().y())  + float(subtitleRect.top()))/ float(rect.height()) + 1.f);
        st.scale(float(m_subtitleLayout.bounds.width())/float(rect.width()),
                -1.f * float(m_subtitleLayout.bounds.height())/float(rect.height()));

        QByteArray uniformData;
        QVideoFrameFormat fmt(m_subtitleLayout.bounds.size().toSize(), QVideoFrameFormat::Format_ARGB8888);
        QVideoTextureHelper::updateUniformData(&uniformData, m_rhi.get(), fmt, QVideoFrame(), st,
                                               1.f);
        rub->updateDynamicBuffer(m_subtitleUniformBuf.get(), 0, uniformData.size(), uniformData.constData());
    }

    QRhiCommandBuffer *cb = m_swapChain->currentFrameCommandBuffer();
    cb->beginPass(m_swapChain->currentFrameRenderTarget(), Qt::black, { 1.0f, 0 }, rub);
    cb->setGraphicsPipeline(m_graphicsPipeline.get());
    auto size = m_swapChain->currentPixelSize();
    cb->setViewport({ 0, 0, float(size.width()), float(size.height()) });
    cb->setShaderResources(m_shaderResourceBindings.get());

    const quint32 vertexOffset = quint32(sizeof(float)) * 16 * frameTransformation.rotationIndex();
    const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuf.get(), vertexOffset);
    cb->setVertexInput(0, 1, &vbufBinding);
    cb->draw(4);

    if (m_hasSubtitle) {
        cb->setGraphicsPipeline(m_subtitlePipeline.get());
        cb->setShaderResources(m_subtitleResourceBindings.get());
        const QRhiCommandBuffer::VertexInput vbufBinding(m_vertexBuf.get(), 0);
        cb->setVertexInput(0, 1, &vbufBinding);
        cb->draw(4);
    }

    cb->endPass();

    m_rhi->endFrame(m_swapChain.get());

    m_texturePool.onFrameEndInvoked();
}

/*!
    \class QVideoWindow
    \internal
*/
QVideoWindow::QVideoWindow(QScreen *screen)
    : QWindow(screen)
    , d(new QVideoWindowPrivate(this))
{
}

QVideoWindow::QVideoWindow(QWindow *parent)
    : QWindow(parent)
    , d(new QVideoWindowPrivate(this))
{
}

QVideoWindow::~QVideoWindow() = default;

QVideoSink *QVideoWindow::videoSink() const
{
    return d->m_sink.get();
}

Qt::AspectRatioMode QVideoWindow::aspectRatioMode() const
{
    return d->aspectRatioMode;
}

void QVideoWindow::setAspectRatioMode(Qt::AspectRatioMode mode)
{
    if (d->aspectRatioMode == mode)
        return;
    d->aspectRatioMode = mode;
    emit aspectRatioModeChanged(mode);
}

bool QVideoWindow::event(QEvent *e)
{
    switch (e->type()) {
    case QEvent::UpdateRequest:
        d->render();
        return true;

    case QEvent::PlatformSurface:
        // this is the proper time to tear down the swapchain (while the native window and surface are still around)
        if (static_cast<QPlatformSurfaceEvent *>(e)->surfaceEventType() == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) {
            d->releaseSwapChain();
            d->isExposed = false;
        }
        break;
    case QEvent::Expose:
        d->isExposed = isExposed();
        if (d->isExposed)
            d->render();
        return true;

    default:
        break;
    }

    return QWindow::event(e);
}

void QVideoWindow::resizeEvent(QResizeEvent *resizeEvent)
{
    if (!d->backingStore)
        return;
    if (!d->initialized)
        d->init();
    d->backingStore->resize(resizeEvent->size());
}

void QVideoWindow::setVideoFrame(const QVideoFrame &frame)
{
    if (d->m_texturePool.currentFrame().subtitleText() != frame.subtitleText())
        d->m_subtitleDirty = true;
    d->m_texturePool.setCurrentFrame(frame);
    if (d->isExposed)
        requestUpdate();
}

QT_END_NAMESPACE

#include "moc_qvideowindow_p.cpp"
