// 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_global_scope.h"

#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_no_argument_constructor.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_object_parser.h"
#include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_paint_callback.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_paint_rendering_context_2d_settings.h"
#include "third_party/blink/renderer/core/css/css_property_names.h"
#include "third_party/blink/renderer/core/css/css_syntax_definition.h"
#include "third_party/blink/renderer/core/css/css_syntax_string_parser.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/inspector/main_thread_debugger.h"
#include "third_party/blink/renderer/core/origin_trials/origin_trial_context.h"
#include "third_party/blink/renderer/core/workers/global_scope_creation_params.h"
#include "third_party/blink/renderer/core/workers/worker_thread.h"
#include "third_party/blink/renderer/modules/csspaint/css_paint_definition.h"
#include "third_party/blink/renderer/modules/csspaint/css_paint_image_generator_impl.h"
#include "third_party/blink/renderer/modules/csspaint/css_paint_worklet.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_proxy_client.h"
#include "third_party/blink/renderer/platform/bindings/callback_method_retriever.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/wtf/wtf.h"

namespace blink {

namespace {

bool ParseInputArguments(v8::Local<v8::Context> context,
                         v8::Local<v8::Object> constructor,
                         Vector<CSSSyntaxDefinition>* input_argument_types,
                         ExceptionState& exception_state) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  TryRethrowScope rethrow_scope(isolate, exception_state);

  if (RuntimeEnabledFeatures::CSSPaintAPIArgumentsEnabled()) {
    v8::Local<v8::Value> input_argument_type_values;
    if (!constructor->Get(context, V8AtomicString(isolate, "inputArguments"))
             .ToLocal(&input_argument_type_values)) {
      return false;
    }

    if (!input_argument_type_values->IsNullOrUndefined()) {
      Vector<String> argument_types =
          NativeValueTraits<IDLSequence<IDLString>>::NativeValue(
              isolate, input_argument_type_values, exception_state);

      if (exception_state.HadException()) {
        return false;
      }

      for (const auto& type : argument_types) {
        std::optional<CSSSyntaxDefinition> syntax_definition =
            CSSSyntaxStringParser(type).Parse();
        if (!syntax_definition) {
          exception_state.ThrowTypeError("Invalid argument types.");
          return false;
        }
        input_argument_types->push_back(std::move(*syntax_definition));
      }
    }
  }
  return true;
}

PaintRenderingContext2DSettings* ParsePaintRenderingContext2DSettings(
    v8::Local<v8::Context> context,
    v8::Local<v8::Object> constructor,
    ExceptionState& exception_state) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  TryRethrowScope rethrow_scope(isolate, exception_state);

  v8::Local<v8::Value> context_settings_value;
  if (!constructor->Get(context, V8AtomicString(isolate, "contextOptions"))
           .ToLocal(&context_settings_value)) {
    return nullptr;
  }
  auto* context_settings =
      NativeValueTraits<PaintRenderingContext2DSettings>::NativeValue(
          isolate, context_settings_value, exception_state);
  if (exception_state.HadException()) {
    return nullptr;
  }
  return context_settings;
}

}  // namespace

// static
PaintWorkletGlobalScope* PaintWorkletGlobalScope::Create(
    LocalFrame* frame,
    std::unique_ptr<GlobalScopeCreationParams> creation_params,
    WorkerReportingProxy& reporting_proxy) {
  auto* global_scope = MakeGarbageCollected<PaintWorkletGlobalScope>(
      frame, std::move(creation_params), reporting_proxy);
  global_scope->ScriptController()->Initialize(NullUrl());
  MainThreadDebugger::Instance(global_scope->GetIsolate())
      ->ContextCreated(global_scope->ScriptController()->GetScriptState(),
                       global_scope->GetFrame(),
                       global_scope->DocumentSecurityOrigin());
  return global_scope;
}

// static
PaintWorkletGlobalScope* PaintWorkletGlobalScope::Create(
    std::unique_ptr<GlobalScopeCreationParams> creation_params,
    WorkerThread* thread) {
  return MakeGarbageCollected<PaintWorkletGlobalScope>(
      std::move(creation_params), thread);
}

PaintWorkletGlobalScope::PaintWorkletGlobalScope(
    LocalFrame* frame,
    std::unique_ptr<GlobalScopeCreationParams> creation_params,
    WorkerReportingProxy& reporting_proxy)
    : WorkletGlobalScope(std::move(creation_params), reporting_proxy, frame) {}

PaintWorkletGlobalScope::PaintWorkletGlobalScope(
    std::unique_ptr<GlobalScopeCreationParams> creation_params,
    WorkerThread* thread)
    : WorkletGlobalScope(std::move(creation_params),
                         thread->GetWorkerReportingProxy(),
                         thread) {}

