// Copyright 2015 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 "src/inspector/v8-debugger-agent-impl.h"

#include <algorithm>
#include <memory>

#include "../../third_party/inspector_protocol/crdtp/json.h"
#include "include/v8-context.h"
#include "include/v8-function.h"
#include "include/v8-inspector.h"
#include "include/v8-microtask-queue.h"
#include "src/base/safe_conversions.h"
#include "src/debug/debug-interface.h"
#include "src/inspector/crc32.h"
#include "src/inspector/injected-script.h"
#include "src/inspector/inspected-context.h"
#include "src/inspector/protocol/Debugger.h"
#include "src/inspector/protocol/Protocol.h"
#include "src/inspector/remote-object-id.h"
#include "src/inspector/search-util.h"
#include "src/inspector/string-util.h"
#include "src/inspector/v8-debugger-script.h"
#include "src/inspector/v8-debugger.h"
#include "src/inspector/v8-inspector-impl.h"
#include "src/inspector/v8-inspector-session-impl.h"
#include "src/inspector/v8-regex.h"
#include "src/inspector/v8-runtime-agent-impl.h"
#include "src/inspector/v8-stack-trace-impl.h"
#include "src/inspector/v8-value-utils.h"

namespace v8_inspector {

using protocol::Array;
using protocol::Debugger::BreakpointId;
using protocol::Debugger::CallFrame;
using protocol::Debugger::Scope;
using protocol::Runtime::ExceptionDetails;
using protocol::Runtime::RemoteObject;
using protocol::Runtime::ScriptId;

namespace InstrumentationEnum =
    protocol::Debugger::SetInstrumentationBreakpoint::InstrumentationEnum;

namespace DebuggerAgentState {
static const char pauseOnExceptionsState[] = "pauseOnExceptionsState";
static const char asyncCallStackDepth[] = "asyncCallStackDepth";
static const char blackboxPattern[] = "blackboxPattern";
static const char skipAnonymousScripts[] = "skipAnonymousScripts";
static const char debuggerEnabled[] = "debuggerEnabled";
static const char breakpointsActiveWhenEnabled[] = "breakpointsActive";
static const char skipAllPauses[] = "skipAllPauses";

static const char breakpointsByRegex[] = "breakpointsByRegex";
static const char breakpointsByUrl[] = "breakpointsByUrl";
static const char breakpointsByScriptHash[] = "breakpointsByScriptHash";
static const char breakpointHints[] = "breakpointHints";
static const char breakpointHintText[] = "text";
static const char breakpointHintPrefixHash[] = "prefixHash";
static const char breakpointHintPrefixLength[] = "prefixLen";
static const char instrumentationBreakpoints[] = "instrumentationBreakpoints";
static const char maxScriptCacheSize[] = "maxScriptCacheSize";

}  // namespace DebuggerAgentState

static const char kBacktraceObjectGroup[] = "backtrace";
static const char kDebuggerNotEnabled[] = "Debugger agent is not enabled";
static const char kDebuggerNotPaused[] =
    "Can only perform operation while paused.";

static const size_t kBreakpointHintMaxLength = 128;
static const intptr_t kBreakpointHintMaxSearchOffset = 80 * 10;
// Limit the number of breakpoints returned, as we otherwise may exceed
// the maximum length of a message in mojo (see https://crbug.com/1105172).
static const size_t kMaxNumBreakpoints = 1000;

#if V8_ENABLE_WEBASSEMBLY
// TODO(1099680): getScriptSource and getWasmBytecode return Wasm wire bytes
// as protocol::Binary, which is encoded as JSON string in the communication
// to the DevTools front-end and hence leads to either crashing the renderer
// that is being debugged or the renderer that's running the front-end if we
// allow arbitrarily big Wasm byte sequences here. Ideally we would find a
// different way to transfer the wire bytes (middle- to long-term), but as a
// short-term solution, we should at least not crash.
static constexpr size_t kWasmBytecodeMaxLength =
    (v8::String::kMaxLength / 4) * 3;
static constexpr const char kWasmBytecodeExceedsTransferLimit[] =
    "WebAssembly bytecode exceeds the transfer limit";
#endif  // V8_ENABLE_WEBASSEMBLY

namespace {

enum class BreakpointType {
  kByUrl = 1,
  kByUrlRegex,
  kByScriptHash,
  kByScriptId,
  kDebugCommand,
  kMonitorCommand,
  kBreakpointAtEntry,
  kInstrumentationBreakpoint
};

String16 generateBreakpointId(BreakpointType type,
                              const String16& scriptSelector, int lineNumber,
                              int columnNumber) {
  String16Builder builder;
  builder.appendNumber(static_cast<int>(type));
  builder.append(':');
  builder.appendNumber(lineNumber);
  builder.append(':');
  builder.appendNumber(columnNumber);
  builder.append(':');
  builder.append(scriptSelector);
  return builder.toString();
}

String16 generateBreakpointId(BreakpointType type,
                              v8::Local<v8::Function> function) {
  String16Builder builder;
  builder.appendNumber(static_cast<int>(type));
  builder.append(':');
  builder.appendNumber(v8::debug::GetDebuggingId(function));
  return builder.toString();
}

String16 generateInstrumentationBreakpointId(const String16& instrumentation) {
  String16Builder builder;
  builder.appendNumber(
      static_cast<int>(BreakpointType::kInstrumentationBreakpoint));
  builder.append(':');
  builder.append(instrumentation);
  return builder.toString();
}

bool parseBreakpointId(const String16& breakpointId, BreakpointType* type,
                       String16* scriptSelector = nullptr,
                       int* lineNumber = nullptr, int* columnNumber = nullptr) {
  size_t typeLineSeparator = breakpointId.find(':');
  if (typeLineSeparator == String16::kNotFound) return false;

  int rawType = breakpointId.substring(0, typeLineSeparator).toInteger();
  if (rawType < static_cast<int>(BreakpointType::kByUrl) ||
      rawType > static_cast<int>(BreakpointType::kInstrumentationBreakpoint)) {
    return false;
  }
  if (type) *type = static_cast<BreakpointType>(rawType);
  if (rawType == static_cast<int>(BreakpointType::kDebugCommand) ||
      rawType == static_cast<int>(BreakpointType::kMonitorCommand) ||
      rawType == static_cast<int>(BreakpointType::kBreakpointAtEntry) ||
      rawType == static_cast<int>(BreakpointType::kInstrumentationBreakpoint)) {
    // The script and source position are not encoded in this case.
    return true;
  }

  size_t lineColumnSeparator = breakpointId.find(':', typeLineSeparator + 1);
  if (lineColumnSeparator == String16::kNotFound) return false;
  size_t columnSelectorSeparator =
      breakpointId.find(':', lineColumnSeparator + 1);
  if (columnSelectorSeparator == String16::kNotFound) return false;
  if (scriptSelector) {
    *scriptSelector = breakpointId.substring(columnSelectorSeparator + 1);
  }
  if (lineNumber) {
    *lineNumber = breakpointId
                      .substring(typeLineSeparator + 1,
                                 lineColumnSeparator - typeLineSeparator - 1)
                      .toInteger();
  }
  if (columnNumber) {
    *columnNumber =
        breakpointId
            .substring(lineColumnSeparator + 1,
                       columnSelectorSeparator - lineColumnSeparator - 1)
            .toInteger();
  }
  return true;
}

bool positionComparator(const std::pair<int, int>& a,
                        const std::pair<int, int>& b) {
  if (a.first != b.first) return a.first < b.first;
  return a.second < b.second;
}

std::unique_ptr<protocol::DictionaryValue> breakpointHint(
    const V8DebuggerScript& script, int breakpointLineNumber,
    int breakpointColumnNumber, int actualLineNumber, int actualColumnNumber) {
  int actualOffset;
  int breakpointOffset;
  if (!script.offset(actualLineNumber, actualColumnNumber).To(&actualOffset) ||
      !script.offset(breakpointLineNumber, breakpointColumnNumber)
           .To(&breakpointOffset)) {
    return {};
  }

  auto hintObject = protocol::DictionaryValue::create();
  String16 rawHint = script.source(actualOffset, kBreakpointHintMaxLength);
  std::pair<size_t, size_t> offsetAndLength =
      rawHint.getTrimmedOffsetAndLength();
  String16 hint =
      rawHint.substring(offsetAndLength.first, offsetAndLength.second);
  for (size_t i = 0; i < hint.length(); ++i) {
    if (hint[i] == '\r' || hint[i] == '\n' || hint[i] == ';') {
      hint = hint.substring(0, i);
      break;
    }
  }
  hintObject->setString(DebuggerAgentState::breakpointHintText, hint);

  // Also store the hash of the text between the requested breakpoint location
  // and the actual breakpoint location. If we see the same prefix text next
  // time, we will keep the breakpoint at the same location (so that
  // breakpoints do not slide around on reloads without any edits).
  if (breakpointOffset <= actualOffset) {
    size_t length = actualOffset - breakpointOffset + offsetAndLength.first;
    String16 prefix = script.source(breakpointOffset, length);
    int crc32 = computeCrc32(prefix);
    hintObject->setInteger(DebuggerAgentState::breakpointHintPrefixHash, crc32);
    hintObject->setInteger(DebuggerAgentState::breakpointHintPrefixLength,
                           v8::base::checked_cast<int32_t>(length));
  }
  return hintObject;
}

void adjustBreakpointLocation(const V8DebuggerScript& script,
                              const protocol::DictionaryValue* hintObject,
                              int* lineNumber, int* columnNumber) {
  if (*lineNumber < script.startLine() || *lineNumber > script.endLine())
    return;
  if (*lineNumber == script.startLine() &&
      *columnNumber < script.startColumn()) {
    return;
  }
  if (*lineNumber == script.endLine() && script.endColumn() < *columnNumber) {
    return;
  }

  int sourceOffset;
  if (!script.offset(*lineNumber, *columnNumber).To(&sourceOffset)) return;

  int prefixLength = 0;
  hintObject->getInteger(DebuggerAgentState::breakpointHintPrefixLength,
                         &prefixLength);
  String16 hint;
  if (!hintObject->getString(DebuggerAgentState::breakpointHintText, &hint) ||
      hint.isEmpty())
    return;

  intptr_t searchRegionOffset = std::max(
      sourceOffset - kBreakpointHintMaxSearchOffset, static_cast<intptr_t>(0));
  size_t offset = sourceOffset - searchRegionOffset;
  size_t searchRegionSize =
      offset + std::max(kBreakpointHintMaxSearchOffset,
                        static_cast<intptr_t>(prefixLength + hint.length()));

  String16 searchArea = script.source(searchRegionOffset, searchRegionSize);

  // Let us see if the breakpoint hint text appears at the same location
  // as before, with the same prefix text in between. If yes, then we just use
  // that position.
  int prefixHash;
  if (hintObject->getInteger(DebuggerAgentState::breakpointHintPrefixHash,
                             &prefixHash) &&
      offset + prefixLength + hint.length() <= searchArea.length() &&
      searchArea.substring(offset + prefixLength, hint.length()) == hint &&
      computeCrc32(searchArea.substring(offset, prefixLength)) == prefixHash) {
    v8::debug::Location hintPosition = script.location(
        static_cast<int>(searchRegionOffset + offset + prefixLength));
    *lineNumber = hintPosition.GetLineNumber();
    *columnNumber = hintPosition.GetColumnNumber();
    return;
  }

  size_t nextMatch = searchArea.find(hint, offset);
  size_t prevMatch = searchArea.reverseFind(hint, offset);
  if (nextMatch == String16::kNotFound && prevMatch == String16::kNotFound) {
    return;
  }
  size_t bestMatch;
  if (nextMatch == String16::kNotFound ||
      nextMatch > offset + kBreakpointHintMaxSearchOffset) {
    bestMatch = prevMatch;
  } else if (prevMatch == String16::kNotFound) {
    bestMatch = nextMatch;
  } else {
    bestMatch = nextMatch - offset < offset - prevMatch ? nextMatch : prevMatch;
  }
  bestMatch += searchRegionOffset;
  v8::debug::Location hintPosition =
      script.location(static_cast<int>(bestMatch));
  if (hintPosition.IsEmpty()) return;
  *lineNumber = hintPosition.GetLineNumber();
  *columnNumber = hintPosition.GetColumnNumber();
}

String16 breakLocationType(v8::debug::BreakLocationType type) {
  switch (type) {
    case v8::debug::kCallBreakLocation:
      return protocol::Debugger::BreakLocation::TypeEnum::Call;
    case v8::debug::kReturnBreakLocation:
      return protocol::Debugger::BreakLocation::TypeEnum::Return;
    case v8::debug::kDebuggerStatementBreakLocation:
      return protocol::Debugger::BreakLocation::TypeEnum::DebuggerStatement;
    case v8::debug::kCommonBreakLocation:
      return String16();
  }
  return String16();
}

String16 scopeType(v8::debug::ScopeIterator::ScopeType type) {
  switch (type) {
    case v8::debug::ScopeIterator::ScopeTypeGlobal:
      return Scope::TypeEnum::Global;
    case v8::debug::ScopeIterator::ScopeTypeLocal:
      return Scope::TypeEnum::Local;
    case v8::debug::ScopeIterator::ScopeTypeWith:
      return Scope::TypeEnum::With;
    case v8::debug::ScopeIterator::ScopeTypeClosure:
      return Scope::TypeEnum::Closure;
    case v8::debug::ScopeIterator::ScopeTypeCatch:
      return Scope::TypeEnum::Catch;
    case v8::debug::ScopeIterator::ScopeTypeBlock:
      return Scope::TypeEnum::Block;
    case v8::debug::ScopeIterator::ScopeTypeScript:
      return Scope::TypeEnum::Script;
    case v8::debug::ScopeIterator::ScopeTypeEval:
      return Scope::TypeEnum::Eval;
    case v8::debug::ScopeIterator::ScopeTypeModule:
      return Scope::TypeEnum::Module;
    case v8::debug::ScopeIterator::ScopeTypeWasmExpressionStack:
      return Scope::TypeEnum::WasmExpressionStack;
  }
  UNREACHABLE();
}

Response buildScopes(v8::Isolate* isolate, v8::debug::ScopeIterator* iterator,
                     InjectedScript* injectedScript,
                     std::unique_ptr<Array<Scope>>* scopes) {
  *scopes = std::make_unique<Array<Scope>>();
  if (!injectedScript) return Response::Success();
  if (iterator->Done()) return Response::Success();

  String16 scriptId = String16::fromInteger(iterator->GetScriptId());

  for (; !iterator->Done(); iterator->Advance()) {
    std::unique_ptr<RemoteObject> object;
    Response result =
        injectedScript->wrapObject(iterator->GetObject(), kBacktraceObjectGroup,
                                   WrapOptions({WrapMode::kIdOnly}), &object);
    if (!result.IsSuccess()) return result;

    auto scope = Scope::create()
                     .setType(scopeType(iterator->GetType()))
                     .setObject(std::move(object))
                     .build();

    String16 name = toProtocolStringWithTypeCheck(
        isolate, iterator->GetFunctionDebugName());
    if (!name.isEmpty()) scope->setName(name);

    if (iterator->HasLocationInfo()) {
      v8::debug::Location start = iterator->GetStartLocation();
      scope->setStartLocation(protocol::Debugger::Location::create()
                                  .setScriptId(scriptId)
                                  .setLineNumber(start.GetLineNumber())
                                  .setColumnNumber(start.GetColumnNumber())
                                  .build());

      v8::debug::Location end = iterator->GetEndLocation();
      scope->setEndLocation(protocol::Debugger::Location::create()
                                .setScriptId(scriptId)
                                .setLineNumber(end.GetLineNumber())
                                .setColumnNumber(end.GetColumnNumber())
                                .build());
    }
    (*scopes)->emplace_back(std::move(scope));
  }
  return Response::Success();
}

protocol::DictionaryValue* getOrCreateObject(protocol::DictionaryValue* object,
                                             const String16& key) {
  protocol::DictionaryValue* value = object->getObject(key);
  if (value) return value;
  std::unique_ptr<protocol::DictionaryValue> newDictionary =
      protocol::DictionaryValue::create();
  value = newDictionary.get();
  object->setObject(key, std::move(newDictionary));
  return value;
}

Response isValidPosition(protocol::Debugger::ScriptPosition* position) {
  if (position->getLineNumber() < 0)
    return Response::ServerError("Position missing 'line' or 'line' < 0.");
  if (position->getColumnNumber() < 0)
    return Response::ServerError("Position missing 'column' or 'column' < 0.");
  return Response::Success();
}

Response isValidRangeOfPositions(std::vector<std::pair<int, int>>& positions) {
  for (size_t i = 1; i < positions.size(); ++i) {
    if (positions[i - 1].first < positions[i].first) continue;
    if (positions[i - 1].first == positions[i].first &&
        positions[i - 1].second < positions[i].second)
      continue;
    return Response::ServerError(
        "Input positions array is not sorted or contains duplicate values.");
  }
  return Response::Success();
}

bool hitBreakReasonEncodedAsOther(v8::debug::BreakReasons breakReasons) {
  // The listed break reasons are not explicitly encoded in CDP when
  // reporting the break. They are summarized as 'other'.
  v8::debug::BreakReasons otherBreakReasons(
      {v8::debug::BreakReason::kDebuggerStatement,
       v8::debug::BreakReason::kScheduled,
       v8::debug::BreakReason::kAlreadyPaused});
  return breakReasons.contains_any(otherBreakReasons);
}
}  // namespace

V8DebuggerAgentImpl::V8DebuggerAgentImpl(
    V8InspectorSessionImpl* session, protocol::FrontendChannel* frontendChannel,
    protocol::DictionaryValue* state)
    : m_inspector(session->inspector()),
      m_debugger(m_inspector->debugger()),
      m_session(session),
      m_enableState(kDisabled),
      m_state(state),
      m_frontend(frontendChannel),
      m_isolate(m_inspector->isolate()) {}

V8DebuggerAgentImpl::~V8DebuggerAgentImpl() = default;

void V8DebuggerAgentImpl::enableImpl() {
  m_enableState = kEnabled;
  m_state->setBoolean(DebuggerAgentState::debuggerEnabled, true);
  m_debugger->enable();

  std::vector<std::unique_ptr<V8DebuggerScript>> compiledScripts =
      m_debugger->getCompiledScripts(m_session->contextGroupId(), this);
  for (auto& script : compiledScripts) {
    didParseSource(std::move(script), true);
  }

  m_breakpointsActive = m_state->booleanProperty(
      DebuggerAgentState::breakpointsActiveWhenEnabled, true);
  if (m_breakpointsActive) {
    m_debugger->setBreakpointsActive(true);
  }
  if (isPaused()) {
    didPause(0, v8::Local<v8::Value>(), std::vector<v8::debug::BreakpointId>(),
             v8::debug::kException, false,
             v8::debug::BreakReasons({v8::debug::BreakReason::kAlreadyPaused}));
  }
}

Response V8DebuggerAgentImpl::enable(std::optional<double> maxScriptsCacheSize,
                                     String16* outDebuggerId) {
  if (m_enableState == kStopping)
    return Response::ServerError("Debugger is stopping");
  m_maxScriptCacheSize = v8::base::saturated_cast<size_t>(
      maxScriptsCacheSize.value_or(std::numeric_limits<double>::max()));
  m_state->setDouble(DebuggerAgentState::maxScriptCacheSize,
                     static_cast<double>(m_maxScriptCacheSize));
  *outDebuggerId =
      m_debugger->debuggerIdFor(m_session->contextGroupId()).toString();
  if (enabled()) return Response::Success();

  if (!m_inspector->client()->canExecuteScripts(m_session->contextGroupId()))
    return Response::ServerError("Script execution is prohibited");

  enableImpl();
  return Response::Success();
}

Response V8DebuggerAgentImpl::disable() {
  if (!enabled()) return Response::Success();

  m_state->remove(DebuggerAgentState::breakpointsByRegex);
  m_state->remove(DebuggerAgentState::breakpointsByUrl);
  m_state->remove(DebuggerAgentState::breakpointsByScriptHash);
  m_state->remove(DebuggerAgentState::breakpointHints);
  m_state->remove(DebuggerAgentState::instrumentationBreakpoints);

  m_state->setInteger(DebuggerAgentState::pauseOnExceptionsState,
                      v8::debug::NoBreakOnException);
  m_state->setInteger(DebuggerAgentState::asyncCallStackDepth, 0);

  if (m_breakpointsActive) {
    m_debugger->setBreakpointsActive(false);
    m_breakpointsActive = false;
  }
  m_blackboxedPositions.clear();
  m_blackboxPattern.reset();
  resetBlackboxedStateCache();
  m_skipList.clear();
  m_scripts.clear();
  m_cachedScripts.clear();
  m_cachedScriptSize = 0;
  m_maxScriptCacheSize = 0;
  m_state->setDouble(DebuggerAgentState::maxScriptCacheSize, 0);
  for (const auto& it : m_debuggerBreakpointIdToBreakpointId) {
    m_debugger->removeBreakpoint(it.first);
  }
  m_breakpointIdToDebuggerBreakpointIds.clear();
  m_debuggerBreakpointIdToBreakpointId.clear();
  m_wasmDisassemblies.clear();
  m_debugger->setAsyncCallStackDepth(this, 0);
  clearBreakDetails();
  m_skipAllPauses = false;
  m_state->setBoolean(DebuggerAgentState::skipAllPauses, false);
  m_state->remove(DebuggerAgentState::blackboxPattern);
  m_enableState = kDisabled;
  m_instrumentationFinished = true;
  m_state->setBoolean(DebuggerAgentState::debuggerEnabled, false);
  m_debugger->disable();
  return Response::Success();
}

void V8DebuggerAgentImpl::restore() {
  DCHECK(m_enableState == kDisabled);
  if (!m_state->booleanProperty(DebuggerAgentState::debuggerEnabled, false))
    return;
  if (!m_inspector->client()->canExecuteScripts(m_session->contextGroupId()))
    return;

  enableImpl();

  double maxScriptCacheSize = 0;
  m_state->getDouble(DebuggerAgentState::maxScriptCacheSize,
                     &maxScriptCacheSize);
  m_maxScriptCacheSize = v8::base::saturated_cast<size_t>(maxScriptCacheSize);

  int pauseState = v8::debug::NoBreakOnException;
  m_state->getInteger(DebuggerAgentState::pauseOnExceptionsState, &pauseState);
  setPauseOnExceptionsImpl(pauseState);

  m_skipAllPauses =
      m_state->booleanProperty(DebuggerAgentState::skipAllPauses, false);

  int asyncCallStackDepth = 0;
  m_state->getInteger(DebuggerAgentState::asyncCallStackDepth,
                      &asyncCallStackDepth);
  m_debugger->setAsyncCallStackDepth(this, asyncCallStackDepth);

  String16 blackboxPattern;
  if (m_state->getString(DebuggerAgentState::blackboxPattern,
                         &blackboxPattern)) {
    setBlackboxPattern(blackboxPattern);
  }
  m_skipAnonymousScripts =
      m_state->booleanProperty(DebuggerAgentState::skipAnonymousScripts, false);
}

Response V8DebuggerAgentImpl::setBreakpointsActive(bool active) {
  m_state->setBoolean(DebuggerAgentState::breakpointsActiveWhenEnabled, active);
  if (!enabled()) return Response::Success();
  if (m_breakpointsActive == active) return Response::Success();
  m_breakpointsActive = active;
  m_debugger->setBreakpointsActive(active);
  if (!active && !m_breakReason.empty()) {
    clearBreakDetails();
    m_debugger->setPauseOnNextCall(false, m_session->contextGroupId());
  }
  return Response::Success();
}

Response V8DebuggerAgentImpl::setSkipAllPauses(bool skip) {
  m_state->setBoolean(DebuggerAgentState::skipAllPauses, skip);
  m_skipAllPauses = skip;
  return Response::Success();
}

namespace {

class Matcher {
 public:
  Matcher(V8InspectorImpl* inspector, BreakpointType type,
          const String16& selector)
      : type_(type), selector_(selector) {
    if (type == BreakpointType::kByUrlRegex) {
      regex_ = std::make_unique<V8Regex>(inspector, selector, true);
    }
  }

