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

#include "base/task/single_thread_task_runner.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_mojo_handle_signals.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_mojo_watch_callback.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/cross_thread_persistent.h"
#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"

namespace blink {

// static
MojoWatcher* MojoWatcher::Create(mojo::Handle handle,
                                 const MojoHandleSignals* signals_dict,
                                 V8MojoWatchCallback* callback,
                                 ExecutionContext* context) {
  MojoWatcher* watcher = MakeGarbageCollected<MojoWatcher>(context, callback);
  MojoResult result = watcher->Watch(handle, signals_dict);
  // TODO(alokp): Consider raising an exception.
  // Current clients expect to receive the initial error returned by MojoWatch
  // via watch callback.
  //
  // Note that the usage of WrapPersistent is intentional so that the initial
  // error is guaranteed to be reported to the client in case where the given
  // handle is invalid and garbage collection happens before the callback
  // is scheduled.
  if (result != MOJO_RESULT_OK) {
    watcher->task_runner_->PostTask(
        FROM_HERE,
        BindOnce(&V8MojoWatchCallback::InvokeAndReportException,
                 WrapPersistent(callback), WrapPersistent(watcher), result));
  }
  return watcher;
}

MojoWatcher::~MojoWatcher() = default;

MojoResult MojoWatcher::cancel() {
  if (!trap_handle_.is_valid())
    return MOJO_RESULT_INVALID_ARGUMENT;

  trap_handle_.reset();
  return MOJO_RESULT_OK;
}

void MojoWatcher::Trace(Visitor* visitor) const {
  visitor->Trace(callback_);
  ScriptWrappable::Trace(visitor);
  ExecutionContextLifecycleObserver::Trace(visitor);
}

bool MojoWatcher::HasPendingActivity() const {
  return handle_.is_valid();
}

void MojoWatcher::ContextDestroyed() {
  cancel();
}

MojoWatcher::MojoWatcher(ExecutionContext* context,
                         V8MojoWatchCallback* callback)
    : ActiveScriptWrappable<MojoWatcher>({}),
      ExecutionContextLifecycleObserver(context),
      task_runner_(context->GetTaskRunner(TaskType::kInternalDefault)),
      callback_(callback) {}

MojoResult MojoWatcher::Watch(mojo::Handle handle,
                              const MojoHandleSignals* signals_dict) {
  ::MojoHandleSignals signals = MOJO_HANDLE_SIGNAL_NONE;
  if (signals_dict->readable())
    signals |= MOJO_HANDLE_SIGNAL_READABLE;
  if (signals_dict->writable())
    signals |= MOJO_HANDLE_SIGNAL_WRITABLE;
  if (signals_dict->peerClosed())
    signals |= MOJO_HANDLE_SIGNAL_PEER_CLOSED;

  MojoResult result =
      mojo::CreateTrap(&MojoWatcher::OnHandleReady, &trap_handle_);
  DCHECK_EQ(MOJO_RESULT_OK, result);

  result = MojoAddTrigger(trap_handle_.get().value(), handle.value(), signals,
                          MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED,
                          reinterpret_cast<uintptr_t>(this), nullptr);
  if (result != MOJO_RESULT_OK)
    return result;

  // If MojoAddTrigger succeeded above, we need this object to stay alive at
  // least until OnHandleReady is invoked with MOJO_RESULT_CANCELLED, which
  // signals the final invocation by the trap.
  keep_alive_ = this;

  handle_ = handle;

  MojoResult ready_result;
  result = Arm(&ready_result);
  if (result == MOJO_RESULT_OK)
    return result;

  if (result == MOJO_RESULT_FAILED_PRECONDITION) {
    // We couldn't arm the watcher because the handle is already ready to
    // trigger a success notification. Post a notification manually.
    task_runner_->PostTask(FROM_HERE,
                           BindOnce(&MojoWatcher::RunReadyCallback,
                                    WrapPersistent(this), ready_result));
    return MOJO_RESULT_OK;
  }

  // If MojoAddTrigger succeeds but Arm does not, that means another thread
  // closed the watched handle in between. Treat it like we'd treat
  // MojoAddTrigger trying to watch an invalid handle.
  trap_handle_.reset();
  return MOJO_RESULT_INVALID_ARGUMENT;
}

MojoResult MojoWatcher::Arm(MojoResult* ready_result) {
  // Nothing to do if the watcher is inactive.
  if (!handle_.is_valid())
    return MOJO_RESULT_OK;

  uint32_t num_blocking_events = 1;
  MojoTrapEvent blocking_event = {sizeof(blocking_event)};
  MojoResult result = MojoArmTrap(trap_handle_.get().value(), nullptr,
                                  &num_blocking_events, &blocking_event);
  if (result == MOJO_RESULT_OK)
    return MOJO_RESULT_OK;

  if (result == MOJO_RESULT_FAILED_PRECONDITION) {
    DCHECK_EQ(1u, num_blocking_events);
    DCHECK_EQ(reinterpret_cast<uintptr_t>(this),
              blocking_event.trigger_context);
    *ready_result = blocking_event.result;
    return result;
  }

  return result;
}

// static
void MojoWatcher::OnHandleReady(const MojoTrapEvent* event) {
  // It is safe to assume the MojoWathcer still exists, because we keep it alive
  // until we've dispatched MOJO_RESULT_CANCELLED from here to RunReadyCallback,
  // and that is always the last notification we'll dispatch.
  MojoWatcher* watcher = reinterpret_cast<MojoWatcher*>(event->trigger_context);
  PostCrossThreadTask(
      *watcher->task_runner_, FROM_HERE,
      CrossThreadBindOnce(&MojoWatcher::RunReadyCallback,
                          WrapCrossThreadWeakPersistent(watcher),
                          event->result));
}

void MojoWatcher::RunReadyCallback(MojoResult result) {
  if (result == MOJO_RESULT_CANCELLED) {
    // Last notification.
    keep_alive_.Clear();
    handle_ = mojo::Handle();

    // Only dispatch to the callback if this cancellation was implicit due to
    // |handle_| closure. If it was explicit, |trap_handlde_| has already been
    // reset.
    if (trap_handle_.is_valid()) {
      trap_handle_.reset();
      callback_->InvokeAndReportException(this, result);
    }
    return;
  }

  // Ignore callbacks if not watching.
  if (!trap_handle_.is_valid())
    return;

  callback_->InvokeAndReportException(this, result);

  // The user callback may have canceled watching.
  if (!trap_handle_.is_valid())
    return;

  // Rearm the watcher so another notification can fire.
  //
  // TODO(rockot): MojoWatcher should expose some better approximation of the
  // new watcher API, including explicit add and removal of handles from the
  // watcher, as well as explicit arming.
  MojoResult ready_result;
  MojoResult arm_result = Arm(&ready_result);
  if (arm_result == MOJO_RESULT_OK)
    return;

  if (arm_result == MOJO_RESULT_FAILED_PRECONDITION) {
    task_runner_->PostTask(FROM_HERE,
                           BindOnce(&MojoWatcher::RunReadyCallback,
                                    WrapWeakPersistent(this), ready_result));
    return;
  }
}

}  // namespace blink