PaintWorkletGlobalScope::~PaintWorkletGlobalScope() = default;

void PaintWorkletGlobalScope::Dispose() {
  DCHECK(IsContextThread());
  if (!IsMainThread()) {
    if (PaintWorkletProxyClient* proxy_client =
            PaintWorkletProxyClient::From(Clients()))
      proxy_client->Dispose();
  } else {
    MainThreadDebugger::Instance(GetIsolate())
        ->ContextWillBeDestroyed(ScriptController()->GetScriptState());
  }
  WorkletGlobalScope::Dispose();

  if (IsMainThread()) {
    // For off-the-main-thread paint worklet, this will be called in
    // WorkerThread::PrepareForShutdownOnWorkerThread().
    NotifyContextDestroyed();
  }
}

void PaintWorkletGlobalScope::registerPaint(const ScriptState* script_state,
                                            const String& name,
                                            V8NoArgumentConstructor* paint_ctor,
                                            ExceptionState& exception_state) {
  // https://drafts.css-houdini.org/css-paint-api/#dom-paintworkletglobalscope-registerpaint

  RegisterWithProxyClientIfNeeded();

  if (name.empty()) {
    exception_state.ThrowTypeError("The empty string is not a valid name.");
    return;
  }

  if (paint_definitions_.Contains(name)) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidModificationError,
        StrCat({"A class with name:'", name, "' is already registered."}));
    return;
  }

  if (!paint_ctor->IsConstructor()) {
    exception_state.ThrowTypeError(
        "The provided callback is not a constructor.");
    return;
  }

  v8::Local<v8::Context> context = ScriptController()->GetContext();

  v8::Local<v8::Object> v8_paint_ctor = paint_ctor->CallbackObject();

  Vector<CSSPropertyID> native_invalidation_properties;
  Vector<AtomicString> custom_invalidation_properties;

  const ExecutionContext* execution_context =
      ExecutionContext::From(script_state);

  if (!V8ObjectParser::ParseCSSPropertyList(
          context, execution_context, v8_paint_ctor,
          AtomicString("inputProperties"), &native_invalidation_properties,
          &custom_invalidation_properties, exception_state)) {
    return;
  }

  // Get input argument types. Parse the argument type values only when
  // cssPaintAPIArguments is enabled.
  Vector<CSSSyntaxDefinition> input_argument_types;
  if (!ParseInputArguments(context, v8_paint_ctor, &input_argument_types,
                           exception_state)) {
    return;
  }

  PaintRenderingContext2DSettings* context_settings =
      ParsePaintRenderingContext2DSettings(context, v8_paint_ctor,
                                           exception_state);
  if (!context_settings)
    return;

  CallbackMethodRetriever retriever(paint_ctor);

  retriever.GetPrototypeObject(exception_state);
  if (exception_state.HadException())
    return;

  v8::Local<v8::Function> v8_paint =
      retriever.GetMethodOrThrow("paint", exception_state);
  if (exception_state.HadException())
    return;
  V8PaintCallback* paint = V8PaintCallback::Create(v8_paint);

  auto* definition = MakeGarbageCollected<CSSPaintDefinition>(
      ScriptController()->GetScriptState(), paint_ctor, paint,
      native_invalidation_properties, custom_invalidation_properties,
      input_argument_types, context_settings, this);
  paint_definitions_.Set(name, definition);

  if (!IsMainThread()) {
    PaintWorkletProxyClient* proxy_client =
        PaintWorkletProxyClient::From(Clients());
    proxy_client->RegisterCSSPaintDefinition(name, definition, exception_state);
  } else {
    PaintWorklet* paint_worklet =
        PaintWorklet::From(*GetFrame()->GetDocument()->domWindow());
    paint_worklet->RegisterCSSPaintDefinition(name, definition,
                                              exception_state);
  }
}

CSSPaintDefinition* PaintWorkletGlobalScope::FindDefinition(
    const String& name) {
  auto it = paint_definitions_.find(name);
  return it != paint_definitions_.end() ? it->value : nullptr;
}

double PaintWorkletGlobalScope::devicePixelRatio() const {
  return IsMainThread()
             ? GetFrame()->DevicePixelRatio()
             : PaintWorkletProxyClient::From(Clients())->DevicePixelRatio();
}

void PaintWorkletGlobalScope::Trace(Visitor* visitor) const {
  visitor->Trace(paint_definitions_);
  WorkletGlobalScope::Trace(visitor);
}

void PaintWorkletGlobalScope::RegisterWithProxyClientIfNeeded() {
  if (registered_ || IsMainThread()) {
    return;
  }

  if (PaintWorkletProxyClient* proxy_client =
          PaintWorkletProxyClient::From(Clients())) {
    proxy_client->AddGlobalScope(this);
    registered_ = true;
  }
}

}  // namespace blink