  bool matches(const V8DebuggerScript& script) {
    switch (type_) {
      case BreakpointType::kByUrl:
        return script.sourceURL() == selector_;
      case BreakpointType::kByScriptHash:
        return script.hash() == selector_;
      case BreakpointType::kByUrlRegex: {
        return regex_->match(script.sourceURL()) != -1;
      }
      case BreakpointType::kByScriptId: {
        return script.scriptId() == selector_;
      }
      default:
        return false;
    }
  }

 private:
  std::unique_ptr<V8Regex> regex_;
  BreakpointType type_;
  const String16& selector_;
};

}  // namespace

Response V8DebuggerAgentImpl::setBreakpointByUrl(
    int lineNumber, std::optional<String16> optionalURL,
    std::optional<String16> optionalURLRegex,
    std::optional<String16> optionalScriptHash,
    std::optional<int> optionalColumnNumber,
    std::optional<String16> optionalCondition, String16* outBreakpointId,
    std::unique_ptr<protocol::Array<protocol::Debugger::Location>>* locations) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);

  *locations = std::make_unique<Array<protocol::Debugger::Location>>();

  int specified = (optionalURL.has_value() ? 1 : 0) +
                  (optionalURLRegex.has_value() ? 1 : 0) +
                  (optionalScriptHash.has_value() ? 1 : 0);
  if (specified != 1) {
    return Response::ServerError(
        "Either url or urlRegex or scriptHash must be specified.");
  }
  int columnNumber = 0;
  if (optionalColumnNumber.has_value()) {
    columnNumber = optionalColumnNumber.value();
    if (columnNumber < 0)
      return Response::ServerError("Incorrect column number");
  }

  BreakpointType type = BreakpointType::kByUrl;
  String16 selector;
  if (optionalURLRegex.has_value()) {
    selector = optionalURLRegex.value();
    type = BreakpointType::kByUrlRegex;
  } else if (optionalURL.has_value()) {
    selector = optionalURL.value();
    type = BreakpointType::kByUrl;
  } else if (optionalScriptHash.has_value()) {
    selector = optionalScriptHash.value();
    type = BreakpointType::kByScriptHash;
  }

  // Note: This constructor can call into JavaScript.
  Matcher matcher(m_inspector, type, selector);

  String16 condition = optionalCondition.value_or(String16());
  String16 breakpointId =
      generateBreakpointId(type, selector, lineNumber, columnNumber);
  protocol::DictionaryValue* breakpoints;
  switch (type) {
    case BreakpointType::kByUrlRegex:
      breakpoints =
          getOrCreateObject(m_state, DebuggerAgentState::breakpointsByRegex);
      break;
    case BreakpointType::kByUrl:
      breakpoints = getOrCreateObject(
          getOrCreateObject(m_state, DebuggerAgentState::breakpointsByUrl),
          selector);
      break;
    case BreakpointType::kByScriptHash:
      breakpoints = getOrCreateObject(
          getOrCreateObject(m_state,
                            DebuggerAgentState::breakpointsByScriptHash),
          selector);
      break;
    default:
      UNREACHABLE();
  }
  if (breakpoints->get(breakpointId)) {
    return Response::ServerError(
        "Breakpoint at specified location already exists.");
  }

  std::unique_ptr<protocol::DictionaryValue> hint;
  for (const auto& script : m_scripts) {
    if (!matcher.matches(*script.second)) continue;
    // Make sure the session was not disabled by some re-entrant call
    // in the script matcher.
    DCHECK(enabled());
    int adjustedLineNumber = lineNumber;
    int adjustedColumnNumber = columnNumber;
    if (hint) {
      adjustBreakpointLocation(*script.second, hint.get(), &adjustedLineNumber,
                               &adjustedColumnNumber);
    }
    std::unique_ptr<protocol::Debugger::Location> location =
        setBreakpointImpl(breakpointId, script.first, condition,
                          adjustedLineNumber, adjustedColumnNumber);
    if (location && type != BreakpointType::kByUrlRegex) {
      hint = breakpointHint(*script.second, lineNumber, columnNumber,
                            location->getLineNumber(),
                            location->getColumnNumber(adjustedColumnNumber));
    }
    if (location) (*locations)->emplace_back(std::move(location));
  }
  breakpoints->setString(breakpointId, condition);
  if (hint) {
    protocol::DictionaryValue* breakpointHints =
        getOrCreateObject(m_state, DebuggerAgentState::breakpointHints);
    breakpointHints->setObject(breakpointId, std::move(hint));
  }
  *outBreakpointId = breakpointId;
  return Response::Success();
}

