// Copyright 2021 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/eyedropper/eye_dropper.h"

#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_color_selection_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_color_selection_result.h"
#include "third_party/blink/renderer/core/dom/abort_signal.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/scoped_abort_state.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/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/graphics/color.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/supplementable.h"
#include "ui/base/ui_base_features.h"

namespace blink {

class EyeDropper::OpenAbortAlgorithm final : public AbortSignal::Algorithm {
 public:
  OpenAbortAlgorithm(EyeDropper* eyedropper, AbortSignal* signal)
      : eyedropper_(eyedropper), abortsignal_(signal) {}
  ~OpenAbortAlgorithm() override = default;

  void Run() override { eyedropper_->AbortCallback(abortsignal_); }

  void Trace(Visitor* visitor) const override {
    visitor->Trace(eyedropper_);
    visitor->Trace(abortsignal_);
    Algorithm::Trace(visitor);
  }

 private:
  Member<EyeDropper> eyedropper_;
  Member<AbortSignal> abortsignal_;
};

constexpr char kNotAvailableMessage[] = "EyeDropper is not available.";

namespace {

// Tracks whether an EyeDropper is open in a window. If a second concurrent
// open() reaches the browser it terminates the renderer with a bad message.
// this lets Blink reject it with a developer-friendly exception.
class ActiveWindowEyeDropper final
    : public GarbageCollected<ActiveWindowEyeDropper>,
      public Supplement<LocalDOMWindow> {
 public:
  static constexpr char kSupplementName[] = "ActiveWindowEyeDropper";

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

  explicit ActiveWindowEyeDropper(LocalDOMWindow& window)
      : Supplement<LocalDOMWindow>(window) {}

  bool IsOpen() const { return is_open_; }
  void SetOpen(bool is_open) { is_open_ = is_open; }

  void Trace(Visitor* visitor) const override {
    Supplement<LocalDOMWindow>::Trace(visitor);
  }

 private:
  bool is_open_ = false;
};

}  // namespace

EyeDropper::EyeDropper(ExecutionContext* context)
    : eye_dropper_chooser_(context) {}

EyeDropper* EyeDropper::Create(ExecutionContext* context) {
  return MakeGarbageCollected<EyeDropper>(context);
}

ScriptPromise<ColorSelectionResult> EyeDropper::open(
    ScriptState* script_state,
    const ColorSelectionOptions* options,
    ExceptionState& exception_state) {
  DCHECK(RuntimeEnabledFeatures::EyeDropperAPIEnabled());

  if (!script_state->ContextIsValid()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kInvalidStateError,
        "The object is no longer associated with a window.");
    return EmptyPromise();
  }

  LocalDOMWindow* window = LocalDOMWindow::From(script_state);
  if (!LocalFrame::HasTransientUserActivation(window->GetFrame())) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotAllowedError,
        "EyeDropper::open() requires user gesture.");
    return EmptyPromise();
  }

  if (!::features::IsEyeDropperEnabled()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kOperationError,
                                      kNotAvailableMessage);
    return EmptyPromise();
  }

  // Reject a second concurrent open() either reusing this EyeDropper or another
  // instance already open in this window.
  if (eye_dropper_chooser_.is_bound() ||
      ActiveWindowEyeDropper::From(*window).IsOpen()) {
    exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
                                      "EyeDropper is already open.");
    return EmptyPromise();
  }

  std::unique_ptr<ScopedAbortState> end_chooser_abort_state = nullptr;
  std::unique_ptr<ScopedAbortState> response_handler_abort_state = nullptr;
  if (auto* signal = options->getSignalOr(nullptr)) {
    if (signal->aborted()) {
      return ScriptPromise<ColorSelectionResult>::Reject(
          script_state, signal->reason(script_state));
    }
    auto* handle = signal->AddAlgorithm(
        MakeGarbageCollected<OpenAbortAlgorithm>(this, signal));
    end_chooser_abort_state =
        std::make_unique<ScopedAbortState>(signal, handle);
    response_handler_abort_state =
        std::make_unique<ScopedAbortState>(signal, handle);
  }

  resolver_ = MakeGarbageCollected<ScriptPromiseResolver<ColorSelectionResult>>(
      script_state, exception_state.GetContext());
  auto promise = resolver_->Promise();

  auto* frame = window->GetFrame();
  frame->GetBrowserInterfaceBroker().GetInterface(
      eye_dropper_chooser_.BindNewPipeAndPassReceiver(
          frame->GetTaskRunner(TaskType::kUserInteraction)));
  eye_dropper_chooser_.set_disconnect_handler(
      BindOnce(&EyeDropper::EndChooser, WrapWeakPersistent(this),
               std::move(end_chooser_abort_state)));
  ActiveWindowEyeDropper::From(*window).SetOpen(true);
  open_window_ = window;
  eye_dropper_chooser_->Choose(resolver_->WrapCallbackInScriptScope(
      BindOnce(&EyeDropper::EyeDropperResponseHandler, WrapPersistent(this),
               std::move(response_handler_abort_state))));
  return promise;
}

