/*
 * Copyright (C) 2024, KylinSoft Co., Ltd.

 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include <QSGImageNode>
#include <QSGTexture>

#include <QOpenGLContext>
#include <QOpenGLTexture>

#include "ThumbnailItem.h"

#include <QGuiApplication>
#include <libdrm/drm_fourcc.h>
#include <qpa/qplatformnativeinterface.h>

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <sys/mman.h>

class ThumbnailItem::Private
{
  public:
    Private(ThumbnailItem *item);
    ~Private();

    void createEglImage(Thumbnail *thumbnail);
    void imageFromMemfd(Thumbnail *thumbnail);

    EGLImage m_image = EGL_NO_IMAGE_KHR;
    QImage image;
    uint32_t format;
    QString m_id;
    QString m_outputid;
    void *mem_ptr = nullptr;
    Thumbnail::BufferFlags thumFlags = Thumbnail::BufferFlag::Dmabuf;

    uint8_t *map = nullptr;
    size_t dataSize = 0;
  private:
    ThumbnailItem *t;
};

ThumbnailItem::Private::Private(ThumbnailItem *item) : t(item) {}

ThumbnailItem::Private::~Private() {}

QSGNode *ThumbnailItem::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
    QSGTexture *texture = NULL;

    if (pri->thumFlags & Thumbnail::BufferFlag::Dmabuf) {
        if (pri->m_image == EGL_NO_IMAGE_KHR) {
            QImage q_image(200, 200, QImage::Format_ARGB32_Premultiplied);
            q_image.fill(Qt::transparent);
            texture = window()->createTextureFromImage(q_image, QQuickWindow::TextureIsOpaque);
        } else {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            const auto *context = window()->openglContext();
#else
            const auto *context = static_cast<QOpenGLContext *>(window()->rendererInterface()->getResource(window(), QSGRendererInterface::OpenGLContextResource));
#endif
            if (!context || !context->isValid()) {
                qWarning() << "OpenGL context is not valid.";
                return NULL;
            }

            QOpenGLTexture *m_texture = new QOpenGLTexture(QOpenGLTexture::Target2D);
            bool created = m_texture->create();
            Q_ASSERT(created);

            static auto s_glEGLImageTargetTexture2DOES =
                (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress(
                    "glEGLImageTargetTexture2DOES");
            if (!s_glEGLImageTargetTexture2DOES) {
                qWarning() << "glEGLImageTargetTexture2DOES is not available" << window();
                return NULL;
            }
            m_texture->bind();
            s_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, (GLeglImageOES)pri->m_image);

            m_texture->setWrapMode(QOpenGLTexture::ClampToEdge);
            m_texture->setMinMagFilters(QOpenGLTexture::Linear, QOpenGLTexture::Linear);
            m_texture->release();
            m_texture->setSize(boundingRect().toRect().size().width(), boundingRect().toRect().size().height());

            int textureId = m_texture->textureId();

            QQuickWindow::CreateTextureOption textureOption =
                pri->format == DRM_FORMAT_ARGB8888 ? QQuickWindow::TextureHasAlphaChannel
                                                   : QQuickWindow::TextureIsOpaque;
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
            texture = window()->createTextureFromNativeObject(QQuickWindow::NativeObjectTexture,
                                                              &textureId, 0 /*a vulkan thing?*/,
                                                              window()->size(), textureOption);
#else
            texture = QNativeInterface::QSGOpenGLTexture::fromNative(textureId, window(),window()->size(), textureOption);
#endif
        }
    } else {
        if (pri->image.isNull()) {
            qWarning() << "image.isNull() " << strerror(errno);
            QImage errorImage(200, 200, QImage::Format_ARGB32_Premultiplied);
            errorImage.fill(Qt::red);
            pri->image = errorImage;
        }

        texture = window()->createTextureFromImage(pri->image, QQuickWindow::TextureIsOpaque);
    }

    QSGImageNode *textureNode = static_cast<QSGImageNode *>(oldNode);
    if (!textureNode) {
        textureNode = window()->createImageNode();
        textureNode->setOwnsTexture(true);
    }
    textureNode->setTexture(texture);

    const auto br = boundingRect().toRect();
    textureNode->setRect(br);

    return textureNode;
}

ThumbnailItem::ThumbnailItem(QQuickItem *parent) : QQuickItem(parent), pri(new Private(this))
{
    setFlag(ItemHasContents, true); // 必须设置，表明这个Item有内容需要绘制

    QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface();
    if (!native) {
        return;
    }

    m_display = reinterpret_cast<wl_display *>(
        native->nativeResourceForIntegration(QByteArrayLiteral("wl_display")));
    if (!m_display) {
        qWarning("Failed to get Wayland display.");
        return;
    }
}

ThumbnailItem::~ThumbnailItem()
{
    if(m_active && !m_thumbnailIsDeleted) {
        if (pri->map) {
            munmap(pri->map, pri->dataSize);
            pri->map = nullptr;
        }
        thumbnail->destory();
    }
}

#define ADD_ATTRIB(name, value)                                                                    \
    do {                                                                                           \
        attribs[num_attribs++] = (name);                                                           \
        attribs[num_attribs++] = (value);                                                          \
        attribs[num_attribs] = EGL_NONE;                                                           \
    } while (0)