Response V8DebuggerAgentImpl::setBreakpoint(
    std::unique_ptr<protocol::Debugger::Location> location,
    std::optional<String16> optionalCondition, String16* outBreakpointId,
    std::unique_ptr<protocol::Debugger::Location>* actualLocation) {
  String16 breakpointId = generateBreakpointId(
      BreakpointType::kByScriptId, location->getScriptId(),
      location->getLineNumber(), location->getColumnNumber(0));
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);

  if (m_breakpointIdToDebuggerBreakpointIds.find(breakpointId) !=
      m_breakpointIdToDebuggerBreakpointIds.end()) {
    return Response::ServerError(
        "Breakpoint at specified location already exists.");
  }
  *actualLocation = setBreakpointImpl(breakpointId, location->getScriptId(),
                                      optionalCondition.value_or(String16()),
                                      location->getLineNumber(),
                                      location->getColumnNumber(0));
  if (!*actualLocation)
    return Response::ServerError("Could not resolve breakpoint");
  *outBreakpointId = breakpointId;
  return Response::Success();
}

Response V8DebuggerAgentImpl::setBreakpointOnFunctionCall(
    const String16& functionObjectId, std::optional<String16> optionalCondition,
    String16* outBreakpointId) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);

  InjectedScript::ObjectScope scope(m_session, functionObjectId);
  Response response = scope.initialize();
  if (!response.IsSuccess()) return response;
  if (!scope.object()->IsFunction()) {
    return Response::ServerError("Could not find function with given id");
  }
  v8::Local<v8::Function> function =
      v8::Local<v8::Function>::Cast(scope.object());
  String16 breakpointId =
      generateBreakpointId(BreakpointType::kBreakpointAtEntry, function);
  if (m_breakpointIdToDebuggerBreakpointIds.find(breakpointId) !=
      m_breakpointIdToDebuggerBreakpointIds.end()) {
    return Response::ServerError(
        "Breakpoint at specified location already exists.");
  }
  v8::Local<v8::String> condition =
      toV8String(m_isolate, optionalCondition.value_or(String16()));
  setBreakpointImpl(breakpointId, function, condition);
  *outBreakpointId = breakpointId;
  return Response::Success();
}

Response V8DebuggerAgentImpl::setInstrumentationBreakpoint(
    const String16& instrumentation, String16* outBreakpointId) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  String16 breakpointId = generateInstrumentationBreakpointId(instrumentation);
  protocol::DictionaryValue* breakpoints = getOrCreateObject(
      m_state, DebuggerAgentState::instrumentationBreakpoints);
  if (breakpoints->get(breakpointId)) {
    return Response::ServerError(
        "Instrumentation breakpoint is already enabled.");
  }
  breakpoints->setBoolean(breakpointId, true);
  *outBreakpointId = breakpointId;
  return Response::Success();
}

Response V8DebuggerAgentImpl::removeBreakpoint(const String16& breakpointId) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  BreakpointType type;
  String16 selector;
  if (!parseBreakpointId(breakpointId, &type, &selector)) {
    return Response::Success();
  }
  Matcher matcher(m_inspector, type, selector);
  protocol::DictionaryValue* breakpoints = nullptr;
  switch (type) {
    case BreakpointType::kByUrl: {
      protocol::DictionaryValue* breakpointsByUrl =
          m_state->getObject(DebuggerAgentState::breakpointsByUrl);
      if (breakpointsByUrl) {
        breakpoints = breakpointsByUrl->getObject(selector);
      }
    } break;
    case BreakpointType::kByScriptHash: {
      protocol::DictionaryValue* breakpointsByScriptHash =
          m_state->getObject(DebuggerAgentState::breakpointsByScriptHash);
      if (breakpointsByScriptHash) {
        breakpoints = breakpointsByScriptHash->getObject(selector);
      }
    } break;
    case BreakpointType::kByUrlRegex:
      breakpoints = m_state->getObject(DebuggerAgentState::breakpointsByRegex);
      break;
    case BreakpointType::kInstrumentationBreakpoint:
      breakpoints =
          m_state->getObject(DebuggerAgentState::instrumentationBreakpoints);
      break;
    default:
      break;
  }
  if (breakpoints) breakpoints->remove(breakpointId);
  protocol::DictionaryValue* breakpointHints =
      m_state->getObject(DebuggerAgentState::breakpointHints);
  if (breakpointHints) breakpointHints->remove(breakpointId);

  // Get a list of scripts to remove breakpoints.
  // TODO(duongn): we can do better here if from breakpoint id we can tell it is
  // not Wasm breakpoint.
  std::vector<V8DebuggerScript*> scripts;
  for (const auto& scriptIter : m_scripts) {
    const bool scriptSelectorMatch = matcher.matches(*scriptIter.second);
    // Make sure the session was not disabled by some re-entrant call
    // in the script matcher.
    DCHECK(enabled());
    const bool isInstrumentation =
        type == BreakpointType::kInstrumentationBreakpoint;
    if (!scriptSelectorMatch && !isInstrumentation) continue;
    V8DebuggerScript* script = scriptIter.second.get();
    if (script->getLanguage() == V8DebuggerScript::Language::WebAssembly) {
      scripts.push_back(script);
    }
  }
  removeBreakpointImpl(breakpointId, scripts);

  return Response::Success();
}

void V8DebuggerAgentImpl::removeBreakpointImpl(
    const String16& breakpointId,
    const std::vector<V8DebuggerScript*>& scripts) {
  DCHECK(enabled());
  BreakpointIdToDebuggerBreakpointIdsMap::iterator
      debuggerBreakpointIdsIterator =
          m_breakpointIdToDebuggerBreakpointIds.find(breakpointId);
  if (debuggerBreakpointIdsIterator ==
      m_breakpointIdToDebuggerBreakpointIds.end()) {
    return;
  }
  for (const auto& id : debuggerBreakpointIdsIterator->second) {
#if V8_ENABLE_WEBASSEMBLY
    for (auto& script : scripts) {
      script->removeWasmBreakpoint(id);
    }
#endif  // V8_ENABLE_WEBASSEMBLY
    m_debugger->removeBreakpoint(id);
    m_debuggerBreakpointIdToBreakpointId.erase(id);
  }
  m_breakpointIdToDebuggerBreakpointIds.erase(breakpointId);
}

Response V8DebuggerAgentImpl::getPossibleBreakpoints(
    std::unique_ptr<protocol::Debugger::Location> start,
    std::unique_ptr<protocol::Debugger::Location> end,
    std::optional<bool> restrictToFunction,
    std::unique_ptr<protocol::Array<protocol::Debugger::BreakLocation>>*
        locations) {
  String16 scriptId = start->getScriptId();

  if (start->getLineNumber() < 0 || start->getColumnNumber(0) < 0)
    return Response::ServerError(
        "start.lineNumber and start.columnNumber should be >= 0");

  v8::debug::Location v8Start(start->getLineNumber(),
                              start->getColumnNumber(0));
  v8::debug::Location v8End;
  if (end) {
    if (end->getScriptId() != scriptId)
      return Response::ServerError(
          "Locations should contain the same scriptId");
    int line = end->getLineNumber();
    int column = end->getColumnNumber(0);
    if (line < 0 || column < 0)
      return Response::ServerError(
          "end.lineNumber and end.columnNumber should be >= 0");
    v8End = v8::debug::Location(line, column);
  }
  auto it = m_scripts.find(scriptId);
  if (it == m_scripts.end()) return Response::ServerError("Script not found");
  std::vector<v8::debug::BreakLocation> v8Locations;
  {
    v8::HandleScope handleScope(m_isolate);
    int contextId = it->second->executionContextId();
    InspectedContext* inspected = m_inspector->getContext(contextId);
    if (!inspected) {
      return Response::ServerError("Cannot retrive script context");
    }
    v8::Context::Scope contextScope(inspected->context());
    v8::MicrotasksScope microtasks(inspected->context(),
                                   v8::MicrotasksScope::kDoNotRunMicrotasks);
    v8::TryCatch tryCatch(m_isolate);
    it->second->getPossibleBreakpoints(
        v8Start, v8End, restrictToFunction.value_or(false), &v8Locations);
  }

  *locations =
      std::make_unique<protocol::Array<protocol::Debugger::BreakLocation>>();

  // TODO(1106269): Return an error instead of capping the number of
  // breakpoints.
  const size_t numBreakpointsToSend =
      std::min(v8Locations.size(), kMaxNumBreakpoints);
  for (size_t i = 0; i < numBreakpointsToSend; ++i) {
    std::unique_ptr<protocol::Debugger::BreakLocation> breakLocation =
        protocol::Debugger::BreakLocation::create()
            .setScriptId(scriptId)
            .setLineNumber(v8Locations[i].GetLineNumber())
            .setColumnNumber(v8Locations[i].GetColumnNumber())
            .build();
    if (v8Locations[i].type() != v8::debug::kCommonBreakLocation) {
      breakLocation->setType(breakLocationType(v8Locations[i].type()));
    }
    (*locations)->emplace_back(std::move(breakLocation));
  }
  return Response::Success();
}

