// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "test/inspector/isolate-data.h"

#include "src/inspector/test-interface.h"
#include "test/inspector/task-runner.h"

namespace {

const int kIsolateDataIndex = 2;
const int kContextGroupIdIndex = 3;

v8::internal::Vector<uint16_t> ToVector(v8::Local<v8::String> str) {
  v8::internal::Vector<uint16_t> buffer =
      v8::internal::Vector<uint16_t>::New(str->Length());
  str->Write(buffer.start(), 0, str->Length());
  return buffer;
}

v8::Local<v8::String> ToString(v8::Isolate* isolate,
                               const v8_inspector::StringView& string) {
  if (string.is8Bit())
    return v8::String::NewFromOneByte(isolate, string.characters8(),
                                      v8::NewStringType::kNormal,
                                      static_cast<int>(string.length()))
        .ToLocalChecked();
  else
    return v8::String::NewFromTwoByte(isolate, string.characters16(),
                                      v8::NewStringType::kNormal,
                                      static_cast<int>(string.length()))
        .ToLocalChecked();
}

void Print(v8::Isolate* isolate, const v8_inspector::StringView& string) {
  v8::Local<v8::String> v8_string = ToString(isolate, string);
  v8::String::Utf8Value utf8_string(v8_string);
  fwrite(*utf8_string, sizeof(**utf8_string), utf8_string.length(), stdout);
}

class Inspectable : public v8_inspector::V8InspectorSession::Inspectable {
 public:
  Inspectable(v8::Isolate* isolate, v8::Local<v8::Value> object)
      : object_(isolate, object) {}
  ~Inspectable() override {}
  v8::Local<v8::Value> get(v8::Local<v8::Context> context) override {
    return object_.Get(context->GetIsolate());
  }

 private:
  v8::Global<v8::Value> object_;
};

}  //  namespace

IsolateData::IsolateData(TaskRunner* task_runner,
                         IsolateData::SetupGlobalTasks setup_global_tasks,
                         v8::StartupData* startup_data, bool with_inspector)
    : task_runner_(task_runner),
      setup_global_tasks_(std::move(setup_global_tasks)) {
  v8::Isolate::CreateParams params;
  params.array_buffer_allocator =
      v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  params.snapshot_blob = startup_data;
  isolate_ = v8::Isolate::New(params);
  isolate_->SetMicrotasksPolicy(v8::MicrotasksPolicy::kScoped);
  if (with_inspector) {
    isolate_->AddMessageListener(&IsolateData::MessageHandler);
    isolate_->SetPromiseRejectCallback(&IsolateData::PromiseRejectHandler);
    inspector_ = v8_inspector::V8Inspector::create(isolate_, this);
  }
}

IsolateData* IsolateData::FromContext(v8::Local<v8::Context> context) {
  return static_cast<IsolateData*>(
      context->GetAlignedPointerFromEmbedderData(kIsolateDataIndex));
}

int IsolateData::CreateContextGroup() {
  v8::HandleScope handle_scope(isolate_);
  v8::Local<v8::ObjectTemplate> global_template =
      v8::ObjectTemplate::New(isolate_);
  for (auto it = setup_global_tasks_.begin(); it != setup_global_tasks_.end();
       ++it) {
    (*it)->Run(isolate_, global_template);
  }
  v8::Local<v8::Context> context =
      v8::Context::New(isolate_, nullptr, global_template);
  context->SetAlignedPointerInEmbedderData(kIsolateDataIndex, this);
  int context_group_id = ++last_context_group_id_;
  // Should be 2-byte aligned.
  context->SetAlignedPointerInEmbedderData(
      kContextGroupIdIndex, reinterpret_cast<void*>(context_group_id * 2));
  contexts_[context_group_id].Reset(isolate_, context);
  if (inspector_) FireContextCreated(context, context_group_id);
  return context_group_id;
}

