/*
 * Copyright (C) 2021 Igalia S.L.
 *
 * 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 "AccessibilityObjectAtspi.h"

#if USE(ATSPI)
#include "AXObjectCache.h"
#include "AccessibilityAtspi.h"
#include "AccessibilityAtspiEnums.h"
#include "AccessibilityObject.h"
#include "AccessibilityObjectInterface.h"
#include "Editing.h"
#include "PlatformScreen.h"
#include "RenderLayer.h"
#include "RenderListItem.h"
#include "RenderListMarker.h"
#include "SurrogatePairAwareTextIterator.h"
#include "TextIterator.h"
#include "VisibleUnits.h"
#include <gio/gio.h>
#include <wtf/unicode/CharacterNames.h>

namespace WebCore {

AccessibilityObjectAtspi::TextGranularity AccessibilityObjectAtspi::atspiBoundaryToTextGranularity(uint32_t boundaryType)
{
    switch (boundaryType) {
    case Atspi::TextBoundaryType::CharBoundary:
        return TextGranularity::Character;
    case Atspi::TextBoundaryType::WordStartBoundary:
        return TextGranularity::WordStart;
    case Atspi::TextBoundaryType::WordEndBoundary:
        return TextGranularity::WordEnd;
    case Atspi::TextBoundaryType::SentenceStartBoundary:
        return TextGranularity::SentenceStart;
    case Atspi::TextBoundaryType::SentenceEndBoundary:
        return TextGranularity::SentenceEnd;
    case Atspi::TextBoundaryType::LineStartBoundary:
        return TextGranularity::LineStart;
    case Atspi::TextBoundaryType::LineEndBoundary:
        return TextGranularity::LineEnd;
    }
    RELEASE_ASSERT_NOT_REACHED();
}

AccessibilityObjectAtspi::TextGranularity AccessibilityObjectAtspi::atspiGranularityToTextGranularity(uint32_t boundaryType)
{
    switch (boundaryType) {
    case Atspi::TextGranularityType::CharGranularity:
        return TextGranularity::Character;
    case Atspi::TextGranularityType::WordGranularity:
        return TextGranularity::WordStart;
    case Atspi::TextGranularityType::SentenceGranularity:
        return TextGranularity::SentenceStart;
    case Atspi::TextGranularityType::LineGranularity:
        return TextGranularity::LineStart;
    case Atspi::TextGranularityType::ParagraphGranularity:
        return TextGranularity::Paragraph;
    }
    RELEASE_ASSERT_NOT_REACHED();
}

GDBusInterfaceVTable AccessibilityObjectAtspi::s_textFunctions = {
    // method_call
    [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* methodName, GVariant* parameters, GDBusMethodInvocation* invocation, gpointer userData) {
        auto atspiObject = Ref { *static_cast<AccessibilityObjectAtspi*>(userData) };
        atspiObject->updateBackingStore();

        if (!g_strcmp0(methodName, "GetStringAtOffset")) {
            int offset;
            uint32_t granularityType;
            g_variant_get(parameters, "(iu)", &offset, &granularityType);
            int start = 0, end = 0;
            auto text = atspiObject->textAtOffset(offset, atspiGranularityToTextGranularity(granularityType), start, end);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(sii)", text.isNull() ? "" : text.data(), start, end));
        } else if (!g_strcmp0(methodName, "GetText")) {
            int start, end;
            g_variant_get(parameters, "(ii)", &start, &end);
            auto text = atspiObject->text(start, end);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", text.isNull() ? "" : text.data()));
        } else if (!g_strcmp0(methodName, "SetCaretOffset")) {
            int offset;
            g_variant_get(parameters, "(i)", &offset);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(offset, offset)));
        } else if (!g_strcmp0(methodName, "GetTextBeforeOffset")) {
            g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "");
        } else if (!g_strcmp0(methodName, "GetTextAtOffset")) {
            int offset;
            uint32_t boundaryType;
            g_variant_get(parameters, "(iu)", &offset, &boundaryType);
            int start = 0, end = 0;
            auto text = atspiObject->textAtOffset(offset, atspiBoundaryToTextGranularity(boundaryType), start, end);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(sii)", text.isNull() ? "" : text.data(), start, end));
        } else if (!g_strcmp0(methodName, "GetTextAfterOffset"))
            g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "");
        else if (!g_strcmp0(methodName, "GetCharacterAtOffset")) {
            int offset;
            g_variant_get(parameters, "(i)", &offset);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->characterAtOffset(offset)));
        } else if (!g_strcmp0(methodName, "GetAttributeValue")) {
            int offset;
            const char* name;
            g_variant_get(parameters, "(i&s)", &offset, &name);
            auto attributes = atspiObject->textAttributesWithUTF8Offset(offset);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", attributes.attributes.get(String::fromUTF8(name)).utf8().data()));
        } else if (!g_strcmp0(methodName, "GetAttributes")) {
            int offset;
            g_variant_get(parameters, "(i)", &offset);
            auto attributes = atspiObject->textAttributesWithUTF8Offset(offset);
            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}"));
            for (const auto& it : attributes.attributes)
                g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data());
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss}ii)", &builder, attributes.startOffset, attributes.endOffset));
        } else if (!g_strcmp0(methodName, "GetAttributeRun")) {
            int offset;
            gboolean includeDefaults;
            g_variant_get(parameters, "(ib)", &offset, &includeDefaults);
            auto attributes = atspiObject->textAttributesWithUTF8Offset(offset, includeDefaults);
            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}"));
            for (const auto& it : attributes.attributes)
                g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data());
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss}ii)", &builder, attributes.startOffset, attributes.endOffset));
        } else if (!g_strcmp0(methodName, "GetDefaultAttributes") || !g_strcmp0(methodName, "GetDefaultAttributeSet")) {
            auto attributes = atspiObject->textAttributesWithUTF8Offset();
            GVariantBuilder builder = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE("a{ss}"));
            for (const auto& it : attributes.attributes)
                g_variant_builder_add(&builder, "{ss}", it.key.utf8().data(), it.value.utf8().data());
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(a{ss})", &builder));
        } else if (!g_strcmp0(methodName, "GetCharacterExtents")) {
            int offset;
            uint32_t coordinateType;
            g_variant_get(parameters, "(iu)", &offset, &coordinateType);
            auto extents = atspiObject->textExtents(offset, offset + 1, coordinateType);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(iiii)", extents.x(), extents.y(), extents.width(), extents.height()));
        } else if (!g_strcmp0(methodName, "GetRangeExtents")) {
            int start, end;
            uint32_t coordinateType;
            g_variant_get(parameters, "(iiu)", &start, &end, &coordinateType);
            auto extents = atspiObject->textExtents(start, end, coordinateType);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(iiii)", extents.x(), extents.y(), extents.width(), extents.height()));
        } else if (!g_strcmp0(methodName, "GetOffsetAtPoint")) {
            int x, y;
            uint32_t coordinateType;
            g_variant_get(parameters, "(iiu)", &x, &y, &coordinateType);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->offsetAtPoint(IntPoint(x, y), coordinateType)));
        } else if (!g_strcmp0(methodName, "GetNSelections")) {
            int start, end;
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(i)", atspiObject->selectionBounds(start, end) && start != end ? 1 : 0));
        } else if (!g_strcmp0(methodName, "GetSelection")) {
            int selectionNumber;
            g_variant_get(parameters, "(i)", &selectionNumber);
            if (selectionNumber)
                g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Not a valid selection: %d", selectionNumber);
            else {
                int start = 0, end = 0;
                if (atspiObject->selectionBounds(start, end) && start == end)
                    start = end = 0;
                g_dbus_method_invocation_return_value(invocation, g_variant_new("(ii)", start, end));
            }
        } else if (!g_strcmp0(methodName, "AddSelection")) {
            int start, end;
            g_variant_get(parameters, "(ii)", &start, &end);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(start, end)));
        } else if (!g_strcmp0(methodName, "SetSelection")) {
            int start, end, selectionNumber;
            g_variant_get(parameters, "(iii)", &selectionNumber, &start, &end);
            if (selectionNumber)
                g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS, "Not a valid selection: %d", selectionNumber);
            else
                g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->selectRange(start, end)));
        } else if (!g_strcmp0(methodName, "RemoveSelection")) {
            int selectionNumber;
            g_variant_get(parameters, "(i)", &selectionNumber);
            int caretOffset = -1;
            if (!selectionNumber) {
                int start, end;
                if (atspiObject->selectionBounds(start, end))
                    caretOffset = end;
            }
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", caretOffset != -1 ? atspiObject->selectRange(caretOffset, caretOffset) : FALSE));
        } else if (!g_strcmp0(methodName, "ScrollSubstringTo")) {
            int start, end;
            uint32_t scrollType;
            g_variant_get(parameters, "(iiu)", &start, &end, &scrollType);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->scrollToMakeVisible(start, end, scrollType)));
        } else if (!g_strcmp0(methodName, "ScrollSubstringToPoint")) {
            int start, end, x, y;
            uint32_t coordinateType;
            g_variant_get(parameters, "(iiuii)", &start, &end, &coordinateType, &x, &y);
            g_dbus_method_invocation_return_value(invocation, g_variant_new("(b)", atspiObject->scrollToPoint(start, end, coordinateType, x, y)));
        } else if (!g_strcmp0(methodName, "GetBoundedRanges") || !g_strcmp0(methodName, "ScrollSubstringToPoint"))
            g_dbus_method_invocation_return_error_literal(invocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "");
    },
    // get_property
    [](GDBusConnection*, const gchar*, const gchar*, const gchar*, const gchar* propertyName, GError** error, gpointer userData) -> GVariant* {
        auto atspiObject = Ref { *static_cast<AccessibilityObjectAtspi*>(userData) };
        atspiObject->updateBackingStore();

        if (!g_strcmp0(propertyName, "CharacterCount"))
            return g_variant_new_int32(g_utf8_strlen(atspiObject->text().utf8().data(), -1));
        if (!g_strcmp0(propertyName, "CaretOffset")) {
            int start = 0, end = 0;
            return g_variant_new_int32(atspiObject->selectionBounds(start, end) ? end : -1);
        }

        g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Unknown property '%s'", propertyName);
        return nullptr;
    },
    // set_property,
    nullptr,
    // padding
    { nullptr }
};

static Vector<unsigned, 128> offsetMapping(const String& text)
{
    if (text.is8Bit())
        return { };

    Vector<unsigned, 128> offsets;
    SurrogatePairAwareTextIterator iterator(text.characters16(), 0, text.length(), text.length());
    UChar32 character;
    unsigned clusterLength = 0;
    unsigned i;
    for (i = 0; iterator.consume(character, clusterLength); iterator.advance(clusterLength), ++i) {
        for (unsigned j = 0; j < clusterLength; ++j)
            offsets.append(i);
    }
    offsets.append(i++);
    return offsets;
}

static inline unsigned UTF16OffsetToUTF8(const Vector<unsigned, 128>& mapping, unsigned offset)
{
    return mapping.isEmpty() ? offset : mapping[offset];
}

static inline unsigned UTF8OffsetToUTF16(const Vector<unsigned, 128>& mapping, unsigned offset)
{
    if (mapping.isEmpty())
        return offset;

    for (unsigned i = offset; i < mapping.size(); ++i) {
        if (mapping[i] == offset)
            return i;
    }
    return mapping.size();
}

String AccessibilityObjectAtspi::text() const
{
    if (!m_coreObject)
        return { };

    m_hasListMarkerAtStart = false;

#if ENABLE(INPUT_TYPE_COLOR)
    if (m_coreObject->roleValue() == AccessibilityRole::ColorWell) {
        auto color = convertColor<SRGBA<float>>(m_coreObject->colorValue()).resolved();
        GUniquePtr<char> colorString(g_strdup_printf("rgb %7.5f %7.5f %7.5f 1", color.red, color.green, color.blue));
        return String::fromUTF8(colorString.get());
    }
#endif

    if (m_coreObject->isTextControl())
        return m_coreObject->doAXStringForRange({ 0, String::MaxLength });

    auto value = m_coreObject->stringValue();
    if (!value.isNull())
        return value;

    auto text = m_coreObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren));
    if (auto* renderer = m_coreObject->renderer()) {
        if (is<RenderListItem>(*renderer) && downcast<RenderListItem>(*renderer).markerRenderer()) {
            if (renderer->style().direction() == TextDirection::LTR) {
                text = makeString(objectReplacementCharacter, text);
                m_hasListMarkerAtStart = true;
            } else
                text = makeString(text, objectReplacementCharacter);
        }
    }
    return text;
}

unsigned AccessibilityObject::getLengthForTextRange() const
{
    // FIXME: this should probably be in sync with AccessibilityObjectAtspi::text().
    unsigned textLength = text().length();
    if (textLength)
        return textLength;

    Node* node = this->node();
    RenderObject* renderer = node ? node->renderer() : nullptr;
    if (is<RenderText>(renderer))
        textLength = downcast<RenderText>(*renderer).text().length();

    if (!textLength && allowsTextRanges())
        textLength = textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren)).length();

    return textLength;
}

bool AccessibilityObject::allowsTextRanges() const
{
    return true;
}

CString AccessibilityObjectAtspi::text(int startOffset, int endOffset) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return { };

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    if (endOffset == -1)
        endOffset = length;

    if (startOffset < 0 || endOffset < 0)
        return { };

    if (endOffset <= startOffset)
        return { };

    if (!startOffset && endOffset == length)
        return utf8Text;

    GUniquePtr<char> substring(g_utf8_substring(utf8Text.data(), startOffset, endOffset));
    return substring.get();
}

static inline int adjustInputOffset(unsigned utf16Offset, bool hasListMarkerAtStart)
{
    return hasListMarkerAtStart && utf16Offset ? utf16Offset - 1 : utf16Offset;
}

static inline int adjustOutputOffset(unsigned utf16Offset, bool hasListMarkerAtStart)
{
    return hasListMarkerAtStart ? utf16Offset + 1 : utf16Offset;
}

void AccessibilityObjectAtspi::textInserted(const String& insertedText, const VisiblePosition& position)
{
    if (!m_interfaces.contains(Interface::Text))
        return;

    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    auto utf16Offset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(position), m_hasListMarkerAtStart);
    String maskedText = m_coreObject->isPasswordField() ? utf16Text.substring(utf16Offset - insertedText.length(), insertedText.length()) : String();
    auto mapping = offsetMapping(utf16Text);
    auto offset = UTF16OffsetToUTF8(mapping, utf16Offset);
    auto utf8InsertedText = maskedText.isNull() ? insertedText.utf8() : maskedText.utf8();
    auto insertedTextLength = g_utf8_strlen(utf8InsertedText.data(), -1);
    AccessibilityAtspi::singleton().textChanged(*this, "insert", WTFMove(utf8InsertedText), offset - insertedTextLength, insertedTextLength);
}

void AccessibilityObjectAtspi::textDeleted(const String& deletedText, const VisiblePosition& position)
{
    if (!m_interfaces.contains(Interface::Text))
        return;

    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    auto utf16Offset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(position), m_hasListMarkerAtStart);
    auto mapping = offsetMapping(utf16Text);
    auto offset = UTF16OffsetToUTF8(mapping, utf16Offset);
    auto utf8DeletedText = deletedText.utf8();
    auto deletedTextLength = g_utf8_strlen(utf8DeletedText.data(), -1);
    AccessibilityAtspi::singleton().textChanged(*this, "delete", WTFMove(utf8DeletedText), offset, deletedTextLength);
}

IntPoint AccessibilityObjectAtspi::boundaryOffset(unsigned utf16Offset, TextGranularity granularity) const
{
    if (!m_coreObject)
        return { };

    VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(adjustInputOffset(utf16Offset, m_hasListMarkerAtStart));
    VisiblePosition startPosition, endPostion;
    switch (granularity) {
    case TextGranularity::Character:
        RELEASE_ASSERT_NOT_REACHED();
    case TextGranularity::WordStart: {
        if (!utf16Offset && m_hasListMarkerAtStart)
            return { 0, 1 };

        startPosition = isStartOfWord(offsetPosition) && deprecatedIsEditingWhitespace(offsetPosition.characterBefore()) ? offsetPosition : startOfWord(offsetPosition, LeftWordIfOnBoundary);
        endPostion = nextWordPosition(startPosition);
        auto positionAfterSpacingAndFollowingWord = nextWordPosition(endPostion);
        if (positionAfterSpacingAndFollowingWord != endPostion) {
            auto previousPosition = previousWordPosition(positionAfterSpacingAndFollowingWord);
            if (previousPosition == startPosition)
                endPostion = positionAfterSpacingAndFollowingWord;
            else
                endPostion = previousPosition;
        }
        break;
    }
    case TextGranularity::WordEnd: {
        if (!utf16Offset && m_hasListMarkerAtStart)
            return { 0, 1 };

        startPosition = previousWordPosition(offsetPosition);
        auto positionBeforeSpacingAndPreviousWord = previousWordPosition(startPosition);
        if (positionBeforeSpacingAndPreviousWord != startPosition)
            startPosition = nextWordPosition(positionBeforeSpacingAndPreviousWord);
        endPostion = endOfWord(offsetPosition);
        break;
    }
    case TextGranularity::SentenceStart: {
        startPosition = startOfSentence(offsetPosition);
        endPostion = endOfSentence(startPosition);
        if (offsetPosition == endPostion) {
            startPosition = nextSentencePosition(startPosition);
            endPostion = endOfSentence(startPosition);
        }
        break;
    }
    case TextGranularity::SentenceEnd:
        startPosition = previousSentencePosition(offsetPosition);
        endPostion = endOfSentence(offsetPosition);
        break;
    case TextGranularity::LineStart:
        startPosition = logicalStartOfLine(offsetPosition);
        endPostion = nextLinePosition(offsetPosition, 0);
        break;
    case TextGranularity::LineEnd:
        startPosition = logicalStartOfLine(offsetPosition);
        endPostion = logicalEndOfLine(offsetPosition);
        break;
    case TextGranularity::Paragraph:
        startPosition = startOfParagraph(offsetPosition);
        endPostion = endOfParagraph(offsetPosition);
        break;
    }

    auto startOffset = m_coreObject->indexForVisiblePosition(startPosition);
    // For no word boundaries, include the list marker if start offset is 0.
    if (!startOffset && m_hasListMarkerAtStart && (granularity == TextGranularity::WordStart || granularity == TextGranularity::WordEnd))
        startOffset = adjustOutputOffset(startOffset, m_hasListMarkerAtStart);
    return { startOffset, adjustOutputOffset(m_coreObject->indexForVisiblePosition(endPostion), m_hasListMarkerAtStart) };
}

CString AccessibilityObjectAtspi::textAtOffset(int offset, TextGranularity granularity, int& startOffset, int& endOffset) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return { };

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    if (offset < 0 || offset > length)
        return { };

    if (granularity == TextGranularity::Character) {
        startOffset = offset;
        endOffset = std::min(offset + 1, length);
    } else {
        auto mapping = offsetMapping(utf16Text);
        auto utf16Offset = UTF8OffsetToUTF16(mapping, offset);
        auto boundaryOffset = this->boundaryOffset(utf16Offset, granularity);
        startOffset = UTF16OffsetToUTF8(mapping, std::max<int>(boundaryOffset.x(), 0));
        endOffset = UTF16OffsetToUTF8(mapping, std::min<int>(boundaryOffset.y(), utf16Text.length()));
    }

    GUniquePtr<char> substring(g_utf8_substring(utf8Text.data(), startOffset, endOffset));
    return substring.get();
}

int AccessibilityObjectAtspi::characterAtOffset(int offset) const
{
    auto utf8Text = text().utf8();
    if (utf8Text.isNull())
        return 0;

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    if (offset < 0 || offset >= length)
        return 0;

    return g_utf8_get_char(g_utf8_offset_to_pointer(utf8Text.data(), offset));
}

std::optional<unsigned> AccessibilityObjectAtspi::characterOffset(UChar character, int index) const
{
    auto utf16Text = text();
    unsigned start = 0;
    size_t offset;
    while ((offset = utf16Text.find(character, start)) != notFound) {
        start = offset + 1;
        if (!index)
            break;
        index--;
    }

    if (offset == notFound)
        return std::nullopt;

    auto mapping = offsetMapping(utf16Text);
    return UTF16OffsetToUTF8(mapping, offset);
}

std::optional<unsigned> AccessibilityObjectAtspi::characterIndex(UChar character, unsigned offset) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return std::nullopt;

    auto length = g_utf8_strlen(utf8Text.data(), -1);
    if (offset >= length)
        return std::nullopt;

    auto mapping = offsetMapping(utf16Text);
    auto utf16Offset = UTF8OffsetToUTF16(mapping, offset);
    if (utf16Text[utf16Offset] != character)
        return std::nullopt;

    unsigned start = 0;
    int index = -1;
    size_t position;
    while ((position = utf16Text.find(character, start)) != notFound) {
        index++;
        if (static_cast<unsigned>(position) == utf16Offset)
            break;
        start = position + 1;
    }

    if (index == -1)
        return std::nullopt;

    return index;
}

IntRect AccessibilityObjectAtspi::boundsForRange(unsigned utf16Offset, unsigned length, uint32_t coordinateType) const
{
    if (!m_coreObject)
        return { };

    auto extents = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16Offset, length));

    auto* frameView = m_coreObject->documentFrameView();
    if (!frameView)
        return extents;

    switch (coordinateType) {
    case Atspi::CoordinateType::ScreenCoordinates:
        return frameView->contentsToScreen(extents);
    case Atspi::CoordinateType::WindowCoordinates:
        return frameView->contentsToWindow(extents);
    case Atspi::CoordinateType::ParentCoordinates:
        return extents;
    }

    RELEASE_ASSERT_NOT_REACHED();
}

IntRect AccessibilityObjectAtspi::textExtents(int startOffset, int endOffset, uint32_t coordinateType) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return { };

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    startOffset = std::clamp(startOffset, 0, length);
    if (endOffset == -1)
        endOffset = length;
    else
        endOffset = std::clamp(endOffset, 0, length);
    if (endOffset <= startOffset)
        return { };

    auto mapping = offsetMapping(utf16Text);
    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
    auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset);
    return boundsForRange(utf16StartOffset, utf16EndOffset - utf16StartOffset, coordinateType);
}

int AccessibilityObjectAtspi::offsetAtPoint(const IntPoint& point, uint32_t coordinateType) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return -1;

    auto convertedPoint = point;
    if (auto* frameView = m_coreObject->documentFrameView()) {
        switch (coordinateType) {
        case Atspi::CoordinateType::ScreenCoordinates:
            convertedPoint = frameView->screenToContents(point);
            break;
        case Atspi::CoordinateType::WindowCoordinates:
            convertedPoint = frameView->windowToContents(point);
            break;
        case Atspi::CoordinateType::ParentCoordinates:
            break;
        }
    }

    auto position = m_coreObject->visiblePositionForPoint(convertedPoint);
    if (position.isNull())
        return -1;

    auto utf16Offset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(position), m_hasListMarkerAtStart);
    if (utf16Offset == -1)
        return -1;

    return UTF16OffsetToUTF8(offsetMapping(utf16Text), utf16Offset);
}

IntPoint AccessibilityObjectAtspi::boundsForSelection(const VisibleSelection& selection) const
{
    if (selection.isNone())
        return { -1, -1 };

    Node* node = nullptr;
    if (!m_coreObject->isNativeTextControl())
        node = m_coreObject->node();
    else {
        auto positionInTextControlInnerElement = m_coreObject->visiblePositionForIndex(0);
        if (auto* innerMostNode = positionInTextControlInnerElement.deepEquivalent().anchorNode())
            node = innerMostNode->parentNode();
    }
    if (!node)
        return { -1, -1 };

    // We need to limit our search to positions that fall inside the domain of the current object.
    auto firstValidPosition = firstPositionInOrBeforeNode(node->firstDescendant());
    auto lastValidPosition = lastPositionInOrAfterNode(node->lastDescendant());

    if (!intersects(makeVisiblePositionRange(makeSimpleRange(firstValidPosition, lastValidPosition)), VisiblePositionRange(selection)))
        return { -1, -1 };

    // Find the proper range for the selection that falls inside the object.
    auto nodeRangeStart = std::max(selection.start(), firstValidPosition);
    auto nodeRangeEnd = std::min(selection.end(), lastValidPosition);

    // Calculate position of the selected range inside the object.
    auto parentFirstPosition = firstPositionInOrBeforeNode(node);
    auto rangeInParent = *makeSimpleRange(parentFirstPosition, nodeRangeStart);

    // Set values for start offsets and calculate initial range length.
    int startOffset = characterCount(rangeInParent, TextIteratorBehavior::EmitsObjectReplacementCharacters);
    auto nodeRange = *makeSimpleRange(nodeRangeStart, nodeRangeEnd);
    int rangeLength = characterCount(nodeRange, TextIteratorBehavior::EmitsObjectReplacementCharacters);
    return { startOffset, startOffset + rangeLength };
}

IntPoint AccessibilityObjectAtspi::selectedRange() const
{
    if (!m_coreObject)
        return { -1, -1 };

    return boundsForSelection(m_coreObject->selection());
}

bool AccessibilityObjectAtspi::selectionBounds(int& startOffset, int& endOffset) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return false;

    auto bounds = selectedRange();
    if (bounds.x() < 0)
        return false;

    auto mapping = offsetMapping(utf16Text);
    startOffset = UTF16OffsetToUTF8(mapping, bounds.x());
    endOffset = UTF16OffsetToUTF8(mapping, bounds.y());

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    endOffset = std::clamp(endOffset, 0, length);
    if (endOffset < startOffset) {
        startOffset = endOffset = 0;
        return false;
    }

    return true;
}

void AccessibilityObjectAtspi::setSelectedRange(unsigned utf16Offset, unsigned length)
{
    if (!m_coreObject)
        return;

    auto range = m_coreObject->visiblePositionRangeForRange(PlainTextRange(utf16Offset, length));
    m_coreObject->setSelectedVisiblePositionRange(range);
}

bool AccessibilityObjectAtspi::selectRange(int startOffset, int endOffset)
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return false;

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    startOffset = std::clamp(startOffset, 0, length);
    if (endOffset == -1)
        endOffset = length;
    else
        endOffset = std::clamp(endOffset, 0, length);

    auto mapping = offsetMapping(utf16Text);
    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
    auto utf16EndOffset = startOffset == endOffset ? utf16StartOffset : UTF8OffsetToUTF16(mapping, endOffset);
    setSelectedRange(utf16StartOffset, utf16EndOffset - utf16StartOffset);

    return true;
}

void AccessibilityObjectAtspi::selectionChanged(const VisibleSelection& selection)
{
    if (!m_interfaces.contains(Interface::Text))
        return;

    if (selection.isNone())
        return;

    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return;

    auto bounds = boundsForSelection(selection);
    if (bounds.y() < 0)
        return;

    auto length = g_utf8_strlen(utf8Text.data(), -1);
    auto mapping = offsetMapping(utf16Text);
    auto caretOffset = UTF16OffsetToUTF8(mapping, bounds.y());
    if (caretOffset <= length)
        AccessibilityAtspi::singleton().textCaretMoved(*this, caretOffset);

    if (selection.isRange())
        AccessibilityAtspi::singleton().textSelectionChanged(*this);
}

AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributes(std::optional<unsigned> utf16Offset, bool includeDefault) const
{
    if (!m_coreObject || !m_coreObject->renderer())
        return { };

    auto accessibilityTextAttributes = [this](AXCoreObject* axObject, const HashMap<String, String>& defaultAttributes) -> HashMap<String, String> {
        HashMap<String, String> attributes;
        auto& style = axObject->renderer()->style();

        auto addAttributeIfNeeded = [&](const String& name, const String& value) {
            if (defaultAttributes.isEmpty() || defaultAttributes.get(name) != value)
                attributes.add(name, value);
        };

        auto bgColor = style.visitedDependentColor(CSSPropertyBackgroundColor);
        if (bgColor.isValid() && bgColor.isVisible()) {
            auto [r, g, b, a] = bgColor.toColorTypeLossy<SRGBA<uint8_t>>().resolved();
            addAttributeIfNeeded("bg-color"_s, makeString(r, ',', g, ',', b));
        }

        auto fgColor = style.visitedDependentColor(CSSPropertyColor);
        if (fgColor.isValid() && fgColor.isVisible()) {
            auto [r, g, b, a] = fgColor.toColorTypeLossy<SRGBA<uint8_t>>().resolved();
            addAttributeIfNeeded("fg-color"_s, makeString(r, ',', g, ',', b));
        }

        addAttributeIfNeeded("family-name"_s, style.fontCascade().firstFamily());
        addAttributeIfNeeded("size"_s, makeString(std::round(style.computedFontPixelSize() * 72 / WebCore::screenDPI()), "pt"));
        addAttributeIfNeeded("weight"_s, makeString(static_cast<float>(style.fontCascade().weight())));
        addAttributeIfNeeded("style"_s, style.fontCascade().italic() ? "italic"_s : "normal"_s);
        addAttributeIfNeeded("strikethrough"_s, style.textDecorationLine() & TextDecorationLine::LineThrough ? "true"_s : "false"_s);
        addAttributeIfNeeded("underline"_s, style.textDecorationLine() & TextDecorationLine::Underline ? "single"_s : "none"_s);
        addAttributeIfNeeded("invisible"_s, style.visibility() == Visibility::Hidden ? "true"_s : "false"_s);
        addAttributeIfNeeded("editable"_s, m_coreObject->canSetValueAttribute() ? "true"_s : "false"_s);
        addAttributeIfNeeded("direction"_s, style.direction() == TextDirection::LTR ? "ltr"_s : "rtl"_s);

        if (!style.textIndent().isUndefined())
            addAttributeIfNeeded("indent"_s, makeString(valueForLength(style.textIndent(), m_coreObject->size().width()).toInt()));

        switch (style.textAlign()) {
        case TextAlignMode::Start:
        case TextAlignMode::End:
            break;
        case TextAlignMode::Left:
        case TextAlignMode::WebKitLeft:
            addAttributeIfNeeded("justification"_s, "left"_s);
            break;
        case TextAlignMode::Right:
        case TextAlignMode::WebKitRight:
            addAttributeIfNeeded("justification"_s, "right"_s);
            break;
        case TextAlignMode::Center:
        case TextAlignMode::WebKitCenter:
            addAttributeIfNeeded("justification"_s, "center"_s);
            break;
        case TextAlignMode::Justify:
            addAttributeIfNeeded("justification"_s, "fill"_s);
            break;
        }

        String invalidStatus = m_coreObject->invalidStatus();
        if (invalidStatus != "false"_s)
            addAttributeIfNeeded("invalid"_s, invalidStatus);

        String language = m_coreObject->language();
        if (!language.isEmpty())
            addAttributeIfNeeded("language"_s, language);

        return attributes;
    };

    auto defaultAttributes = accessibilityTextAttributes(m_coreObject, { });
    if (!utf16Offset)
        return { defaultAttributes, -1, -1 };

    if (is<RenderListMarker>(*m_coreObject->renderer()))
        return { defaultAttributes, 0, static_cast<int>(m_coreObject->stringValue().length()) };

    if (!m_coreObject->node())
        return { defaultAttributes, -1, -1 };

    if (!*utf16Offset && m_hasListMarkerAtStart) {
        // Always consider list marker an independent run.
        auto attributes = accessibilityTextAttributes(m_coreObject->children()[0].get(), defaultAttributes);
        if (!includeDefault)
            return { attributes, 0, 1 };

        for (const auto& it : attributes)
            defaultAttributes.set(it.key, it.value);
        return { defaultAttributes, 0, 1 };
    }

    VisiblePosition offsetPosition = m_coreObject->visiblePositionForIndex(adjustInputOffset(*utf16Offset, m_hasListMarkerAtStart));
    auto* childNode = offsetPosition.deepEquivalent().deprecatedNode();
    if (!childNode)
        return { defaultAttributes, -1, -1 };

    auto* childRenderer = childNode->renderer();
    if (!childRenderer)
        return { defaultAttributes, -1, -1 };

    auto* childAxObject = childRenderer->document().axObjectCache()->get(childRenderer);
    if (!childAxObject || childAxObject == m_coreObject)
        return { defaultAttributes, -1, -1 };

    auto attributes = accessibilityTextAttributes(childAxObject, defaultAttributes);
    auto firstValidPosition = firstPositionInOrBeforeNode(m_coreObject->node()->firstDescendant());
    auto lastValidPosition = lastPositionInOrAfterNode(m_coreObject->node()->lastDescendant());

    auto* startRenderer = childRenderer;
    auto startPosition = firstPositionInOrBeforeNode(startRenderer->node());
    for (RenderObject* r = childRenderer->previousInPreOrder(); r && startPosition > firstValidPosition; r = r->previousInPreOrder()) {
        if (r->firstChildSlow())
            continue;

        auto childAttributes = accessibilityTextAttributes(r->document().axObjectCache()->get(r), defaultAttributes);
        if (childAttributes != attributes)
            break;

        startRenderer = r;
        startPosition = firstPositionInOrBeforeNode(startRenderer->node());
    }

    auto* endRenderer = childRenderer;
    auto endPosition = lastPositionInOrAfterNode(endRenderer->node());
    for (RenderObject* r = childRenderer->nextInPreOrder(); r && endPosition < lastValidPosition; r = r->nextInPreOrder()) {
        if (r->firstChildSlow())
            continue;

        auto childAttributes = accessibilityTextAttributes(r->document().axObjectCache()->get(r), defaultAttributes);
        if (childAttributes != attributes)
            break;

        endRenderer = r;
        endPosition = lastPositionInOrAfterNode(endRenderer->node());
    }

    auto startOffset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(startPosition), m_hasListMarkerAtStart);
    auto endOffset = adjustOutputOffset(m_coreObject->indexForVisiblePosition(endPosition), m_hasListMarkerAtStart);
    if (!includeDefault)
        return { attributes, startOffset, endOffset };

    for (const auto& it : attributes)
        defaultAttributes.set(it.key, it.value);

    return { defaultAttributes, startOffset, endOffset };
}

AccessibilityObjectAtspi::TextAttributes AccessibilityObjectAtspi::textAttributesWithUTF8Offset(std::optional<int> offset, bool includeDefault) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return { };

    auto mapping = offsetMapping(utf16Text);
    std::optional<unsigned> utf16Offset;
    if (offset) {
        auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
        if (*offset < 0 || *offset >= length)
            return { };

        utf16Offset = UTF8OffsetToUTF16(mapping, *offset);
    }

    auto attributes = textAttributes(utf16Offset, includeDefault);
    if (attributes.startOffset != -1)
        attributes.startOffset = UTF16OffsetToUTF8(mapping, attributes.startOffset);
    if (attributes.endOffset != -1)
        attributes.endOffset = UTF16OffsetToUTF8(mapping, attributes.endOffset);

    return attributes;
}

void AccessibilityObjectAtspi::textAttributesChanged()
{
    if (!m_interfaces.contains(Interface::Text))
        return;

    AccessibilityAtspi::singleton().textAttributesChanged(*this);
}

bool AccessibilityObjectAtspi::scrollToMakeVisible(int startOffset, int endOffset, uint32_t scrollType) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return false;

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    if (startOffset < 0 || startOffset > length)
        return false;
    if (endOffset < 0 || endOffset > length)
        return false;
    if (endOffset < startOffset)
        std::swap(startOffset, endOffset);

    auto mapping = offsetMapping(utf16Text);
    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
    auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset);
    if (!m_coreObject->renderer())
        return true;

    IntRect rect = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16StartOffset, utf16EndOffset - utf16StartOffset));

    if (m_coreObject->isScrollView()) {
        if (auto* parent = m_coreObject->parentObject())
            parent->scrollToMakeVisible();
    }

    ScrollAlignment alignX;
    ScrollAlignment alignY;
    switch (scrollType) {
    case Atspi::ScrollType::TopLeft:
        alignX = ScrollAlignment::alignLeftAlways;
        alignY = ScrollAlignment::alignTopAlways;
        break;
    case Atspi::ScrollType::BottomRight:
        alignX = ScrollAlignment::alignRightAlways;
        alignY = ScrollAlignment::alignBottomAlways;
        break;
    case Atspi::ScrollType::TopEdge:
    case Atspi::ScrollType::BottomEdge:
        // Align to a particular edge is not supported, it's always the closest edge.
        alignX = ScrollAlignment::alignCenterIfNeeded;
        alignY = ScrollAlignment::alignToEdgeIfNeeded;
        break;
    case Atspi::ScrollType::LeftEdge:
    case Atspi::ScrollType::RightEdge:
        // Align to a particular edge is not supported, it's always the closest edge.
        alignX = ScrollAlignment::alignToEdgeIfNeeded;
        alignY = ScrollAlignment::alignCenterIfNeeded;
        break;
    case Atspi::ScrollType::Anywhere:
        alignX = ScrollAlignment::alignCenterIfNeeded;
        alignY = ScrollAlignment::alignCenterIfNeeded;
        break;
    }

    m_coreObject->renderer()->scrollRectToVisible(rect, false, { SelectionRevealMode::Reveal, alignX, alignY, ShouldAllowCrossOriginScrolling::Yes });
    return true;
}

bool AccessibilityObjectAtspi::scrollToPoint(int startOffset, int endOffset, uint32_t coordinateType, int x, int y) const
{
    auto utf16Text = text();
    auto utf8Text = utf16Text.utf8();
    if (utf8Text.isNull())
        return false;

    auto length = static_cast<int>(g_utf8_strlen(utf8Text.data(), -1));
    if (startOffset < 0 || startOffset > length)
        return false;
    if (endOffset < 0 || endOffset > length)
        return false;
    if (endOffset < startOffset)
        std::swap(startOffset, endOffset);

    auto mapping = offsetMapping(utf16Text);
    auto utf16StartOffset = UTF8OffsetToUTF16(mapping, startOffset);
    auto utf16EndOffset = UTF8OffsetToUTF16(mapping, endOffset);
    IntPoint point(x, y);
    if (coordinateType == Atspi::CoordinateType::ScreenCoordinates) {
        if (auto* frameView = m_coreObject->documentFrameView())
            point = frameView->contentsToWindow(frameView->screenToContents(point));
    }

    IntRect rect = m_coreObject->doAXBoundsForRange(PlainTextRange(utf16StartOffset, utf16EndOffset - utf16StartOffset));
    point.move(-rect.x(), -rect.y());
    m_coreObject->scrollToGlobalPoint(point);
    return true;
}

} // namespace WebCore

#endif // USE(ATSPI)
