// Copyright 2016 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/modules/csspaint/paint_worklet.h"

#include <utility>

#include "base/rand_util.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/css/cssom/prepopulated_computed_style_property_map.h"
#include "third_party/blink/renderer/core/dom/document.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/layout/layout_object.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/modules/csspaint/css_paint_definition.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_global_scope.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_id_generator.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_messaging_proxy.h"
#include "third_party/blink/renderer/platform/bindings/script_forbidden_scope.h"
#include "third_party/blink/renderer/platform/graphics/paint_generated_image.h"

namespace blink {

const wtf_size_t PaintWorklet::kNumGlobalScopesPerThread = 2u;
const size_t kMaxPaintCountToSwitch = 30u;

// static
PaintWorklet* PaintWorklet::From(LocalDOMWindow& window) {
  PaintWorklet* supplement =
      Supplement<LocalDOMWindow>::From<PaintWorklet>(window);
  if (!supplement && window.GetFrame()) {
    supplement = MakeGarbageCollected<PaintWorklet>(window);
    ProvideTo(window, supplement);
  }
  return supplement;
}

PaintWorklet::PaintWorklet(LocalDOMWindow& window)
    : Worklet(window),
      Supplement<LocalDOMWindow>(window),
      pending_generator_registry_(
          MakeGarbageCollected<PaintWorkletPendingGeneratorRegistry>()),
      worklet_id_(PaintWorkletIdGenerator::NextId()),
      is_paint_off_thread_(Thread::CompositorThread()) {}

PaintWorklet::~PaintWorklet() = default;

void PaintWorklet::AddPendingGenerator(const String& name,
                                       CSSPaintImageGeneratorImpl* generator) {
  pending_generator_registry_->AddPendingGenerator(name, generator);
}

void PaintWorklet::ResetIsPaintOffThreadForTesting(bool is_off_thread) {
  is_paint_off_thread_ = is_off_thread;
}

// We start with a random global scope when a new frame starts. Then within this
// frame, we switch to the other global scope after certain amount of paint
// calls (rand(kMaxPaintCountToSwitch)).
// This approach ensures non-deterministic of global scope selecting, and that
// there is a max of one switching within one frame.
wtf_size_t PaintWorklet::SelectGlobalScope() {
  size_t current_paint_frame_count =
      DomWindow()->GetFrame()->View()->PaintFrameCount();
  // Whether a new frame starts or not.
  bool frame_changed = current_paint_frame_count != active_frame_count_;
  if (frame_changed) {
    paints_before_switching_global_scope_ = GetPaintsBeforeSwitching();
    active_frame_count_ = current_paint_frame_count;
  }
  // We switch when |paints_before_switching_global_scope_| is 1 instead of 0
  // because the var keeps decrementing and stays at 0.
  if (frame_changed || paints_before_switching_global_scope_ == 1)
    active_global_scope_ = SelectNewGlobalScope();
  if (paints_before_switching_global_scope_ > 0)
    paints_before_switching_global_scope_--;
  return active_global_scope_;
}

int PaintWorklet::GetPaintsBeforeSwitching() {
  // TODO(xidachen): Try not to reset |paints_before_switching_global_scope_|
  // every frame. For example, if one frame typically has ~5 paint, then we can
  // switch to another global scope after few frames where the accumulated
  // number of paint calls during these frames reached the
  // |paints_before_switching_global_scope_|.
  // TODO(xidachen): Try to set |paints_before_switching_global_scope_|
  // according to the actual paints per frame. For example, if we found that
  // there are typically ~1000 paints in each frame, we'd want to set the number
  // to average at 500.
  return base::RandIntInclusive(0, kMaxPaintCountToSwitch - 1);
}

wtf_size_t PaintWorklet::SelectNewGlobalScope() {
  return static_cast<wtf_size_t>(
      base::RandGenerator(kNumGlobalScopesPerThread));
}

scoped_refptr<Image> PaintWorklet::Paint(const String& name,
                                         const ImageResourceObserver& observer,
                                         const gfx::SizeF& container_size,
                                         const GCedCSSStyleValueVector* data) {
  if (!document_definition_map_.Contains(name))
    return nullptr;

  // Check if the existing document definition is valid or not.
  DocumentPaintDefinition* document_definition =
      document_definition_map_.at(name);
  if (!document_definition)
    return nullptr;

  PaintWorkletGlobalScopeProxy* proxy =
      PaintWorkletGlobalScopeProxy::From(FindAvailableGlobalScope());
  CSSPaintDefinition* paint_definition = proxy->FindDefinition(name);
  if (!paint_definition)
    return nullptr;
  // TODO(crbug.com/946515): Break dependency on LayoutObject.
  const LayoutObject& layout_object =
      static_cast<const LayoutObject&>(observer);
  float zoom = layout_object.StyleRef().EffectiveZoom();

  StylePropertyMapReadOnly* style_map =
      MakeGarbageCollected<PrepopulatedComputedStylePropertyMap>(
          layout_object.GetDocument(), layout_object.StyleRef(),
          paint_definition->NativeInvalidationProperties(),
          paint_definition->CustomInvalidationProperties());
  // The PaintWorkletGlobalScope is sufficiently isolated that it is safe to
  // run during the lifecycle update without concern for it causing
  // invalidations to the lifecycle.
  ScriptForbiddenScope::AllowUserAgentScript allow_script;
  PaintRecord paint_record =
      paint_definition->Paint(container_size, zoom, style_map, data);
  if (paint_record.empty()) {
    return nullptr;
  }
  return PaintGeneratedImage::Create(std::move(paint_record), container_size);
}

// static
const char PaintWorklet::kSupplementName[] = "PaintWorklet";

void PaintWorklet::Trace(Visitor* visitor) const {
  visitor->Trace(pending_generator_registry_);
  visitor->Trace(proxy_client_);
  Worklet::Trace(visitor);
  Supplement<LocalDOMWindow>::Trace(visitor);
}

void PaintWorklet::RegisterCSSPaintDefinition(const String& name,
                                              CSSPaintDefinition* definition,
                                              ExceptionState& exception_state) {
  if (document_definition_map_.Contains(name)) {
    DocumentPaintDefinition* existing_document_definition =
        document_definition_map_.at(name);
    if (!existing_document_definition)
      return;
    if (!existing_document_definition->RegisterAdditionalPaintDefinition(
            *definition)) {
      document_definition_map_.Set(name, nullptr);
      exception_state.ThrowDOMException(
          DOMExceptionCode::kNotSupportedError,
          StrCat({"A class with name:'", name,
                  "' was registered with a different definition."}));
      return;
    }
    // Notify the generator ready only when register paint is called the
    // second time with the same |name| (i.e. there is already a document
    // definition associated with |name|
    //
    // We are looking for kNumGlobalScopesPerThread number of definitions
    // registered from RegisterCSSPaintDefinition and one extra definition from
    // RegisterMainThreadDocumentPaintDefinition
    unsigned required_registered_count = is_paint_off_thread_
                                             ? kNumGlobalScopesPerThread + 1
                                             : kNumGlobalScopesPerThread;
    if (existing_document_definition->GetRegisteredDefinitionCount() ==
        required_registered_count)
      pending_generator_registry_->NotifyGeneratorReady(name);
  } else {
    auto document_definition = std::make_unique<DocumentPaintDefinition>(
        definition->NativeInvalidationProperties(),
        definition->CustomInvalidationProperties(),
        definition->InputArgumentTypes(),
        definition->GetPaintRenderingContext2DSettings()->alpha());
    document_definition_map_.insert(name, std::move(document_definition));
  }
}

void PaintWorklet::RegisterMainThreadDocumentPaintDefinition(
    const String& name,
    Vector<CSSPropertyID> native_properties,
    Vector<AtomicString> custom_properties,
    Vector<CSSSyntaxDefinition> input_argument_types,
    double alpha) {
  if (document_definition_map_.Contains(name)) {
    DocumentPaintDefinition* document_definition =
        document_definition_map_.at(name);
    if (!document_definition)
      return;
    if (!document_definition->RegisterAdditionalPaintDefinition(
            native_properties, custom_properties, input_argument_types,
            alpha)) {
      document_definition_map_.Set(name, nullptr);
      return;
    }
  } else {
    auto document_definition = std::make_unique<DocumentPaintDefinition>(
        std::move(native_properties), std::move(custom_properties),
        std::move(input_argument_types), alpha);
    document_definition_map_.insert(name, std::move(document_definition));
  }
  DocumentPaintDefinition* document_definition =
      document_definition_map_.at(name);
  // We are looking for kNumGlobalScopesPerThread number of definitions
  // registered from RegisterCSSPaintDefinition and one extra definition from
  // RegisterMainThreadDocumentPaintDefinition
  if (document_definition->GetRegisteredDefinitionCount() ==
      kNumGlobalScopesPerThread + 1)
    pending_generator_registry_->NotifyGeneratorReady(name);
}

bool PaintWorklet::NeedsToCreateGlobalScope() {
  wtf_size_t num_scopes_needed = kNumGlobalScopesPerThread;
  // If we are running off main thread, we will need twice as many global scopes
  if (is_paint_off_thread_)
    num_scopes_needed *= 2;
  return GetNumberOfGlobalScopes() < num_scopes_needed;
}

WorkletGlobalScopeProxy* PaintWorklet::CreateGlobalScope() {
  DCHECK(NeedsToCreateGlobalScope());
  // The main thread global scopes must be created first so that they are at the
  // front of the vector.  This is because SelectNewGlobalScope selects global
  // scopes from the beginning of the vector.  If this code is changed to put
  // the main thread global scopes at the end, then SelectNewGlobalScope must
  // also be changed.
  if (!is_paint_off_thread_ ||
      GetNumberOfGlobalScopes() < kNumGlobalScopesPerThread) {
    return MakeGarbageCollected<PaintWorkletGlobalScopeProxy>(
        To<LocalDOMWindow>(GetExecutionContext())->GetFrame(),
        ModuleResponsesMap(), GetNumberOfGlobalScopes() + 1);
  }

  if (!proxy_client_) {
    proxy_client_ = PaintWorkletProxyClient::Create(
        To<LocalDOMWindow>(GetExecutionContext()), worklet_id_);
  }

  auto* worker_clients = MakeGarbageCollected<WorkerClients>();
  ProvidePaintWorkletProxyClientTo(worker_clients, proxy_client_);

  PaintWorkletMessagingProxy* proxy =
      MakeGarbageCollected<PaintWorkletMessagingProxy>(GetExecutionContext());
  proxy->Initialize(worker_clients, ModuleResponsesMap());
  return proxy;
}

}  // namespace blink