v8::Local<v8::Context> IsolateData::GetContext(int context_group_id) {
  return contexts_[context_group_id].Get(isolate_);
}

int IsolateData::GetContextGroupId(v8::Local<v8::Context> context) {
  return static_cast<int>(
      reinterpret_cast<intptr_t>(
          context->GetAlignedPointerFromEmbedderData(kContextGroupIdIndex)) /
      2);
}

void IsolateData::RegisterModule(v8::Local<v8::Context> context,
                                 v8::internal::Vector<uint16_t> name,
                                 v8::ScriptCompiler::Source* source) {
  v8::Local<v8::Module> module;
  if (!v8::ScriptCompiler::CompileModule(isolate(), source).ToLocal(&module))
    return;
  if (!module->InstantiateModule(context, &IsolateData::ModuleResolveCallback)
           .FromMaybe(false)) {
    return;
  }
  v8::Local<v8::Value> result;
  if (!module->Evaluate(context).ToLocal(&result)) return;
  modules_[name] = v8::Global<v8::Module>(isolate_, module);
}

// static
v8::MaybeLocal<v8::Module> IsolateData::ModuleResolveCallback(
    v8::Local<v8::Context> context, v8::Local<v8::String> specifier,
    v8::Local<v8::Module> referrer) {
  std::string str = *v8::String::Utf8Value(specifier);
  IsolateData* data = IsolateData::FromContext(context);
  return data->modules_[ToVector(specifier)].Get(data->isolate_);
}

int IsolateData::ConnectSession(int context_group_id,
                                const v8_inspector::StringView& state,
                                v8_inspector::V8Inspector::Channel* channel) {
  int session_id = ++last_session_id_;
  sessions_[session_id] = inspector_->connect(context_group_id, channel, state);
  context_group_by_session_[sessions_[session_id].get()] = context_group_id;
  return session_id;
}

std::unique_ptr<v8_inspector::StringBuffer> IsolateData::DisconnectSession(
    int session_id) {
  auto it = sessions_.find(session_id);
  CHECK(it != sessions_.end());
  context_group_by_session_.erase(it->second.get());
  std::unique_ptr<v8_inspector::StringBuffer> result = it->second->stateJSON();
  sessions_.erase(it);
  return result;
}

void IsolateData::SendMessage(int session_id,
                              const v8_inspector::StringView& message) {
  auto it = sessions_.find(session_id);
  if (it != sessions_.end()) it->second->dispatchProtocolMessage(message);
}

void IsolateData::BreakProgram(int context_group_id,
                               const v8_inspector::StringView& reason,
                               const v8_inspector::StringView& details) {
  for (int session_id : GetSessionIds(context_group_id)) {
    auto it = sessions_.find(session_id);
    if (it != sessions_.end()) it->second->breakProgram(reason, details);
  }
}

void IsolateData::SchedulePauseOnNextStatement(
    int context_group_id, const v8_inspector::StringView& reason,
    const v8_inspector::StringView& details) {
  for (int session_id : GetSessionIds(context_group_id)) {
    auto it = sessions_.find(session_id);
    if (it != sessions_.end())
      it->second->schedulePauseOnNextStatement(reason, details);
  }
}

void IsolateData::CancelPauseOnNextStatement(int context_group_id) {
  for (int session_id : GetSessionIds(context_group_id)) {
    auto it = sessions_.find(session_id);
    if (it != sessions_.end()) it->second->cancelPauseOnNextStatement();
  }
}

void IsolateData::AsyncTaskScheduled(const v8_inspector::StringView& name,
                                     void* task, bool recurring) {
  inspector_->asyncTaskScheduled(name, task, recurring);
}

void IsolateData::AsyncTaskStarted(void* task) {
  inspector_->asyncTaskStarted(task);
}

void IsolateData::AsyncTaskFinished(void* task) {
  inspector_->asyncTaskFinished(task);
}