Response V8DebuggerAgentImpl::continueToLocation(
    std::unique_ptr<protocol::Debugger::Location> location,
    std::optional<String16> targetCallFrames) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  ScriptsMap::iterator it = m_scripts.find(location->getScriptId());
  if (it == m_scripts.end()) {
    return Response::ServerError("Cannot continue to specified location");
  }
  V8DebuggerScript* script = it->second.get();
  int contextId = script->executionContextId();
  InspectedContext* inspected = m_inspector->getContext(contextId);
  if (!inspected)
    return Response::ServerError("Cannot continue to specified location");
  v8::HandleScope handleScope(m_isolate);
  v8::Context::Scope contextScope(inspected->context());
  return m_debugger->continueToLocation(
      m_session->contextGroupId(), script, std::move(location),
      targetCallFrames.value_or(
          protocol::Debugger::ContinueToLocation::TargetCallFramesEnum::Any));
}

Response V8DebuggerAgentImpl::getStackTrace(
    std::unique_ptr<protocol::Runtime::StackTraceId> inStackTraceId,
    std::unique_ptr<protocol::Runtime::StackTrace>* outStackTrace) {
  bool isOk = false;
  int64_t id = inStackTraceId->getId().toInteger64(&isOk);
  if (!isOk) return Response::ServerError("Invalid stack trace id");

  internal::V8DebuggerId debuggerId;
  if (inStackTraceId->hasDebuggerId()) {
    debuggerId =
        internal::V8DebuggerId(inStackTraceId->getDebuggerId(String16()));
  } else {
    debuggerId = m_debugger->debuggerIdFor(m_session->contextGroupId());
  }
  if (!debuggerId.isValid())
    return Response::ServerError("Invalid stack trace id");

  V8StackTraceId v8StackTraceId(id, debuggerId.pair());
  if (v8StackTraceId.IsInvalid())
    return Response::ServerError("Invalid stack trace id");
  auto stack =
      m_debugger->stackTraceFor(m_session->contextGroupId(), v8StackTraceId);
  if (!stack) {
    return Response::ServerError("Stack trace with given id is not found");
  }
  *outStackTrace = stack->buildInspectorObject(
      m_debugger, m_debugger->maxAsyncCallChainDepth());
  return Response::Success();
}

bool V8DebuggerAgentImpl::isFunctionBlackboxed(const String16& scriptId,
                                               const v8::debug::Location& start,
                                               const v8::debug::Location& end) {
  ScriptsMap::iterator it = m_scripts.find(scriptId);
  if (it == m_scripts.end()) {
    // Unknown scripts are blackboxed.
    return true;
  }
  const String16& scriptSourceURL = it->second->sourceURL();
  if (m_blackboxPattern && !scriptSourceURL.isEmpty()
      && m_blackboxPattern->match(scriptSourceURL) != -1) {
    return true;
  }
  if (m_skipAnonymousScripts && scriptSourceURL.isEmpty()) {
    return true;
  }
  if (!m_blackboxedExecutionContexts.empty()) {
    int contextId = it->second->executionContextId();
    InspectedContext* inspected = m_inspector->getContext(contextId);
    if (inspected && m_blackboxedExecutionContexts.count(
                         inspected->uniqueId().toString()) > 0) {
      return true;
    }
  }
  auto itBlackboxedPositions = m_blackboxedPositions.find(scriptId);
  if (itBlackboxedPositions == m_blackboxedPositions.end()) return false;

  const std::vector<std::pair<int, int>>& ranges =
      itBlackboxedPositions->second;
  auto itStartRange = std::lower_bound(
      ranges.begin(), ranges.end(),
      std::make_pair(start.GetLineNumber(), start.GetColumnNumber()),
      positionComparator);
  auto itEndRange = std::lower_bound(
      itStartRange, ranges.end(),
      std::make_pair(end.GetLineNumber(), end.GetColumnNumber()),
      positionComparator);
  // Ranges array contains positions in script where blackbox state is changed.
  // [(0,0) ... ranges[0]) isn't blackboxed, [ranges[0] ... ranges[1]) is
  // blackboxed...
  return itStartRange == itEndRange &&
         std::distance(ranges.begin(), itStartRange) % 2;
}

bool V8DebuggerAgentImpl::shouldBeSkipped(const String16& scriptId, int line,
                                          int column) {
  if (m_skipList.empty()) return false;

  auto it = m_skipList.find(scriptId);
  if (it == m_skipList.end()) return false;

  const std::vector<std::pair<int, int>>& ranges = it->second;
  DCHECK(!ranges.empty());
  const std::pair<int, int> location = std::make_pair(line, column);
  auto itLowerBound = std::lower_bound(ranges.begin(), ranges.end(), location,
                                       positionComparator);

  bool shouldSkip = false;
  if (itLowerBound != ranges.end()) {
    // Skip lists are defined as pairs of locations that specify the
    // start and the end of ranges to skip: [ranges[0], ranges[1], ..], where
    // locations in [ranges[0], ranges[1]) should be skipped, i.e.
    // [(lineStart, columnStart), (lineEnd, columnEnd)).
    const bool isSameAsLowerBound = location == *itLowerBound;
    const bool isUnevenIndex = (itLowerBound - ranges.begin()) % 2;
    shouldSkip = isSameAsLowerBound ^ isUnevenIndex;
  }

  return shouldSkip;
}

bool V8DebuggerAgentImpl::acceptsPause(bool isOOMBreak) const {
  return enabled() && (isOOMBreak || !m_skipAllPauses);
}

std::unique_ptr<protocol::Debugger::Location>
V8DebuggerAgentImpl::setBreakpointImpl(const String16& breakpointId,
                                       const String16& scriptId,
                                       const String16& condition,
                                       int lineNumber, int columnNumber) {
  v8::HandleScope handles(m_isolate);
  DCHECK(enabled());

  ScriptsMap::iterator scriptIterator = m_scripts.find(scriptId);
  if (scriptIterator == m_scripts.end()) return nullptr;
  V8DebuggerScript* script = scriptIterator->second.get();

  v8::debug::BreakpointId debuggerBreakpointId;
  v8::debug::Location location(lineNumber, columnNumber);
  int contextId = script->executionContextId();
  InspectedContext* inspected = m_inspector->getContext(contextId);
  if (!inspected) return nullptr;

  {
    v8::Context::Scope contextScope(inspected->context());
    if (!script->setBreakpoint(condition, &location, &debuggerBreakpointId)) {
      return nullptr;
    }
  }

  m_debuggerBreakpointIdToBreakpointId[debuggerBreakpointId] = breakpointId;
  m_breakpointIdToDebuggerBreakpointIds[breakpointId].push_back(
      debuggerBreakpointId);

  return protocol::Debugger::Location::create()
      .setScriptId(scriptId)
      .setLineNumber(location.GetLineNumber())
      .setColumnNumber(location.GetColumnNumber())
      .build();
}

void V8DebuggerAgentImpl::setBreakpointImpl(const String16& breakpointId,
                                            v8::Local<v8::Function> function,
                                            v8::Local<v8::String> condition) {
  v8::debug::BreakpointId debuggerBreakpointId;
  if (!v8::debug::SetFunctionBreakpoint(function, condition,
                                        &debuggerBreakpointId)) {
    return;
  }
  m_debuggerBreakpointIdToBreakpointId[debuggerBreakpointId] = breakpointId;
  m_breakpointIdToDebuggerBreakpointIds[breakpointId].push_back(
      debuggerBreakpointId);
}

Response V8DebuggerAgentImpl::searchInContent(
    const String16& scriptId, const String16& query,
    std::optional<bool> optionalCaseSensitive,
    std::optional<bool> optionalIsRegex,
    std::unique_ptr<Array<protocol::Debugger::SearchMatch>>* results) {
  v8::HandleScope handles(m_isolate);
  ScriptsMap::iterator it = m_scripts.find(scriptId);
  if (it == m_scripts.end())
    return Response::ServerError("No script for id: " + scriptId.utf8());

  *results = std::make_unique<protocol::Array<protocol::Debugger::SearchMatch>>(
      searchInTextByLinesImpl(m_session, it->second->source(0), query,
                              optionalCaseSensitive.value_or(false),
                              optionalIsRegex.value_or(false)));
  return Response::Success();
}

namespace {
const char* buildStatus(v8::debug::LiveEditResult::Status status) {
  switch (status) {
    case v8::debug::LiveEditResult::OK:
      return protocol::Debugger::SetScriptSource::StatusEnum::Ok;
    case v8::debug::LiveEditResult::COMPILE_ERROR:
      return protocol::Debugger::SetScriptSource::StatusEnum::CompileError;
    case v8::debug::LiveEditResult::BLOCKED_BY_ACTIVE_FUNCTION:
      return protocol::Debugger::SetScriptSource::StatusEnum::
          BlockedByActiveFunction;
    case v8::debug::LiveEditResult::BLOCKED_BY_RUNNING_GENERATOR:
      return protocol::Debugger::SetScriptSource::StatusEnum::
          BlockedByActiveGenerator;
    case v8::debug::LiveEditResult::BLOCKED_BY_TOP_LEVEL_ES_MODULE_CHANGE:
      return protocol::Debugger::SetScriptSource::StatusEnum::
          BlockedByTopLevelEsModuleChange;
  }
}
}  // namespace

Response V8DebuggerAgentImpl::setScriptSource(
    const String16& scriptId, const String16& newContent,
    std::optional<bool> dryRun, std::optional<bool> allowTopFrameEditing,
    std::unique_ptr<protocol::Array<protocol::Debugger::CallFrame>>*
        newCallFrames,
    std::optional<bool>* stackChanged,
    std::unique_ptr<protocol::Runtime::StackTrace>* asyncStackTrace,
    std::unique_ptr<protocol::Runtime::StackTraceId>* asyncStackTraceId,
    String16* status,
    std::unique_ptr<protocol::Runtime::ExceptionDetails>* optOutCompileError) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);

  ScriptsMap::iterator it = m_scripts.find(scriptId);
  if (it == m_scripts.end()) {
    return Response::ServerError("No script with given id found");
  }
  int contextId = it->second->executionContextId();
  InspectedContext* inspected = m_inspector->getContext(contextId);
  if (!inspected) {
    return Response::InternalError();
  }
  v8::HandleScope handleScope(m_isolate);
  v8::Local<v8::Context> context = inspected->context();
  v8::Context::Scope contextScope(context);
  const bool allowTopFrameLiveEditing = allowTopFrameEditing.value_or(false);

  v8::debug::LiveEditResult result;
  it->second->setSource(newContent, dryRun.value_or(false),
                        allowTopFrameLiveEditing, &result);
  *status = buildStatus(result.status);
  if (result.status == v8::debug::LiveEditResult::COMPILE_ERROR) {
    *optOutCompileError =
        protocol::Runtime::ExceptionDetails::create()
            .setExceptionId(m_inspector->nextExceptionId())
            .setText(toProtocolString(m_isolate, result.message))
            .setLineNumber(result.line_number != -1 ? result.line_number - 1
                                                    : 0)
            .setColumnNumber(result.column_number != -1 ? result.column_number
                                                        : 0)
            .build();
    return Response::Success();
  }

  if (result.restart_top_frame_required) {
    CHECK(allowTopFrameLiveEditing);
    // Nothing could have happened to the JS stack since the live edit so
    // restarting the top frame is guaranteed to be successful.
    CHECK(m_debugger->restartFrame(m_session->contextGroupId(),
                                   /* callFrameOrdinal */ 0));
    m_session->releaseObjectGroup(kBacktraceObjectGroup);
  }

  return Response::Success();
}

