// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "third_party/blink/renderer/core/script/module_map.h"

#include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
#include "third_party/blink/renderer/core/loader/modulescript/module_script_loader.h"
#include "third_party/blink/renderer/core/loader/modulescript/module_script_loader_client.h"
#include "third_party/blink/renderer/core/loader/modulescript/module_script_loader_registry.h"
#include "third_party/blink/renderer/core/script/modulator.h"
#include "third_party/blink/renderer/core/script/module_script.h"
#include "third_party/blink/renderer/platform/bindings/name_client.h"

namespace blink {

// Entry struct represents a value in "module map" spec object.
// https://html.spec.whatwg.org/C/#module-map
class ModuleMap::Entry final : public GarbageCollected<Entry>,
                               public NameClient,
                               public ModuleScriptLoaderClient {

 public:
  explicit Entry(ModuleMap*);
  ~Entry() override {}

  void Trace(Visitor*) const override;
  const char* GetHumanReadableName() const override {
    return "ModuleMap::Entry";
  }

  // Notify fetched |m_moduleScript| to the client asynchronously.
  void AddClient(SingleModuleClient*, ModuleImportPhase);

  // Set a module script that has been created outside of a fetch context.
  void SetModuleScript(ModuleScript*);

  // This is only to be used from ModuleRecordResolver implementations.
  ModuleScript* GetModuleScript() const;

 private:
  void DispatchFinishedNotificationAsync(SingleModuleClient*,
                                         ModuleImportPhase);

  // Implements ModuleScriptLoaderClient
  void NotifyNewSingleModuleFinished(ModuleScript*, ModuleImportPhase) override;

  Member<ModuleScript> module_script_;
  Member<ModuleMap> map_;

  // Correspond to the HTML spec: "fetching" state.
  bool is_fetching_ = true;

  HeapHashSet<Member<SingleModuleClient>> clients_;
};

ModuleMap::Entry::Entry(ModuleMap* map) : map_(map) {
  DCHECK(map_);
}

void ModuleMap::Entry::Trace(Visitor* visitor) const {
  visitor->Trace(module_script_);
  visitor->Trace(map_);
  visitor->Trace(clients_);
}

void ModuleMap::Entry::DispatchFinishedNotificationAsync(
    SingleModuleClient* client,
    ModuleImportPhase import_phase) {
  map_->GetModulator()->TaskRunner()->PostTask(
      FROM_HERE,
      blink::BindOnce(&SingleModuleClient::NotifyModuleLoadFinished,
                      WrapPersistent(client),
                      WrapPersistent(module_script_.Get()), import_phase));
}

void ModuleMap::Entry::AddClient(SingleModuleClient* new_client,
                                 ModuleImportPhase import_phase) {
  DCHECK(!clients_.Contains(new_client));
  if (!is_fetching_) {
    DCHECK(clients_.empty());
    DispatchFinishedNotificationAsync(new_client, import_phase);
    return;
  }

  clients_.insert(new_client);
}

void ModuleMap::Entry::NotifyNewSingleModuleFinished(
    ModuleScript* module_script,
    ModuleImportPhase import_phase) {
  CHECK(is_fetching_);
  module_script_ = module_script;
  is_fetching_ = false;

  for (const auto& client : clients_) {
    DispatchFinishedNotificationAsync(client, import_phase);
  }
  clients_.clear();
}

void ModuleMap::Entry::SetModuleScript(ModuleScript* module_script) {
  CHECK(clients_.empty());
  CHECK(!module_script_);
  module_script_ = module_script;
  is_fetching_ = false;
}

ModuleScript* ModuleMap::Entry::GetModuleScript() const {
  return module_script_.Get();
}

ModuleMap::ModuleMap(Modulator* modulator)
    : modulator_(modulator),
      loader_registry_(MakeGarbageCollected<ModuleScriptLoaderRegistry>()) {
  DCHECK(modulator);
}

void ModuleMap::Trace(Visitor* visitor) const {
  visitor->Trace(map_);
  visitor->Trace(modulator_);
  visitor->Trace(loader_registry_);
}

// <specdef href="https://html.spec.whatwg.org/C/#fetch-a-single-module-script">
void ModuleMap::FetchSingleModuleScript(
    const ModuleScriptFetchRequest& request,
    ResourceFetcher* fetch_client_settings_object_fetcher,
    ModuleGraphLevel level,
    ModuleScriptCustomFetchType custom_fetch_type,
    SingleModuleClient* client) {
  // <spec step="1">Let moduleMap be module map settings object's module
  // map.</spec>
  //
  // Note: |this| is the ModuleMap.

  // <spec step="2">If moduleMap[url] is "fetching", wait in parallel until that
  // entry's value changes, then queue a task on the networking task source to
  // proceed with running the following steps.</spec>
  MapImpl::AddResult result = map_.insert(
      std::make_pair(request.Url(), request.GetExpectedModuleType()), nullptr);
  Member<Entry>& entry = result.stored_value->value;
  if (result.is_new_entry) {
    entry = MakeGarbageCollected<Entry>(this);

    // Steps 4-9 loads a new single module script.
    // Delegates to ModuleScriptLoader via Modulator.
    ModuleScriptLoader::Fetch(request, fetch_client_settings_object_fetcher,
                              level, modulator_, custom_fetch_type,
                              loader_registry_, entry);
  }
  DCHECK(entry);

  // <spec step="3">If moduleMap[url] exists, asynchronously complete this
  // algorithm with moduleMap[url], and abort these steps.</spec>
  //
  // <spec step="14">Set moduleMap[url] to module script, and asynchronously
  // complete this algorithm with module script.</spec>
  if (client)
    entry->AddClient(client, request.GetModuleImportPhase());
}

ModuleScript* ModuleMap::GetFetchedModuleScript(const KURL& url,
                                                ModuleType module_type) const {
  MapImpl::const_iterator it = map_.find(std::make_pair(url, module_type));
  if (it == map_.end())
    return nullptr;
  return it->value->GetModuleScript();
}

void ModuleMap::AddEntry(const KURL& url,
                         ModuleType type,
                         ModuleScript* script) {
  Entry* entry = MakeGarbageCollected<Entry>(this);
  entry->SetModuleScript(script);

  // TODO(crbug.com/448174611) - what should happen with duplicate entries?
  map_.insert(std::make_pair(url, type), entry);
}

}  // namespace blink
