// Copyright (C) 2025 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 "qspimatchrulematcher_p.h"

#if QT_CONFIG(accessibility)

#include "atspiadaptor_p.h"
#include "qspi_constant_mappings_p.h"

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

Q_STATIC_LOGGING_CATEGORY(lcAccessibilityAtspi, "qt.accessibility.atspi")

QSpiMatchRuleMatcher::QSpiMatchRuleMatcher(const QSpiMatchRule &matchRule)
    : m_states(spiStatesFromSpiStateSet(matchRule.states)),
      m_stateMatchType(matchRule.stateMatchType),
      m_attributes(matchRule.attributes),
      m_attributeMatchType(matchRule.attributeMatchType),
      m_roleMatchType(matchRule.roleMatchType),
      m_interfaceMatchType(matchRule.interfaceMatchType)
{
    // extract roles encoded in bitset stored in multiple 32 bit integers
    for (qsizetype i = 0; i < matchRule.roles.size(); ++i) {
        const auto role = matchRule.roles.at(i);
        for (int j = 0; j < 32; j++) {
            if (role & (1 << j)) {
                const auto atspiRole = i * 32 + j;
                if (atspiRole < ATSPI_ROLE_LAST_DEFINED)
                    m_roles.insert(AtspiRole(atspiRole));
                else
                    qCWarning(lcAccessibilityAtspi)
                            << "Ignoring invalid AT-SPI role value" << atspiRole;
            }
        }
    }

    // use qualified interface names to match what accessibleInterfaces() returns
    m_interfaces.reserve(matchRule.interfaces.size());
    for (const QString &ifaceName : matchRule.interfaces)
        m_interfaces.push_back("org.a11y.atspi."_L1 + ifaceName);
}

bool QSpiMatchRuleMatcher::matchAttributes(QAccessibleInterface &iface) const
{
    switch (m_attributeMatchType) {
    case ATSPI_Collection_MATCH_EMPTY:
        if (m_attributes.empty())
            return AtSpiAdaptor::getAttributes(&iface).isEmpty();
        [[fallthrough]];
    case ATSPI_Collection_MATCH_ALL: {
        if (m_attributes.empty())
            return true;
        const QSpiAttributeSet attributes = AtSpiAdaptor::getAttributes(&iface);
        for (const auto &[key, value] : m_attributes.asKeyValueRange()) {
            if (!attributes.contains(key) || attributes[key] != value)
                return false;
        }
        return true;
    }
    case ATSPI_Collection_MATCH_ANY: {
        const QSpiAttributeSet attributes = AtSpiAdaptor::getAttributes(&iface);
        for (const auto &[key, value] : m_attributes.asKeyValueRange()) {
            if (attributes.contains(key) && attributes[key] == value)
                return true;
        }
        return false;
    }
    case ATSPI_Collection_MATCH_NONE: {
        const QSpiAttributeSet attributes = AtSpiAdaptor::getAttributes(&iface);
        for (const auto &[key, value] : m_attributes.asKeyValueRange()) {
            if (attributes.contains(key) && attributes[key] == value)
                return false;
        }
        return true;
    }
    default:
        qCWarning(lcAccessibilityAtspi)
                << "QSpiMatchRuleMatcher::matchAttributes called with invalid match type "
                << m_attributeMatchType;
        return false;
    }
}

bool QSpiMatchRuleMatcher::matchInterfaces(QAccessibleInterface &iface) const
{
    switch (m_interfaceMatchType) {
    case ATSPI_Collection_MATCH_EMPTY:
        if (m_interfaces.empty())
            return AtSpiAdaptor::accessibleInterfaces(&iface).isEmpty();
        [[fallthrough]];
    case ATSPI_Collection_MATCH_ALL: {
        if (m_interfaces.empty())
            return true;
        const QStringList interfaces = AtSpiAdaptor::accessibleInterfaces(&iface);
        for (const QString &atSpiInterface : m_interfaces) {
            if (!interfaces.contains(atSpiInterface))
                return false;
        }
        return true;
    }
    case ATSPI_Collection_MATCH_ANY: {
        const QStringList interfaces = AtSpiAdaptor::accessibleInterfaces(&iface);
        for (const QString &atSpiInterface : m_interfaces) {
            if (interfaces.contains(atSpiInterface))
                return true;
        }
        return false;
    }
    case ATSPI_Collection_MATCH_NONE: {
        const QStringList interfaces = AtSpiAdaptor::accessibleInterfaces(&iface);
        for (const QString &atSpiInterface : m_interfaces) {
            if (interfaces.contains(atSpiInterface))
                return false;
        }
        return true;
    }
    default:
        qCWarning(lcAccessibilityAtspi)
                << "QSpiMatchRuleMatcher::matchInterfaces called with invalid match type "
                << m_interfaceMatchType;
        return false;
    }
}

bool QSpiMatchRuleMatcher::matchRoles(QAccessibleInterface &iface) const
{
    switch (m_roleMatchType) {
    case ATSPI_Collection_MATCH_EMPTY:
        if (m_roles.empty())
            // accessible always has exactly one role, i.e. can't have no roles
            return false;
        [[fallthrough]];
    case ATSPI_Collection_MATCH_ALL:
        if (m_roles.empty())
            return true;
        if (m_roles.size() > 1)
            // accessible only has a single role
            return false;
        [[fallthrough]];
    case ATSPI_Collection_MATCH_ANY:
        return m_roles.find(AtSpiAdaptor::getRole(&iface)) != m_roles.end();
    case ATSPI_Collection_MATCH_NONE:
        return m_roles.find(AtSpiAdaptor::getRole(&iface)) == m_roles.end();
    default:
        qCWarning(lcAccessibilityAtspi)
                << "QSpiMatchRuleMatcher::matchRoles called with invalid match type "
                << m_roleMatchType;
        return false;
    }
}

bool QSpiMatchRuleMatcher::matchStates(QAccessibleInterface &iface) const
{
    switch (m_stateMatchType) {
    case ATSPI_Collection_MATCH_EMPTY:
        if (m_states == 0)
            return spiStatesFromQState(iface.state()) == 0;
        [[fallthrough]];
    case ATSPI_Collection_MATCH_ALL:
        return (spiStatesFromQState(iface.state()) & m_states) == m_states;
    case ATSPI_Collection_MATCH_ANY:
        return (spiStatesFromQState(iface.state()) & m_states) != 0;
    case ATSPI_Collection_MATCH_NONE:
        return (spiStatesFromQState(iface.state()) & m_states) == 0;
    default:
        qCWarning(lcAccessibilityAtspi)
                << "QSpiMatchRuleMatcher::matchStates called with invalid match type "
                << m_stateMatchType;
        return false;
    }
}

bool QSpiMatchRuleMatcher::match(QAccessibleInterface &iface) const
{
    return matchRoles(iface) && matchStates(iface) && matchInterfaces(iface)
        && matchAttributes(iface);
}

QT_END_NAMESPACE

#endif // QT_CONFIG(accessibility)