Response V8DebuggerAgentImpl::restartFrame(
    const String16& callFrameId, std::optional<String16> mode,
    std::unique_ptr<Array<CallFrame>>* newCallFrames,
    std::unique_ptr<protocol::Runtime::StackTrace>* asyncStackTrace,
    std::unique_ptr<protocol::Runtime::StackTraceId>* asyncStackTraceId) {
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  if (!mode.has_value()) {
    return Response::ServerError(
        "Restarting frame without 'mode' not supported");
  }
  if (mode.value() != protocol::Debugger::RestartFrame::ModeEnum::StepInto) {
    return Response::InvalidParams("'StepInto' is the only valid mode");
  }

  InjectedScript::CallFrameScope scope(m_session, callFrameId);
  Response response = scope.initialize();
  if (!response.IsSuccess()) return response;
  int callFrameOrdinal = static_cast<int>(scope.frameOrdinal());

  if (!m_debugger->restartFrame(m_session->contextGroupId(),
                                callFrameOrdinal)) {
    return Response::ServerError("Restarting frame failed");
  }
  m_session->releaseObjectGroup(kBacktraceObjectGroup);
  *newCallFrames = std::make_unique<Array<CallFrame>>();
  return Response::Success();
}

Response V8DebuggerAgentImpl::getScriptSource(
    const String16& scriptId, String16* scriptSource,
    std::optional<protocol::Binary>* bytecode) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  ScriptsMap::iterator it = m_scripts.find(scriptId);
  if (it == m_scripts.end()) {
    auto cachedScriptIt =
        std::find_if(m_cachedScripts.begin(), m_cachedScripts.end(),
                     [&scriptId](const CachedScript& cachedScript) {
                       return cachedScript.scriptId == scriptId;
                     });
    if (cachedScriptIt != m_cachedScripts.end()) {
      *scriptSource = cachedScriptIt->source;
      *bytecode = protocol::Binary::fromSpan(v8::MemorySpan<const uint8_t>(
          cachedScriptIt->bytecode.begin(), cachedScriptIt->bytecode.size()));
      return Response::Success();
    }
    return Response::ServerError("No script for id: " + scriptId.utf8());
  }
  *scriptSource = it->second->source(0);
#if V8_ENABLE_WEBASSEMBLY
  v8::MemorySpan<const uint8_t> span;
  if (it->second->wasmBytecode().To(&span)) {
    if (span.size() > kWasmBytecodeMaxLength) {
      return Response::ServerError(kWasmBytecodeExceedsTransferLimit);
    }
    *bytecode = protocol::Binary::fromSpan(span);
  }
#endif  // V8_ENABLE_WEBASSEMBLY
  return Response::Success();
}

struct DisassemblyChunk {
  DisassemblyChunk() = default;
  DisassemblyChunk(const DisassemblyChunk& other) = delete;
  DisassemblyChunk& operator=(const DisassemblyChunk& other) = delete;
  DisassemblyChunk(DisassemblyChunk&& other) V8_NOEXCEPT = default;
  DisassemblyChunk& operator=(DisassemblyChunk&& other) V8_NOEXCEPT = default;

  std::vector<String16> lines;
  std::vector<int> lineOffsets;

  void Reserve(size_t size) {
    lines.reserve(size);
    lineOffsets.reserve(size);
  }
};

class DisassemblyCollectorImpl final : public v8::debug::DisassemblyCollector {
 public:
  DisassemblyCollectorImpl() = default;

  void ReserveLineCount(size_t count) override {
    if (count == 0) return;
    size_t num_chunks = (count + kLinesPerChunk - 1) / kLinesPerChunk;
    chunks_.resize(num_chunks);
    for (size_t i = 0; i < num_chunks - 1; i++) {
      chunks_[i].Reserve(kLinesPerChunk);
    }
    size_t last = num_chunks - 1;
    size_t last_size = count % kLinesPerChunk;
    if (last_size == 0) last_size = kLinesPerChunk;
    chunks_[last].Reserve(last_size);
  }

  void AddLine(const char* src, size_t length,
               uint32_t bytecode_offset) override {
    chunks_[writing_chunk_index_].lines.emplace_back(src, length);
    chunks_[writing_chunk_index_].lineOffsets.push_back(
        static_cast<int>(bytecode_offset));
    if (chunks_[writing_chunk_index_].lines.size() == kLinesPerChunk) {
      writing_chunk_index_++;
    }
    total_number_of_lines_++;
  }

  size_t total_number_of_lines() { return total_number_of_lines_; }

  bool HasNextChunk() { return reading_chunk_index_ < chunks_.size(); }
  DisassemblyChunk NextChunk() {
    return std::move(chunks_[reading_chunk_index_++]);
  }

 private:
  // For a large Ritz module, the average is about 50 chars per line,
  // so (with 2-byte String16 chars) this should give approximately 20 MB
  // per chunk.
  static constexpr size_t kLinesPerChunk = 200'000;

  size_t writing_chunk_index_ = 0;
  size_t reading_chunk_index_ = 0;
  size_t total_number_of_lines_ = 0;
  std::vector<DisassemblyChunk> chunks_;
};

Response V8DebuggerAgentImpl::disassembleWasmModule(
    const String16& in_scriptId, std::optional<String16>* out_streamId,
    int* out_totalNumberOfLines,
    std::unique_ptr<protocol::Array<int>>* out_functionBodyOffsets,
    std::unique_ptr<protocol::Debugger::WasmDisassemblyChunk>* out_chunk) {
#if V8_ENABLE_WEBASSEMBLY
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  ScriptsMap::iterator it = m_scripts.find(in_scriptId);
  std::unique_ptr<DisassemblyCollectorImpl> collector =
      std::make_unique<DisassemblyCollectorImpl>();
  std::vector<int> functionBodyOffsets;
  if (it != m_scripts.end()) {
    V8DebuggerScript* script = it->second.get();
    if (script->getLanguage() != V8DebuggerScript::Language::WebAssembly) {
      return Response::InvalidParams("Script with id " + in_scriptId.utf8() +
                                     " is not WebAssembly");
    }
    script->Disassemble(collector.get(), &functionBodyOffsets);
  } else {
    auto cachedScriptIt =
        std::find_if(m_cachedScripts.begin(), m_cachedScripts.end(),
                     [&in_scriptId](const CachedScript& cachedScript) {
                       return cachedScript.scriptId == in_scriptId;
                     });
    if (cachedScriptIt == m_cachedScripts.end()) {
      return Response::InvalidParams("No script for id: " + in_scriptId.utf8());
    }
    v8::debug::Disassemble(v8::base::VectorOf(cachedScriptIt->bytecode),
                           collector.get(), &functionBodyOffsets);
  }
  *out_totalNumberOfLines =
      static_cast<int>(collector->total_number_of_lines());
  *out_functionBodyOffsets =
      std::make_unique<protocol::Array<int>>(std::move(functionBodyOffsets));
  // Even an empty module would disassemble to "(module)", never to zero lines.
  DCHECK(collector->HasNextChunk());
  DisassemblyChunk chunk(collector->NextChunk());
  *out_chunk = protocol::Debugger::WasmDisassemblyChunk::create()
                   .setBytecodeOffsets(std::make_unique<protocol::Array<int>>(
                       std::move(chunk.lineOffsets)))
                   .setLines(std::make_unique<protocol::Array<String16>>(
                       std::move(chunk.lines)))
                   .build();
  if (collector->HasNextChunk()) {
    String16 streamId = String16::fromInteger(m_nextWasmDisassemblyStreamId++);
    *out_streamId = streamId;
    m_wasmDisassemblies[streamId] = std::move(collector);
  }
  return Response::Success();
#else
  return Response::ServerError("WebAssembly is disabled");
#endif  // V8_ENABLE_WEBASSEMBLY
}

Response V8DebuggerAgentImpl::nextWasmDisassemblyChunk(
    const String16& in_streamId,
    std::unique_ptr<protocol::Debugger::WasmDisassemblyChunk>* out_chunk) {
#if V8_ENABLE_WEBASSEMBLY
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  auto it = m_wasmDisassemblies.find(in_streamId);
  if (it == m_wasmDisassemblies.end()) {
    return Response::InvalidParams("No chunks available for stream " +
                                   in_streamId.utf8());
  }
  if (it->second->HasNextChunk()) {
    DisassemblyChunk chunk(it->second->NextChunk());
    *out_chunk = protocol::Debugger::WasmDisassemblyChunk::create()
                     .setBytecodeOffsets(std::make_unique<protocol::Array<int>>(
                         std::move(chunk.lineOffsets)))
                     .setLines(std::make_unique<protocol::Array<String16>>(
                         std::move(chunk.lines)))
                     .build();
  } else {
    *out_chunk =
        protocol::Debugger::WasmDisassemblyChunk::create()
            .setBytecodeOffsets(std::make_unique<protocol::Array<int>>())
            .setLines(std::make_unique<protocol::Array<String16>>())
            .build();
    m_wasmDisassemblies.erase(it);
  }
  return Response::Success();
#else
  return Response::ServerError("WebAssembly is disabled");
#endif  // V8_ENABLE_WEBASSEMBLY
}

Response V8DebuggerAgentImpl::getWasmBytecode(const String16& scriptId,
                                              protocol::Binary* bytecode) {
#if V8_ENABLE_WEBASSEMBLY
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  ScriptsMap::iterator it = m_scripts.find(scriptId);
  if (it == m_scripts.end())
    return Response::ServerError("No script for id: " + scriptId.utf8());
  v8::MemorySpan<const uint8_t> span;
  if (!it->second->wasmBytecode().To(&span))
    return Response::ServerError("Script with id " + scriptId.utf8() +
                                 " is not WebAssembly");
  if (span.size() > kWasmBytecodeMaxLength) {
    return Response::ServerError(kWasmBytecodeExceedsTransferLimit);
  }
  *bytecode = protocol::Binary::fromSpan(span);
  return Response::Success();
#else
  return Response::ServerError("WebAssembly is disabled");
#endif  // V8_ENABLE_WEBASSEMBLY
}

void V8DebuggerAgentImpl::pushBreakDetails(
    const String16& breakReason,
    std::unique_ptr<protocol::DictionaryValue> breakAuxData) {
  m_breakReason.push_back(std::make_pair(breakReason, std::move(breakAuxData)));
}

void V8DebuggerAgentImpl::popBreakDetails() {
  if (m_breakReason.empty()) return;
  m_breakReason.pop_back();
}

void V8DebuggerAgentImpl::clearBreakDetails() {
  std::vector<BreakReason> emptyBreakReason;
  m_breakReason.swap(emptyBreakReason);
}

void V8DebuggerAgentImpl::schedulePauseOnNextStatement(
    const String16& breakReason,
    std::unique_ptr<protocol::DictionaryValue> data) {
  if (isPaused() || !acceptsPause(false) || !m_breakpointsActive) return;
  if (m_breakReason.empty()) {
    m_debugger->setPauseOnNextCall(true, m_session->contextGroupId());
  }
  pushBreakDetails(breakReason, std::move(data));
}

void V8DebuggerAgentImpl::cancelPauseOnNextStatement() {
  if (isPaused() || !acceptsPause(false) || !m_breakpointsActive) return;
  if (m_breakReason.size() == 1) {
    m_debugger->setPauseOnNextCall(false, m_session->contextGroupId());
  }
  popBreakDetails();
}

Response V8DebuggerAgentImpl::pause() {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);

  if (m_debugger->isInInstrumentationPause()) {
    // If we are inside an instrumentation pause, remember the pause request
    // so that we can enter the requested pause once we are done
    // with the instrumentation.
    m_debugger->requestPauseAfterInstrumentation();
  } else if (isPaused()) {
    // Ignore the pause request if we are already paused.
    return Response::Success();
  } else if (m_debugger->canBreakProgram()) {
    m_debugger->interruptAndBreak(m_session->contextGroupId());
  } else {
    pushBreakDetails(protocol::Debugger::Paused::ReasonEnum::Other, nullptr);
    m_debugger->setPauseOnNextCall(true, m_session->contextGroupId());
  }

  return Response::Success();
}

Response V8DebuggerAgentImpl::resume(std::optional<bool> terminateOnResume) {
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  m_session->releaseObjectGroup(kBacktraceObjectGroup);

  m_instrumentationFinished = true;
  m_debugger->continueProgram(m_session->contextGroupId(),
                              terminateOnResume.value_or(false));
  return Response::Success();
}

Response V8DebuggerAgentImpl::stepOver(
    std::unique_ptr<protocol::Array<protocol::Debugger::LocationRange>>
        inSkipList) {
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);

  if (inSkipList) {
    const Response res = processSkipList(*inSkipList);
    if (res.IsError()) return res;
  } else {
    m_skipList.clear();
  }

  m_session->releaseObjectGroup(kBacktraceObjectGroup);
  m_debugger->stepOverStatement(m_session->contextGroupId());
  return Response::Success();
}

