/*
 * Copyright (C) 2021 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 "WasmStreamingCompiler.h"

#include "JSBigInt.h"
#include "JSWebAssembly.h"
#include "JSWebAssemblyCompileError.h"
#include "JSWebAssemblyHelpers.h"
#include "JSWebAssemblyInstance.h"
#include "JSWebAssemblyModule.h"
#include "StrongInlines.h"
#include "WasmLLIntPlan.h"
#include "WasmStreamingPlan.h"
#include "WasmWorklist.h"

#if ENABLE(WEBASSEMBLY)

namespace JSC { namespace Wasm {

StreamingCompiler::StreamingCompiler(VM& vm, CompilerMode compilerMode, JSGlobalObject* globalObject, JSPromise* promise, JSObject* importObject)
    : m_vm(vm)
    , m_compilerMode(compilerMode)
    , m_info(Wasm::ModuleInformation::create())
    , m_parser(m_info.get(), *this)
{
    Vector<Strong<JSCell>> dependencies;
    dependencies.append(Strong<JSCell>(vm, globalObject));
    if (importObject)
        dependencies.append(Strong<JSCell>(vm, importObject));
    m_ticket = vm.deferredWorkTimer->addPendingWork(vm, promise, WTFMove(dependencies));
    ASSERT(vm.deferredWorkTimer->hasPendingWork(m_ticket));
    ASSERT(vm.deferredWorkTimer->hasDependancyInPendingWork(m_ticket, globalObject));
    ASSERT(!importObject || vm.deferredWorkTimer->hasDependancyInPendingWork(m_ticket, importObject));
}

StreamingCompiler::~StreamingCompiler()
{
    if (m_ticket) {
        auto ticket = std::exchange(m_ticket, nullptr);
        m_vm.deferredWorkTimer->scheduleWorkSoon(ticket, [](DeferredWorkTimer::Ticket) mutable { });
    }
}

Ref<StreamingCompiler> StreamingCompiler::create(VM& vm, CompilerMode compilerMode, JSGlobalObject* globalObject, JSPromise* promise, JSObject* importObject)
{
    return adoptRef(*new StreamingCompiler(vm, compilerMode, globalObject, promise, importObject));
}

bool StreamingCompiler::didReceiveFunctionData(unsigned functionIndex, const Wasm::FunctionData&)
{
    if (!m_plan) {
        m_plan = adoptRef(*new LLIntPlan(&m_vm.wasmContext, m_info.copyRef(), m_compilerMode, Plan::dontFinalize()));
        // Plan already failed in preparation. We do not start threaded compilation.
        // Keep Plan failed, and "finalize" will reject promise with that failure.
        if (!m_plan->failed()) {
            m_remainingCompilationRequests = m_info->functions.size();
            m_threadedCompilationStarted = true;
        }
    }

    if (m_threadedCompilationStarted) {
        Ref<Plan> plan = adoptRef(*new StreamingPlan(&m_vm.wasmContext, m_info.copyRef(), *m_plan, functionIndex, createSharedTask<Plan::CallbackType>([compiler = Ref { *this }](Plan& plan) {
            compiler->didCompileFunction(static_cast<StreamingPlan&>(plan));
        })));
        ensureWorklist().enqueue(WTFMove(plan));
    }
    return true;
}

void StreamingCompiler::didCompileFunction(StreamingPlan& plan)
{
    Locker locker { m_lock };
    ASSERT(m_threadedCompilationStarted);
    if (plan.failed())
        m_plan->didFailInStreaming(plan.errorMessage());
    m_remainingCompilationRequests--;
    if (!m_remainingCompilationRequests)
        m_plan->didCompileFunctionInStreaming();
    completeIfNecessary();
}

void StreamingCompiler::didFinishParsing()
{
    if (!m_plan) {
        // Reaching here means that this WebAssembly module has no functions.
        ASSERT(!m_info->functions.size());
        ASSERT(!m_remainingCompilationRequests);
        m_plan = adoptRef(*new LLIntPlan(&m_vm.wasmContext, m_info.copyRef(), m_compilerMode, Plan::dontFinalize()));
        // If plan is already failed in preparation, we will reject promise with plan's failure soon in finalize.
    }
}

void StreamingCompiler::completeIfNecessary()
{
    if (m_eagerFailed)
        return;

    if (!m_remainingCompilationRequests && m_finalized) {
        m_plan->completeInStreaming();
        didComplete();
    }
}

void StreamingCompiler::didComplete()
{

    auto makeValidationResult = [](JSC::Wasm::LLIntPlan& plan) -> Module::ValidationResult {
        ASSERT(!plan.hasWork());
        if (plan.failed())
            return Unexpected<String>(plan.errorMessage());
        return JSC::Wasm::Module::ValidationResult(Module::create(plan));
    };

    auto result = makeValidationResult(*m_plan);
    auto ticket = std::exchange(m_ticket, nullptr);
    switch (m_compilerMode) {
    case CompilerMode::Validation: {
        m_vm.deferredWorkTimer->scheduleWorkSoon(ticket, [result = WTFMove(result)](DeferredWorkTimer::Ticket ticket) mutable {
            JSPromise* promise = jsCast<JSPromise*>(ticket->target());
            JSGlobalObject* globalObject = jsCast<JSGlobalObject*>(ticket->dependencies[0].get());
            VM& vm = globalObject->vm();
            auto scope = DECLARE_THROW_SCOPE(vm);

            JSWebAssemblyModule* module = JSWebAssemblyModule::createStub(vm, globalObject, globalObject->webAssemblyModuleStructure(), WTFMove(result));
            if (UNLIKELY(scope.exception())) {
                promise->rejectWithCaughtException(globalObject, scope);
                return;
            }

            scope.release();
            promise->resolve(globalObject, module);
        });
        return;
    }

    case CompilerMode::FullCompile: {
        m_vm.deferredWorkTimer->scheduleWorkSoon(ticket, [result = WTFMove(result)](DeferredWorkTimer::Ticket ticket) mutable {
            JSPromise* promise = jsCast<JSPromise*>(ticket->target());
            JSGlobalObject* globalObject = jsCast<JSGlobalObject*>(ticket->dependencies[0].get());
            JSObject* importObject = jsCast<JSObject*>(ticket->dependencies[1].get());
            VM& vm = globalObject->vm();
            auto scope = DECLARE_THROW_SCOPE(vm);

            JSWebAssemblyModule* module = JSWebAssemblyModule::createStub(vm, globalObject, globalObject->webAssemblyModuleStructure(), WTFMove(result));
            if (UNLIKELY(scope.exception())) {
                promise->rejectWithCaughtException(globalObject, scope);
                return;
            }

            JSWebAssembly::instantiateForStreaming(vm, globalObject, promise, module, importObject);
            if (UNLIKELY(scope.exception())) {
                promise->rejectWithCaughtException(globalObject, scope);
                return;
            }
        });
        return;
    }
    }
}

void StreamingCompiler::finalize(JSGlobalObject* globalObject)
{
    auto state = m_parser.finalize();
    if (state != StreamingParser::State::Finished) {
        fail(globalObject, createJSWebAssemblyCompileError(globalObject, globalObject->vm(), m_parser.errorMessage()));
        return;
    }
    {
        Locker locker { m_lock };
        m_finalized = true;
        completeIfNecessary();
    }
}

void StreamingCompiler::fail(JSGlobalObject* globalObject, JSValue error)
{
    {
        Locker locker { m_lock };
        ASSERT(!m_finalized);
        if (m_eagerFailed)
            return;
        m_eagerFailed = true;
    }
    auto ticket = std::exchange(m_ticket, nullptr);
    JSPromise* promise = jsCast<JSPromise*>(ticket->target());
    // The pending work TicketData was keeping the promise alive. We need to
    // make sure it is reachable from the stack before we remove it from the
    // pending work list. Note: m_ticket stores it as a PackedPtr, which is not
    // scannable by the GC.
    WTF::compilerFence();
    m_vm.deferredWorkTimer->cancelPendingWork(ticket);
    promise->reject(globalObject, error);
}

void StreamingCompiler::cancel()
{
    {
        Locker locker { m_lock };
        ASSERT(!m_finalized);
        if (m_eagerFailed)
            return;
        m_eagerFailed = true;
    }
    auto ticket = std::exchange(m_ticket, nullptr);
    m_vm.deferredWorkTimer->cancelPendingWork(ticket);
}


} } // namespace JSC::Wasm

#endif // ENABLE(WEBASSEMBLY)
