/*
 *  SPDX-FileCopyrightText: 2011 Marco Martin <mart@kde.org>
 *  SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
 *
 *  SPDX-License-Identifier: LGPL-2.0-or-later
 */

#include "delegaterecycler.h"

#include "loggingcategory.h"
#include <QDebug>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>

DelegateRecyclerAttached::DelegateRecyclerAttached(QObject *parent)
    : QObject(parent)
{
}

DelegateRecyclerAttached::~DelegateRecyclerAttached()
{
}
/*
void setRecycler(DelegateRecycler *recycler)
{
    m_recycler = recycler;
}

DelegateRecycler *recycler() const
{
    return m_recycler;
}
*/

class DelegateCache
{
public:
    DelegateCache();
    ~DelegateCache();

    void ref(QQmlComponent *);
    void deref(QQmlComponent *);

    void insert(QQmlComponent *, QQuickItem *);
    QQuickItem *take(QQmlComponent *);

private:
    static const int s_cacheSize = 40;
    QHash<QQmlComponent *, int> m_refs;
    QHash<QQmlComponent *, QList<QQuickItem *>> m_unusedItems;
};

Q_GLOBAL_STATIC(DelegateCache, s_delegateCache)

DelegateCache::DelegateCache()
{
}

DelegateCache::~DelegateCache()
{
    for (auto &item : std::as_const(m_unusedItems)) {
        qDeleteAll(item);
    }
}

void DelegateCache::ref(QQmlComponent *component)
{
    m_refs[component]++;
}

void DelegateCache::deref(QQmlComponent *component)
{
    auto itRef = m_refs.find(component);
    if (itRef == m_refs.end()) {
        return;
    }

    (*itRef)--;
    if (*itRef <= 0) {
        m_refs.erase(itRef);

        qDeleteAll(m_unusedItems.take(component));
    }
}

void DelegateCache::insert(QQmlComponent *component, QQuickItem *item)
{
    auto &items = m_unusedItems[component];
    if (items.length() >= s_cacheSize) {
        item->deleteLater();
        return;
    }

    DelegateRecyclerAttached *attached = qobject_cast<DelegateRecyclerAttached *>(qmlAttachedPropertiesObject<DelegateRecycler>(item, false));
    if (attached) {
        Q_EMIT attached->pooled();
    }

    item->setParentItem(nullptr);
    items.append(item);
}

QQuickItem *DelegateCache::take(QQmlComponent *component)
{
    auto it = m_unusedItems.find(component);
    if (it != m_unusedItems.end() && !it->isEmpty()) {
        return it->takeFirst();
    }
    return nullptr;
}

DelegateRecycler::DelegateRecycler(QQuickItem *parent)
    : QQuickItem(parent)
{
    setFlags(QQuickItem::ItemIsFocusScope);
}

DelegateRecycler::~DelegateRecycler()
{
    if (m_sourceComponent) {
        s_delegateCache->insert(m_sourceComponent, m_item);
        s_delegateCache->deref(m_sourceComponent);
    }
}

void DelegateRecycler::syncIndex()
{
    const QVariant newIndex = m_propertiesTracker->property("trackedIndex");
    if (!m_item || !newIndex.isValid()) {
        return;
    }
    QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
    ctx->setContextProperty(QStringLiteral("index"), newIndex);
}

void DelegateRecycler::syncModel()
{
    const QVariant newModel = m_propertiesTracker->property("trackedModel");
    if (!m_item || !newModel.isValid()) {
        return;
    }
    QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
    ctx->setContextProperty(QStringLiteral("model"), newModel);

    // try to bind all properties
    QObject *modelObj = newModel.value<QObject *>();
    if (modelObj) {
        const QMetaObject *metaObj = modelObj->metaObject();
        for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) {
            ctx->setContextProperty(QString::fromUtf8(metaObj->property(i).name()), metaObj->property(i).read(modelObj));
        }
    }
}

void DelegateRecycler::syncModelProperties()
{
    const QVariant model = m_propertiesTracker->property("trackedModel");
    if (!m_item || !model.isValid()) {
        return;
    }
    QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();

    // try to bind all properties
    QObject *modelObj = model.value<QObject *>();
    if (modelObj) {
        const QMetaObject *metaObj = modelObj->metaObject();
        for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) {
            ctx->setContextProperty(QString::fromUtf8(metaObj->property(i).name()), metaObj->property(i).read(modelObj));
        }
    }
}

