/*
 * Copyright (C) 2015-2023 Apple Inc. All rights reserved.
 *
 * 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 "CustomElementReactionQueue.h"

#include "CustomElementRegistry.h"
#include "ElementInlines.h"
#include "EventLoop.h"
#include "HTMLFormElement.h"
#include "JSCustomElementInterface.h"
#include "JSDOMBinding.h"
#include "LocalDOMWindow.h"
#include "WindowEventLoop.h"
#include <JavaScriptCore/CatchScope.h>
#include <JavaScriptCore/Heap.h>
#include <wtf/NeverDestroyed.h>
#include <wtf/Ref.h>
#include <wtf/SetForScope.h>
#include <wtf/TZoneMallocInlines.h>

namespace WebCore {

WTF_MAKE_TZONE_ALLOCATED_IMPL(CustomElementReactionQueueItem);
WTF_MAKE_TZONE_ALLOCATED_IMPL(CustomElementQueue);
WTF_MAKE_TZONE_ALLOCATED_IMPL(CustomElementReactionQueue);

inline CustomElementReactionQueueItem::AdoptedPayload::~AdoptedPayload() = default;
inline CustomElementReactionQueueItem::FormAssociatedPayload::~FormAssociatedPayload() = default;

inline CustomElementReactionQueueItem::CustomElementReactionQueueItem() = default;
inline CustomElementReactionQueueItem::CustomElementReactionQueueItem(CustomElementReactionQueueItem&&) = default;

inline CustomElementReactionQueueItem::CustomElementReactionQueueItem(Type type, Payload payload)
        : m_type(type)
        , m_payload(payload)
{ }

inline CustomElementReactionQueueItem::~CustomElementReactionQueueItem() = default;

inline void CustomElementReactionQueueItem::invoke(Element& element, JSCustomElementInterface& elementInterface)
{
        switch (m_type) {
    case Type::Invalid:
        ASSERT_NOT_REACHED();
        break;
        case Type::ElementUpgrade:
            ASSERT(!m_payload.has_value());
            elementInterface.upgradeElement(element);
            break;
        case Type::Connected:
            ASSERT(!m_payload.has_value());
            elementInterface.invokeConnectedCallback(element);
            break;
        case Type::Disconnected:
            ASSERT(!m_payload.has_value());
            elementInterface.invokeDisconnectedCallback(element);
            break;
        case Type::Adopted: {
            ASSERT(m_payload.has_value() && std::holds_alternative<AdoptedPayload>(m_payload.value()));
            auto& payload = std::get<AdoptedPayload>(m_payload.value());
        elementInterface.invokeAdoptedCallback(element, payload.oldDocument, payload.newDocument);
            break;
        }
        case Type::AttributeChanged: {
            ASSERT(m_payload.has_value() && std::holds_alternative<AttributeChangedPayload>(m_payload.value()));
            auto& payload = std::get<AttributeChangedPayload>(m_payload.value());
            elementInterface.invokeAttributeChangedCallback(element, std::get<0>(payload), std::get<1>(payload), std::get<2>(payload));
            break;
        }
        case Type::FormAssociated:
            ASSERT(m_payload.has_value() && std::holds_alternative<FormAssociatedPayload>(m_payload.value()));
        elementInterface.invokeFormAssociatedCallback(element, std::get<FormAssociatedPayload>(m_payload.value()).form.get());
            break;
        case Type::FormReset:
            ASSERT(!m_payload.has_value());
            elementInterface.invokeFormResetCallback(element);
            break;
        case Type::FormDisabled:
            ASSERT(m_payload.has_value() && std::holds_alternative<FormDisabledPayload>(m_payload.value()));
            elementInterface.invokeFormDisabledCallback(element, std::get<FormDisabledPayload>(m_payload.value()));
            break;
        case Type::FormStateRestore:
            ASSERT(m_payload.has_value() && std::holds_alternative<FormStateRestorePayload>(m_payload.value()));
            elementInterface.invokeFormStateRestoreCallback(element, std::get<FormStateRestorePayload>(m_payload.value()));
            break;
        }
}

CustomElementReactionQueue::CustomElementReactionQueue(JSCustomElementInterface& elementInterface)
    : m_interface(elementInterface)
{ }

CustomElementReactionQueue::~CustomElementReactionQueue()
{
    ASSERT(m_items.isEmpty());
}

void CustomElementReactionQueue::clear()
{
    m_items.clear();
}

#if ASSERT_ENABLED
bool CustomElementReactionQueue::hasJustUpgradeReaction() const
{
    return m_items.size() == 1 && m_items[0].type() == CustomElementReactionQueueItem::Type::ElementUpgrade;
}
#endif

void CustomElementReactionQueue::enqueueElementUpgrade(Element& element, bool alreadyScheduledToUpgrade)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.reactionQueue());
    auto& queue = *element.reactionQueue();
    if (alreadyScheduledToUpgrade)
        ASSERT(queue.hasJustUpgradeReaction());
    else
        queue.m_items.append(Item::Type::ElementUpgrade);
    enqueueElementOnAppropriateElementQueue(element);
}

// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-try-upgrade
void CustomElementReactionQueue::tryToUpgradeElement(Element& element)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.isCustomElementUpgradeCandidate());
    RefPtr registry = CustomElementRegistry::registryForElement(element);
    if (!registry)
        return;

    RefPtr elementInterface = registry->findInterface(element);
    if (!elementInterface)
        return;

    element.enqueueToUpgrade(*elementInterface);
}

void CustomElementReactionQueue::enqueueConnectedCallbackIfNeeded(Element& element)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.isDefinedCustomElement());
    ASSERT(element.document().refCount() > 0);
    ASSERT(element.reactionQueue());
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasConnectedCallback())
        return;
    queue.m_items.append(Item::Type::Connected);
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueDisconnectedCallbackIfNeeded(Element& element)
{
    ASSERT(element.isDefinedCustomElement());
    if (element.document().wasRemovedLastRefCalled())
        return; // Don't enqueue disconnectedCallback if the entire document is getting destructed.
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.reactionQueue());
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasDisconnectedCallback())
        return;
    queue.m_items.append(Item::Type::Disconnected);
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueAdoptedCallbackIfNeeded(Element& element, Document& oldDocument, Document& newDocument)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.isDefinedCustomElement());
    ASSERT(element.document().refCount() > 0);
    ASSERT(element.reactionQueue());
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasAdoptedCallback())
        return;
    queue.m_items.append({ Item::Type::Adopted, Item::AdoptedPayload { Ref { oldDocument }, Ref { newDocument } } });
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueAttributeChangedCallbackIfNeeded(Element& element, const QualifiedName& attributeName, const AtomString& oldValue, const AtomString& newValue)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.isDefinedCustomElement());
    ASSERT(element.document().refCount() > 0);
    ASSERT(element.reactionQueue());
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->observesAttribute(attributeName.localName()))
        return;
    queue.m_items.append({ Item::Type::AttributeChanged, std::make_tuple(attributeName, oldValue, newValue) });
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueFormAssociatedCallbackIfNeeded(Element& element, HTMLFormElement* associatedForm)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    if (element.document().wasRemovedLastRefCalled())
        return; // Don't enqueue formAssociatedCallback if the entire document is getting destructed.
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasFormAssociatedCallback())
        return;
    ASSERT(queue.isFormAssociated());
    queue.m_items.append({ Item::Type::FormAssociated, Item::FormAssociatedPayload { associatedForm } });
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueFormResetCallbackIfNeeded(Element& element)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.document().refCount() > 0);
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasFormResetCallback())
        return;
    ASSERT(queue.isFormAssociated());
    queue.m_items.append(Item::Type::FormReset);
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueFormDisabledCallbackIfNeeded(Element& element, bool isDisabled)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.document().refCount() > 0);
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasFormDisabledCallback())
        return;
    ASSERT(queue.isFormAssociated());
    queue.m_items.append({ Item::Type::FormDisabled, isDisabled });
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueueFormStateRestoreCallbackIfNeeded(Element& element, CustomElementFormValue&& state)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.document().refCount() > 0);
    auto& queue = *element.reactionQueue();
    if (!queue.m_interface->hasFormStateRestoreCallback())
        return;
    ASSERT(queue.isFormAssociated());
    queue.m_items.append({ Item::Type::FormStateRestore, WTFMove(state) });
    enqueueElementOnAppropriateElementQueue(element);
}

void CustomElementReactionQueue::enqueuePostUpgradeReactions(Element& element)
{
    ASSERT(CustomElementReactionDisallowedScope::isReactionAllowed());
    ASSERT(element.isCustomElementUpgradeCandidate());
    if (!element.hasAttributes() && !element.isConnected())
        return;

    ASSERT(element.reactionQueue());
    auto& queue = *element.reactionQueue();

    if (element.hasAttributes()) {
        for (auto& attribute : element.attributes()) {
            if (queue.m_interface->observesAttribute(attribute.localName()))
                queue.m_items.append({ Item::Type::AttributeChanged, std::make_tuple(attribute.name(), nullAtom(), attribute.value()) });
        }
    }

    if (element.isConnected() && queue.m_interface->hasConnectedCallback())
        queue.m_items.append(Item::Type::Connected);
}

bool CustomElementReactionQueue::observesStyleAttribute() const
{
    return m_interface->observesAttribute(HTMLNames::styleAttr->localName());
}

bool CustomElementReactionQueue::isElementInternalsDisabled() const
{
    return m_interface->isElementInternalsDisabled();
}

bool CustomElementReactionQueue::isFormAssociated() const
{
    return m_interface->isFormAssociated();
}

bool CustomElementReactionQueue::hasFormStateRestoreCallback() const
{
    return m_interface->hasFormStateRestoreCallback();
}

bool CustomElementReactionQueue::isElementInternalsAttached() const
{
    return m_elementInternalsAttached;
}

void CustomElementReactionQueue::setElementInternalsAttached()
{
    m_elementInternalsAttached = true;
}

void CustomElementReactionQueue::invokeAll(Element& element)
{
    while (!m_items.isEmpty()) {
        auto items = std::exchange(m_items, { });
        for (auto& item : items)
            item.invoke(element, m_interface.get());
    }
}

inline void CustomElementQueue::add(Element& element)
{
    ASSERT(!m_invoking);
    // FIXME: Avoid inserting the same element multiple times.
    m_elements.append(element);
}

inline void CustomElementQueue::invokeAll()
{
    RELEASE_ASSERT(!m_invoking);
    SetForScope invoking(m_invoking, true);
    unsigned originalSize = m_elements.size();
    // It's possible for more elements to be enqueued if some IDL attributes were missing CEReactions.
    // Invoke callbacks slightly later here instead of crashing / ignoring those cases.
    for (unsigned i = 0; i < m_elements.size(); ++i) {
        Ref element = m_elements[i].get();
        element->clearIsInCustomElementReactionQueue();
        auto* queue = element->reactionQueue();
        ASSERT(queue);
        queue->invokeAll(element);
    }
    ASSERT_UNUSED(originalSize, m_elements.size() == originalSize);
    m_elements.clear();
}

void CustomElementQueue::processQueue(JSC::JSGlobalObject* state)
{
    if (!state) {
        invokeAll();
        return;
    }

    Ref vm = state->vm();
    JSC::JSLockHolder lock(vm);

    JSC::Exception* previousException = nullptr;
    {
        auto catchScope = DECLARE_CATCH_SCOPE(vm);
        previousException = catchScope.exception();
        if (previousException)
            catchScope.clearException();
    }

    invokeAll();

    if (previousException) {
        auto throwScope = DECLARE_THROW_SCOPE(vm);
        throwException(state, throwScope, previousException);
    }
}

Vector<Ref<Element>, 4> CustomElementQueue::takeElements()
{
    RELEASE_ASSERT(!m_invoking);
    return std::exchange(m_elements, { });
}

void CustomElementReactionQueue::enqueueElementsOnAppropriateElementQueue(const Vector<Ref<Element>>& elements)
{
    for (auto& element : elements)
        enqueueElementOnAppropriateElementQueue(element);
}

// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-an-element-on-the-appropriate-element-queue
void CustomElementReactionQueue::enqueueElementOnAppropriateElementQueue(Element& element)
{
    ASSERT(element.reactionQueue());
    element.setIsInCustomElementReactionQueue();
    if (!CustomElementReactionStack::s_currentProcessingStack) {
        element.protectedDocument()->windowEventLoop().backupElementQueue().add(element);
        return;
    }

    CustomElementReactionStack::s_currentProcessingStack->m_queue.add(element);
}

#if ASSERT_ENABLED
unsigned CustomElementReactionDisallowedScope::s_customElementReactionDisallowedCount = 0;
#endif

CustomElementReactionStack* CustomElementReactionStack::s_currentProcessingStack = nullptr;

void CustomElementReactionQueue::processBackupQueue(CustomElementQueue& backupElementQueue)
{
    backupElementQueue.processQueue(nullptr);
}

}