void IsolateData::AddInspectedObject(int session_id,
                                     v8::Local<v8::Value> object) {
  auto it = sessions_.find(session_id);
  if (it == sessions_.end()) return;
  std::unique_ptr<Inspectable> inspectable(new Inspectable(isolate_, object));
  it->second->addInspectedObject(std::move(inspectable));
}

void IsolateData::SetMaxAsyncTaskStacksForTest(int limit) {
  v8_inspector::SetMaxAsyncTaskStacksForTest(inspector_.get(), limit);
}

void IsolateData::DumpAsyncTaskStacksStateForTest() {
  v8_inspector::DumpAsyncTaskStacksStateForTest(inspector_.get());
}

// static
int IsolateData::HandleMessage(v8::Local<v8::Message> message,
                               v8::Local<v8::Value> exception) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::Local<v8::Context> context = isolate->GetEnteredContext();
  if (context.IsEmpty()) return 0;
  v8_inspector::V8Inspector* inspector =
      IsolateData::FromContext(context)->inspector_.get();

  v8::Local<v8::StackTrace> stack = message->GetStackTrace();
  int script_id =
      static_cast<int>(message->GetScriptOrigin().ScriptID()->Value());
  if (!stack.IsEmpty() && stack->GetFrameCount() > 0) {
    int top_script_id = stack->GetFrame(0)->GetScriptId();
    if (top_script_id == script_id) script_id = 0;
  }
  int line_number = message->GetLineNumber(context).FromMaybe(0);
  int column_number = 0;
  if (message->GetStartColumn(context).IsJust())
    column_number = message->GetStartColumn(context).FromJust() + 1;

  v8_inspector::StringView detailed_message;
  v8::internal::Vector<uint16_t> message_text_string = ToVector(message->Get());
  v8_inspector::StringView message_text(message_text_string.start(),
                                        message_text_string.length());
  v8::internal::Vector<uint16_t> url_string;
  if (message->GetScriptOrigin().ResourceName()->IsString()) {
    url_string =
        ToVector(message->GetScriptOrigin().ResourceName().As<v8::String>());
  }
  v8_inspector::StringView url(url_string.start(), url_string.length());

  return inspector->exceptionThrown(
      context, message_text, exception, detailed_message, url, line_number,
      column_number, inspector->createStackTrace(stack), script_id);
}

// static
void IsolateData::MessageHandler(v8::Local<v8::Message> message,
                                 v8::Local<v8::Value> exception) {
  HandleMessage(message, exception);
}

// static
void IsolateData::PromiseRejectHandler(v8::PromiseRejectMessage data) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::Local<v8::Context> context = isolate->GetEnteredContext();
  if (context.IsEmpty()) return;
  v8::Local<v8::Promise> promise = data.GetPromise();
  v8::Local<v8::Private> id_private = v8::Private::ForApi(
      isolate,
      v8::String::NewFromUtf8(isolate, "id", v8::NewStringType::kNormal)
          .ToLocalChecked());

  if (data.GetEvent() == v8::kPromiseHandlerAddedAfterReject) {
    v8::Local<v8::Value> id;
    if (!promise->GetPrivate(context, id_private).ToLocal(&id)) return;
    if (!id->IsInt32()) return;
    v8_inspector::V8Inspector* inspector =
        IsolateData::FromContext(context)->inspector_.get();
    const char* reason_str = "Handler added to rejected promise";
    inspector->exceptionRevoked(
        context, id.As<v8::Int32>()->Value(),
        v8_inspector::StringView(reinterpret_cast<const uint8_t*>(reason_str),
                                 strlen(reason_str)));
    return;
  }

  v8::Local<v8::Value> exception = data.GetValue();
  int exception_id = HandleMessage(
      v8::Exception::CreateMessage(isolate, exception), exception);
  if (exception_id) {
    promise
        ->SetPrivate(isolate->GetCurrentContext(), id_private,
                     v8::Int32::New(isolate, exception_id))
        .ToChecked();
  }
}