void ThumbnailItem::Private::createEglImage(Thumbnail *thumbnail)
{
    EGLDisplay display = EGL_NO_DISPLAY;
    display = static_cast<EGLDisplay>(
        QGuiApplication::platformNativeInterface()->nativeResourceForIntegration("egldisplay"));

    if (display == EGL_NO_DISPLAY) {
        qWarning() << "egl get display failed ";
        return;
    }

    if (m_image) {
        if (thumFlags & Thumbnail::BufferFlag::Reused)
            return;
        static auto eglDestroyImageKHR =
            (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
        eglDestroyImageKHR(display, m_image);
    }

    EGLint attribs[20] = { EGL_NONE };
    int num_attribs = 0;

    ADD_ATTRIB(EGL_WIDTH, thumbnail->size().width());
    ADD_ATTRIB(EGL_HEIGHT, thumbnail->size().height());
    ADD_ATTRIB(EGL_LINUX_DRM_FOURCC_EXT, thumbnail->format());
    ADD_ATTRIB(EGL_DMA_BUF_PLANE0_FD_EXT, thumbnail->fd());
    ADD_ATTRIB(EGL_DMA_BUF_PLANE0_OFFSET_EXT, thumbnail->offset());
    ADD_ATTRIB(EGL_DMA_BUF_PLANE0_PITCH_EXT, thumbnail->stride());
    if (thumbnail->modifier() != DRM_FORMAT_MOD_INVALID) {
        ADD_ATTRIB(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, thumbnail->modifier() & 0xFFFFFFFF);
        ADD_ATTRIB(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, thumbnail->modifier() >> 32);
    }
    ADD_ATTRIB(EGL_IMAGE_PRESERVED_KHR, EGL_TRUE);

    format = thumbnail->format();

    static auto eglCreateImageKHR =
        (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
    Q_ASSERT(eglCreateImageKHR);
    m_image = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
    if (m_image == EGL_NO_IMAGE_KHR) {
        qWarning() << "invalid eglCreateImageKHR" << glGetError();
    }
}

void ThumbnailItem::Private::imageFromMemfd(Thumbnail *thumbnail)
{
    if (thumFlags & Thumbnail::BufferFlag::Reused)
        return;

    dataSize = thumbnail->size().height() * thumbnail->stride() + thumbnail->offset();
    map = static_cast<uint8_t *>(mmap(nullptr, dataSize, PROT_READ, MAP_PRIVATE, thumbnail->fd(), 0));

    if (map == MAP_FAILED) {
        qWarning() << "Failed to mmap the memory: " << strerror(errno);
        return;
    }
}

void ThumbnailItem::BufferImportDmabuf()
{
    Thumbnail *thum = qobject_cast<Thumbnail *>(sender());
    if (thum->flags() & Thumbnail::BufferFlag::Dmabuf) {
        pri->createEglImage(thum);
    } else {
        pri->imageFromMemfd(thum);
        if (pri->map) {
            const QImage::Format qtFormat = thumbnail->stride() / thumbnail->size().width() == 3
                                                ? QImage::Format_RGB888
                                                : QImage::Format_ARGB32;

            QImage img(pri->map, thumbnail->size().width(), thumbnail->size().height(), thumbnail->stride(),
                    qtFormat);
            pri->image = img.copy();
        }
    }

    pri->format = thumbnail->format();
    pri->thumFlags = thumbnail->flags();
    update();
}

void ThumbnailItem::setOutputId(QString outputId)
{
    pri->m_outputid = outputId;
    emit outputIdChanged();
}

void ThumbnailItem::setWindowId(QString windowId)
{
    if (pri->m_id != windowId) {
        pri->m_id = windowId;
        active(m_active && !windowId.isEmpty());
        emit windowIdChanged();
    }
}

void ThumbnailItem::setThumbnailType(int type)
{
    m_thumbnailType = type;
    emit thumbnailTypeChanged();
}

void ThumbnailItem::setActive(bool active)
{
    this->active(active);
}

QString ThumbnailItem::outputId() const
{
    return pri->m_outputid;
}

QString ThumbnailItem::windowId() const
{
    return pri->m_id;
}

void ThumbnailItem::active(bool active)
{
    if(active && !pri->m_id.isEmpty()) {
        if(m_currentUuid == pri->m_id || !isVisible()) {
            return;
        }
        if(!context) {
            context = new Context(m_display, Context::Capability::Thumbnail);
            context->start();
        }
        if(!thumbnail) {
            thumbnail = new Thumbnail(this);
            connect(thumbnail, &Thumbnail::bufferUpdate, this, &ThumbnailItem::BufferImportDmabuf);
            connect(thumbnail, &Thumbnail::deleted, this, &ThumbnailItem::thumbnailIsDeleted);

        }
        context->thumbnail_init(thumbnail, (Thumbnail::Type)m_thumbnailType, pri->m_id, pri->m_outputid);
        m_active = true;
        emit isActiveChanged();
        m_currentUuid = pri->m_id;
        m_thumbnailIsDeleted = false;
    } else {
        if(m_active && !m_thumbnailIsDeleted) {
            if (pri->map) {
                munmap(pri->map, pri->dataSize);
                pri->map = nullptr;
            }
            thumbnail->destory();
        }
    }
}

void ThumbnailItem::thumbnailIsDeleted()
{
    m_thumbnailIsDeleted = true;
    thumbnail = nullptr;
    m_active = false;
    emit isActiveChanged();
}
