// Copyright (C) 2017 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 "qsharedimageloader_p.h"
#include <private/qobject_p.h>
#include <private/qimage_p.h>

#include <QtCore/qpointer.h>
#include <QSharedMemory>

#include <memory>

QT_BEGIN_NAMESPACE

Q_LOGGING_CATEGORY(lcSharedImage, "qt.quick.sharedimage");

struct SharedImageHeader {
    quint8 magic;
    quint8 version;
    quint16 offset;
    qint32 width;
    qint32 height;
    qint32 bpl;
    QImage::Format format;
};
Q_STATIC_ASSERT(sizeof(SharedImageHeader) % 4 == 0);

#if QT_CONFIG(sharedmemory)
struct SharedImageInfo {
    QString path;
    QPointer<QSharedMemory> shmp;
};

void cleanupSharedImage(void *cleanupInfo)
{
    if (!cleanupInfo)
        return;
    SharedImageInfo *sii = static_cast<SharedImageInfo *>(cleanupInfo);
    qCDebug(lcSharedImage) << "Cleanup called for" << sii->path;
    if (sii->shmp.isNull()) {
        qCDebug(lcSharedImage) << "shm is 0 for" << sii->path;
        return;
    }
    QSharedMemory *shm = sii->shmp.data();
    sii->shmp.clear();
    delete shm;    // destructor detaches
    delete sii;
}
#else
void cleanupSharedImage(void *) {}
#endif

class QSharedImageLoaderPrivate : public QObjectPrivate
{
    Q_DECLARE_PUBLIC(QSharedImageLoader)

public:
    QSharedImageLoaderPrivate() {}

    QImage load(const QString &path, QSharedImageLoader::ImageParameters *params);

    void storeImageToMem(void *data, const QImage &img);

    bool verifyMem(const void *data, int size);

    QImage createImageFromMem(const void *data, void *cleanupInfo);

};


void QSharedImageLoaderPrivate::storeImageToMem(void *data, const QImage &img)
{
    Q_ASSERT(data && !img.isNull());

    SharedImageHeader *h = static_cast<SharedImageHeader *>(data);
    h->magic = 'Q';
    h->version = 1;
    h->offset = sizeof(SharedImageHeader);
    h->width = img.width();
    h->height = img.height();
    h->bpl = img.bytesPerLine();
    h->format = img.format();

    uchar *p = static_cast<uchar *>(data) + sizeof(SharedImageHeader);
    memcpy(p, img.constBits(), img.sizeInBytes());
}


bool QSharedImageLoaderPrivate::verifyMem(const void *data, int size)
{
    if (!data || size < int(sizeof(SharedImageHeader)))
        return false;

    const SharedImageHeader *h = static_cast<const SharedImageHeader *>(data);
    if ((h->magic != 'Q')
        || (h->version < 1)
        || (h->offset < sizeof(SharedImageHeader))
        || (h->width <= 0)
        || (h->height <= 0)
        || (h->bpl <= 0)
        || (h->format <= QImage::Format_Invalid)
        || (h->format >= QImage::NImageFormats)) {
        return false;
    }

    int availSize = size - h->offset;
    if (h->height * h->bpl > availSize)
        return false;
    if ((qt_depthForFormat(h->format) * h->width * h->height) > (8 * availSize))
        return false;

    return true;
}


QImage QSharedImageLoaderPrivate::createImageFromMem(const void *data, void *cleanupInfo)
{
    const SharedImageHeader *h = static_cast<const SharedImageHeader *>(data);
    const uchar *p = static_cast<const uchar *>(data) + h->offset;

    QImage img(p, h->width, h->height, h->bpl, h->format, cleanupSharedImage, cleanupInfo);
    return img;
}


QImage QSharedImageLoaderPrivate::load(const QString &path, QSharedImageLoader::ImageParameters *params)
{
#if QT_CONFIG(sharedmemory)
    Q_Q(QSharedImageLoader);

    QImage nil;
    if (path.isEmpty())
        return nil;

    auto shm = std::make_unique<QSharedMemory>(QSharedMemory::legacyNativeKey(q->key(path, params)));
    bool locked = false;

    if (!shm->attach(QSharedMemory::ReadOnly)) {
        QImage img = q->loadFile(path, params);
        if (img.isNull())
            return nil;
        size_t size = sizeof(SharedImageHeader) + img.sizeInBytes();
        if (size > size_t(std::numeric_limits<int>::max())) {
            qCDebug(lcSharedImage) << "Image" << path << "to large to load";
            return nil;
        } else if (shm->create(int(size))) {
            qCDebug(lcSharedImage) << "Created new shm segment of size" << size << "for image" << path;
            if (!shm->lock()) {
                qCDebug(lcSharedImage) << "Lock1 failed!?" << shm->errorString();
                return nil;
            }
            locked = true;
            storeImageToMem(shm->data(), img);
        } else if (shm->error() == QSharedMemory::AlreadyExists) {
            // race handling: other process may have created the share while
            // we loaded the image, so try again to just attach
            if (!shm->attach(QSharedMemory::ReadOnly)) {
                qCDebug(lcSharedImage) << "Attach to existing failed?" << shm->errorString();
                return nil;
            }
        } else {
            qCDebug(lcSharedImage) << "Create failed?" << shm->errorString();
            return nil;
        }
    }

    Q_ASSERT(shm->isAttached());

    if (!locked) {
        if (!shm->lock()) {
            qCDebug(lcSharedImage) << "Lock2 failed!?" << shm->errorString();
            return nil;
        }
        locked = true;
    }

    if (!verifyMem(shm->constData(), shm->size())) {
        qCDebug(lcSharedImage) << "Verifymem failed!?";
        shm->unlock();
        return nil;
    }

    QSharedMemory *shmp = shm.release();
    SharedImageInfo *sii = new SharedImageInfo;
    sii->path = path;
    sii->shmp = shmp;
    QImage shImg = createImageFromMem(shmp->constData(), sii);

    if (!shmp->unlock()) {
        qCDebug(lcSharedImage) << "UnLock failed!?";
    }

    return shImg;
#else
    Q_UNUSED(path);
    Q_UNUSED(params);
    return QImage();
#endif
}


QSharedImageLoader::QSharedImageLoader(QObject *parent)
    : QObject(*new QSharedImageLoaderPrivate, parent)
{
}

QSharedImageLoader::~QSharedImageLoader()
{
}

QImage QSharedImageLoader::load(const QString &path, ImageParameters *params)
{
    Q_D(QSharedImageLoader);

    return d->load(path, params);
}

QImage QSharedImageLoader::loadFile(const QString &path, ImageParameters *params)
{
    Q_UNUSED(params);

    return QImage(path);
}

QString QSharedImageLoader::key(const QString &path, ImageParameters *params)
{
    Q_UNUSED(params);

    return path;
}


QT_END_NAMESPACE

#include "moc_qsharedimageloader_p.cpp"
