/*
 * 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 "AXObjectCache.h"

#if USE(ATSPI)
#include "AXTextStateChangeIntent.h"
#include "AccessibilityObject.h"
#include "AccessibilityObjectAtspi.h"
#include "AccessibilityRenderObject.h"
#include "Document.h"
#include "Element.h"
#include "HTMLSelectElement.h"
#include "Range.h"
#include "TextIterator.h"

namespace WebCore {

void AXObjectCache::attachWrapper(AXCoreObject* axObject)
{
    auto wrapper = AccessibilityObjectAtspi::create(axObject, document().page()->accessibilityRootObject());
    axObject->setWrapper(wrapper.ptr());

    m_deferredParentChangedList.add(axObject);
    m_performCacheUpdateTimer.startOneShot(0_s);
}

void AXObjectCache::platformPerformDeferredCacheUpdate()
{
    auto handleParentChanged = [&](const AXCoreObject& axObject) {
        auto* wrapper = axObject.wrapper();
        if (!wrapper)
            return;

        auto* axParent = axObject.parentObjectUnignored();
        if (!axParent) {
            if (axObject.isScrollView() && axObject.scrollView() == document().view())
                wrapper->setParent(nullptr); // nullptr means root.
            return;
        }

        if (auto* axParentWrapper = axParent->wrapper())
            wrapper->setParent(axParentWrapper);
    };

    for (const auto& axObject : m_deferredParentChangedList)
        handleParentChanged(*axObject);
    m_deferredParentChangedList.clear();
}

void AXObjectCache::postPlatformNotification(AXCoreObject* coreObject, AXNotification notification)
{
    auto* wrapper = coreObject->wrapper();
    if (!wrapper)
        return;

    switch (notification) {
    case AXCheckedStateChanged:
        if (coreObject->isCheckboxOrRadio() || coreObject->isSwitch())
            wrapper->stateChanged("checked", coreObject->isChecked());
        break;
    case AXSelectedStateChanged:
        wrapper->stateChanged("selected", coreObject->isSelected());
        break;
    case AXMenuListItemSelected: {
        // Menu list popup items are handled by AXSelectedStateChanged.
        auto* parent = coreObject->parentObjectUnignored();
        if (parent && !parent->isMenuListPopup())
            wrapper->stateChanged("selected", coreObject->isSelected());
        break;
    }
    case AXSelectedChildrenChanged:
        wrapper->selectionChanged();
        break;
    case AXMenuListValueChanged: {
        const auto& children = coreObject->children();
        if (children.size() == 1) {
            if (auto* childWrapper = children[0]->wrapper())
                childWrapper->selectionChanged();
        }
        break;
    }
    case AXValueChanged:
        if (wrapper->interfaces().contains(AccessibilityObjectAtspi::Interface::Value))
            wrapper->valueChanged(coreObject->valueForRange());
        break;
    case AXInvalidStatusChanged:
        wrapper->stateChanged("invalid-entry", coreObject->invalidStatus() != "false"_s);
        break;
    case AXElementBusyChanged:
        wrapper->stateChanged("busy", coreObject->isBusy());
        break;
    case AXCurrentStateChanged:
        wrapper->stateChanged("active", coreObject->currentState() != AccessibilityCurrentState::False);
        break;
    case AXRowExpanded:
        wrapper->stateChanged("expanded", true);
        break;
    case AXRowCollapsed:
        wrapper->stateChanged("expanded", false);
        break;
    case AXExpandedChanged:
        wrapper->stateChanged("expanded", coreObject->isExpanded());
        break;
    case AXDisabledStateChanged: {
        bool enabledState = coreObject->isEnabled();
        wrapper->stateChanged("enabled", enabledState);
        wrapper->stateChanged("sensitive", enabledState);
        break;
    }
    case AXPressedStateChanged:
        wrapper->stateChanged("pressed", coreObject->isPressed());
        break;
    case AXReadOnlyStatusChanged:
        wrapper->stateChanged("read-only", coreObject->canSetValueAttribute());
        break;
    case AXRequiredStatusChanged:
        wrapper->stateChanged("required", coreObject->isRequired());
        break;
    case AXActiveDescendantChanged:
        if (auto* descendant = coreObject->activeDescendant())
            platformHandleFocusedUIElementChanged(nullptr, descendant->node());
        break;
    case AXAriaRoleChanged:
        break;
    case AXAutocorrectionOccured:
        break;
    case AXChildrenChanged:
        coreObject->updateChildrenIfNecessary();
        break;
    case AXFocusedUIElementChanged:
        break;
    case AXFrameLoadComplete:
        break;
    case AXIdAttributeChanged:
        break;
    case AXImageOverlayChanged:
        break;
    case AXLanguageChanged:
        break;
    case AXLayoutComplete:
        break;
    case AXLoadComplete:
        break;
    case AXNewDocumentLoadComplete:
        break;
    case AXPageScrolled:
        break;
    case AXSelectedTextChanged:
        break;
    case AXScrolledToAnchor:
        break;
    case AXLiveRegionCreated:
        break;
    case AXLiveRegionChanged:
        break;
    case AXMenuClosed:
        break;
    case AXMenuOpened:
        break;
    case AXRowCountChanged:
        break;
    case AXPressDidSucceed:
        break;
    case AXPressDidFail:
        break;
    case AXSortDirectionChanged:
        break;
    case AXTextChanged:
        break;
    case AXDraggingStarted:
        break;
    case AXDraggingEnded:
        break;
    case AXDraggingEnteredDropZone:
        break;
    case AXDraggingDropped:
        break;
    case AXDraggingExitedDropZone:
        break;
    case AXGrabbedStateChanged:
        break;
    case AXPositionInSetChanged:
        break;
    case AXDescribedByChanged:
        break;
    case AXHasPopupChanged:
        break;
    case AXSetSizeChanged:
        break;
    case AXLevelChanged:
        break;
    case AXMaximumValueChanged:
        break;
    case AXMinimumValueChanged:
        break;
    case AXMultiSelectableStateChanged:
        break;
    case AXIsAtomicChanged:
        break;
    case AXLiveRegionRelevantChanged:
        break;
    case AXLiveRegionStatusChanged:
        break;
    case AXOrientationChanged:
        break;
    case AXPlaceholderChanged:
        break;
    }
}

void AXObjectCache::postTextStateChangePlatformNotification(AXCoreObject* coreObject, const AXTextStateChangeIntent&, const VisibleSelection& selection)
{
    if (!coreObject)
        coreObject = rootWebArea();

    if (!coreObject)
        return;

    auto* wrapper = coreObject->wrapper();
    if (!wrapper)
        return;

    wrapper->selectionChanged(selection);
}

void AXObjectCache::postTextStateChangePlatformNotification(AccessibilityObject* coreObject, AXTextEditType editType, const String& text, const VisiblePosition& position)
{
    if (text.isEmpty())
        return;

    auto* wrapper = coreObject->wrapper();
    if (!wrapper)
        return;

    switch (editType) {
    case AXTextEditTypeDelete:
    case AXTextEditTypeCut:
        wrapper->textDeleted(text, position);
        break;
    case AXTextEditTypeInsert:
    case AXTextEditTypeTyping:
    case AXTextEditTypeDictation:
    case AXTextEditTypePaste:
        wrapper->textInserted(text, position);
        break;
    case AXTextEditTypeAttributesChange:
        wrapper->textAttributesChanged();
        break;
    case AXTextEditTypeUnknown:
        break;
    }
}

void AXObjectCache::postTextReplacementPlatformNotificationForTextControl(AXCoreObject* coreObject, const String& deletedText, const String& insertedText, HTMLTextFormControlElement&)
{
    if (!coreObject)
        coreObject = rootWebArea();

    if (!coreObject)
        return;

    if (deletedText.isEmpty() && insertedText.isEmpty())
        return;

    auto* wrapper = coreObject->wrapper();
    if (!wrapper)
        return;

    if (!deletedText.isEmpty())
        wrapper->textDeleted(deletedText, coreObject->visiblePositionForIndex(0));
    if (!insertedText.isEmpty())
        wrapper->textInserted(insertedText, coreObject->visiblePositionForIndex(insertedText.length()));
}

void AXObjectCache::postTextReplacementPlatformNotification(AXCoreObject* coreObject, AXTextEditType, const String& deletedText, AXTextEditType, const String& insertedText, const VisiblePosition& position)
{
    if (!coreObject)
        coreObject = rootWebArea();

    if (!coreObject)
        return;

    if (deletedText.isEmpty() && insertedText.isEmpty())
        return;

    auto* wrapper = coreObject->wrapper();
    if (!wrapper)
        return;

    if (!deletedText.isEmpty())
        wrapper->textDeleted(deletedText, position);
    if (!insertedText.isEmpty())
        wrapper->textInserted(insertedText, position);
}

void AXObjectCache::frameLoadingEventPlatformNotification(AccessibilityObject* coreObject, AXLoadingEvent loadingEvent)
{
    if (!coreObject)
        return;

    if (coreObject->roleValue() != AccessibilityRole::WebArea)
        return;

    auto* wrapper = coreObject->wrapper();
    if (!wrapper)
        return;

    switch (loadingEvent) {
    case AXObjectCache::AXLoadingStarted:
        wrapper->stateChanged("busy", true);
        break;
    case AXObjectCache::AXLoadingReloaded:
        wrapper->stateChanged("busy", true);
        wrapper->loadEvent("Reload");
        break;
    case AXObjectCache::AXLoadingFailed:
        wrapper->stateChanged("busy", false);
        wrapper->loadEvent("LoadStopped");
        break;
    case AXObjectCache::AXLoadingFinished:
        wrapper->stateChanged("busy", false);
        wrapper->loadEvent("LoadComplete");
        break;
    }
}

void AXObjectCache::platformHandleFocusedUIElementChanged(Node* oldFocusedNode, Node* newFocusedNode)
{
    if (auto* axObject = get(oldFocusedNode)) {
        if (auto* wrapper = axObject->wrapper())
            wrapper->stateChanged("focused", false);
    }
    if (auto* axObject = getOrCreate(newFocusedNode)) {
        if (auto* wrapper = axObject->wrapper())
            wrapper->stateChanged("focused", true);
    }
}

void AXObjectCache::handleScrolledToAnchor(const Node*)
{
}

} // namespace WebCore

#endif // USE(ATSPI)
