/*
 * Copyright (C) 2021-2022 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. AND ITS CONTRIBUTORS ``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 ITS 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 "SharedWorker.h"

#include "ClientOrigin.h"
#include "ContentSecurityPolicy.h"
#include "Document.h"
#include "EventNames.h"
#include "Logging.h"
#include "MessageChannel.h"
#include "MessagePort.h"
#include "ResourceError.h"
#include "SecurityOrigin.h"
#include "SharedWorkerObjectConnection.h"
#include "SharedWorkerProvider.h"
#include "WorkerOptions.h"
#include <JavaScriptCore/IdentifiersFactory.h>
#include <wtf/IsoMallocInlines.h>

namespace WebCore {

WTF_MAKE_ISO_ALLOCATED_IMPL(SharedWorker);

#define SHARED_WORKER_RELEASE_LOG(fmt, ...) RELEASE_LOG(SharedWorker, "%p - [identifier=%{public}s] SharedWorker::" fmt, this, m_identifier.toString().utf8().data(), ##__VA_ARGS__)
#define SHARED_WORKER_RELEASE_LOG_ERROR(fmt, ...) RELEASE_LOG_ERROR(SharedWorker, "%p - [identifier=%{public}s] SharedWorker::" fmt, this, m_identifier.toString().utf8().data(), ##__VA_ARGS__)

static HashMap<SharedWorkerObjectIdentifier, SharedWorker*>& allSharedWorkers()
{
    ASSERT(isMainThread());
    static NeverDestroyed<HashMap<SharedWorkerObjectIdentifier, SharedWorker*>> allSharedWorkers;
    return allSharedWorkers;
}

SharedWorker* SharedWorker::fromIdentifier(SharedWorkerObjectIdentifier identifier)
{
    return allSharedWorkers().get(identifier);
}

static inline SharedWorkerObjectConnection* mainThreadConnection()
{
    return SharedWorkerProvider::singleton().sharedWorkerConnection();
}

ExceptionOr<Ref<SharedWorker>> SharedWorker::create(Document& document, String&& scriptURLString, std::optional<std::variant<String, WorkerOptions>>&& maybeOptions)
{
    if (!mainThreadConnection())
        return Exception { NotSupportedError, "Shared workers are not supported"_s };

    auto url = document.completeURL(scriptURLString);
    if (!url.isValid())
        return Exception { SyntaxError, "Invalid script URL"_s };

    // Per the specification, any same-origin URL (including blob: URLs) can be used. data: URLs can also be used, but they create a worker with an opaque origin.
    if (!document.securityOrigin().canRequest(url) && !url.protocolIsData())
        return Exception { SecurityError, "URL of the shared worker is cross-origin"_s };

    if (auto* contentSecurityPolicy = document.contentSecurityPolicy()) {
        if (!contentSecurityPolicy->allowWorkerFromSource(url))
            return Exception { SecurityError };
    }

    WorkerOptions options;
    if (maybeOptions) {
        WTF::switchOn(*maybeOptions, [&] (const String& name) {
            options.name = name;
        }, [&] (const WorkerOptions& optionsFromVariant) {
            options = optionsFromVariant;
        });
    }

    auto channel = MessageChannel::create(document);
    auto transferredPort = channel->port2().disentangle();

    ClientOrigin clientOrigin { document.topDocument().securityOrigin().data(), document.securityOrigin().data() };
    SharedWorkerKey key { clientOrigin, url, options.name };

    auto sharedWorker = adoptRef(*new SharedWorker(document, key, channel->port1()));
    sharedWorker->suspendIfNeeded();

    mainThreadConnection()->requestSharedWorker(key, sharedWorker->identifier(), WTFMove(transferredPort), options);
    return sharedWorker;
}

SharedWorker::SharedWorker(Document& document, const SharedWorkerKey& key, Ref<MessagePort>&& port)
    : ActiveDOMObject(&document)
    , m_key(key)
    , m_identifier(SharedWorkerObjectIdentifier::generate())
    , m_port(WTFMove(port))
    , m_identifierForInspector("SharedWorker:" + Inspector::IdentifiersFactory::createIdentifier())
{
    SHARED_WORKER_RELEASE_LOG("SharedWorker:");
    allSharedWorkers().add(m_identifier, this);

    // If the shared worker URL is a blob URL, make sure to keep it alive until the worker has finished loading.
    if (m_key.url.protocolIsBlob())
        m_blobURLExtension = m_key.url;
}

SharedWorker::~SharedWorker()
{
    ASSERT(allSharedWorkers().get(m_identifier) == this);
    SHARED_WORKER_RELEASE_LOG("~SharedWorker:");
    allSharedWorkers().remove(m_identifier);
    ASSERT(!m_isActive);
}

ScriptExecutionContext* SharedWorker::scriptExecutionContext() const
{
    return ActiveDOMObject::scriptExecutionContext();
}

const char* SharedWorker::activeDOMObjectName() const
{
    return "SharedWorker";
}

EventTargetInterface SharedWorker::eventTargetInterface() const
{
    return SharedWorkerEventTargetInterfaceType;
}

void SharedWorker::didFinishLoading(const ResourceError& error)
{
    SHARED_WORKER_RELEASE_LOG("finishLoading: success=%d", error.isNull());
    if (!error.isNull()) {
        queueTaskToDispatchEvent(*this, TaskSource::DOMManipulation, Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::Yes));
        m_isActive = false;
    }
    m_blobURLExtension.clear();
}

bool SharedWorker::virtualHasPendingActivity() const
{
    return m_isActive;
}

void SharedWorker::stop()
{
    SHARED_WORKER_RELEASE_LOG("stop:");
    m_isActive = false;
    mainThreadConnection()->sharedWorkerObjectIsGoingAway(m_key, m_identifier);
}

void SharedWorker::suspend(ReasonForSuspension reason)
{
    if (reason == ReasonForSuspension::BackForwardCache) {
        mainThreadConnection()->suspendForBackForwardCache(m_key, m_identifier);
        m_isSuspendedForBackForwardCache = true;
    }
}

void SharedWorker::resume()
{
    if (m_isSuspendedForBackForwardCache) {
        mainThreadConnection()->resumeForBackForwardCache(m_key, m_identifier);
        m_isSuspendedForBackForwardCache = false;
    }
}

#undef SHARED_WORKER_RELEASE_LOG
#undef SHARED_WORKER_RELEASE_LOG_ERROR

} // namespace WebCore
