/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "SyncModuleLoader.h"

#include "nsISupportsImpl.h"

#include "js/loader/ModuleLoadRequest.h"
#include "js/RootingAPI.h"          // JS::Rooted
#include "js/PropertyAndElement.h"  // JS_SetProperty
#include "js/Value.h"               // JS::Value, JS::NumberValue
#include "mozJSModuleLoader.h"
#include "nsContentSecurityUtils.h"

using namespace JS::loader;

namespace mozilla {
namespace loader {

//////////////////////////////////////////////////////////////
// SyncScriptLoader
//////////////////////////////////////////////////////////////

NS_IMPL_ISUPPORTS0(SyncScriptLoader)

nsIURI* SyncScriptLoader::GetBaseURI() const { return nullptr; }

void SyncScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest,
                                            nsresult aResult) const {}

void SyncScriptLoader::ReportWarningToConsole(
    ScriptLoadRequest* aRequest, const char* aMessageName,
    const nsTArray<nsString>& aParams) const {}

nsresult SyncScriptLoader::FillCompileOptionsForRequest(
    JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions,
    JS::MutableHandle<JSScript*> aIntroductionScript) {
  return NS_OK;
}

//////////////////////////////////////////////////////////////
// SyncModuleLoader
//////////////////////////////////////////////////////////////

NS_IMPL_ADDREF_INHERITED(SyncModuleLoader, JS::loader::ModuleLoaderBase)
NS_IMPL_RELEASE_INHERITED(SyncModuleLoader, JS::loader::ModuleLoaderBase)

NS_IMPL_CYCLE_COLLECTION_INHERITED(SyncModuleLoader,
                                   JS::loader::ModuleLoaderBase, mLoadRequests)

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SyncModuleLoader)
NS_INTERFACE_MAP_END_INHERITING(JS::loader::ModuleLoaderBase)

SyncModuleLoader::SyncModuleLoader(SyncScriptLoader* aScriptLoader,
                                   nsIGlobalObject* aGlobalObject)
    : ModuleLoaderBase(aScriptLoader, aGlobalObject) {}

SyncModuleLoader::~SyncModuleLoader() { MOZ_ASSERT(mLoadRequests.isEmpty()); }

already_AddRefed<ModuleLoadRequest> SyncModuleLoader::CreateRequest(
    JSContext* aCx, nsIURI* aURI, JS::Handle<JSObject*> aModuleRequest,
    JS::Handle<JS::Value> aHostDefined, JS::Handle<JS::Value> aPayload,
    bool aIsDynamicImport, ScriptFetchOptions* aOptions,
    dom::ReferrerPolicy aReferrerPolicy, nsIURI* aBaseURL,
    const dom::SRIMetadata& aSriMetadata) {
  RefPtr<SyncLoadContext> context = new SyncLoadContext();
  JS::ModuleType moduleType = GetModuleRequestType(aCx, aModuleRequest);

  ModuleLoadRequest::Kind kind;
  ModuleLoadRequest* root = nullptr;
  if (aIsDynamicImport) {
    kind = ModuleLoadRequest::Kind::DynamicImport;
  } else {
    MOZ_ASSERT(!aHostDefined.isUndefined());
    root = static_cast<ModuleLoadRequest*>(aHostDefined.toPrivate());
    MOZ_ASSERT(root);
    kind = ModuleLoadRequest::Kind::StaticImport;
  }

  RefPtr<ModuleLoadRequest> request = new ModuleLoadRequest(
      moduleType, dom::SRIMetadata(), aBaseURL, context, kind, this, root);
  request->NoCacheEntryFound(aReferrerPolicy, aOptions, aURI);
  return request.forget();
}

void SyncModuleLoader::OnDynamicImportStarted(ModuleLoadRequest* aRequest) {
  MOZ_ASSERT(aRequest->IsDynamicImport());
  MOZ_ASSERT(!mLoadRequests.Contains(aRequest));

  if (aRequest->IsFetching()) {
    // This module is newly imported.
    //
    // DynamicImportRequests() can contain multiple requests when a dynamic
    // import is performed while evaluating the top-level script of other
    // dynamic imports.
    //
    // mLoadRequests should be empty given evaluation is performed after
    // handling all fetching requests.
    MOZ_ASSERT(DynamicImportRequests().Contains(aRequest));
    MOZ_ASSERT(mLoadRequests.isEmpty());

    nsresult rv = OnFetchComplete(aRequest, NS_OK);
    if (NS_FAILED(rv)) {
      mLoadRequests.CancelRequestsAndClear();
      CancelDynamicImport(aRequest, rv);
      return;
    }

    rv = ProcessRequests();
    if (NS_FAILED(rv)) {
      CancelDynamicImport(aRequest, rv);
      return;
    }
  } else {
    // This module had already been imported.
    MOZ_ASSERT(DynamicImportRequests().isEmpty());
    MOZ_ASSERT(mLoadRequests.isEmpty());
  }

  ProcessDynamicImport(aRequest);
}

