/*
 * Copyright (C) 2010 Google, 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 "third_party/blink/renderer/core/script/pending_script.h"

#include <optional>

#include "base/trace_event/trace_event.h"
#include "third_party/blink/public/mojom/script/script_type.mojom-shared.h"
#include "third_party/blink/public/mojom/use_counter/metrics/web_feature.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/document_parser_timing.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/loader/render_blocking_resource_manager.h"
#include "third_party/blink/renderer/core/probe/async_task_context.h"
#include "third_party/blink/renderer/core/probe/core_probes.h"
#include "third_party/blink/renderer/core/scheduler/task_attribution_util.h"
#include "third_party/blink/renderer/core/script/ignore_destructive_write_count_incrementer.h"
#include "third_party/blink/renderer/core/script/script_element_base.h"
#include "third_party/blink/renderer/core/timing/resource_timing_context.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/frame_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/public/task_attribution_tracker.h"

namespace blink {

namespace {
WebScopedVirtualTimePauser CreateWebScopedVirtualTimePauser(
    ScriptElementBase* element) {
  if (!element || !element->GetDocument().GetFrame())
    return WebScopedVirtualTimePauser();
  return element->GetDocument()
      .GetFrame()
      ->GetFrameScheduler()
      ->CreateWebScopedVirtualTimePauser(
          "PendingScript",
          WebScopedVirtualTimePauser::VirtualTaskDuration::kInstant);
}
}  // namespace

// See the comment about |is_in_document_write| in ScriptLoader::PrepareScript()
// about IsInDocumentWrite() use here.
PendingScript::PendingScript(ScriptElementBase* element,
                             const TextPosition& starting_position,
                             scheduler::TaskAttributionInfo* task_state)
    : element_(element),
      starting_position_(starting_position),
      virtual_time_pauser_(CreateWebScopedVirtualTimePauser(element)),
      client_(nullptr),
      original_element_document_(&element->GetDocument()),
      original_execution_context_(element->GetExecutionContext()),
      created_during_document_write_(
          element->GetDocument().IsInDocumentWrite()),
      task_state_(task_state) {
  async_task_context_.Schedule(original_execution_context_, "PendingScript",
                               probe::AsyncTaskContext::StackOptions::kScan);
}

PendingScript::~PendingScript() {}

void PendingScript::Dispose() {
  StopWatchingForLoad();
  DCHECK(!Client());
  DCHECK(!IsWatchingForLoad());

  starting_position_ = TextPosition::BelowRangePosition();
  parser_blocking_load_start_time_ = base::TimeTicks();

  DisposeInternal();
  element_ = nullptr;
}

void PendingScript::WatchForLoad(PendingScriptClient* client) {
  CheckState();

  DCHECK(!IsWatchingForLoad());
  DCHECK(client);
  // addClient() will call streamingFinished() if the load is complete. Callers
  // who do not expect to be re-entered from this call should not call
  // watchForLoad for a PendingScript which isReady. We also need to set
  // m_watchingForLoad early, since addClient() can result in calling
  // notifyFinished and further stopWatchingForLoad().
  client_ = client;
  if (IsReady()) {
    PendingScriptFinished();
  } else {
    virtual_time_pauser_.PauseVirtualTime();
  }
}

void PendingScript::StopWatchingForLoad() {
  if (!IsWatchingForLoad())
    return;
  CheckState();
  DCHECK(IsExternalOrModule());
  client_ = nullptr;
  virtual_time_pauser_.UnpauseVirtualTime();
}

void PendingScript::PendingScriptFinished() {
  virtual_time_pauser_.UnpauseVirtualTime();
  if (client_) {
    client_->PendingScriptFinished(this);
  }
}

ScriptElementBase* PendingScript::GetElement() const {
  // As mentioned in the comment at |m_element| declaration,
  // |m_element|  must point to the corresponding ScriptLoader's
  // client.
  CHECK(element_);
  return element_.Get();
}

void PendingScript::MarkParserBlockingLoadStartTime() {
  DCHECK(parser_blocking_load_start_time_.is_null());
  parser_blocking_load_start_time_ = base::TimeTicks::Now();
}

// <specdef href="https://html.spec.whatwg.org/C/#execute-the-script-block">
void PendingScript::ExecuteScriptBlock() {
  TRACE_EVENT0("blink", "PendingScript::ExecuteScriptBlock");
  probe::AsyncTask async_task(original_execution_context_,
                              &async_task_context_);

  ExecutionContext* context = element_->GetExecutionContext();
  if (!context) {
    Dispose();
    return;
  }

  LocalFrame* frame = To<LocalDOMWindow>(context)->GetFrame();
  if (!frame) {
    Dispose();
    return;
  }

  if (original_execution_context_ != context) {
    // Do not execute scripts if they are moved between contexts.
    Dispose();
    return;
  }

  if (original_element_document_ != &element_->GetDocument()) {
    // Do not execute scripts if they are moved between element documents (under
    // the same context Document).
    Dispose();
    return;
  }

  std::optional<scheduler::TaskAttributionTracker::TaskScope>
      task_attribution_script_execution_scope(SetCurrentTaskStateIfTopLevel(
          task_state_, context, TaskScopeType::kScriptExecution));

  Script* script = GetSource();

  const bool created_during_document_write = WasCreatedDuringDocumentWrite();
  std::optional<scheduler::TaskAttributionTracker::TaskScope>
      task_attribution_resource_timing_scope;

  // The `InitiatorUrl` in ResourceTiming doesn't track scripts generated by
  // Document:write().
  // https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ResourceTimingInitiatorInfo/explainer.md#3-ua-only-partially-implements-the-initiator-info
  if (!created_during_document_write &&
      RuntimeEnabledFeatures::ResourceTimingInitiatorEnabled() && script) {
    ResourceTimingContext* resource_timing_context =
        MakeGarbageCollected<ResourceTimingContext>(script->SourceUrl());

    task_attribution_resource_timing_scope =
        SetTaskStateVariable(resource_timing_context, context);
  }

  const bool was_canceled = WasCanceled();
  const bool is_external = IsExternal();
  const base::TimeTicks parser_blocking_load_start_time =
      ParserBlockingLoadStartTime();
  const bool is_controlled_by_script_runner = IsControlledByScriptRunner();
  ScriptElementBase* element = element_;
  Dispose();

  // ExecuteScriptBlockInternal() is split just in order to prevent accidential
  // access to |this| after Dispose().
  ExecuteScriptBlockInternal(
      script, element, was_canceled, is_external, created_during_document_write,
      parser_blocking_load_start_time, is_controlled_by_script_runner);
}

// <specdef href="https://html.spec.whatwg.org/C/#execute-the-script-block">
void PendingScript::ExecuteScriptBlockInternal(
    Script* script,
    ScriptElementBase* element,
    bool was_canceled,
    bool is_external,
    bool created_during_document_write,
    base::TimeTicks parser_blocking_load_start_time,
    bool is_controlled_by_script_runner) {
  Document& element_document = element->GetDocument();
  Document* context_document =
      To<LocalDOMWindow>(element_document.GetExecutionContext())->document();

  // Unblock rendering on scriptElement.
  if (element_document.GetRenderBlockingResourceManager()) {
    element_document.GetRenderBlockingResourceManager()->RemovePendingScript(
        *element);
  }

  // <spec step="2">If the script's script is null, fire an event named error at
  // the element, and return.</spec>
  if (!script) {
    element->DispatchErrorEvent();
    return;
  }

  if (parser_blocking_load_start_time > base::TimeTicks()) {
    DocumentParserTiming::From(element_document)
        .RecordParserBlockedOnScriptLoadDuration(
            base::TimeTicks::Now() - parser_blocking_load_start_time,
            created_during_document_write);
  }

  if (was_canceled)
    return;

  base::TimeTicks script_exec_start_time = base::TimeTicks::Now();

  {
    if (element->ElementHasDuplicateAttributes()) {
      UseCounter::Count(element_document,
                        WebFeature::kDuplicatedAttributeForExecutedScript);
    }

    const bool is_imported_script = context_document != &element_document;

    // <spec step="3">If the script is from an external file, or the script's
    // type is "module", ...</spec>
    const bool needs_increment =
        is_external || is_imported_script ||
        script->GetScriptType() == mojom::blink::ScriptType::kModule;

    // <spec step="3">... then increment the ignore-destructive-writes counter
    // of the script element's node document. Let neutralized doc be that
    // Document.</spec>
    IgnoreDestructiveWriteCountIncrementer incrementer(
        needs_increment ? context_document : nullptr);

    if (script->GetScriptType() == mojom::blink::ScriptType::kModule)
      context_document->IncrementIgnoreDestructiveWriteModuleScriptCount();

    // <spec step="4.A.1">Let old script element be the value to which the
    // script element's node document's currentScript object was most recently
    // set.</spec>
    //
    // This is implemented as push/popCurrentScript().

    // <spec step="4">Switch on the script's type:</spec>

    // <spec step="4.A">"classic"</spec>
    //
    // <spec step="4.A.2">If the script element's root is not a shadow root,
    // then set the script element's node document's currentScript attribute to
    // the script element. Otherwise, set it to null.</spec>
    //
    // Note: The shadow root check is implemented in
    // HTMLScriptElement::SetScriptElementForBinding().

    // <spec step="4.B">"module"</spec>
    //
    // <spec step="4.B.1">Assert: The script element's node document's
    // currentScript attribute is null.</spec>
    ScriptElementBase* current_script = nullptr;
    if (script->GetScriptType() == mojom::blink::ScriptType::kClassic)
      current_script = element;
    context_document->PushCurrentScript(current_script);

    // <spec step="4.A">"classic"</spec>
    //
    // <spec step="4.A.3">Run the classic script given by the script's
    // script.</spec>
    //
    // Note: This is where the script is compiled and actually executed.
    //
    // <spec step="4.B">"module"</spec>
    //
    // <spec step="4.B.2">Run the module script given by the script's
    // script.</spec>
    script->RunScript(context_document->domWindow());

    // <spec step="4.A.4">Set the script element's node document's currentScript
    // attribute to old script element.</spec>
    context_document->PopCurrentScript(current_script);

    // <spec step="5">Decrement the ignore-destructive-writes counter of
    // neutralized doc, if it was incremented in the earlier step.</spec>
    //
    // Implemented as the scope out of IgnoreDestructiveWriteCountIncrementer.
  }

  // We want to record the time spent executing scripts which is blocking the
  // parser. If the script is nested, we only want to count the outermost
  // script execution time.
  if (context_document->IsCurrentScriptStackEmpty() &&
      !is_controlled_by_script_runner) {
    DocumentParserTiming::From(element_document)
        .RecordParserBlockedOnScriptExecutionDuration(
            base::TimeTicks::Now() - script_exec_start_time,
            created_during_document_write);
  }

  // <spec step="6">If the script is from an external file, then fire an event
  // named load at the script element.</spec>
  if (is_external)
    element->DispatchLoadEvent();
}

void PendingScript::Trace(Visitor* visitor) const {
  visitor->Trace(element_);
  visitor->Trace(client_);
  visitor->Trace(original_execution_context_);
  visitor->Trace(original_element_document_);
  visitor->Trace(task_state_);
}

bool PendingScript::IsControlledByScriptRunner() const {
  switch (scheduling_type_) {
    case ScriptSchedulingType::kNotSet:
      NOTREACHED();

    case ScriptSchedulingType::kDefer:
    case ScriptSchedulingType::kParserBlocking:
    case ScriptSchedulingType::kParserBlockingInline:
    case ScriptSchedulingType::kImmediate:
      return false;

    case ScriptSchedulingType::kDeprecatedForceDefer:
      NOTREACHED()
          << "kDeprecatedForceDefer is deprecated and should not be in use";

    case ScriptSchedulingType::kInOrder:
    case ScriptSchedulingType::kAsync:
      return true;
  }
}

}  // namespace blink