void IsolateData::FireContextCreated(v8::Local<v8::Context> context,
                                     int context_group_id) {
  v8_inspector::V8ContextInfo info(context, context_group_id,
                                   v8_inspector::StringView());
  info.hasMemoryOnConsole = true;
  inspector_->contextCreated(info);
}

void IsolateData::FireContextDestroyed(v8::Local<v8::Context> context) {
  inspector_->contextDestroyed(context);
}

void IsolateData::FreeContext(v8::Local<v8::Context> context) {
  int context_group_id = GetContextGroupId(context);
  auto it = contexts_.find(context_group_id);
  if (it == contexts_.end()) return;
  contexts_.erase(it);
}

std::vector<int> IsolateData::GetSessionIds(int context_group_id) {
  std::vector<int> result;
  for (auto& it : sessions_) {
    if (context_group_by_session_[it.second.get()] == context_group_id)
      result.push_back(it.first);
  }
  return result;
}

bool IsolateData::formatAccessorsAsProperties(v8::Local<v8::Value> object) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();
  v8::Local<v8::Context> context = isolate->GetCurrentContext();
  v8::Local<v8::Private> shouldFormatAccessorsPrivate = v8::Private::ForApi(
      isolate, v8::String::NewFromUtf8(isolate, "allowAccessorFormatting",
                                       v8::NewStringType::kNormal)
                   .ToLocalChecked());
  CHECK(object->IsObject());
  return object.As<v8::Object>()
      ->HasPrivate(context, shouldFormatAccessorsPrivate)
      .FromMaybe(false);
}

v8::Local<v8::Context> IsolateData::ensureDefaultContextInGroup(
    int context_group_id) {
  return GetContext(context_group_id);
}

void IsolateData::SetCurrentTimeMS(double time) {
  current_time_ = time;
  current_time_set_ = true;
}

double IsolateData::currentTimeMS() {
  if (current_time_set_) return current_time_;
  return v8::base::OS::TimeCurrentMillis();
}

void IsolateData::SetMemoryInfo(v8::Local<v8::Value> memory_info) {
  memory_info_.Reset(isolate_, memory_info);
}

void IsolateData::SetLogConsoleApiMessageCalls(bool log) {
  log_console_api_message_calls_ = log;
}

void IsolateData::SetLogMaxAsyncCallStackDepthChanged(bool log) {
  log_max_async_call_stack_depth_changed_ = log;
}

v8::MaybeLocal<v8::Value> IsolateData::memoryInfo(v8::Isolate* isolate,
                                                  v8::Local<v8::Context>) {
  if (memory_info_.IsEmpty()) return v8::MaybeLocal<v8::Value>();
  return memory_info_.Get(isolate);
}

void IsolateData::runMessageLoopOnPause(int) {
  task_runner_->RunMessageLoop(true);
}

void IsolateData::quitMessageLoopOnPause() { task_runner_->QuitMessageLoop(); }

void IsolateData::consoleAPIMessage(int contextGroupId,
                                    v8::Isolate::MessageErrorLevel level,
                                    const v8_inspector::StringView& message,
                                    const v8_inspector::StringView& url,
                                    unsigned lineNumber, unsigned columnNumber,
                                    v8_inspector::V8StackTrace* stack) {
  if (!log_console_api_message_calls_) return;
  Print(isolate_, message);
  fprintf(stdout, " (");
  Print(isolate_, url);
  fprintf(stdout, ":%d:%d)", lineNumber, columnNumber);
  Print(isolate_, stack->toString()->string());
  fprintf(stdout, "\n");
}

void IsolateData::maxAsyncCallStackDepthChanged(int depth) {
  if (!log_max_async_call_stack_depth_changed_) return;
  fprintf(stdout, "maxAsyncCallStackDepthChanged: %d\n", depth);
}