Response V8DebuggerAgentImpl::stepInto(
    std::optional<bool> inBreakOnAsyncCall,
    std::unique_ptr<protocol::Array<protocol::Debugger::LocationRange>>
        inSkipList) {
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);

  if (inSkipList) {
    const Response res = processSkipList(*inSkipList);
    if (res.IsError()) return res;
  } else {
    m_skipList.clear();
  }

  m_session->releaseObjectGroup(kBacktraceObjectGroup);
  m_debugger->stepIntoStatement(m_session->contextGroupId(),
                                inBreakOnAsyncCall.value_or(false));
  return Response::Success();
}

Response V8DebuggerAgentImpl::stepOut() {
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  m_session->releaseObjectGroup(kBacktraceObjectGroup);
  m_debugger->stepOutOfFunction(m_session->contextGroupId());
  return Response::Success();
}

Response V8DebuggerAgentImpl::pauseOnAsyncCall(
    std::unique_ptr<protocol::Runtime::StackTraceId> inParentStackTraceId) {
  // Deprecated, just return OK.
  return Response::Success();
}

Response V8DebuggerAgentImpl::setPauseOnExceptions(
    const String16& stringPauseState) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  v8::debug::ExceptionBreakState pauseState;
  if (stringPauseState == "none") {
    pauseState = v8::debug::NoBreakOnException;
  } else if (stringPauseState == "all") {
    pauseState = v8::debug::BreakOnAnyException;
  } else if (stringPauseState == "caught") {
    pauseState = v8::debug::BreakOnCaughtException;
  } else if (stringPauseState == "uncaught") {
    pauseState = v8::debug::BreakOnUncaughtException;
  } else {
    return Response::ServerError("Unknown pause on exceptions mode: " +
                                 stringPauseState.utf8());
  }
  setPauseOnExceptionsImpl(pauseState);
  return Response::Success();
}

void V8DebuggerAgentImpl::setPauseOnExceptionsImpl(int pauseState) {
  // TODO(dgozman): this changes the global state and forces all context groups
  // to pause. We should make this flag be per-context-group.
  m_debugger->setPauseOnExceptionsState(
      static_cast<v8::debug::ExceptionBreakState>(pauseState));
  m_state->setInteger(DebuggerAgentState::pauseOnExceptionsState, pauseState);
}

Response V8DebuggerAgentImpl::evaluateOnCallFrame(
    const String16& callFrameId, const String16& expression,
    std::optional<String16> objectGroup,
    std::optional<bool> includeCommandLineAPI, std::optional<bool> silent,
    std::optional<bool> returnByValue, std::optional<bool> generatePreview,
    std::optional<bool> throwOnSideEffect, std::optional<double> timeout,
    std::unique_ptr<RemoteObject>* result,
    std::unique_ptr<protocol::Runtime::ExceptionDetails>* exceptionDetails) {
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  InjectedScript::CallFrameScope scope(m_session, callFrameId);
  Response response = scope.initialize();
  if (!response.IsSuccess()) return response;
  if (includeCommandLineAPI.value_or(false)) scope.installCommandLineAPI();
  if (silent.value_or(false)) scope.ignoreExceptionsAndMuteConsole();

  int frameOrdinal = static_cast<int>(scope.frameOrdinal());
  auto it = v8::debug::StackTraceIterator::Create(m_isolate, frameOrdinal);
  if (it->Done()) {
    return Response::ServerError("Could not find call frame with given id");
  }

  v8::MaybeLocal<v8::Value> maybeResultValue;
  {
    V8InspectorImpl::EvaluateScope evaluateScope(scope);
    if (timeout.has_value()) {
      response = evaluateScope.setTimeout(timeout.value() / 1000.0);
      if (!response.IsSuccess()) return response;
    }
    maybeResultValue = it->Evaluate(toV8String(m_isolate, expression),
                                    throwOnSideEffect.value_or(false));
  }
  // Re-initialize after running client's code, as it could have destroyed
  // context or session.
  response = scope.initialize();
  if (!response.IsSuccess()) return response;
  WrapOptions wrapOptions = generatePreview.value_or(false)
                                ? WrapOptions({WrapMode::kPreview})
                                : WrapOptions({WrapMode::kIdOnly});
  if (returnByValue.value_or(false))
    wrapOptions = WrapOptions({WrapMode::kJson});
  return scope.injectedScript()->wrapEvaluateResult(
      maybeResultValue, scope.tryCatch(), objectGroup.value_or(""), wrapOptions,
      throwOnSideEffect.value_or(false), result, exceptionDetails);
}

Response V8DebuggerAgentImpl::setVariableValue(
    int scopeNumber, const String16& variableName,
    std::unique_ptr<protocol::Runtime::CallArgument> newValueArgument,
    const String16& callFrameId) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  InjectedScript::CallFrameScope scope(m_session, callFrameId);
  Response response = scope.initialize();
  if (!response.IsSuccess()) return response;
  v8::Local<v8::Value> newValue;
  response = scope.injectedScript()->resolveCallArgument(newValueArgument.get(),
                                                         &newValue);
  if (!response.IsSuccess()) return response;

  int frameOrdinal = static_cast<int>(scope.frameOrdinal());
  auto it = v8::debug::StackTraceIterator::Create(m_isolate, frameOrdinal);
  if (it->Done()) {
    return Response::ServerError("Could not find call frame with given id");
  }
  auto scopeIterator = it->GetScopeIterator();
  while (!scopeIterator->Done() && scopeNumber > 0) {
    --scopeNumber;
    scopeIterator->Advance();
  }
  if (scopeNumber != 0) {
    return Response::ServerError("Could not find scope with given number");
  }

  if (!scopeIterator->SetVariableValue(toV8String(m_isolate, variableName),
                                       newValue) ||
      scope.tryCatch().HasCaught()) {
    return Response::InternalError();
  }
  return Response::Success();
}

Response V8DebuggerAgentImpl::setReturnValue(
    std::unique_ptr<protocol::Runtime::CallArgument> protocolNewValue) {
  if (!enabled()) return Response::ServerError(kDebuggerNotEnabled);
  if (!isPaused()) return Response::ServerError(kDebuggerNotPaused);
  v8::HandleScope handleScope(m_isolate);
  auto iterator = v8::debug::StackTraceIterator::Create(m_isolate);
  if (iterator->Done()) {
    return Response::ServerError("Could not find top call frame");
  }
  if (iterator->GetReturnValue().IsEmpty()) {
    return Response::ServerError(
        "Could not update return value at non-return position");
  }
  InjectedScript::ContextScope scope(m_session, iterator->GetContextId());
  Response response = scope.initialize();
  if (!response.IsSuccess()) return response;
  v8::Local<v8::Value> newValue;
  response = scope.injectedScript()->resolveCallArgument(protocolNewValue.get(),
                                                         &newValue);
  if (!response.IsSuccess()) return response;
  v8::debug::SetReturnValue(m_isolate, newValue);
  return Response::Success();
}

Response V8DebuggerAgentImpl::setAsyncCallStackDepth(int depth) {
  if (!enabled() && !m_session->runtimeAgent()->enabled()) {
    return Response::ServerError(kDebuggerNotEnabled);
  }
  m_state->setInteger(DebuggerAgentState::asyncCallStackDepth, depth);
  m_debugger->setAsyncCallStackDepth(this, depth);
  return Response::Success();
}

Response V8DebuggerAgentImpl::setBlackboxPatterns(
    std::unique_ptr<protocol::Array<String16>> patterns,
    std::optional<bool> skipAnonymous) {
  m_skipAnonymousScripts = skipAnonymous.value_or(false);
  m_state->setBoolean(DebuggerAgentState::skipAnonymousScripts,
                      m_skipAnonymousScripts);
  if (patterns->empty()) {
    m_blackboxPattern = nullptr;
    resetBlackboxedStateCache();
    m_state->remove(DebuggerAgentState::blackboxPattern);
    return Response::Success();
  }

  String16Builder patternBuilder;
  patternBuilder.append('(');
  for (size_t i = 0; i < patterns->size() - 1; ++i) {
    patternBuilder.append((*patterns)[i]);
    patternBuilder.append("|");
  }
  patternBuilder.append(patterns->back());
  patternBuilder.append(')');
  String16 pattern = patternBuilder.toString();
  Response response = setBlackboxPattern(pattern);
  if (!response.IsSuccess()) return response;
  resetBlackboxedStateCache();
  m_state->setString(DebuggerAgentState::blackboxPattern, pattern);
  return Response::Success();
}

Response V8DebuggerAgentImpl::setBlackboxExecutionContexts(
    std::unique_ptr<protocol::Array<String16>> uniqueIds) {
  m_blackboxedExecutionContexts.clear();
  for (const String16& uniqueId : *uniqueIds) {
    m_blackboxedExecutionContexts.insert(uniqueId);
  }
  return Response::Success();
}

Response V8DebuggerAgentImpl::setBlackboxPattern(const String16& pattern) {
  std::unique_ptr<V8Regex> regex(new V8Regex(
      m_inspector, pattern, true /** caseSensitive */, false /** multiline */));
  if (!regex->isValid())
    return Response::ServerError("Pattern parser error: " +
                                 regex->errorMessage().utf8());
  m_blackboxPattern = std::move(regex);
  return Response::Success();
}

void V8DebuggerAgentImpl::resetBlackboxedStateCache() {
  for (const auto& it : m_scripts) {
    it.second->resetBlackboxedStateCache();
  }
}

Response V8DebuggerAgentImpl::setBlackboxedRanges(
    const String16& scriptId,
    std::unique_ptr<protocol::Array<protocol::Debugger::ScriptPosition>>
        inPositions) {
  auto it = m_scripts.find(scriptId);
  if (it == m_scripts.end())
    return Response::ServerError("No script with passed id.");

  if (inPositions->empty()) {
    m_blackboxedPositions.erase(scriptId);
    it->second->resetBlackboxedStateCache();
    return Response::Success();
  }

  std::vector<std::pair<int, int>> positions;
  positions.reserve(inPositions->size());
  for (const std::unique_ptr<protocol::Debugger::ScriptPosition>& position :
       *inPositions) {
    Response res = isValidPosition(position.get());
    if (res.IsError()) return res;

    positions.push_back(
        std::make_pair(position->getLineNumber(), position->getColumnNumber()));
  }
  Response res = isValidRangeOfPositions(positions);
  if (res.IsError()) return res;

  m_blackboxedPositions[scriptId] = positions;
  it->second->resetBlackboxedStateCache();
  return Response::Success();
}

Response V8DebuggerAgentImpl::currentCallFrames(
    std::unique_ptr<Array<CallFrame>>* result) {
  if (!isPaused()) {
    *result = std::make_unique<Array<CallFrame>>();
    return Response::Success();
  }
  v8::HandleScope handles(m_isolate);
  *result = std::make_unique<Array<CallFrame>>();
  auto iterator = v8::debug::StackTraceIterator::Create(m_isolate);
  int frameOrdinal = 0;
  for (; !iterator->Done(); iterator->Advance(), frameOrdinal++) {
    int contextId = iterator->GetContextId();
    InjectedScript* injectedScript = nullptr;
    if (contextId) m_session->findInjectedScript(contextId, injectedScript);
    String16 callFrameId = RemoteCallFrameId::serialize(
        m_inspector->isolateId(), contextId, frameOrdinal);

    v8::debug::Location loc = iterator->GetSourceLocation();

    std::unique_ptr<Array<Scope>> scopes;
    auto scopeIterator = iterator->GetScopeIterator();
    Response res =
        buildScopes(m_isolate, scopeIterator.get(), injectedScript, &scopes);
    if (!res.IsSuccess()) return res;

    std::unique_ptr<RemoteObject> protocolReceiver;
    if (injectedScript) {
      v8::Local<v8::Value> receiver;
      if (iterator->GetReceiver().ToLocal(&receiver)) {
        res = injectedScript->wrapObject(receiver, kBacktraceObjectGroup,
                                         WrapOptions({WrapMode::kIdOnly}),
                                         &protocolReceiver);
        if (!res.IsSuccess()) return res;
      }
    }
    if (!protocolReceiver) {
      protocolReceiver = RemoteObject::create()
                             .setType(RemoteObject::TypeEnum::Undefined)
                             .build();
    }

    v8::Local<v8::debug::Script> script = iterator->GetScript();
    DCHECK(!script.IsEmpty());
    std::unique_ptr<protocol::Debugger::Location> location =
        protocol::Debugger::Location::create()
            .setScriptId(String16::fromInteger(script->Id()))
            .setLineNumber(loc.GetLineNumber())
            .setColumnNumber(loc.GetColumnNumber())
            .build();

    auto frame = CallFrame::create()
                     .setCallFrameId(callFrameId)
                     .setFunctionName(toProtocolString(
                         m_isolate, iterator->GetFunctionDebugName()))
                     .setLocation(std::move(location))
                     .setUrl(String16())
                     .setScopeChain(std::move(scopes))
                     .setThis(std::move(protocolReceiver))
                     .setCanBeRestarted(iterator->CanBeRestarted())
                     .build();

    v8::debug::Location func_loc = iterator->GetFunctionLocation();
    if (!func_loc.IsEmpty()) {
      frame->setFunctionLocation(
          protocol::Debugger::Location::create()
              .setScriptId(String16::fromInteger(script->Id()))
              .setLineNumber(func_loc.GetLineNumber())
              .setColumnNumber(func_loc.GetColumnNumber())
              .build());
    }

    v8::Local<v8::Value> returnValue = iterator->GetReturnValue();
    if (!returnValue.IsEmpty() && injectedScript) {
      std::unique_ptr<RemoteObject> value;
      res =
          injectedScript->wrapObject(returnValue, kBacktraceObjectGroup,
                                     WrapOptions({WrapMode::kIdOnly}), &value);
      if (!res.IsSuccess()) return res;
      frame->setReturnValue(std::move(value));
    }
    (*result)->emplace_back(std::move(frame));
  }
  return Response::Success();
}

