/*
 * Copyright (C) 2021 Igalia S.L.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "AccessibilityUIElement.h"

#if USE(ATSPI)
#include "AccessibilityNotificationHandler.h"
#include "InjectedBundle.h"
#include "InjectedBundlePage.h"
#include <JavaScriptCore/JSStringRef.h>
#include <JavaScriptCore/OpaqueJSString.h>
#include <WebCore/AccessibilityAtspiEnums.h>
#include <WebCore/AccessibilityObjectAtspi.h>
#include <WebKit/WKBundleFrame.h>
#include <wtf/HashSet.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/URL.h>
#include <wtf/text/CString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringToIntegerConversion.h>
#include <wtf/unicode/CharacterNames.h>

namespace WTR {

RefPtr<AccessibilityController> AccessibilityUIElement::s_controller;

AccessibilityUIElement::AccessibilityUIElement(PlatformUIElement element)
    : m_element(element)
{
    if (!s_controller)
        s_controller = InjectedBundle::singleton().accessibilityController();
}

AccessibilityUIElement::AccessibilityUIElement(const AccessibilityUIElement& other)
    : JSWrappable()
    , m_element(other.m_element)
{
}

AccessibilityUIElement::~AccessibilityUIElement()
{
}

bool AccessibilityUIElement::isEqual(AccessibilityUIElement* otherElement)
{
    return otherElement && m_element.get() == otherElement->platformUIElement();
}

void AccessibilityUIElement::getChildren(Vector<RefPtr<AccessibilityUIElement> >& children)
{
}

void AccessibilityUIElement::getChildrenWithRange(Vector<RefPtr<AccessibilityUIElement> >& children, unsigned location, unsigned length)
{
}

int AccessibilityUIElement::childrenCount()
{
    m_element->updateBackingStore();
    return m_element->childCount();
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::elementAtPoint(int x, int y)
{
    m_element->updateBackingStore();
    auto* element = m_element->hitTest({ x, y }, WebCore::Atspi::CoordinateType::WindowCoordinates);
    return AccessibilityUIElement::create(element ? element : m_element.get());
}

unsigned AccessibilityUIElement::indexOfChild(AccessibilityUIElement* element)
{
    return 0;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::childAtIndex(unsigned index)
{
    m_element->updateBackingStore();
    if (auto* child = m_element->childAt(index))
        return AccessibilityUIElement::create(child);
    return nullptr;
}

static RefPtr<AccessibilityUIElement> elementForRelationAtIndex(WebCore::AccessibilityObjectAtspi* element, WebCore::Atspi::Relation relation, unsigned index)
{
    element->updateBackingStore();
    auto relationMap = element->relationMap();
    auto targets = relationMap.get(relation);
    if (targets.isEmpty() || index >= targets.size())
        return nullptr;

    auto target = targets[index];
    if (!target)
        return nullptr;

    return AccessibilityUIElement::create(target.get());
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::linkedUIElementAtIndex(unsigned index)
{
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaOwnsElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::NodeParentOf, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaOwnsReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::NodeChildOf, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaFlowToElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::FlowsTo, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaFlowToReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::FlowsFrom, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaControlsElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::ControllerFor, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaControlsReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::ControlledBy, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaLabelledByElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::LabelledBy, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaLabelledByReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::LabelFor, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaDescribedByElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::DescribedBy, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaDescribedByReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::DescriptionFor, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaDetailsElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::Details, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaDetailsReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::DetailsFor, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaErrorMessageElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::ErrorMessage, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::ariaErrorMessageReferencingElementAtIndex(unsigned index)
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::ErrorFor, index);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::disclosedRowAtIndex(unsigned index)
{
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::rowAtIndex(unsigned index)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return nullptr;

    m_element->updateBackingStore();
    auto rows = m_element->rows();
    if (index >= rows.size())
        return nullptr;

    return AccessibilityUIElement::create(rows[index].get());
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::selectedChildAtIndex(unsigned index) const
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Selection))
        return nullptr;

    m_element->updateBackingStore();
    if (auto* selectedChild = m_element->selectedChild(index))
        return AccessibilityUIElement::create(selectedChild);
    return nullptr;
}

unsigned AccessibilityUIElement::selectedChildrenCount() const
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Selection))
        return 0;

    m_element->updateBackingStore();
    return m_element->selectionCount();
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::selectedRowAtIndex(unsigned index)
{
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::titleUIElement()
{
    return elementForRelationAtIndex(m_element.get(), WebCore::Atspi::Relation::LabelledBy, 0);
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::parentElement()
{
    m_element->updateBackingStore();
    if (auto* parent = m_element->parent().value_or(nullptr))
        return AccessibilityUIElement::create(parent);
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::disclosedByRow()
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfLinkedUIElements()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfDocumentLinks()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

static String attributesOfElement(AccessibilityUIElement& element)
{
    StringBuilder builder;

    builder.append(element.role()->string(), '\n');

    builder.append("AXParent: ");
    if (auto parent = element.parentElement()) {
        builder.append(parent->role()->string().substring(8));
        auto parentName = parent->title()->string().substring(9);
        if (!parentName.isEmpty())
            builder.append(": ", parentName);
    } else
        builder.append("(null)");
    builder.append('\n');

    builder.append("AXChildren: ", element.childrenCount(), '\n');

    builder.append("AXPosition:  { ", FormattedNumber::fixedPrecision(element.x(), 6, KeepTrailingZeros));
    builder.append(", ", FormattedNumber::fixedPrecision(element.y(), 6, KeepTrailingZeros));
    builder.append(" }\n");

    builder.append("AXSize: { ", FormattedNumber::fixedPrecision(element.width(), 6, KeepTrailingZeros));
    builder.append(", ", FormattedNumber::fixedPrecision(element.height(), 6, KeepTrailingZeros));
    builder.append(" }\n");

    String title = element.title()->string();
    if (!title.isEmpty()) {
        builder.append(title);
        builder.append('\n');
    }

    String description = element.description()->string();
    if (!description.isEmpty())
        builder.append(description.utf8().data(), '\n');

    String value = element.stringValue()->string();
    if (!value.isEmpty())
        builder.append(value, '\n');

    builder.append("AXFocusable: ", element.isFocusable(), '\n');
    builder.append("AXFocused: ", element.isFocused(), '\n');
    builder.append("AXSelectable: ", element.isSelectable(), '\n');
    builder.append("AXSelected: ", element.isSelected(), '\n');
    builder.append("AXMultiSelectable: ", element.isMultiSelectable(), '\n');
    builder.append("AXEnabled: ", element.isEnabled(), '\n');
    builder.append("AXExpanded: ", element.isExpanded(), '\n');
    builder.append("AXRequired: ", element.isRequired(), '\n');
    builder.append("AXChecked: ", element.isChecked(), '\n');

    String url = element.url()->string();
    if (!url.isEmpty())
        builder.append(url, '\n');

    // We append the platform attributes as a single line at the end.
    builder.append("AXPlatformAttributes: ");
    auto attributes = element.platformUIElement()->attributes();
    auto keys = copyToVector(attributes.keys());
    std::sort(keys.begin(), keys.end(), WTF::codePointCompareLessThan);

    bool isFirst = true;
    for (const auto& key : keys) {
        if (key == "id"_s || key == "toolkit"_s)
            continue;

        if (!isFirst)
            builder.append(", ");
        isFirst = false;
        builder.append(key, ':', attributes.get(key));
    }

    return builder.toString();
}

static String attributesOfElements(Vector<RefPtr<AccessibilityUIElement>>& elements)
{
    StringBuilder builder;
    for (auto& element : elements)
        builder.append(attributesOfElement(*element), "\n------------\n");
    return builder.toString();
}

static Vector<RefPtr<AccessibilityUIElement>> elementsVector(const Vector<RefPtr<WebCore::AccessibilityObjectAtspi>>& wrappers)
{
    Vector<RefPtr<AccessibilityUIElement>> elements;
    elements.reserveInitialCapacity(wrappers.size());
    for (auto& wrapper : wrappers)
        elements.uncheckedAppend(AccessibilityUIElement::create(wrapper.get()));
    return elements;
}

static String attributesOfElements(const Vector<RefPtr<WebCore::AccessibilityObjectAtspi>>& wrappers)
{
    auto elements = elementsVector(wrappers);
    return attributesOfElements(elements);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfChildren()
{
    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(attributesOfElements(m_element->children())).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::allAttributes()
{
    return OpaqueJSString::tryCreate(attributesOfElement(*this)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::stringDescriptionOfAttributeValue(JSStringRef attribute)
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

static bool checkElementState(WebCore::AccessibilityObjectAtspi* element, WebCore::Atspi::State state)
{
    return element->state() & (G_GUINT64_CONSTANT(1) << state);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::stringAttributeValue(JSStringRef attribute)
{
    String attributeName = toWTFString(attribute);
    if (attributeName == "AXSelectedText"_s) {
        if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
            return JSStringCreateWithCharacters(nullptr, 0);

        m_element->updateBackingStore();
        auto text = m_element->text();
        auto offset = m_element->selectedRange();
        return OpaqueJSString::tryCreate(text.substring(offset.x(), offset.y() - offset.x())).leakRef();
    }

    m_element->updateBackingStore();
    auto attributes = m_element->attributes();
    if (attributeName == "AXPlaceholderValue"_s)
        return OpaqueJSString::tryCreate(attributes.get("placeholder-text"_s)).leakRef();
    if (attributeName == "AXInvalid"_s) {
        auto textAttributes = m_element->textAttributes();
        auto value = textAttributes.attributes.get("invalid"_s);
        if (value.isEmpty())
            value = checkElementState(m_element.get(), WebCore::Atspi::State::InvalidEntry) ? "true"_s : "false"_s;
        return OpaqueJSString::tryCreate(value).leakRef();
    }
    if (attributeName == "AXARIALive"_s)
        return OpaqueJSString::tryCreate(attributes.get("live"_s)).leakRef();
    if (attributeName == "AXARIARelevant"_s)
        return OpaqueJSString::tryCreate(attributes.get("relevant"_s)).leakRef();

    return JSStringCreateWithCharacters(nullptr, 0);
}

double AccessibilityUIElement::numberAttributeValue(JSStringRef attribute)
{
    String attributeName = toWTFString(attribute);
    m_element->updateBackingStore();
    auto attributes = m_element->attributes();
    if (attributeName == "AXARIASetSize"_s)
        return attributes.get("setsize"_s).toDouble();
    if (attributeName == "AXARIAPosInSet"_s)
        return attributes.get("posinset"_s).toDouble();
    if (attributeName == "AXARIAColumnCount"_s)
        return attributes.get("colcount"_s).toDouble();
    if (attributeName == "AXARIARowCount"_s)
        return attributes.get("rowcount"_s).toDouble();
    if (attributeName == "AXARIAColumnIndex"_s)
        return attributes.get("colindex"_s).toDouble();
    if (attributeName == "AXARIARowIndex"_s)
        return attributes.get("rowindex"_s).toDouble();
    if (attributeName == "AXARIAColumnSpan"_s)
        return attributes.get("colspan"_s).toDouble();
    if (attributeName == "AXARIARowSpan"_s)
        return attributes.get("rowspan"_s).toDouble();

    return 0;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::currentStateValue() const
{
    m_element->updateBackingStore();
    auto value = m_element->attributes().get("current"_s);
    return OpaqueJSString::tryCreate(!value.isNull() ? value : "false"_s).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::domIdentifier() const
{
    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(m_element->attributes().get("id"_s)).leakRef();
}

JSValueRef AccessibilityUIElement::uiElementArrayAttributeValue(JSStringRef attribute) const
{
    return nullptr;
}

static JSValueRef makeJSArray(const Vector<RefPtr<AccessibilityUIElement>>& elements)
{
    WKBundleFrameRef mainFrame = WKBundlePageGetMainFrame(InjectedBundle::singleton().page()->page());
    JSContextRef context = WKBundleFrameGetJavaScriptContext(mainFrame);

    size_t elementCount = elements.size();
    auto valueElements = makeUniqueArray<JSValueRef>(elementCount);
    for (size_t i = 0; i < elementCount; i++)
        valueElements[i] = JSObjectMake(context, elements[i]->wrapperClass(), elements[i].get());

    return JSObjectMakeArray(context, elementCount, valueElements.get(), nullptr);
}

JSValueRef AccessibilityUIElement::rowHeaders() const
{
    if (m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table)) {
        m_element->updateBackingStore();
        return makeJSArray(elementsVector(m_element->rowHeaders()));
    }

    if (m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::TableCell)) {
        m_element->updateBackingStore();
        return makeJSArray(elementsVector(m_element->cellRowHeaders()));
    }

    return makeJSArray({ });
}

JSValueRef AccessibilityUIElement::columnHeaders() const
{
    if (m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table)) {
        m_element->updateBackingStore();
        return makeJSArray(elementsVector(m_element->columnHeaders()));
    }

    if (m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::TableCell)) {
        m_element->updateBackingStore();
        return makeJSArray(elementsVector(m_element->cellColumnHeaders()));
    }

    return makeJSArray({ });
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::uiElementAttributeValue(JSStringRef attribute) const
{
    return nullptr;
}

bool AccessibilityUIElement::boolAttributeValue(JSStringRef attribute)
{
    String attributeName = toWTFString(attribute);
    m_element->updateBackingStore();
    if (attributeName == "AXElementBusy"_s)
        return checkElementState(m_element.get(), WebCore::Atspi::State::Busy);
    if (attributeName == "AXModal"_s)
        return checkElementState(m_element.get(), WebCore::Atspi::State::Modal);
    if (attributeName == "AXSupportsAutoCompletion"_s)
        return checkElementState(m_element.get(), WebCore::Atspi::State::SupportsAutocompletion);
    if (attributeName == "AXInterfaceTable"_s)
        return m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table);
    if (attributeName == "AXInterfaceTableCell"_s)
        return m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::TableCell);
    if (attributeName == "AXARIAAtomic"_s)
        return m_element->attributes().get("atomic"_s) == "true"_s;

    return false;
}

bool AccessibilityUIElement::isAttributeSettable(JSStringRef attribute)
{
    String attributeName = toWTFString(attribute);
    if (attributeName != "AXValue"_s)
        return false;

    m_element->updateBackingStore();
    if (checkElementState(m_element.get(), WebCore::Atspi::State::ReadOnly))
        return false;

    if (checkElementState(m_element.get(), WebCore::Atspi::State::Editable))
        return true;

    if (checkElementState(m_element.get(), WebCore::Atspi::State::Checkable))
        return true;

    auto attributes = m_element->attributes();
    String isReadOnly = attributes.get("readonly"_s);
    if (!isReadOnly.isEmpty())
        return isReadOnly == "true"_s ? false : true;

    // If we have a listbox or combobox and the value can be set, the options should be selectable.
    auto elementRole = m_element->role();
    switch (elementRole) {
    case WebCore::Atspi::Role::ComboBox:
    case WebCore::Atspi::Role::ListBox:
        if (auto child = childAtIndex(0)) {
            if (elementRole == WebCore::Atspi::Role::ComboBox) {
                // First child is the menu.
                child = child->childAtIndex(0);
            }

            if (child)
                return checkElementState(child->m_element.get(), WebCore::Atspi::State::Selectable);
        }
        break;
    default:
        break;
    }

    if (m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Value) && checkElementState(m_element.get(), WebCore::Atspi::State::Focusable)) {
        if (m_element->minimumValue() != m_element->maximumValue())
            return true;
    }

    return false;
}

bool AccessibilityUIElement::isAttributeSupported(JSStringRef attribute)
{
    String attributeName = toWTFString(attribute);
    m_element->updateBackingStore();
    auto attributes = m_element->attributes();
    if (attributeName == "AXARIASetSize"_s)
        return attributes.contains("setsize"_s);
    if (attributeName == "AXARIAPosInSet"_s)
        return attributes.contains("posinset"_s);
    if (attributeName == "AXARIALive"_s) {
        auto value = attributes.get("live"_s);
        return !value.isEmpty() && value != "off"_s;
    }
    if (attributeName == "AXARIARelevant"_s)
        return attributes.contains("relevant"_s);
    if (attributeName == "AXARIAAtomic"_s)
        return attributes.contains("atomic"_s);
    if (attributeName == "AXElementBusy"_s)
        return true;

    return false;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::parameterizedAttributeNames()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

static String xmlRoleValueString(const String& xmlRoles)
{
    static NeverDestroyed<HashSet<String, ASCIICaseInsensitiveHash>> regionRoles = HashSet<String, ASCIICaseInsensitiveHash>({
        "doc-acknowledgments"_s,
        "doc-afterword"_s,
        "doc-appendix"_s,
        "doc-bibliography"_s,
        "doc-chapter"_s,
        "doc-conclusion"_s,
        "doc-credits"_s,
        "doc-endnotes"_s,
        "doc-epilogue"_s,
        "doc-errata"_s,
        "doc-foreword"_s,
        "doc-glossary"_s,
        "doc-glossref"_s,
        "doc-index"_s,
        "doc-introduction"_s,
        "doc-pagelist"_s,
        "doc-part"_s,
        "doc-preface"_s,
        "doc-prologue"_s,
        "doc-toc"_s,
        "region"_s
    });

    if (regionRoles->contains(xmlRoles))
        return "AXLandmarkRegion"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "banner"_s))
        return "AXLandmarkBanner"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "complementary"_s))
        return "AXLandmarkComplementary"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "contentinfo"_s))
        return "AXLandmarkContentInfo"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "form"_s))
        return "AXLandmarkForm"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "main"_s))
        return "AXLandmarkMain"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "navigation"_s))
        return "AXLandmarkNavigation"_s;
    if (equalLettersIgnoringASCIICase(xmlRoles, "search"_s))
        return "AXLandmarkSearch"_s;

    return { };
}

static String roleValueToString(unsigned roleValue)
{
    switch (roleValue) {
    case WebCore::Atspi::Role::Alert:
        return "AXAlert"_s;
    case WebCore::Atspi::Role::Article:
        return "AXArticle"_s;
    case WebCore::Atspi::Role::Audio:
        return "AXAudio"_s;
    case WebCore::Atspi::Role::BlockQuote:
        return "AXBlockquote"_s;
    case WebCore::Atspi::Role::Canvas:
        return "AXCanvas"_s;
    case WebCore::Atspi::Role::Caption:
        return "AXCaption"_s;
    case WebCore::Atspi::Role::CheckBox:
        return "AXCheckBox"_s;
    case WebCore::Atspi::Role::CheckMenuItem:
        return "AXCheckMenuItem"_s;
    case WebCore::Atspi::Role::ColorChooser:
        return "AXColorWell"_s;
    case WebCore::Atspi::Role::ColumnHeader:
    case WebCore::Atspi::Role::TableColumnHeader:
        return "AXColumnHeader"_s;
    case WebCore::Atspi::Role::ComboBox:
        return "AXComboBox"_s;
    case WebCore::Atspi::Role::Comment:
        return "AXComment"_s;
    case WebCore::Atspi::Role::ContentDeletion:
        return "AXDeletion"_s;
    case WebCore::Atspi::Role::ContentInsertion:
        return "AXInsertion"_s;
    case WebCore::Atspi::Role::Definition:
        return "AXDefinition"_s;
    case WebCore::Atspi::Role::DescriptionList:
        return "AXDescriptionList"_s;
    case WebCore::Atspi::Role::DescriptionTerm:
        return "AXDescriptionTerm"_s;
    case WebCore::Atspi::Role::DescriptionValue:
        return "AXDescriptionValue"_s;
    case WebCore::Atspi::Role::Dialog:
        return "AXDialog"_s;
    case WebCore::Atspi::Role::DocumentFrame:
        return "AXDocument"_s;
    case WebCore::Atspi::Role::DocumentWeb:
        return "AXWebArea"_s;
    case WebCore::Atspi::Role::Embedded:
        return "AXEmbedded"_s;
    case WebCore::Atspi::Role::Entry:
        return "AXTextField"_s;
    case WebCore::Atspi::Role::Footer:
        return "AXFooter"_s;
    case WebCore::Atspi::Role::Footnote:
        return "AXFootnote"_s;
    case WebCore::Atspi::Role::Form:
        return "AXForm"_s;
    case WebCore::Atspi::Role::Grouping:
    case WebCore::Atspi::Role::Panel:
        return "AXGroup"_s;
    case WebCore::Atspi::Role::Heading:
        return "AXHeading"_s;
    case WebCore::Atspi::Role::Image:
        return "AXImage"_s;
    case WebCore::Atspi::Role::ImageMap:
        return "AXImageMap"_s;
    case WebCore::Atspi::Role::InvalidRole:
        return "AXInvalid"_s;
    case WebCore::Atspi::Role::Label:
        return "AXLabel"_s;
    case WebCore::Atspi::Role::LevelBar:
        return "AXLevelIndicator"_s;
    case WebCore::Atspi::Role::Link:
        return "AXLink"_s;
    case WebCore::Atspi::Role::ListBox:
        return "AXListBox"_s;
    case WebCore::Atspi::Role::List:
        return "AXList"_s;
    case WebCore::Atspi::Role::ListItem:
        return "AXListItem"_s;
    case WebCore::Atspi::Role::Log:
        return "AXLog"_s;
    case WebCore::Atspi::Role::Marquee:
        return "AXMarquee"_s;
    case WebCore::Atspi::Role::Math:
        return "AXMath"_s;
    case WebCore::Atspi::Role::MathFraction:
        return "AXMathFraction"_s;
    case WebCore::Atspi::Role::MathRoot:
        return "AXMathRoot"_s;
    case WebCore::Atspi::Role::Menu:
        return "AXMenu"_s;
    case WebCore::Atspi::Role::MenuBar:
        return "AXMenuBar"_s;
    case WebCore::Atspi::Role::MenuItem:
        return "AXMenuItem"_s;
    case WebCore::Atspi::Role::Notification:
        return "AXNotification"_s;
    case WebCore::Atspi::Role::PageTab:
        return "AXTab"_s;
    case WebCore::Atspi::Role::PageTabList:
        return "AXTabGroup"_s;
    case WebCore::Atspi::Role::Paragraph:
        return "AXParagraph"_s;
    case WebCore::Atspi::Role::PasswordText:
        return "AXPasswordField"_s;
    case WebCore::Atspi::Role::ProgressBar:
        return "AXProgressIndicator"_s;
    case WebCore::Atspi::Role::PushButton:
        return "AXButton"_s;
    case WebCore::Atspi::Role::RadioButton:
        return "AXRadioButton"_s;
    case WebCore::Atspi::Role::RadioMenuItem:
        return "AXRadioMenuItem"_s;
    case WebCore::Atspi::Role::RowHeader:
    case WebCore::Atspi::Role::TableRowHeader:
        return "AXRowHeader"_s;
    case WebCore::Atspi::Role::Ruler:
        return "AXRuler"_s;
    case WebCore::Atspi::Role::ScrollBar:
        return "AXScrollBar"_s;
    case WebCore::Atspi::Role::ScrollPane:
        return "AXScrollArea"_s;
    case WebCore::Atspi::Role::Section:
        return "AXSection"_s;
    case WebCore::Atspi::Role::Separator:
        return "AXSeparator"_s;
    case WebCore::Atspi::Role::Slider:
        return "AXSlider"_s;
    case WebCore::Atspi::Role::SpinButton:
        return "AXSpinButton"_s;
    case WebCore::Atspi::Role::Static:
    case WebCore::Atspi::Role::Text:
        return "AXStatic"_s;
    case WebCore::Atspi::Role::StatusBar:
        return "AXStatusBar"_s;
    case WebCore::Atspi::Role::Subscript:
        return "AXSubscript"_s;
    case WebCore::Atspi::Role::Superscript:
        return "AXSuperscript"_s;
    case WebCore::Atspi::Role::Table:
        return "AXTable"_s;
    case WebCore::Atspi::Role::TableCell:
        return "AXCell"_s;
    case WebCore::Atspi::Role::TableRow:
        return "AXRow"_s;
    case WebCore::Atspi::Role::Timer:
        return "AXTimer"_s;
    case WebCore::Atspi::Role::ToggleButton:
        return "AXToggleButton"_s;
    case WebCore::Atspi::Role::ToolBar:
        return "AXToolbar"_s;
    case WebCore::Atspi::Role::ToolTip:
        return "AXUserInterfaceTooltip"_s;
    case WebCore::Atspi::Role::Tree:
        return "AXTree"_s;
    case WebCore::Atspi::Role::TreeTable:
        return "AXTreeGrid"_s;
    case WebCore::Atspi::Role::TreeItem:
        return "AXTreeItem"_s;
    case WebCore::Atspi::Role::Unknown:
        return "AXUnknown"_s;
    case WebCore::Atspi::Role::Video:
        return "AXVideo"_s;
    case WebCore::Atspi::Role::Window:
        return "AXWindow"_s;
    default:
        break;
    }

    return { };
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::role()
{
    m_element->updateBackingStore();
    auto roleValue = m_element->role();
    auto roleValueString = roleValue == WebCore::Atspi::Role::Landmark ? xmlRoleValueString(m_element->attributes().get("xml-roles"_s)) : roleValueToString(roleValue);
    if (roleValueString.isEmpty())
        return JSStringCreateWithCharacters(nullptr, 0);

    return OpaqueJSString::tryCreate(makeString("AXRole: ", roleValueString)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::subrole()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::roleDescription()
{
    m_element->updateBackingStore();
    auto roleDescription = m_element->attributes().get("roledescription"_s);
    return OpaqueJSString::tryCreate(makeString("AXRoleDescription: ", roleDescription)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::computedRoleString()
{
    m_element->updateBackingStore();
    auto computedRole = m_element->attributes().get("computed-role"_s);
    if (computedRole.isEmpty())
        return JSStringCreateWithCharacters(nullptr, 0);

    return OpaqueJSString::tryCreate(computedRole).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::title()
{
    m_element->updateBackingStore();
    auto titleValue = makeString("AXTitle: ", String::fromUTF8(m_element->name()));
    return OpaqueJSString::tryCreate(titleValue).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::description()
{
    m_element->updateBackingStore();
    auto descriptionValue = makeString("AXDescription: ", String::fromUTF8(m_element->description()));
    return OpaqueJSString::tryCreate(descriptionValue).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::orientation() const
{
    m_element->updateBackingStore();
    const char* orientation = nullptr;
    if (checkElementState(m_element.get(), WebCore::Atspi::State::Horizontal))
        orientation = "AXHorizontalOrientation";
    else if (checkElementState(m_element.get(), WebCore::Atspi::State::Vertical))
        orientation = "AXVerticalOrientation";
    else
        orientation = "AXUnknownOrientation";

    auto orientationValue = makeString("AXOrientation: ", orientation);
    return OpaqueJSString::tryCreate(orientationValue).leakRef();
}

bool AccessibilityUIElement::isAtomicLiveRegion() const
{
    return false;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::liveRegionRelevant() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::liveRegionStatus() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::stringValue()
{
    m_element->updateBackingStore();
    if (m_element->role() == WebCore::Atspi::Role::ComboBox) {
        // Tests expect the combo box to expose the selected element name as the string value.
        if (auto menu = childAtIndex(0)) {
            if (auto* selectedChild = menu->m_element->selectedChild(0))
                return OpaqueJSString::tryCreate(makeString("AXValue: ", String::fromUTF8(selectedChild->name()))).leakRef();
        }
    }

    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    auto value = makeString("AXValue: ", makeStringByReplacingAll(makeStringByReplacingAll(m_element->text(), '\n', "<\\n>"_s), objectReplacementCharacter, "<obj>"_s));
    return OpaqueJSString::tryCreate(value).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::language()
{
    m_element->updateBackingStore();
    auto locale = m_element->locale();
    if (locale.isEmpty())
        return JSStringCreateWithCharacters(nullptr, 0);

    return OpaqueJSString::tryCreate(makeString("AXLanguage: ", locale)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::helpText() const
{
    m_element->updateBackingStore();
    auto relationMap = m_element->relationMap();
    auto targets = relationMap.get(WebCore::Atspi::Relation::DescribedBy);
    if (targets.isEmpty())
        return JSStringCreateWithCharacters(nullptr, 0);

    StringBuilder builder;
    builder.append("AXHelp: ");

    bool isFirst = true;
    for (const auto& target : targets) {
        if (!isFirst)
            builder.append(' ');
        isFirst = false;
        target->updateBackingStore();
        builder.append(target->text());
    }

    return OpaqueJSString::tryCreate(builder.toString()).leakRef();
}

double AccessibilityUIElement::x()
{
    m_element->updateBackingStore();
    return m_element->elementRect(WebCore::Atspi::CoordinateType::ScreenCoordinates).x();
}

double AccessibilityUIElement::y()
{
    m_element->updateBackingStore();
    return m_element->elementRect(WebCore::Atspi::CoordinateType::ScreenCoordinates).y();
}

double AccessibilityUIElement::width()
{
    m_element->updateBackingStore();
    return m_element->elementRect(WebCore::Atspi::CoordinateType::ScreenCoordinates).width();
}

double AccessibilityUIElement::height()
{
    m_element->updateBackingStore();
    return m_element->elementRect(WebCore::Atspi::CoordinateType::ScreenCoordinates).height();
}

double AccessibilityUIElement::clickPointX()
{
    m_element->updateBackingStore();
    auto rect = m_element->elementRect(WebCore::Atspi::CoordinateType::WindowCoordinates);
    return rect.center().x();
}

double AccessibilityUIElement::clickPointY()
{
    m_element->updateBackingStore();
    auto rect = m_element->elementRect(WebCore::Atspi::CoordinateType::WindowCoordinates);
    return rect.center().y();
}

double AccessibilityUIElement::intValue() const
{
    m_element->updateBackingStore();
    if (m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Value))
        return m_element->currentValue();

    // Consider headings as an special case when returning the int value.
    if (m_element->role() == WebCore::Atspi::Role::Heading)
        return m_element->attributes().get("level"_s).toDouble();

    return 0;
}

double AccessibilityUIElement::minValue()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Value))
        return 0;

    m_element->updateBackingStore();
    return m_element->minimumValue();
}

double AccessibilityUIElement::maxValue()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Value))
        return 0;

    m_element->updateBackingStore();
    return m_element->maximumValue();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::valueDescription()
{
    m_element->updateBackingStore();
    auto attributes = m_element->attributes();
    auto value = makeString("AXValueDescription: ", attributes.get("valuetext"_s));
    return OpaqueJSString::tryCreate(value).leakRef();
}

int AccessibilityUIElement::insertionPointLineNumber()
{
    return -1;
}

bool AccessibilityUIElement::isPressActionSupported()
{
    m_element->updateBackingStore();
    auto name = m_element->actionName();
    return name == "press"_s || name == "jump"_s;
}

bool AccessibilityUIElement::isIncrementActionSupported()
{
    return false;
}

bool AccessibilityUIElement::isDecrementActionSupported()
{
    return false;
}

bool AccessibilityUIElement::isBusy() const
{
    // FIXME: Implement.
    return false;
}

bool AccessibilityUIElement::isEnabled()
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Enabled);
}

bool AccessibilityUIElement::isRequired() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Required);
}

bool AccessibilityUIElement::isFocused() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Focused);
}

bool AccessibilityUIElement::isSelected() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Selected);
}

bool AccessibilityUIElement::isSelectedOptionActive() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Active);
}

bool AccessibilityUIElement::isExpanded() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Expanded);
}

bool AccessibilityUIElement::isChecked() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Checked);
}

bool AccessibilityUIElement::isIndeterminate() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Indeterminate);
}

int AccessibilityUIElement::hierarchicalLevel() const
{
    m_element->updateBackingStore();
    auto level = m_element->attributes().get("level"_s);
    if (level.isEmpty())
        return 0;

    return parseIntegerAllowingTrailingJunk<int>(level).value_or(0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::speakAs()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

bool AccessibilityUIElement::ariaIsGrabbed() const
{
    m_element->updateBackingStore();
    return m_element->attributes().get("grabbed"_s) == "true"_s;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::ariaDropEffects() const
{
    m_element->updateBackingStore();
    auto dropEffects = m_element->attributes().get("dropeffect"_s);
    if (dropEffects.isEmpty())
        return JSStringCreateWithCharacters(nullptr, 0);
    return OpaqueJSString::tryCreate(dropEffects).leakRef();
}

int AccessibilityUIElement::lineForIndex(int index)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return -1;

    m_element->updateBackingStore();
    auto text = m_element->text();
    if (index < 0 || index > static_cast<int>(text.length()))
        return -1;

    int lineNumber = 0;
    for (int i = 0; i < index; ++i) {
        if (text[i] == '\n')
            lineNumber++;
    }

    return lineNumber;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::rangeForLine(int line)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    WebCore::IntPoint offset;
    for (int i = 0; i <= line; ++i)
        offset = m_element->boundaryOffset(offset.y(), WebCore::AccessibilityObjectAtspi::TextGranularity::LineStart);

    auto range = makeString('{', offset.x(), ", ", offset.y() - offset.x(), '}');
    return OpaqueJSString::tryCreate(range).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::rangeForPosition(int x, int y)
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::boundsForRange(unsigned location, unsigned length)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto rect = m_element->boundsForRange(location, length, WebCore::Atspi::CoordinateType::WindowCoordinates);
    auto bounds = makeString('{', rect.x(), ", ", rect.y(), ", ", rect.width(), ", ", rect.height(), '}');
    return OpaqueJSString::tryCreate(bounds).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::stringForRange(unsigned location, unsigned length)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(m_element->text().substring(location, length)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributedStringForRange(unsigned location, unsigned length)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto text = m_element->text();
    auto limit = location + length;

    if (limit > text.length())
        return JSStringCreateWithCharacters(nullptr, 0);

    StringBuilder builder;

    auto buildAttributes = [&](const WebCore::AccessibilityObjectAtspi::TextAttributes& attributes) {
        for (const auto& it : attributes.attributes) {
            builder.append("\n\t\t");
            builder.append(it.key, ':', it.value);
        }
    };

    m_element->updateBackingStore();
    builder.append("\n\tDefault text attributes:");
    buildAttributes(m_element->textAttributes());

    int endOffset = 0;
    for (unsigned i = location; i < limit; i = endOffset) {
        auto attributes = m_element->textAttributes(i);
        auto rangeStart = std::max<int>(location, attributes.startOffset);
        auto rangeEnd = std::min<int>(limit, attributes.endOffset);
        builder.append("\n\tRange attributes for '", makeStringByReplacingAll(makeStringByReplacingAll(text.substring(rangeStart, rangeEnd - rangeStart), '\n', "<\\n>"_s), objectReplacementCharacter, "<obj>"_s), "':");
        buildAttributes(attributes);
        endOffset = attributes.endOffset;
    }

    return OpaqueJSString::tryCreate(builder.toString()).leakRef();
}

bool AccessibilityUIElement::attributedStringRangeIsMisspelled(unsigned location, unsigned length)
{
    return false;
}

unsigned AccessibilityUIElement::uiElementCountForSearchPredicate(JSContextRef context, AccessibilityUIElement* startElement, bool isDirectionNext, JSValueRef searchKey, JSStringRef searchText, bool visibleOnly, bool immediateDescendantsOnly)
{
    return 0;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::uiElementForSearchPredicate(JSContextRef context, AccessibilityUIElement* startElement, bool isDirectionNext, JSValueRef searchKey, JSStringRef searchText, bool visibleOnly, bool immediateDescendantsOnly)
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::selectTextWithCriteria(JSContextRef context, JSStringRef ambiguityResolution, JSValueRef searchStrings, JSStringRef replacementString, JSStringRef activity)
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfColumnHeaders()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(attributesOfElements(m_element->columnHeaders())).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfRowHeaders()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(attributesOfElements(m_element->rowHeaders())).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfColumns()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfRows()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(attributesOfElements(m_element->rows())).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfVisibleCells()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(attributesOfElements(m_element->cells())).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributesOfHeader()
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

int AccessibilityUIElement::rowCount()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return 0;

    m_element->updateBackingStore();
    return m_element->rowCount();
}

int AccessibilityUIElement::columnCount()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return 0;

    m_element->updateBackingStore();
    return m_element->columnCount();
}

int AccessibilityUIElement::indexInTable()
{
    return -1;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::rowIndexRange()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::TableCell))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto position = m_element->cellPosition().first;
    auto span = m_element->rowSpan();
    if (!position || !span)
        return JSStringCreateWithCharacters(nullptr, 0);

    return OpaqueJSString::tryCreate(makeString('{', *position, ", ", span, '}')).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::columnIndexRange()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::TableCell))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto position = m_element->cellPosition().second;
    auto span = m_element->columnSpan();
    if (!position || !span)
        return JSStringCreateWithCharacters(nullptr, 0);

    return OpaqueJSString::tryCreate(makeString('{', *position, ", ", span, '}')).leakRef();
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::cellForColumnAndRow(unsigned column, unsigned row)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Table))
        return nullptr;

    m_element->updateBackingStore();
    if (auto* cell = m_element->cell(row, column))
        return AccessibilityUIElement::create(cell);
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::horizontalScrollbar() const
{
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::verticalScrollbar() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::selectedTextRange()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto offset = m_element->selectedRange();
    auto range = makeString('{', offset.x(), ", ", offset.y() - offset.x(), '}');
    return OpaqueJSString::tryCreate(range).leakRef();
}

bool AccessibilityUIElement::setSelectedTextRange(unsigned location, unsigned length)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return false;

    m_element->updateBackingStore();
    auto textLength = m_element->text().length();
    m_element->setSelectedRange(std::min(location, textLength), std::min(length, textLength));
    return true;
}

void AccessibilityUIElement::increment()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Value))
        return;

    m_element->updateBackingStore();
    m_element->setCurrentValue(intValue() + m_element->minimumIncrement());
}

void AccessibilityUIElement::decrement()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Value))
        return;

    m_element->updateBackingStore();
    m_element->setCurrentValue(intValue() - m_element->minimumIncrement());
}

void AccessibilityUIElement::showMenu()
{
}

void AccessibilityUIElement::press()
{
    m_element->updateBackingStore();
    m_element->doAction();
}

void AccessibilityUIElement::setSelectedChild(AccessibilityUIElement* element) const
{
}

void AccessibilityUIElement::setSelectedChildAtIndex(unsigned index) const
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Selection))
        return;

    m_element->updateBackingStore();
    m_element->setChildSelected(index, true);
}

void AccessibilityUIElement::removeSelectionAtIndex(unsigned index) const
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Selection))
        return;

    m_element->updateBackingStore();
    m_element->setChildSelected(index, false);
}

void AccessibilityUIElement::clearSelectedChildren() const
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Selection))
        return;

    m_element->updateBackingStore();
    m_element->clearSelection();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::accessibilityValue() const
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::documentEncoding()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Document))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(m_element->documentAttribute("Encoding"_s)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::documentURI()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Document))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    return OpaqueJSString::tryCreate(m_element->documentAttribute("URI"_s)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::url()
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Hyperlink))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto axURL = m_element->url();
    if (axURL.isNull())
        return JSStringCreateWithUTF8CString("AXURL: (null)");

    auto stringURL = axURL.string();
    if (axURL.isLocalFile()) {
        // Do not expose absolute paths.
        auto index = stringURL.find("LayoutTests"_s);
        if (index != notFound)
            stringURL = stringURL.substring(index);
    }
    return OpaqueJSString::tryCreate(makeString("AXURL: ", stringURL)).leakRef();
}

bool AccessibilityUIElement::addNotificationListener(JSValueRef functionCallback)
{
    if (!functionCallback)
        return false;

    if (m_notificationHandler)
        return false;

    m_notificationHandler = makeUnique<AccessibilityNotificationHandler>(functionCallback, m_element.get());
    return true;
}

bool AccessibilityUIElement::removeNotificationListener()
{
    ASSERT(m_notificationHandler);
    m_notificationHandler = nullptr;
    return true;
}

bool AccessibilityUIElement::isFocusable() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Focusable);
}

bool AccessibilityUIElement::isSelectable() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Selectable);
}

bool AccessibilityUIElement::isMultiSelectable() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Multiselectable);
}

bool AccessibilityUIElement::isVisible() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Visible);
}

bool AccessibilityUIElement::isOffScreen() const
{
    m_element->updateBackingStore();
    return !checkElementState(m_element.get(), WebCore::Atspi::State::Showing);
}

bool AccessibilityUIElement::isCollapsed() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::Collapsed);
}

bool AccessibilityUIElement::isIgnored() const
{
    m_element->updateBackingStore();
    return m_element->isIgnored();
}

bool AccessibilityUIElement::isSingleLine() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::SingleLine);
}

bool AccessibilityUIElement::isMultiLine() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::MultiLine);
}

bool AccessibilityUIElement::hasPopup() const
{
    m_element->updateBackingStore();
    return checkElementState(m_element.get(), WebCore::Atspi::State::HasPopup);
}

void AccessibilityUIElement::takeFocus()
{
}

void AccessibilityUIElement::takeSelection()
{
}

void AccessibilityUIElement::addSelection()
{
}

void AccessibilityUIElement::removeSelection()
{
}

// Text markers
RefPtr<AccessibilityTextMarkerRange> AccessibilityUIElement::lineTextMarkerRangeForTextMarker(AccessibilityTextMarker* textMarker)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarkerRange> AccessibilityUIElement::textMarkerRangeForElement(AccessibilityUIElement* element)
{
    return nullptr;
}

int AccessibilityUIElement::textMarkerRangeLength(AccessibilityTextMarkerRange* range)
{
    return 0;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::previousTextMarker(AccessibilityTextMarker* textMarker)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::nextTextMarker(AccessibilityTextMarker* textMarker)
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::stringForTextMarkerRange(AccessibilityTextMarkerRange* markerRange)
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::rectsForTextMarkerRange(AccessibilityTextMarkerRange* markerRange, JSStringRef searchText)
{
    return JSStringCreateWithCharacters(nullptr, 0);
}

RefPtr<AccessibilityTextMarkerRange> AccessibilityUIElement::textMarkerRangeForMarkers(AccessibilityTextMarker* startMarker, AccessibilityTextMarker* endMarker)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::startTextMarkerForTextMarkerRange(AccessibilityTextMarkerRange* range)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::endTextMarkerForTextMarkerRange(AccessibilityTextMarkerRange* range)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::endTextMarkerForBounds(int x, int y, int width, int height)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::startTextMarkerForBounds(int x, int y, int width, int height)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::textMarkerForPoint(int x, int y)
{
    return nullptr;
}

RefPtr<AccessibilityUIElement> AccessibilityUIElement::accessibilityElementForTextMarker(AccessibilityTextMarker* marker)
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributedStringForTextMarkerRange(AccessibilityTextMarkerRange*)
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::attributedStringForTextMarkerRangeWithOptions(AccessibilityTextMarkerRange*, bool)
{
    return nullptr;
}

bool AccessibilityUIElement::attributedStringForTextMarkerRangeContainsAttribute(JSStringRef attribute, AccessibilityTextMarkerRange* range)
{
    return false;
}

int AccessibilityUIElement::indexForTextMarker(AccessibilityTextMarker* marker)
{
    return -1;
}

bool AccessibilityUIElement::isTextMarkerValid(AccessibilityTextMarker* textMarker)
{
    return false;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::textMarkerForIndex(int textIndex)
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::startTextMarker()
{
    return nullptr;
}

RefPtr<AccessibilityTextMarker> AccessibilityUIElement::endTextMarker()
{
    return nullptr;
}

bool AccessibilityUIElement::setSelectedTextMarkerRange(AccessibilityTextMarkerRange*)
{
    return false;
}

void AccessibilityUIElement::scrollToMakeVisible()
{
    m_element->updateBackingStore();
    m_element->scrollToMakeVisible(WebCore::Atspi::ScrollType::Anywhere);
}

void AccessibilityUIElement::scrollToGlobalPoint(int x, int y)
{
    m_element->updateBackingStore();
    m_element->scrollToPoint({ x, y }, WebCore::Atspi::CoordinateType::WindowCoordinates);
}

void AccessibilityUIElement::scrollToMakeVisibleWithSubFocus(int x, int y, int width, int height)
{
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::supportedActions() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::pathDescription() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::mathPostscriptsDescription() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::mathPrescriptsDescription() const
{
    return nullptr;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::classList() const
{
    return nullptr;
}

static String stringAtOffset(WebCore::AccessibilityObjectAtspi* element, int offset, WebCore::AccessibilityObjectAtspi::TextGranularity granularity)
{
    if (!element || !element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return { };

    element->updateBackingStore();
    auto text = element->text();
    if (offset < 0 || offset > static_cast<int>(text.length()))
        return { };

    auto bounds = element->boundaryOffset(offset, granularity);
    unsigned startOffset = std::max<int>(bounds.x(), 0);
    unsigned endOffset = std::min<int>(bounds.y(), text.length());
    return makeString(text.substring(startOffset, endOffset - startOffset), ", ", startOffset, ", ", endOffset);
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::characterAtOffset(int offset)
{
    if (!m_element->interfaces().contains(WebCore::AccessibilityObjectAtspi::Interface::Text))
        return JSStringCreateWithCharacters(nullptr, 0);

    m_element->updateBackingStore();
    auto text = m_element->text();
    if (offset < 0 || offset > static_cast<int>(text.length()))
        return JSStringCreateWithCharacters(nullptr, 0);

    auto string = makeString(text.substring(offset, 1), ", ", offset, ", ", offset + 1);
    return OpaqueJSString::tryCreate(string).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::wordAtOffset(int offset)
{
    return OpaqueJSString::tryCreate(stringAtOffset(m_element.get(), offset, WebCore::AccessibilityObjectAtspi::TextGranularity::WordStart)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::lineAtOffset(int offset)
{
    return OpaqueJSString::tryCreate(stringAtOffset(m_element.get(), offset, WebCore::AccessibilityObjectAtspi::TextGranularity::LineStart)).leakRef();
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::sentenceAtOffset(int offset)
{
    return OpaqueJSString::tryCreate(stringAtOffset(m_element.get(), offset, WebCore::AccessibilityObjectAtspi::TextGranularity::SentenceStart)).leakRef();
}

bool AccessibilityUIElement::replaceTextInRange(JSStringRef, int, int)
{
    return false;
}

bool AccessibilityUIElement::insertText(JSStringRef)
{
    return false;
}

JSRetainPtr<JSStringRef> AccessibilityUIElement::popupValue() const
{
    return nullptr;
}

} // namespace WTR

#endif // USE(ATSPI)