bool SyncModuleLoader::CanStartLoad(ModuleLoadRequest* aRequest,
                                    nsresult* aRvOut) {
  return nsContentSecurityUtils::IsTrustedScheme(aRequest->URI());
}

nsresult SyncModuleLoader::StartFetch(ModuleLoadRequest* aRequest) {
  MOZ_ASSERT(aRequest->HasLoadContext());

  aRequest->SetBaseURL(aRequest->URI());

  // Loading script source and compilation are intertwined in
  // mozJSModuleLoader. Perform both operations here but only report load
  // failures. Compilation failure is reported in CompileFetchedModule.

  dom::AutoJSAPI jsapi;
  if (!jsapi.Init(GetGlobalObject())) {
    return NS_ERROR_FAILURE;
  }

  JSContext* cx = jsapi.cx();

  JS::RootedObject module(cx);
  nsresult rv =
      mozJSModuleLoader::LoadSingleModule(this, cx, aRequest, &module);
  MOZ_ASSERT_IF(jsapi.HasException(), NS_FAILED(rv));
  MOZ_ASSERT(bool(module) == NS_SUCCEEDED(rv));

  // Check for failure to load script source and abort.
  bool threwException = jsapi.HasException();
  if (NS_FAILED(rv) && !threwException) {
    nsAutoCString uri;
    nsresult rv2 = aRequest->URI()->GetSpec(uri);
    NS_ENSURE_SUCCESS(rv2, rv2);

    JS_ReportErrorUTF8(cx, "Failed to load %s", PromiseFlatCString(uri).get());

    // Remember the error for MaybeReportLoadError.
    if (!mLoadException.initialized()) {
      mLoadException.init(cx);
    }
    if (!jsapi.StealException(&mLoadException)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    if (mLoadException.isObject()) {
      // Expose `nsresult`.
      JS::Rooted<JS::Value> resultVal(cx, JS::NumberValue(uint32_t(rv)));
      JS::Rooted<JSObject*> exceptionObj(cx, &mLoadException.toObject());
      if (!JS_SetProperty(cx, exceptionObj, "result", resultVal)) {
        // Ignore the error and keep reporting the exception without the result
        // property.
        JS_ClearPendingException(cx);
      }
    }

    return rv;
  }

  // Otherwise remember the results in this context so we can report them later.
  SyncLoadContext* context = aRequest->GetSyncLoadContext();
  context->mRv = rv;
  if (threwException) {
    context->mExceptionValue.init(cx);
    if (!jsapi.StealException(&context->mExceptionValue)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }
  if (module) {
    context->mModule.init(cx);
    context->mModule = module;
  }

  if (!aRequest->IsDynamicImport()) {
    // NOTE: Dynamic import is stored into mDynamicImportRequests.
    mLoadRequests.AppendElement(aRequest);
  }

  return NS_OK;
}

nsresult SyncModuleLoader::CompileFetchedModule(
    JSContext* aCx, JS::Handle<JSObject*> aGlobal, JS::CompileOptions& aOptions,
    ModuleLoadRequest* aRequest, JS::MutableHandle<JSObject*> aModuleOut) {
  // Compilation already happened in StartFetch. Report the result here.
  SyncLoadContext* context = aRequest->GetSyncLoadContext();
  nsresult rv = context->mRv;
  if (context->mModule) {
    aModuleOut.set(context->mModule);
    context->mModule = nullptr;
  }
  if (NS_FAILED(rv)) {
    JS_SetPendingException(aCx, context->mExceptionValue);
    context->mExceptionValue = JS::UndefinedValue();
  }

  MOZ_ASSERT(JS_IsExceptionPending(aCx) == NS_FAILED(rv));
  MOZ_ASSERT(bool(aModuleOut) == NS_SUCCEEDED(rv));

  return rv;
}

void SyncModuleLoader::MaybeReportLoadError(JSContext* aCx) {
  if (JS_IsExceptionPending(aCx)) {
    // Do not override.
    return;
  }

  if (mLoadException.isUndefined()) {
    return;
  }

  JS_SetPendingException(aCx, mLoadException);
  mLoadException = JS::UndefinedValue();
}

void SyncModuleLoader::OnModuleLoadComplete(ModuleLoadRequest* aRequest) {}

nsresult SyncModuleLoader::ProcessRequests() {
  // Work list to drive module loader since this is all synchronous.
  while (!mLoadRequests.isEmpty()) {
    RefPtr<ScriptLoadRequest> request = mLoadRequests.StealFirst();
    nsresult rv = OnFetchComplete(request->AsModuleRequest(), NS_OK);
    if (NS_FAILED(rv)) {
      mLoadRequests.CancelRequestsAndClear();
      return rv;
    }
  }

  return NS_OK;
}

}  // namespace loader
}  // namespace mozilla