std::unique_ptr<protocol::Runtime::StackTrace>
V8DebuggerAgentImpl::currentAsyncStackTrace() {
  std::shared_ptr<AsyncStackTrace> asyncParent =
      m_debugger->currentAsyncParent();
  if (!asyncParent) return nullptr;
  return asyncParent->buildInspectorObject(
      m_debugger, m_debugger->maxAsyncCallChainDepth() - 1);
}

std::unique_ptr<protocol::Runtime::StackTraceId>
V8DebuggerAgentImpl::currentExternalStackTrace() {
  V8StackTraceId externalParent = m_debugger->currentExternalParent();
  if (externalParent.IsInvalid()) return nullptr;
  return protocol::Runtime::StackTraceId::create()
      .setId(stackTraceIdToString(externalParent.id))
      .setDebuggerId(
          internal::V8DebuggerId(externalParent.debugger_id).toString())
      .build();
}

bool V8DebuggerAgentImpl::isPaused() const {
  return m_debugger->isPausedInContextGroup(m_session->contextGroupId());
}

static String16 getScriptLanguage(const V8DebuggerScript& script) {
  switch (script.getLanguage()) {
    case V8DebuggerScript::Language::WebAssembly:
      return protocol::Debugger::ScriptLanguageEnum::WebAssembly;
    case V8DebuggerScript::Language::JavaScript:
      return protocol::Debugger::ScriptLanguageEnum::JavaScript;
  }
}

#if V8_ENABLE_WEBASSEMBLY
static const char* getDebugSymbolTypeName(
    v8::debug::WasmScript::DebugSymbols::Type type) {
  switch (type) {
    case v8::debug::WasmScript::DebugSymbols::Type::SourceMap:
      return v8_inspector::protocol::Debugger::DebugSymbols::TypeEnum::
          SourceMap;
    case v8::debug::WasmScript::DebugSymbols::Type::EmbeddedDWARF:
      return v8_inspector::protocol::Debugger::DebugSymbols::TypeEnum::
          EmbeddedDWARF;
    case v8::debug::WasmScript::DebugSymbols::Type::ExternalDWARF:
      return v8_inspector::protocol::Debugger::DebugSymbols::TypeEnum::
          ExternalDWARF;
  }
}

static void getDebugSymbols(
    const V8DebuggerScript& script,
    std::unique_ptr<Array<protocol::Debugger::DebugSymbols>>* debug_symbols) {
  std::vector<v8::debug::WasmScript::DebugSymbols> v8_script_debug_symbols =
      script.getDebugSymbols();

  *debug_symbols = std::make_unique<Array<protocol::Debugger::DebugSymbols>>();
  for (size_t i = 0; i < v8_script_debug_symbols.size(); ++i) {
    v8::debug::WasmScript::DebugSymbols& symbol = v8_script_debug_symbols[i];
    std::unique_ptr<protocol::Debugger::DebugSymbols> protocolDebugSymbol =
        v8_inspector::protocol::Debugger::DebugSymbols::create()
            .setType(getDebugSymbolTypeName(symbol.type))
            .build();
    if (symbol.external_url.size() > 0) {
      protocolDebugSymbol->setExternalURL(
          String16(symbol.external_url.data(), symbol.external_url.size()));
    }
    (*debug_symbols)->emplace_back(std::move(protocolDebugSymbol));
  }
}
#endif  // V8_ENABLE_WEBASSEMBLY

void V8DebuggerAgentImpl::didParseSource(
    std::unique_ptr<V8DebuggerScript> script, bool success) {
  v8::HandleScope handles(m_isolate);
  if (!success) {
    String16 scriptSource = script->source(0);
    script->setSourceURL(findSourceURL(scriptSource, false));
    script->setSourceMappingURL(findSourceMapURL(scriptSource, false));
  }

  int contextId = script->executionContextId();
  int contextGroupId = m_inspector->contextGroupId(contextId);
  InspectedContext* inspected =
      m_inspector->getContext(contextGroupId, contextId);
  std::unique_ptr<protocol::DictionaryValue> executionContextAuxData;
  if (inspected) {
    // Script reused between different groups/sessions can have a stale
    // execution context id.
    const String16& aux = inspected->auxData();
    std::vector<uint8_t> cbor;
    v8_crdtp::json::ConvertJSONToCBOR(
        v8_crdtp::span<uint16_t>(aux.characters16(), aux.length()), &cbor);
    executionContextAuxData = protocol::DictionaryValue::cast(
        protocol::Value::parseBinary(cbor.data(), cbor.size()));
  }
  bool isLiveEdit = script->isLiveEdit();
  bool hasSourceURLComment = script->hasSourceURLComment();
  bool isModule = script->isModule();
  String16 scriptId = script->scriptId();
  String16 scriptURL = script->sourceURL();
  String16 embedderName = script->embedderName();
  String16 scriptLanguage = getScriptLanguage(*script);
  std::optional<int> codeOffset;
  std::unique_ptr<Array<protocol::Debugger::DebugSymbols>> debugSymbols;
#if V8_ENABLE_WEBASSEMBLY
  if (script->getLanguage() == V8DebuggerScript::Language::WebAssembly) {
    codeOffset = script->codeOffset();
    getDebugSymbols(*script, &debugSymbols);
  }
#endif  // V8_ENABLE_WEBASSEMBLY

  m_scripts[scriptId] = std::move(script);
  // Release the strong reference to get notified when debugger is the only
  // one that holds the script. Has to be done after script added to m_scripts.
  m_scripts[scriptId]->MakeWeak();

  ScriptsMap::iterator scriptIterator = m_scripts.find(scriptId);
  DCHECK(scriptIterator != m_scripts.end());
  V8DebuggerScript* scriptRef = scriptIterator->second.get();
  // V8 could create functions for parsed scripts before reporting and asks
  // inspector about blackboxed state, we should reset state each time when we
  // make any change that change isFunctionBlackboxed output - adding parsed
  // script is changing.
  scriptRef->resetBlackboxedStateCache();

  std::optional<String16> sourceMapURLParam = scriptRef->sourceMappingURL();
  const bool* isLiveEditParam = isLiveEdit ? &isLiveEdit : nullptr;
  const bool* hasSourceURLParam =
      hasSourceURLComment ? &hasSourceURLComment : nullptr;
  const bool* isModuleParam = isModule ? &isModule : nullptr;
  std::unique_ptr<V8StackTraceImpl> stack =
      V8StackTraceImpl::capture(m_inspector->debugger(), 1);
  std::unique_ptr<protocol::Runtime::StackTrace> stackTrace =
      stack && !stack->isEmpty()
          ? stack->buildInspectorObjectImpl(m_debugger, 0)
          : nullptr;

  if (!success) {
    m_frontend.scriptFailedToParse(
        scriptId, scriptURL, scriptRef->startLine(), scriptRef->startColumn(),
        scriptRef->endLine(), scriptRef->endColumn(), contextId,
        scriptRef->hash(), scriptRef->buildId(),
        std::move(executionContextAuxData), std::move(sourceMapURLParam),
        hasSourceURLParam, isModuleParam, scriptRef->length(),
        std::move(stackTrace), std::move(codeOffset), std::move(scriptLanguage),
        embedderName);
    return;
  }

  m_frontend.scriptParsed(
      scriptId, scriptURL, scriptRef->startLine(), scriptRef->startColumn(),
      scriptRef->endLine(), scriptRef->endColumn(), contextId,
      scriptRef->hash(), scriptRef->buildId(),
      std::move(executionContextAuxData), isLiveEditParam,
      std::move(sourceMapURLParam), hasSourceURLParam, isModuleParam,
      scriptRef->length(), std::move(stackTrace), std::move(codeOffset),
      std::move(scriptLanguage), std::move(debugSymbols), embedderName);

  std::vector<protocol::DictionaryValue*> potentialBreakpoints;
  if (!scriptURL.isEmpty()) {
    protocol::DictionaryValue* breakpointsByUrl =
        m_state->getObject(DebuggerAgentState::breakpointsByUrl);
    if (breakpointsByUrl) {
      potentialBreakpoints.push_back(breakpointsByUrl->getObject(scriptURL));
    }
    potentialBreakpoints.push_back(
        m_state->getObject(DebuggerAgentState::breakpointsByRegex));
  }
  protocol::DictionaryValue* breakpointsByScriptHash =
      m_state->getObject(DebuggerAgentState::breakpointsByScriptHash);
  if (breakpointsByScriptHash) {
    potentialBreakpoints.push_back(
        breakpointsByScriptHash->getObject(scriptRef->hash()));
  }
  protocol::DictionaryValue* breakpointHints =
      m_state->getObject(DebuggerAgentState::breakpointHints);
  for (auto breakpoints : potentialBreakpoints) {
    if (!breakpoints) continue;
    for (size_t i = 0; i < breakpoints->size(); ++i) {
      auto breakpointWithCondition = breakpoints->at(i);
      String16 breakpointId = breakpointWithCondition.first;

      BreakpointType type;
      String16 selector;
      int lineNumber = 0;
      int columnNumber = 0;
      parseBreakpointId(breakpointId, &type, &selector, &lineNumber,
                        &columnNumber);
      Matcher matcher(m_inspector, type, selector);

      if (!matcher.matches(*scriptRef)) continue;
      // Make sure the session was not disabled by some re-entrant call
      // in the script matcher.
      DCHECK(enabled());
      String16 condition;
      breakpointWithCondition.second->asString(&condition);
      protocol::DictionaryValue* hint =
          breakpointHints ? breakpointHints->getObject(breakpointId) : nullptr;
      if (hint) {
        adjustBreakpointLocation(*scriptRef, hint, &lineNumber, &columnNumber);
      }
      std::unique_ptr<protocol::Debugger::Location> location =
          setBreakpointImpl(breakpointId, scriptId, condition, lineNumber,
                            columnNumber);
      if (location)
        m_frontend.breakpointResolved(breakpointId, std::move(location));
    }
  }
  setScriptInstrumentationBreakpointIfNeeded(scriptRef);
}

void V8DebuggerAgentImpl::setScriptInstrumentationBreakpointIfNeeded(
    V8DebuggerScript* scriptRef) {
  protocol::DictionaryValue* breakpoints =
      m_state->getObject(DebuggerAgentState::instrumentationBreakpoints);
  if (!breakpoints) return;
  bool isBlackboxed = isFunctionBlackboxed(
      scriptRef->scriptId(), v8::debug::Location(0, 0),
      v8::debug::Location(scriptRef->endLine(), scriptRef->endColumn()));
  if (isBlackboxed) return;

  String16 sourceMapURL = scriptRef->sourceMappingURL();
  String16 breakpointId = generateInstrumentationBreakpointId(
      InstrumentationEnum::BeforeScriptExecution);
  if (!breakpoints->get(breakpointId)) {
    if (sourceMapURL.isEmpty()) return;
    breakpointId = generateInstrumentationBreakpointId(
        InstrumentationEnum::BeforeScriptWithSourceMapExecution);
    if (!breakpoints->get(breakpointId)) return;
  }
  v8::debug::BreakpointId debuggerBreakpointId;
  if (!scriptRef->setInstrumentationBreakpoint(&debuggerBreakpointId)) return;

  m_debuggerBreakpointIdToBreakpointId[debuggerBreakpointId] = breakpointId;
  m_breakpointIdToDebuggerBreakpointIds[breakpointId].push_back(
      debuggerBreakpointId);
}