void DelegateRecycler::syncModelData()
{
    const QVariant newModelData = m_propertiesTracker->property("trackedModelData");
    if (!m_item || !newModelData.isValid()) {
        return;
    }
    QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
    ctx->setContextProperty(QStringLiteral("modelData"), newModelData);
}

QQmlComponent *DelegateRecycler::sourceComponent() const
{
    return m_sourceComponent;
}

void DelegateRecycler::setSourceComponent(QQmlComponent *component)
{
    if (component && component->parent() == this) {
        qCWarning(KirigamiLog) << "Error: source components cannot be declared inside DelegateRecycler";
        return;
    }
    if (m_sourceComponent == component) {
        return;
    }

    if (!m_propertiesTracker) {
        static QHash<QQmlEngine *, QQmlComponent *> propertiesTrackerComponent;
        auto engine = qmlEngine(this);
        auto it = propertiesTrackerComponent.find(engine);
        if (it == propertiesTrackerComponent.end()) {
            connect(engine, &QObject::destroyed, engine, [engine] {
                propertiesTrackerComponent.remove(engine);
            });
            it = propertiesTrackerComponent.insert(engine, new QQmlComponent(engine, engine));

            /* clang-format off */
            (*it)->setData(QByteArrayLiteral(R"(
import QtQuick 2.3
QtObject {
    property int trackedIndex: index
    property var trackedModel: typeof model != 'undefined' ? model : null
    property var trackedModelData: typeof modelData != 'undefined' ? modelData : null
}
)"), QUrl(QStringLiteral("delegaterecycler.cpp")));
        }
        /* clang-format on */
        m_propertiesTracker = (*it)->create(QQmlEngine::contextForObject(this));

        connect(m_propertiesTracker, SIGNAL(trackedIndexChanged()), this, SLOT(syncIndex()));
        connect(m_propertiesTracker, SIGNAL(trackedModelChanged()), this, SLOT(syncModel()));
        connect(m_propertiesTracker, SIGNAL(trackedModelDataChanged()), this, SLOT(syncModelData()));
    }

    if (m_sourceComponent) {
        if (m_item) {
            disconnect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints);
            disconnect(m_item.data(), &QQuickItem::implicitHeightChanged, this, &DelegateRecycler::updateHints);
            s_delegateCache->insert(component, m_item);
        }
        s_delegateCache->deref(component);
    }

    m_sourceComponent = component;
    s_delegateCache->ref(component);

    m_item = s_delegateCache->take(component);

    if (!m_item) {
        QQuickItem *candidate = parentItem();
        QQmlContext *ctx = nullptr;
        if (component->creationContext()) {
            ctx = new QQmlContext(component->creationContext());
        }
        while (!ctx && candidate) {
            QQmlContext *parentCtx = QQmlEngine::contextForObject(candidate);
            if (parentCtx) {
                ctx = new QQmlContext(parentCtx, candidate);
                break;
            } else {
                candidate = candidate->parentItem();
            }
        }

        Q_ASSERT(ctx);

        QObject *contextObjectToSet = nullptr;
        {
            // Find the first parent that has a context object with a valid translationDomain property, i.e. is a KLocalizedContext
            QQmlContext *auxCtx = ctx;
            while (auxCtx != nullptr) {
                QObject *auxCtxObj = auxCtx->contextObject();
                if (auxCtxObj && auxCtxObj->property("translationDomain").isValid()) {
                    contextObjectToSet = auxCtxObj;
                    break;
                }
                auxCtx = auxCtx->parentContext();
            }
        }
        if (contextObjectToSet) {
            ctx->setContextObject(contextObjectToSet);
        }

        QObject *modelObj = m_propertiesTracker->property("trackedModel").value<QObject *>();
        if (modelObj) {
            const QMetaObject *metaObj = modelObj->metaObject();
            for (int i = metaObj->propertyOffset(); i < metaObj->propertyCount(); ++i) {
                QMetaProperty prop = metaObj->property(i);
                ctx->setContextProperty(QString::fromUtf8(prop.name()), prop.read(modelObj));
                if (prop.hasNotifySignal()) {
                    QMetaMethod updateSlot = metaObject()->method(metaObject()->indexOfSlot("syncModelProperties()"));
                    connect(modelObj, prop.notifySignal(), this, updateSlot);
                }
            }
        }

        ctx->setContextProperty(QStringLiteral("model"), m_propertiesTracker->property("trackedModel"));
        ctx->setContextProperty(QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData"));
        ctx->setContextProperty(QStringLiteral("index"), m_propertiesTracker->property("trackedIndex"));
        ctx->setContextProperty(QStringLiteral("delegateRecycler"), this);

        QObject *obj = component->create(ctx);
        m_item = qobject_cast<QQuickItem *>(obj);
        if (!m_item) {
            obj->deleteLater();
        } else {
            connect(m_item.data(), &QObject::destroyed, ctx, &QObject::deleteLater);
            // if the user binded an explicit width, consider it, otherwise base upon implicit
            m_widthFromItem = m_item->width() > 0 && m_item->width() != m_item->implicitWidth();
            m_heightFromItem = m_item->height() > 0 && m_item->height() != m_item->implicitHeight();

            if (m_widthFromItem && m_heightFromItem) {
                connect(m_item.data(), &QQuickItem::heightChanged, this, [this]() {
                    updateSize(false);
                });
            }
        }
    } else {
        syncModel();

        QQmlContext *ctx = QQmlEngine::contextForObject(m_item)->parentContext();
        ctx->setContextProperties({QQmlContext::PropertyPair{QStringLiteral("modelData"), m_propertiesTracker->property("trackedModelData")},
                                   QQmlContext::PropertyPair{QStringLiteral("index"), m_propertiesTracker->property("trackedIndex")},
                                   QQmlContext::PropertyPair{QStringLiteral("delegateRecycler"), QVariant::fromValue<QObject *>(this)}});

        DelegateRecyclerAttached *attached = qobject_cast<DelegateRecyclerAttached *>(qmlAttachedPropertiesObject<DelegateRecycler>(m_item, false));
        if (attached) {
            Q_EMIT attached->reused();
        }
    }

    if (m_item) {
        m_item->setParentItem(this);
        connect(m_item.data(), &QQuickItem::implicitWidthChanged, this, &DelegateRecycler::updateHints);
        connect(m_item.data(), &QQuickItem::implicitHeightChanged, this, &DelegateRecycler::updateHints);

        updateSize(true);
    }

    Q_EMIT sourceComponentChanged();
}

void DelegateRecycler::resetSourceComponent()
{
    s_delegateCache->deref(m_sourceComponent);
    m_sourceComponent = nullptr;
}

DelegateRecyclerAttached *DelegateRecycler::qmlAttachedProperties(QObject *object)
{
    return new DelegateRecyclerAttached(object);
}

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
void DelegateRecycler::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry)
#else
void DelegateRecycler::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
#endif
{
    if (m_item && newGeometry.size() != oldGeometry.size()) {
        updateSize(true);
    }
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QQuickItem::geometryChanged(newGeometry, oldGeometry);
#else
    QQuickItem::geometryChange(newGeometry, oldGeometry);
#endif
}

