/*
 * Copyright (C) 2021 Igalia S.L.
 * Copyright (C) 2022 Apple Inc. All rights reserved.
 *
 * This library 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 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include "AccessibilityAtspi.h"

#if USE(ATSPI)
#include "AXObjectCache.h"
#include "AccessibilityAtspiEnums.h"
#include "AccessibilityAtspiInterfaces.h"
#include "AccessibilityObjectAtspi.h"
#include "AccessibilityRootAtspi.h"
#include <gio/gio.h>
#include <glib/gi18n-lib.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/SortedArrayMap.h>
#include <wtf/UUID.h>
#include <wtf/glib/RunLoopSourcePriority.h>

namespace WebCore {

AccessibilityAtspi& AccessibilityAtspi::singleton()
{
    static NeverDestroyed<AccessibilityAtspi> atspi;
    return atspi;
}

AccessibilityAtspi::AccessibilityAtspi()
    : m_cacheUpdateTimer(RunLoop::main(), this, &AccessibilityAtspi::cacheUpdateTimerFired)
    , m_cacheClearTimer(RunLoop::main(), this, &AccessibilityAtspi::cacheClearTimerFired)
{
    m_cacheUpdateTimer.setPriority(RunLoopSourcePriority::RunLoopDispatcher);
    m_cacheClearTimer.setPriority(RunLoopSourcePriority::ReleaseUnusedResourcesTimer);
}

void AccessibilityAtspi::connect(const String& busAddress)
{
    if (busAddress.isEmpty())
        return;

    m_isConnecting = true;
    g_dbus_connection_new_for_address(busAddress.utf8().data(),
        static_cast<GDBusConnectionFlags>(G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION), nullptr, nullptr,
        [](GObject*, GAsyncResult* result, gpointer userData) {
            auto& atspi = *static_cast<AccessibilityAtspi*>(userData);
            GUniqueOutPtr<GError> error;
            atspi.didConnect(adoptGRef(g_dbus_connection_new_for_address_finish(result, &error.outPtr())));
            if (error)
                g_warning("Can't connect to a11y bus: %s", error->message);
        }, this);
}

void AccessibilityAtspi::didConnect(GRefPtr<GDBusConnection>&& connection)
{
    m_isConnecting = false;
    m_connection = WTFMove(connection);
    if (!m_connection)
        return;

    for (auto& pendingRegistration : m_pendingRootRegistrations)
        registerRoot(pendingRegistration.root, WTFMove(pendingRegistration.interfaces), WTFMove(pendingRegistration.completionHandler));
    m_pendingRootRegistrations.clear();

    initializeRegistry();
}

void AccessibilityAtspi::initializeRegistry()
{
    RELEASE_ASSERT(m_connection);
    g_dbus_proxy_new(m_connection.get(), G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES, nullptr,
        "org.a11y.atspi.Registry", "/org/a11y/atspi/registry", "org.a11y.atspi.Registry", nullptr, [](GObject*, GAsyncResult* result, gpointer userData) {
        auto& atspi = *static_cast<AccessibilityAtspi*>(userData);
        GUniqueOutPtr<GError> error;
        atspi.m_registry = adoptGRef(g_dbus_proxy_new_finish(result, &error.outPtr()));
        if (!atspi.m_registry) {
            g_warning("Failed to connect to atspi registry: %s\n", error->message);
            return;
        }

        g_signal_connect(atspi.m_registry.get(), "g-signal", G_CALLBACK(+[](GDBusProxy*, char*, char* signal, GVariant* parameters, AccessibilityAtspi* atspi) {
            const char* dbusName;
            const char* eventName;
            if (!g_strcmp0(signal, "EventListenerRegistered")) {
                g_variant_get(parameters, "(&s&s@as)", &dbusName, &eventName, nullptr);
                atspi->addEventListener(dbusName, eventName);
            } else if (!g_strcmp0(signal, "EventListenerDeregistered")) {
                g_variant_get(parameters, "(&s&s)", &dbusName, &eventName);
                atspi->removeEventListener(dbusName, eventName);
            }
        }), &atspi);

        g_dbus_proxy_call(atspi.m_registry.get(), "GetRegisteredEvents", nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, [](GObject* proxy, GAsyncResult* result, gpointer userData) {
            auto& atspi = *static_cast<AccessibilityAtspi*>(userData);
            GUniqueOutPtr<GError> error;
            GRefPtr<GVariant> resultVariant = adoptGRef(g_dbus_proxy_call_finish(G_DBUS_PROXY(proxy), result, &error.outPtr()));
            if (!resultVariant) {
                g_warning("Failed to get atspi registered event listeners: %s\n", error->message);
                return;
            }

            GRefPtr<GVariant> events;
            g_variant_get(resultVariant.get(), "(@a(ss))", &events.outPtr());
            GVariantIter iter;
            g_variant_iter_init(&iter, events.get());
            const char* dbusName;
            const char* eventName;
            while (g_variant_iter_loop(&iter, "(&s&s)", &dbusName, &eventName))
                atspi.addEventListener(dbusName, eventName);
        }, &atspi);
    }, this);
}

static GUniquePtr<char*> eventConvertingDetailToNonCamelCase(const char* eventName)
{
    GUniquePtr<char*> event(g_strsplit(eventName, ":", 3));
    if (!event.get()[0] || !event.get()[1] || !event.get()[2] || !*event.get()[2])
        return event;

    char* converted = static_cast<char*>(g_malloc(strlen(event.get()[2]) * 2 + 1));
    char* convertedPtr = converted;
    char* detailPtr = event.get()[2];

    while (*detailPtr) {
        if (isASCIIUpper(*detailPtr)) {
            if (convertedPtr > converted)
                *convertedPtr++ = '-';
            *convertedPtr++ = toASCIILower(*detailPtr++);
        } else
            *convertedPtr++ = *detailPtr++;
    }
    *convertedPtr = '\0';

    g_free(event.get()[2]);
    event.get()[2] = converted;

    return event;
}

void AccessibilityAtspi::addEventListener(const char* dbusName, const char* eventName)
{
    auto& listeners = m_eventListeners.ensure(dbusName, [] {
        return Vector<GUniquePtr<char*>> { };
    }).iterator->value;
    listeners.append(eventConvertingDetailToNonCamelCase(eventName));
    addClient(dbusName);
}

static bool eventIsSubtype(char** needle, char** haystack)
{
    while (*haystack && **haystack) {
        if (g_strcmp0(*needle, *haystack))
            return false;
        needle++;
        haystack++;
    }

    return true;
}

void AccessibilityAtspi::removeEventListener(const char* dbusName, const char* eventName)
{
    if (!eventName || !*eventName) {
        m_eventListeners.remove(dbusName);
        return;
    }

    auto it = m_eventListeners.find(dbusName);
    if (it == m_eventListeners.end())
        return;

    auto needle = eventConvertingDetailToNonCamelCase(eventName);
    it->value.removeAllMatching([&](const GUniquePtr<char*>& event) {
        return eventIsSubtype(needle.get(), event.get());
    });

    if (it->value.isEmpty())
        m_eventListeners.remove(it);
}

void AccessibilityAtspi::addClient(const char* dbusName)
{
    if (m_clients.isEmpty())
        AXObjectCache::enableAccessibility();

    auto addResult = m_clients.add(dbusName, 0);
    if (!addResult.isNewEntry)
        return;

    m_cacheClearTimer.stop();

    addResult.iterator->value = g_dbus_connection_signal_subscribe(m_connection.get(), nullptr, "org.freedesktop.DBus", "NameOwnerChanged", nullptr, dbusName,
        G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE, [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar*, GVariant* parameters, gpointer userData) {
            auto& atspi = *static_cast<AccessibilityAtspi*>(userData);
            const char* interface;
            const char* oldName;
            const char* newName;
            g_variant_get(parameters, "(&s&s&s)", &interface, &oldName, &newName);
            if (*oldName != '\0' && *newName == '\0')
                atspi.removeClient(oldName);
        }, this, nullptr);
}

void AccessibilityAtspi::removeClient(const char* dbusName)
{
    auto id = m_clients.take(dbusName);
    if (!id)
        return;

    g_dbus_connection_signal_unsubscribe(m_connection.get(), id);

    if (!m_clients.isEmpty())
        return;

    m_cacheUpdateList.clear();
    m_cacheUpdateTimer.stop();
    m_cacheClearTimer.startOneShot(10_s);
}

bool AccessibilityAtspi::shouldEmitSignal(const char* interface, const char* name, const char* detail)
{
    // Always emit signals if we couldn't connect to the registry.
    if (!m_registry)
        return true;

    if (m_eventListeners.isEmpty())
        return false;

    const char* needle[4] = { interface, name, detail, nullptr };
    for (const auto& listeners : m_eventListeners.values()) {
        auto result = listeners.findIf([&](const GUniquePtr<char*>& event) {
            return eventIsSubtype(const_cast<char**>(needle), event.get());
        });
        if (result != notFound)
            return true;
    }

    return false;
}

const char* AccessibilityAtspi::uniqueName() const
{
    return m_connection ? g_dbus_connection_get_unique_name(m_connection.get()) : nullptr;
}

GVariant* AccessibilityAtspi::nullReference() const
{
    return g_variant_new("(so)", uniqueName(), "/org/a11y/atspi/null");
}

GVariant* AccessibilityAtspi::applicationReference() const
{
    // The application is the same for all root objects, so just use the first root object that is already embedded.
    for (auto* rootObject : m_rootObjects.keys()) {
        if (!rootObject->path().isNull())
            return rootObject->applicationReference();
    }

    return nullReference();
}

void AccessibilityAtspi::registerRoot(AccessibilityRootAtspi& rootObject, Vector<std::pair<GDBusInterfaceInfo*, GDBusInterfaceVTable*>>&& interfaces, CompletionHandler<void(const String&)>&& completionHandler)
{
    if (m_isConnecting) {
        m_pendingRootRegistrations.append({ rootObject, WTFMove(interfaces), WTFMove(completionHandler) });
        return;
    }

    if (!m_connection) {
        completionHandler({ });
        return;
    }

    ensureCache();
    String path = makeString("/org/a11y/webkit/accessible/", makeStringByReplacingAll(createVersion4UUIDString(), '-', '_'));
    Vector<unsigned, 3> registeredObjects;
    registeredObjects.reserveInitialCapacity(interfaces.size());
    for (const auto& interface : interfaces) {
        auto id = g_dbus_connection_register_object(m_connection.get(), path.utf8().data(), interface.first, interface.second, &rootObject, nullptr, nullptr);
        registeredObjects.uncheckedAppend(id);
    }
    m_rootObjects.add(&rootObject, WTFMove(registeredObjects));
    String reference = makeString(uniqueName(), ':', path);
    rootObject.setPath(WTFMove(path));
    completionHandler(reference);
}

void AccessibilityAtspi::unregisterRoot(AccessibilityRootAtspi& rootObject)
{
    for (unsigned i = 0; i < m_pendingRootRegistrations.size(); ++i) {
        auto& pendingRootRegistration = m_pendingRootRegistrations[i];
        if (pendingRootRegistration.root.ptr() == &rootObject) {
            pendingRootRegistration.completionHandler({ });
            m_pendingRootRegistrations.remove(i);
            return;
        }
    }

    if (!m_connection)
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, rootObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "StateChanged",
        g_variant_new("(siiva{sv})", "defunct", TRUE, 0, g_variant_new_string("0"), nullptr), nullptr);

    auto registeredObjects = m_rootObjects.take(&rootObject);
    g_dbus_connection_emit_signal(m_connection.get(), nullptr, "/org/a11y/atspi/cache", "org.a11y.atspi.Cache", "RemoveAccessible",
        g_variant_new("((so))", uniqueName(), rootObject.path().utf8().data()), nullptr);
    for (auto id : registeredObjects)
        g_dbus_connection_unregister_object(m_connection.get(), id);
}

String AccessibilityAtspi::registerObject(AccessibilityObjectAtspi& atspiObject, Vector<std::pair<GDBusInterfaceInfo*, GDBusInterfaceVTable*>>&& interfaces)
{
    RELEASE_ASSERT(!m_isConnecting);
    if (!m_connection)
        return { };

    ensureCache();
    String path = makeString("/org/a11y/atspi/accessible/", makeStringByReplacingAll(createVersion4UUIDString(), '-', '_'));
    Vector<unsigned, 7> registeredObjects;
    registeredObjects.reserveInitialCapacity(interfaces.size());
    for (const auto& interface : interfaces) {
        auto id = g_dbus_connection_register_object(m_connection.get(), path.utf8().data(), interface.first, interface.second, &atspiObject, nullptr, nullptr);
        registeredObjects.uncheckedAppend(id);
    }
    m_atspiObjects.add(&atspiObject, WTFMove(registeredObjects));

    m_cacheUpdateList.add(&atspiObject);
    if (!m_cacheUpdateTimer.isActive())
        m_cacheUpdateTimer.startOneShot(0_s);

    return path;
}

void AccessibilityAtspi::unregisterObject(AccessibilityObjectAtspi& atspiObject)
{
    RELEASE_ASSERT(!m_isConnecting);
    if (!m_connection)
        return;

    if (m_atspiHyperlinks.contains(&atspiObject)) {
        auto registeredObjects = m_atspiHyperlinks.take(&atspiObject);
        for (auto id : registeredObjects)
            g_dbus_connection_unregister_object(m_connection.get(), id);
    }

    const auto& path = atspiObject.path();
    g_dbus_connection_emit_signal(m_connection.get(), nullptr, path.utf8().data(), "org.a11y.atspi.Event.Object", "StateChanged",
        g_variant_new("(siiva{sv})", "defunct", TRUE, 0, g_variant_new_string("0"), nullptr), nullptr);

    if (!m_cacheUpdateList.remove(&atspiObject) && m_cache.remove(path)) {
        g_dbus_connection_emit_signal(m_connection.get(), nullptr, "/org/a11y/atspi/cache", "org.a11y.atspi.Cache", "RemoveAccessible",
            g_variant_new("((so))", uniqueName(), path.utf8().data()), nullptr);
    }

    if (m_cacheUpdateList.isEmpty())
        m_cacheUpdateTimer.stop();

    auto registeredObjects = m_atspiObjects.take(&atspiObject);
    for (auto id : registeredObjects)
        g_dbus_connection_unregister_object(m_connection.get(), id);
}

String AccessibilityAtspi::registerHyperlink(AccessibilityObjectAtspi& atspiObject, Vector<std::pair<GDBusInterfaceInfo*, GDBusInterfaceVTable*>>&& interfaces)
{
    if (!m_connection)
        return { };

    String path = makeString("/org/a11y/atspi/accessible/", makeStringByReplacingAll(createVersion4UUIDString(), '-', '_'));
    Vector<unsigned, 1> registeredObjects;
    registeredObjects.reserveInitialCapacity(interfaces.size());
    for (const auto& interface : interfaces) {
        auto id = g_dbus_connection_register_object(m_connection.get(), path.utf8().data(), interface.first, interface.second, &atspiObject, nullptr, nullptr);
        registeredObjects.uncheckedAppend(id);
    }
    m_atspiHyperlinks.add(&atspiObject, WTFMove(registeredObjects));

    return path;
}

void AccessibilityAtspi::parentChanged(AccessibilityObjectAtspi& atspiObject)
{
    if (!m_connection)
        return;

    // Always emit parentChanged when there are clients because the atspi cache always consumes it.
    if (!m_clients.isEmpty())
        return;

    // Do not emit parent-changed for pending objects, since AddAccessible will update the cache.
    if (m_cacheUpdateList.contains(&atspiObject))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "PropertyChange",
        g_variant_new("(siiva{sv})", "accessible-parent", 0, 0, atspiObject.parentReference(), nullptr), nullptr);
}

void AccessibilityAtspi::parentChanged(AccessibilityRootAtspi& rootObject)
{
    if (!m_connection)
        return;

    // Always emit parentChanged when there are clients because the atspi cache always consumes it.
    if (m_clients.isEmpty())
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, rootObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "PropertyChange",
        g_variant_new("(siiva{sv})", "accessible-parent", 0, 0, rootObject.parentReference(), nullptr), nullptr);
}

void AccessibilityAtspi::childrenChanged(AccessibilityObjectAtspi& atspiObject, AccessibilityObjectAtspi& child, ChildrenChanged change)
{
    if (!m_connection)
        return;

    // Always emit ChildrenChanged when there are clients because the atspi cache always consumes it.
    if (m_clients.isEmpty())
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "ChildrenChanged",
        g_variant_new("(siiv(so))", change == ChildrenChanged::Added ? "add" : "remove", child.indexInParentForChildrenChanged(change),
        0, g_variant_new("(so)", uniqueName(), child.path().utf8().data()), uniqueName(), atspiObject.path().utf8().data()), nullptr);
}

void AccessibilityAtspi::childrenChanged(AccessibilityRootAtspi& rootObject, AccessibilityObjectAtspi& child, ChildrenChanged change)
{
    if (!m_connection)
        return;

    // Always emit ChildrenChanged when there are clients because the atspi cache always consumes it.
    if (m_clients.isEmpty())
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, rootObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "ChildrenChanged",
        g_variant_new("(siiv(so))", change == ChildrenChanged::Added ? "add" : "remove", 0,
        0, g_variant_new("(so)", uniqueName(), child.path().utf8().data()), uniqueName(), rootObject.path().utf8().data()), nullptr);
}

void AccessibilityAtspi::stateChanged(AccessibilityObjectAtspi& atspiObject, const char* name, bool value)
{
#if ENABLE(DEVELOPER_MODE)
    notifyStateChanged(atspiObject, name, value);
#endif

    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "StateChanged", name))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "StateChanged",
        g_variant_new("(siiva{sv})", name, value, 0, g_variant_new_string("0"), nullptr), nullptr);
}

void AccessibilityAtspi::textChanged(AccessibilityObjectAtspi& atspiObject, const char* changeType, CString&& text, unsigned offset, unsigned length)
{
#if ENABLE(DEVELOPER_MODE)
    notifyTextChanged(atspiObject);
#endif

    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "TextChanged", changeType))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "TextChanged",
        g_variant_new("(siiva{sv})", changeType, offset, length, g_variant_new_string(text.data()), nullptr), nullptr);
}

void AccessibilityAtspi::textAttributesChanged(AccessibilityObjectAtspi& atspiObject)
{
    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "TextAttributesChanged"))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "TextAttributesChanged",
        g_variant_new("(siiva{sv})", "", 0, 0, g_variant_new_string(""), nullptr), nullptr);
}

void AccessibilityAtspi::textCaretMoved(AccessibilityObjectAtspi& atspiObject, unsigned caretOffset)
{
#if ENABLE(DEVELOPER_MODE)
    notifyTextCaretMoved(atspiObject, caretOffset);
#endif

    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "TextCaretMoved"))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "TextCaretMoved",
        g_variant_new("(siiva{sv})", "", caretOffset, 0, g_variant_new_string(""), nullptr), nullptr);
}

void AccessibilityAtspi::textSelectionChanged(AccessibilityObjectAtspi& atspiObject)
{
    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "TextSelectionChanged"))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "TextSelectionChanged",
        g_variant_new("(siiva{sv})", "", 0, 0, g_variant_new_string(""), nullptr), nullptr);
}

void AccessibilityAtspi::valueChanged(AccessibilityObjectAtspi& atspiObject, double value)
{
#if ENABLE(DEVELOPER_MODE)
    notifyValueChanged(atspiObject);
#endif

    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "PropertyChange", "accessible-value"))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "PropertyChange",
        g_variant_new("(siiva{sv})", "accessible-value", 0, 0, g_variant_new_double(value), nullptr), nullptr);
}

void AccessibilityAtspi::selectionChanged(AccessibilityObjectAtspi& atspiObject)
{
#if ENABLE(DEVELOPER_MODE)
    if (atspiObject.role() == Atspi::Role::Menu)
        notifyMenuSelectionChanged(atspiObject);
    else
        notifySelectionChanged(atspiObject);
#endif

    if (!m_connection)
        return;

    if (!shouldEmitSignal("Object", "SelectionChanged"))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Object", "SelectionChanged",
        g_variant_new("(siiva{sv})", "", 0, 0, g_variant_new_string(""), nullptr), nullptr);
}

void AccessibilityAtspi::loadEvent(AccessibilityObjectAtspi& atspiObject, CString&& event)
{
#if ENABLE(DEVELOPER_MODE)
    notifyLoadEvent(atspiObject, event);
#endif

    if (!m_connection)
        return;

    if (!shouldEmitSignal("Document", event.data()))
        return;

    g_dbus_connection_emit_signal(m_connection.get(), nullptr, atspiObject.path().utf8().data(), "org.a11y.atspi.Event.Document", event.data(),
        g_variant_new("(siiva{sv})", "", 0, 0, g_variant_new_string(""), nullptr), nullptr);
}

struct RoleNameEntry {
    const char* name;
    const char* localizedName;
};

static constexpr std::pair<AccessibilityRole, RoleNameEntry> roleNames[] = {
    { AccessibilityRole::Application, { "application", N_("application") } },
    { AccessibilityRole::ApplicationAlert, { "notification", N_("notification") } },
    { AccessibilityRole::ApplicationAlertDialog, { "alert", N_("alert") } },
    { AccessibilityRole::ApplicationDialog, { "dialog", N_("dialog") } },
    { AccessibilityRole::ApplicationGroup, { "grouping", N_("grouping") } },
    { AccessibilityRole::ApplicationLog, { "log", N_("log") } },
    { AccessibilityRole::ApplicationMarquee, { "marquee", N_("marquee") } },
    { AccessibilityRole::ApplicationStatus, { "statusbar", N_("statusbar") } },
    { AccessibilityRole::ApplicationTextGroup, { "section", N_("section") } },
    { AccessibilityRole::ApplicationTimer, { "timer", N_("timer") } },
    { AccessibilityRole::Audio, { "audio", N_("audio") } },
    { AccessibilityRole::Blockquote, { "block quote", N_("block quote") } },
    { AccessibilityRole::BusyIndicator, { "progress bar", N_("progress bar") } },
    { AccessibilityRole::Button, { "push button", N_("push button") } },
    { AccessibilityRole::Canvas, { "canvas", N_("canvas") } },
    { AccessibilityRole::Caption, { "caption", N_("caption") } },
    { AccessibilityRole::Cell, { "table cell", N_("table cell") } },
    { AccessibilityRole::CheckBox, { "check box", N_("check box") } },
    { AccessibilityRole::ColorWell, { "push button", N_("push button") } },
    { AccessibilityRole::ColumnHeader, { "column header", N_("column header") } },
    { AccessibilityRole::ComboBox, { "combo box", N_("combo box") } },
    { AccessibilityRole::Definition, { "definition", N_("definition") } },
    { AccessibilityRole::Deletion, { "content deletion", N_("content deletion") } },
    { AccessibilityRole::DescriptionList, { "description list", N_("description list") } },
    { AccessibilityRole::DescriptionListDetail, { "description value", N_("description value") } },
    { AccessibilityRole::DescriptionListTerm, { "description term", N_("description term") } },
    { AccessibilityRole::Directory, { "directory pane", N_("directory pane") } },
    { AccessibilityRole::Div, { "section", N_("section") } },
    { AccessibilityRole::Document, { "document frame", N_("document frame") } },
    { AccessibilityRole::DocumentArticle, { "article", N_("article") } },
    { AccessibilityRole::DocumentMath, { "math", N_("math") } },
    { AccessibilityRole::DocumentNote, { "comment", N_("comment") } },
    { AccessibilityRole::Feed, { "panel", N_("panel") } },
    { AccessibilityRole::Figure, { "panel", N_("panel") } },
    { AccessibilityRole::Footer, { "footer", N_("footer") } },
    { AccessibilityRole::Footnote, { "footnote", N_("footnote") } },
    { AccessibilityRole::Form, { "form", N_("form") } },
    { AccessibilityRole::GraphicsDocument, { "document frame", N_("document frame") } },
    { AccessibilityRole::GraphicsObject, { "panel", N_("panel") } },
    { AccessibilityRole::GraphicsSymbol, { "image", N_("image") } },
    { AccessibilityRole::Grid, { "table", N_("table") } },
    { AccessibilityRole::GridCell, { "table cell", N_("table cell") } },
    { AccessibilityRole::Group, { "panel", N_("panel") } },
    { AccessibilityRole::Heading, { "heading", N_("heading") } },
    { AccessibilityRole::HorizontalRule, { "separator", N_("separator") } },
    { AccessibilityRole::Inline, { "text", N_("text") } },
    { AccessibilityRole::Image, { "image", N_("image") } },
    { AccessibilityRole::ImageMap, { "image map", N_("image map") } },
    { AccessibilityRole::ImageMapLink, { "link", N_("link") } },
    { AccessibilityRole::Insertion, { "content insertion", N_("content insertion") } },
    { AccessibilityRole::Label, { "label", N_("label") } },
    { AccessibilityRole::LandmarkBanner, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkComplementary, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkContentInfo, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkDocRegion, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkMain, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkNavigation, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkRegion, { "landmark", N_("landmark") } },
    { AccessibilityRole::LandmarkSearch, { "landmark", N_("landmark") } },
    { AccessibilityRole::Legend, { "label", N_("label") } },
    { AccessibilityRole::Link, { "link", N_("link") } },
    { AccessibilityRole::List, { "list", N_("list") } },
    { AccessibilityRole::ListBox, { "heading", N_("list box") } },
    { AccessibilityRole::ListBoxOption, { "list item", N_("list item") } },
    { AccessibilityRole::ListItem, { "list item", N_("list item") } },
    { AccessibilityRole::ListMarker, { "text", N_("text") } },
    { AccessibilityRole::Mark, { "mar", N_("mark") } },
    { AccessibilityRole::MathElement, { "math", N_("math") } },
    { AccessibilityRole::Menu, { "menu", N_("menu") } },
    { AccessibilityRole::MenuBar, { "menu bar", N_("menu bar") } },
    { AccessibilityRole::MenuButton, { "menu item", N_("menu item") } },
    { AccessibilityRole::MenuItem, { "menu item", N_("menu item") } },
    { AccessibilityRole::MenuItemCheckbox, { "check menu item", N_("check menu item") } },
    { AccessibilityRole::MenuItemRadio, { "radio menu item", N_("radio menu item") } },
    { AccessibilityRole::MenuListPopup, { "menu", N_("menu") } },
    { AccessibilityRole::MenuListOption, { "menu item", N_("menu item") } },
    { AccessibilityRole::Meter, { "level bar", N_("level bar") } },
    { AccessibilityRole::Outline, { "tree", N_("tree") } },
    { AccessibilityRole::Paragraph, { "paragraph", N_("paragraph") } },
    { AccessibilityRole::PopUpButton, { "combo box", N_("combo box") } },
    { AccessibilityRole::Pre, { "section", N_("section") } },
    { AccessibilityRole::ProgressIndicator, { "progress bar", N_("progress bar") } },
    { AccessibilityRole::RadioButton, { "radio button", N_("radio button") } },
    { AccessibilityRole::RadioGroup, { "panel", N_("panel") } },
    { AccessibilityRole::RowHeader, { "row header", N_("row header") } },
    { AccessibilityRole::Row, { "table row", N_("table row") } },
    { AccessibilityRole::ScrollArea, { "scroll pane", N_("scroll pane") } },
    { AccessibilityRole::ScrollBar, { "scroll bar", N_("scroll bar") } },
    { AccessibilityRole::SearchField, { "entry", N_("entry") } },
    { AccessibilityRole::Slider, { "slider", N_("slider") } },
    { AccessibilityRole::SpinButton, { "spin button", N_("spin button") } },
    { AccessibilityRole::SplitGroup, { "split pane", N_("split pane") } },
    { AccessibilityRole::Splitter, { "separator", N_("separator") } },
    { AccessibilityRole::StaticText, { "text", N_("text") } },
    { AccessibilityRole::Subscript, { "subscript", N_("subscript") } },
    { AccessibilityRole::Superscript, { "superscript", N_("superscript") } },
    { AccessibilityRole::Switch, { "toggle button", N_("toggle button") } },
    { AccessibilityRole::SVGRoot, { "panel", N_("panel") } },
    { AccessibilityRole::SVGText, { "section", N_("section") } },
    { AccessibilityRole::SVGTSpan, { "text", N_("text") } },
    { AccessibilityRole::SVGTextPath, { "text", N_("text") } },
    { AccessibilityRole::TabGroup, { "page tab list", N_("page tab list") } },
    { AccessibilityRole::TabList, { "page tab list", N_("page tab list") } },
    { AccessibilityRole::TabPanel, { "scroll pane", N_("scroll pane") } },
    { AccessibilityRole::Tab, { "page tab", N_("page tab") } },
    { AccessibilityRole::Table, { "table", N_("table") } },
    { AccessibilityRole::Term, { "description term", N_("description term") } },
    { AccessibilityRole::TextArea, { "entry", N_("entry") } },
    { AccessibilityRole::TextField, { "entry", N_("entry") } },
    { AccessibilityRole::TextGroup, { "section", N_("section") } },
    { AccessibilityRole::Time, { "text", N_("text") } },
    { AccessibilityRole::Tree, { "tree", N_("tree") } },
    { AccessibilityRole::TreeGrid, { "tree table", N_("tree table") } },
    { AccessibilityRole::TreeItem, { "tree item", N_("tree item") } },
    { AccessibilityRole::ToggleButton, { "toggle button", N_("toggle button") } },
    { AccessibilityRole::Toolbar, { "tool bar", N_("tool bar") } },
    { AccessibilityRole::Unknown, { "unknown", N_("unknown") } },
    { AccessibilityRole::UserInterfaceTooltip, { "tool tip", N_("tool tip") } },
    { AccessibilityRole::Video, { "video", N_("video") } },
    { AccessibilityRole::WebArea, { "document web", N_("document web") } },
    { AccessibilityRole::WebCoreLink, { "link", N_("link") } },
    { AccessibilityRole::Window, { "window", N_("window") } }
};

const char* AccessibilityAtspi::localizedRoleName(AccessibilityRole role)
{
    static constexpr SortedArrayMap roleNamesMap { roleNames };
    if (auto entry = roleNamesMap.tryGet(role))
        return entry->localizedName;

    return _("unknown");
}

#define ITEM_SIGNATURE "(so)(so)(so)iiassusau"
#define GET_ITEMS_SIGNATURE "a(" ITEM_SIGNATURE ")"

GDBusInterfaceVTable AccessibilityAtspi::s_cacheFunctions = {
    // method_call
    [](GDBusConnection*, const gchar* sender, const gchar*, const gchar*, const gchar* methodName, GVariant*, GDBusMethodInvocation* invocation, gpointer userData) {
        if (!g_strcmp0(methodName, "GetItems")) {
            auto& atspi = *static_cast<AccessibilityAtspi*>(userData);
            atspi.addClient(sender);
            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("(" GET_ITEMS_SIGNATURE ")"));
            g_variant_builder_open(&builder, G_VARIANT_TYPE(GET_ITEMS_SIGNATURE));
            for (auto* rootObject : atspi.m_rootObjects.keys()) {
                g_variant_builder_open(&builder, G_VARIANT_TYPE("(" ITEM_SIGNATURE ")"));
                rootObject->serialize(&builder);
                g_variant_builder_close(&builder);
            }

            // We need to call updateBackingStore() on every object before calling serialize()
            // and updating the backing store can detach the object and remove it from the cache.
            auto paths = copyToVector(atspi.m_cache.keys());
            for (const auto& path : paths) {
                auto wrapper = atspi.m_cache.get(path);
                wrapper->updateBackingStore();
                if (!atspi.m_cache.contains(path) || wrapper->isDefunct())
                    continue;
                g_variant_builder_open(&builder, G_VARIANT_TYPE("(" ITEM_SIGNATURE ")"));
                wrapper->serialize(&builder);
                g_variant_builder_close(&builder);
            }

            g_variant_builder_close(&builder);
            g_dbus_method_invocation_return_value(invocation, g_variant_builder_end(&builder));
        }
    },
    // get_property
    [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* propertyName, GError** error, gpointer) -> GVariant* {
        g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Unknown property '%s'", propertyName);
        return nullptr;
    },
    // set_property,
    nullptr,
    // padding
    { nullptr }
};

void AccessibilityAtspi::ensureCache()
{
    if (m_cacheID || !m_connection)
        return;

    m_cacheID = g_dbus_connection_register_object(m_connection.get(), "/org/a11y/atspi/cache", const_cast<GDBusInterfaceInfo*>(&webkit_cache_interface), &s_cacheFunctions, this, nullptr, nullptr);
}

void AccessibilityAtspi::addToCacheIfNeeded(AccessibilityObjectAtspi& atspiObject)
{
    atspiObject.updateBackingStore();
    if (atspiObject.isDefunct())
        return;

    auto addResult = m_cache.add(atspiObject.path(), &atspiObject);
    if (!addResult.isNewEntry)
        return;

    GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("(" ITEM_SIGNATURE ")"));
    atspiObject.serialize(&builder);
    g_dbus_connection_emit_signal(m_connection.get(), nullptr, "/org/a11y/atspi/cache", "org.a11y.atspi.Cache", "AddAccessible",
        g_variant_new("(@(" ITEM_SIGNATURE "))", g_variant_builder_end(&builder)), nullptr);
}

void AccessibilityAtspi::cacheUpdateTimerFired()
{
    auto cacheUpdateList = std::exchange(m_cacheUpdateList, { });
    for (auto& atspiObject : cacheUpdateList)
        addToCacheIfNeeded(*atspiObject);
}

void AccessibilityAtspi::cacheClearTimerFired()
{
    for (const auto& registeredObjects : m_atspiHyperlinks.values()) {
        for (auto id : registeredObjects)
            g_dbus_connection_unregister_object(m_connection.get(), id);
    }
    m_atspiHyperlinks.clear();
    for (const auto& it : m_atspiObjects) {
        for (auto id : it.value)
            g_dbus_connection_unregister_object(m_connection.get(), id);

        it.key->didUnregisterObject();
    }
    m_atspiObjects.clear();
    m_cache.clear();

    RELEASE_ASSERT(m_cacheUpdateList.isEmpty());
    m_cacheUpdateTimer.stop();
    m_cacheClearTimer.stop();
}

namespace Accessibility {

PlatformRoleMap createPlatformRoleMap()
{
    PlatformRoleMap roleMap;
    for (const auto& entry : roleNames)
        roleMap.add(static_cast<unsigned>(entry.first), String::fromUTF8(entry.second.name));
    return roleMap;
}

} // namespace Accessibility

#if ENABLE(DEVELOPER_MODE)
void AccessibilityAtspi::addNotificationObserver(void* context, NotificationObserver&& observer)
{
    AXObjectCache::enableAccessibility();
    m_notificationObservers.add(context, WTFMove(observer));
}

void AccessibilityAtspi::removeNotificationObserver(void* context)
{
    m_notificationObservers.remove(context);
}

void AccessibilityAtspi::notifyStateChanged(AccessibilityObjectAtspi& atspiObject, const char* name, bool value) const
{
    if (m_notificationObservers.isEmpty())
        return;

    auto notificationName = [&](const char* name) -> const char* {
        if (!g_strcmp0(name, "checked"))
            return "CheckedStateChanged";
        if (!g_strcmp0(name, "invalid-entry"))
            return "AXInvalidStatusChanged";
        if (!g_strcmp0(name, "active"))
            return "ActiveStateChanged";
        if (!g_strcmp0(name, "busy"))
            return "AXElementBusyChanged";
        if (!g_strcmp0(name, "enabled"))
            return "AXDisabledStateChanged";
        if (!g_strcmp0(name, "expanded"))
            return "AXExpandedChanged";
        if (!g_strcmp0(name, "pressed"))
            return "AXPressedStateChanged";
        if (!g_strcmp0(name, "read-only"))
            return "AXReadOnlyStatusChanged";
        if (!g_strcmp0(name, "required"))
            return "AXRequiredStatusChanged";
        if (!g_strcmp0(name, "sensitive"))
            return "AXSensitiveStateChanged";
        if (!g_strcmp0(name, "focused") && value)
            return "AXFocusedUIElementChanged";

        return nullptr;
    };

    const char* notification = notificationName(name);
    if (!notification)
        return;

    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, notification, value);
}

void AccessibilityAtspi::notifySelectionChanged(AccessibilityObjectAtspi& atspiObject) const
{
    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, "AXSelectedChildrenChanged", nullptr);
}

void AccessibilityAtspi::notifyMenuSelectionChanged(AccessibilityObjectAtspi& atspiObject) const
{
    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, "AXMenuItemSelected", nullptr);
}

void AccessibilityAtspi::notifyTextChanged(AccessibilityObjectAtspi& atspiObject) const
{
    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, "AXTextChanged", nullptr);
}

void AccessibilityAtspi::notifyTextCaretMoved(AccessibilityObjectAtspi& atspiObject, unsigned caretOffset) const
{
    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, "AXTextCaretMoved", caretOffset);
}

void AccessibilityAtspi::notifyValueChanged(AccessibilityObjectAtspi& atspiObject) const
{
    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, "AXValueChanged", nullptr);
}

void AccessibilityAtspi::notifyLoadEvent(AccessibilityObjectAtspi& atspiObject, const CString& event) const
{
    if (event != "LoadComplete")
        return;

    for (const auto& observer : m_notificationObservers.values())
        observer(atspiObject, "AXLoadComplete", nullptr);
}

#endif

} // namespace WebCore

#endif // USE(ATSPI)