void V8DebuggerAgentImpl::didPauseOnInstrumentation(
    v8::debug::BreakpointId instrumentationId) {
  String16 breakReason = protocol::Debugger::Paused::ReasonEnum::Other;
  std::unique_ptr<protocol::DictionaryValue> breakAuxData;

  std::unique_ptr<Array<CallFrame>> protocolCallFrames;
  Response response = currentCallFrames(&protocolCallFrames);
  if (!response.IsSuccess())
    protocolCallFrames = std::make_unique<Array<CallFrame>>();

  if (m_debuggerBreakpointIdToBreakpointId.find(instrumentationId) !=
      m_debuggerBreakpointIdToBreakpointId.end()) {
    DCHECK_GT(protocolCallFrames->size(), 0);
    if (!protocolCallFrames->empty()) {
      m_instrumentationFinished = false;
      breakReason = protocol::Debugger::Paused::ReasonEnum::Instrumentation;
      const String16 scriptId =
          protocolCallFrames->at(0)->getLocation()->getScriptId();
      DCHECK_NE(m_scripts.find(scriptId), m_scripts.end());
      const auto& script = m_scripts[scriptId];

      breakAuxData = protocol::DictionaryValue::create();
      breakAuxData->setString("scriptId", script->scriptId());
      breakAuxData->setString("url", script->sourceURL());
      if (!script->sourceMappingURL().isEmpty()) {
        breakAuxData->setString("sourceMapURL", (script->sourceMappingURL()));
      }
    }
  }

  m_frontend.paused(std::move(protocolCallFrames), breakReason,
                    std::move(breakAuxData),
                    std::make_unique<Array<String16>>(),
                    currentAsyncStackTrace(), currentExternalStackTrace());
}

void V8DebuggerAgentImpl::didPause(
    int contextId, v8::Local<v8::Value> exception,
    const std::vector<v8::debug::BreakpointId>& hitBreakpoints,
    v8::debug::ExceptionType exceptionType, bool isUncaught,
    v8::debug::BreakReasons breakReasons) {
  v8::HandleScope handles(m_isolate);

  std::vector<BreakReason> hitReasons;

  if (breakReasons.contains(v8::debug::BreakReason::kOOM)) {
    hitReasons.push_back(
        std::make_pair(protocol::Debugger::Paused::ReasonEnum::OOM, nullptr));
  } else if (breakReasons.contains(v8::debug::BreakReason::kAssert)) {
    hitReasons.push_back(std::make_pair(
        protocol::Debugger::Paused::ReasonEnum::Assert, nullptr));
  } else if (breakReasons.contains(v8::debug::BreakReason::kException)) {
    InjectedScript* injectedScript = nullptr;
    m_session->findInjectedScript(contextId, injectedScript);
    if (injectedScript) {
      String16 breakReason =
          exceptionType == v8::debug::kPromiseRejection
              ? protocol::Debugger::Paused::ReasonEnum::PromiseRejection
              : protocol::Debugger::Paused::ReasonEnum::Exception;
      std::unique_ptr<protocol::Runtime::RemoteObject> obj;
      injectedScript->wrapObject(exception, kBacktraceObjectGroup,
                                 WrapOptions({WrapMode::kIdOnly}), &obj);
      std::unique_ptr<protocol::DictionaryValue> breakAuxData;
      if (obj) {
        std::vector<uint8_t> serialized;
        obj->AppendSerialized(&serialized);
        breakAuxData = protocol::DictionaryValue::cast(
            protocol::Value::parseBinary(serialized.data(), serialized.size()));
        breakAuxData->setBoolean("uncaught", isUncaught);
      }
      hitReasons.push_back(
          std::make_pair(breakReason, std::move(breakAuxData)));
    }
  }

  if (breakReasons.contains(v8::debug::BreakReason::kStep) ||
      breakReasons.contains(v8::debug::BreakReason::kAsyncStep)) {
    hitReasons.push_back(
        std::make_pair(protocol::Debugger::Paused::ReasonEnum::Step, nullptr));
  }

  auto hitBreakpointIds = std::make_unique<Array<String16>>();
  bool hitRegularBreakpoint = false;
  for (const auto& id : hitBreakpoints) {
    auto breakpointIterator = m_debuggerBreakpointIdToBreakpointId.find(id);
    if (breakpointIterator == m_debuggerBreakpointIdToBreakpointId.end()) {
      continue;
    }
    const String16& breakpointId = breakpointIterator->second;
    hitBreakpointIds->emplace_back(breakpointId);
    BreakpointType type;
    parseBreakpointId(breakpointId, &type);
    if (type == BreakpointType::kDebugCommand) {
      hitReasons.push_back(std::make_pair(
          protocol::Debugger::Paused::ReasonEnum::DebugCommand, nullptr));
    } else {
      hitRegularBreakpoint = true;
    }
  }

  for (size_t i = 0; i < m_breakReason.size(); ++i) {
    hitReasons.push_back(std::move(m_breakReason[i]));
  }
  clearBreakDetails();

  // Make sure that we only include (other: nullptr) once.
  const BreakReason otherHitReason =
      std::make_pair(protocol::Debugger::Paused::ReasonEnum::Other, nullptr);
  const bool otherBreakReasons =
      hitRegularBreakpoint || hitBreakReasonEncodedAsOther(breakReasons);
  if (otherBreakReasons && std::find(hitReasons.begin(), hitReasons.end(),
                                     otherHitReason) == hitReasons.end()) {
    hitReasons.push_back(
        std::make_pair(protocol::Debugger::Paused::ReasonEnum::Other, nullptr));
  }

  // We should always know why we pause: either the pause relates to this agent
  // (`hitReason` is non empty), or it relates to another agent (hit a
  // breakpoint there, or a triggered pause was scheduled by other agent).
  DCHECK(hitReasons.size() > 0 || !hitBreakpoints.empty() ||
         breakReasons.contains(v8::debug::BreakReason::kAgent));
  String16 breakReason = protocol::Debugger::Paused::ReasonEnum::Other;
  std::unique_ptr<protocol::DictionaryValue> breakAuxData;
  if (hitReasons.size() == 1) {
    breakReason = hitReasons[0].first;
    breakAuxData = std::move(hitReasons[0].second);
  } else if (hitReasons.size() > 1) {
    breakReason = protocol::Debugger::Paused::ReasonEnum::Ambiguous;
    std::unique_ptr<protocol::ListValue> reasons =
        protocol::ListValue::create();
    for (size_t i = 0; i < hitReasons.size(); ++i) {
      std::unique_ptr<protocol::DictionaryValue> reason =
          protocol::DictionaryValue::create();
      reason->setString("reason", hitReasons[i].first);
      if (hitReasons[i].second)
        reason->setObject("auxData", std::move(hitReasons[i].second));
      reasons->pushValue(std::move(reason));
    }
    breakAuxData = protocol::DictionaryValue::create();
    breakAuxData->setArray("reasons", std::move(reasons));
  }

  std::unique_ptr<Array<CallFrame>> protocolCallFrames;
  Response response = currentCallFrames(&protocolCallFrames);
  if (!response.IsSuccess())
    protocolCallFrames = std::make_unique<Array<CallFrame>>();

  v8::debug::NotifyDebuggerPausedEventSent(m_debugger->isolate());
  m_frontend.paused(std::move(protocolCallFrames), breakReason,
                    std::move(breakAuxData), std::move(hitBreakpointIds),
                    currentAsyncStackTrace(), currentExternalStackTrace());
}

void V8DebuggerAgentImpl::didContinue() {
  m_frontend.resumed();
  m_frontend.flush();
}

void V8DebuggerAgentImpl::breakProgram(
    const String16& breakReason,
    std::unique_ptr<protocol::DictionaryValue> data) {
  if (!enabled() || m_skipAllPauses || !m_debugger->canBreakProgram()) return;
  std::vector<BreakReason> currentScheduledReason;
  currentScheduledReason.swap(m_breakReason);
  pushBreakDetails(breakReason, std::move(data));

  int contextGroupId = m_session->contextGroupId();
  int sessionId = m_session->sessionId();
  V8InspectorImpl* inspector = m_inspector;
  m_debugger->breakProgram(contextGroupId);
  // Check that session and |this| are still around.
  if (!inspector->sessionById(contextGroupId, sessionId)) return;
  if (!enabled()) return;

  popBreakDetails();
  m_breakReason.swap(currentScheduledReason);
  if (!m_breakReason.empty()) {
    m_debugger->setPauseOnNextCall(true, m_session->contextGroupId());
  }
}

void V8DebuggerAgentImpl::setBreakpointFor(v8::Local<v8::Function> function,
                                           v8::Local<v8::String> condition,
                                           BreakpointSource source) {
  String16 breakpointId = generateBreakpointId(
      source == DebugCommandBreakpointSource ? BreakpointType::kDebugCommand
                                             : BreakpointType::kMonitorCommand,
      function);
  if (m_breakpointIdToDebuggerBreakpointIds.find(breakpointId) !=
      m_breakpointIdToDebuggerBreakpointIds.end()) {
    return;
  }
  setBreakpointImpl(breakpointId, function, condition);
}

void V8DebuggerAgentImpl::removeBreakpointFor(v8::Local<v8::Function> function,
                                              BreakpointSource source) {
  String16 breakpointId = generateBreakpointId(
      source == DebugCommandBreakpointSource ? BreakpointType::kDebugCommand
                                             : BreakpointType::kMonitorCommand,
      function);
  std::vector<V8DebuggerScript*> scripts;
  removeBreakpointImpl(breakpointId, scripts);
}

void V8DebuggerAgentImpl::reset() {
  if (!enabled()) return;
  m_blackboxedPositions.clear();
  resetBlackboxedStateCache();
  m_skipList.clear();
  m_scripts.clear();
  m_cachedScripts.clear();
  m_cachedScriptSize = 0;
  m_debugger->allAsyncTasksCanceled();
  m_blackboxedExecutionContexts.clear();
}

void V8DebuggerAgentImpl::ScriptCollected(const V8DebuggerScript* script) {
  DCHECK_NE(m_scripts.find(script->scriptId()), m_scripts.end());
  std::vector<uint8_t> bytecode;
#if V8_ENABLE_WEBASSEMBLY
  v8::MemorySpan<const uint8_t> span;
  if (script->wasmBytecode().To(&span)) {
    bytecode.reserve(span.size());
    bytecode.insert(bytecode.begin(), span.data(), span.data() + span.size());
  }
#endif
  CachedScript cachedScript{script->scriptId(), script->source(0),
                            std::move(bytecode)};
  m_cachedScriptSize += cachedScript.size();
  m_cachedScripts.push_back(std::move(cachedScript));
  m_scripts.erase(script->scriptId());

  while (m_cachedScriptSize > m_maxScriptCacheSize) {
    const CachedScript& cachedScript = m_cachedScripts.front();
    DCHECK_GE(m_cachedScriptSize, cachedScript.size());
    m_cachedScriptSize -= cachedScript.size();
    m_cachedScripts.pop_front();
  }
}

Response V8DebuggerAgentImpl::processSkipList(
    protocol::Array<protocol::Debugger::LocationRange>& skipList) {
  std::unordered_map<String16, std::vector<std::pair<int, int>>> skipListInit;
  for (std::unique_ptr<protocol::Debugger::LocationRange>& range : skipList) {
    protocol::Debugger::ScriptPosition* start = range->getStart();
    protocol::Debugger::ScriptPosition* end = range->getEnd();
    String16 scriptId = range->getScriptId();

    auto it = m_scripts.find(scriptId);
    if (it == m_scripts.end())
      return Response::ServerError("No script with passed id.");

    Response res = isValidPosition(start);
    if (res.IsError()) return res;

    res = isValidPosition(end);
    if (res.IsError()) return res;

    skipListInit[scriptId].emplace_back(start->getLineNumber(),
                                        start->getColumnNumber());
    skipListInit[scriptId].emplace_back(end->getLineNumber(),
                                        end->getColumnNumber());
  }

  // Verify that the skipList is sorted, and that all ranges
  // are properly defined (start comes before end).
  for (auto skipListPair : skipListInit) {
    Response res = isValidRangeOfPositions(skipListPair.second);
    if (res.IsError()) return res;
  }

  m_skipList = std::move(skipListInit);
  return Response::Success();
}

void V8DebuggerAgentImpl::stop() {
  disable();
  m_enableState = kStopping;
}
}  // namespace v8_inspector