void DelegateRecycler::focusInEvent(QFocusEvent *event)
{
    QQuickItem::focusInEvent(event);
    if (!m_item) {
        return;
    }

    m_item->setFocus(event->reason());
}

void DelegateRecycler::updateHints()
{
    updateSize(false);
}

void DelegateRecycler::updateSize(bool parentResized)
{
    if (!m_item) {
        return;
    }

    const bool needToUpdateWidth = !m_widthFromItem && parentResized && widthValid();
    const bool needToUpdateHeight = !m_heightFromItem && parentResized && heightValid();

    if (parentResized) {
        m_item->setPosition(QPoint(0, 0));
    }
    if (needToUpdateWidth && needToUpdateHeight) {
        m_item->setSize(QSizeF(width(), height()));
    } else if (needToUpdateWidth) {
        m_item->setWidth(width());
    } else if (needToUpdateHeight) {
        m_item->setHeight(height());
    }

    if (m_updatingSize) {
        return;
    }

    m_updatingSize = true;

    if (m_heightFromItem) {
        setHeight(m_item->height());
    }
    if (m_widthFromItem) {
        setWidth(m_item->width());
    }

    setImplicitSize(m_item->implicitWidth() >= 0 ? m_item->implicitWidth() : m_item->width(),
                    m_item->implicitHeight() >= 0 ? m_item->implicitHeight() : m_item->height());

    m_updatingSize = false;
}

#include "moc_delegaterecycler.cpp"
