// Copyright (C) 2016 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 "qaccessiblecache_p.h"
#include <QtCore/qdebug.h>
#include <QtCore/qloggingcategory.h>
#include <private/qguiapplication_p.h>

#if QT_CONFIG(accessibility)

QT_BEGIN_NAMESPACE

Q_STATIC_LOGGING_CATEGORY(lcAccessibilityCache, "qt.accessibility.cache");

/*!
    \class QAccessibleCache
    \internal
    \brief Maintains a cache of accessible interfaces.
*/

static QAccessibleCache *accessibleCache = nullptr;
static bool inCacheDestructor = false;

static void cleanupAccessibleCache()
{
    delete accessibleCache;
    accessibleCache = nullptr;
}

QAccessibleObjectDestroyedEvent::~QAccessibleObjectDestroyedEvent()
{
}

QAccessibleCache::~QAccessibleCache()
{
    inCacheDestructor = true;

    for (QAccessible::Id id: idToInterface.keys())
        deleteInterface(id);
}

QAccessibleCache *QAccessibleCache::instance()
{
    if (!accessibleCache) {
        accessibleCache = new QAccessibleCache;
        qAddPostRoutine(cleanupAccessibleCache);
    }
    return accessibleCache;
}

/*
  The ID is always in the range [INT_MAX+1, UINT_MAX].
  This makes it easy on windows to reserve the positive integer range
  for the index of a child and not clash with the unique ids.
*/
QAccessible::Id QAccessibleCache::acquireId() const
{
    static const QAccessible::Id FirstId = QAccessible::Id(INT_MAX) + 1;
    static QAccessible::Id nextId = FirstId;

    while (idToInterface.contains(nextId)) {
        // (wrap back when when we reach UINT_MAX - 1)
        // -1 because on Android -1 is taken for the "View" so just avoid it completely for consistency
        if (nextId == UINT_MAX - 1)
            nextId = FirstId;
        else
            ++nextId;
    }

    return nextId++;
}

QAccessibleInterface *QAccessibleCache::interfaceForId(QAccessible::Id id) const
{
    return idToInterface.value(id);
}

QAccessible::Id QAccessibleCache::idForInterface(QAccessibleInterface *iface) const
{
    return interfaceToId.value(iface);
}

QAccessible::Id QAccessibleCache::idForObject(QObject *obj) const
{
    if (obj) {
        const QMetaObject *mo = obj->metaObject();
        for (auto pair : objectToId.values(obj)) {
            if (pair.second == mo) {
                return pair.first;
            }
        }
    }
    return 0;
}

/*!
 * \internal
 *
 * returns true if the cache has an interface for the object and its corresponding QMetaObject
 */
bool QAccessibleCache::containsObject(QObject *obj) const
{
    if (obj) {
        const QMetaObject *mo = obj->metaObject();
        for (auto pair : objectToId.values(obj)) {
            if (pair.second == mo) {
                return true;
            }
        }
    }
    return false;
}

QAccessible::Id QAccessibleCache::insert(QObject *object, QAccessibleInterface *iface) const
{
    Q_ASSERT(iface);
    Q_UNUSED(object);

    // object might be 0
    Q_ASSERT(!containsObject(object));
    Q_ASSERT_X(!interfaceToId.contains(iface), "", "Accessible interface inserted into cache twice!");

    QAccessible::Id id = acquireId();
    QObject *obj = iface->object();
    Q_ASSERT(object == obj);
    if (obj) {
        objectToId.insert(obj, std::pair(id, obj->metaObject()));
        connect(obj, &QObject::destroyed, this, &QAccessibleCache::objectDestroyed);
    }
    idToInterface.insert(id, iface);
    interfaceToId.insert(iface, id);
    qCDebug(lcAccessibilityCache) << "insert - id:" << id << " iface:" << iface;
    return id;
}

void QAccessibleCache::objectDestroyed(QObject* obj)
{
    /*
    In some cases we might add a not fully-constructed object to the cache. This might happen with
    for instance QWidget subclasses that are in the construction phase. If updateAccessibility() is
    called in the constructor of QWidget (directly or indirectly), it will end up asking for the
    classname of that widget in order to know which accessibility interface subclass the
    accessibility factory should instantiate and return. However, since that requires a virtual
    call to metaObject(), it will return the metaObject() of QWidget (not for the subclass), and so
    the factory will ultimately return a rather generic QAccessibleWidget instead of a more
    specialized interface. Even though it is a "incomplete" interface it will be put in the cache
    and it will be usable as if the object is a widget. In order for the cache to not just return
    the same generic QAccessibleWidget for that object, we have to check if the cache matches
    the objects QMetaObject. We therefore use a QMultiHash and also store the QMetaObject * in
    the value. We therefore might potentially store several values for the corresponding object
    (in theory one for each level in the class inheritance chain)

    This means that after the object have been fully constructed, we will at some point again query
    for the interface for the same object, but now its metaObject() returns the correct
    QMetaObject, so it won't return the QAccessibleWidget that is associated with the object in the
    cache. Instead it will go to the factory and create the _correct_ specialized interface for the
    object. If that succeeded, it will also put that entry in the cache. We will therefore in those
    cases insert *two* cache entries for the same object (using QMultiHash). They both must live
    until the object is destroyed.

    So when the object is destroyed we might have to delete two entries from the cache.
    */
    for (auto pair : objectToId.values(obj)) {
        QAccessible::Id id = pair.first;
        Q_ASSERT_X(idToInterface.contains(id), "", "QObject with accessible interface deleted, where interface not in cache!");
        deleteInterface(id, obj);
    }
}

void QAccessibleCache::sendObjectDestroyedEvent(QObject *obj)
{
    for (auto pair : objectToId.values(obj)) {
        QAccessible::Id id = pair.first;
        Q_ASSERT_X(idToInterface.contains(id), "", "QObject with accessible interface deleted, where interface not in cache!");

        QAccessibleObjectDestroyedEvent event(id);
        QAccessible::updateAccessibility(&event);
    }
}

void QAccessibleCache::deleteInterface(QAccessible::Id id, QObject *obj)
{
    const auto it = idToInterface.find(id);
    if (it == idToInterface.end()) // the interface may be deleted already
        return;

    QAccessibleInterface *iface = *it;
    qCDebug(lcAccessibilityCache) << "delete - id:" << id << " iface:" << iface;
    if (!iface) {
        idToInterface.erase(it);
        return;
    }

    // QObjects send this from their destructors, but the interfaces
    // with no associated object call deleteInterface directly.
    if (!inCacheDestructor && !obj && !iface->object()) {
        if (QGuiApplicationPrivate::is_app_running && !QGuiApplicationPrivate::is_app_closing && QAccessible::isActive()) {
            QAccessibleObjectDestroyedEvent event(id);
            QAccessible::updateAccessibility(&event);
        }
    }

    idToInterface.erase(it);
    interfaceToId.take(iface);
    if (!obj)
        obj = iface->object();
    if (obj)
        objectToId.remove(obj);
    delete iface;

#ifdef Q_OS_APPLE
    removeAccessibleElement(id);
#endif
}

QT_END_NAMESPACE

#include "moc_qaccessiblecache_p.cpp"

#endif