void EyeDropper::AbortCallback(AbortSignal* signal) {
  if (resolver_) {
    ScriptState* script_state = resolver_->GetScriptState();
    if (IsInParallelAlgorithmRunnable(resolver_->GetExecutionContext(),
                                      script_state)) {
      ScriptState::Scope script_state_scope(script_state);
      resolver_->Reject(signal->reason(script_state));
    }
  }

  eye_dropper_chooser_.reset();
  resolver_ = nullptr;
  ClearWindowEyeDropper();
}

void EyeDropper::EyeDropperResponseHandler(
    std::unique_ptr<ScopedAbortState> scoped_abort_state,
    ScriptPromiseResolver<ColorSelectionResult>* resolver,
    bool success,
    uint32_t color) {
  eye_dropper_chooser_.reset();
  ClearWindowEyeDropper();

  // The abort callback resets the Mojo remote if an abort is signalled,
  // so by receiving a reply, the eye dropper operation must *not* have
  // been aborted by the abort signal. Thus, the promise is not yet resolved,
  // so resolver_ must be non-null.
  DCHECK_EQ(resolver_, resolver);

  if (success) {
    ColorSelectionResult* result = ColorSelectionResult::Create();
    // TODO(https://1351544): The EyeDropper should return a Color or an
    // SkColor4f, instead of an SkColor.
    result->setSRGBHex(Color::FromRGBA32(color).SerializeAsCanvasColor());
    resolver->Resolve(result);
    resolver_ = nullptr;
  } else {
    RejectPromiseHelper(DOMExceptionCode::kAbortError,
                        "The user canceled the selection.");
  }
}

void EyeDropper::EndChooser(
    std::unique_ptr<ScopedAbortState> scoped_abort_state) {
  eye_dropper_chooser_.reset();
  ClearWindowEyeDropper();

  if (!resolver_ ||
      !IsInParallelAlgorithmRunnable(resolver_->GetExecutionContext(),
                                     resolver_->GetScriptState())) {
    return;
  }

  ScriptState::Scope script_state_scope(resolver_->GetScriptState());

  RejectPromiseHelper(DOMExceptionCode::kOperationError, kNotAvailableMessage);
}

void EyeDropper::RejectPromiseHelper(DOMExceptionCode exception_code,
                                     const String& message) {
  resolver_->RejectWithDOMException(exception_code, message);
  resolver_ = nullptr;
}

void EyeDropper::ClearWindowEyeDropper() {
  if (open_window_) {
    ActiveWindowEyeDropper::From(*open_window_).SetOpen(false);
    open_window_ = nullptr;
  }
}

void EyeDropper::Trace(Visitor* visitor) const {
  visitor->Trace(eye_dropper_chooser_);
  visitor->Trace(resolver_);
  visitor->Trace(open_window_);
  ScriptWrappable::Trace(visitor);
}

}  // namespace blink
