CTION_NAME_PATTERN = /^(?:function\*? )?([^(\s]+)\(/;
function extractFunctionName(description) {
  const fnNameMatch =
    RegExpPrototypeSymbolMatch(FUNCTION_NAME_PATTERN, description);
  return fnNameMatch ? `: ${fnNameMatch[1]}` : '';
}

const {
  moduleIds: PUBLIC_BUILTINS,
} = internalBinding('native_module');
const NATIVES = internalBinding('natives');
function isNativeUrl(url) {
  url = RegExpPrototypeSymbolReplace(/\.js$/, url, '');

  return StringPrototypeStartsWith(url, 'node:internal/') ||
         ArrayPrototypeIncludes(PUBLIC_BUILTINS, url) ||
         url in NATIVES || url === 'bootstrap_node';
}

function getRelativePath(filenameOrURL) {
  const dir = StringPrototypeSlice(Path.join(Path.resolve(), 'x'), 0, -1);

  const filename = StringPrototypeStartsWith(filenameOrURL, 'file://') ?
    fileURLToPath(filenameOrURL) : filenameOrURL;

  // Change path to relative, if possible
  if (StringPrototypeStartsWith(filename, dir)) {
    return StringPrototypeSlice(filename, dir.length);
  }
  return filename;
}

// Adds spaces and prefix to number
// maxN is a maximum number we should have space for
function leftPad(n, prefix, maxN) {
  const s = n.toString();
  const nchars = MathMax(2, String(maxN).length);
  const nspaces = nchars - s.length;

  return prefix + StringPrototypeRepeat(' ', nspaces) + s;
}

function markSourceColumn(sourceText, position, useColors) {
  if (!sourceText) return '';

  const head = StringPrototypeSlice(sourceText, 0, position);
  let tail = StringPrototypeSlice(sourceText, position);

  // Colourize char if stdout supports colours
  if (useColors) {
    tail = RegExpPrototypeSymbolReplace(/(.+?)([^\w]|$)/, tail,
                                        '\u001b[32m$1\u001b[39m$2');
  }

  // Return source line with coloured char at `position`
  return head + tail;
}

function extractErrorMessage(stack) {
  if (!stack) return '<unknown>';
  const m = RegExpPrototypeSymbolMatch(/^\w+: ([^\n]+)/, stack);
  return m?.[1] ?? stack;
}

function convertResultToError(result) {
  const { className, description } = result;
  const err = new ERR_DEBUGGER_ERROR(extractErrorMessage(description));
  err.stack = description;
  ObjectDefineProperty(err, 'name', { value: className });
  return err;
}

class RemoteObject {
  constructor(attributes) {
    ObjectAssign(this, attributes);
    if (this.type === 'number') {
      this.value =
        this.unserializableValue ? +this.unserializableValue : +this.value;
    }
  }

  [customInspectSymbol](depth, opts) {
    function formatProperty(prop) {
      switch (prop.type) {
        case 'string':
        case 'undefined':
          return utilInspect(prop.value, opts);

        case 'number':
        case 'boolean':
          return opts.stylize(prop.value, prop.type);

        case 'object':
        case 'symbol':
          if (prop.subtype === 'date') {
            return utilInspect(new Date(prop.value), opts);
          }
          if (prop.subtype === 'array') {
            return opts.stylize(prop.value, 'special');
          }
          return opts.stylize(prop.value, prop.subtype || 'special');

        default:
          return prop.value;
      }
    }
    switch (this.type) {
      case 'boolean':
      case 'number':
      case 'string':
      case 'undefined':
        return utilInspect(this.value, opts);

      case 'symbol':
        return opts.stylize(this.description, 'special');

      case 'function': {
        const fnName = extractFunctionName(this.description);
        const formatted = `[${this.className}${fnName}]`;
        return opts.stylize(formatted, 'special');
      }

      case 'object':
        switch (this.subtype) {
          case 'date':
            return utilInspect(new Date(this.description), opts);

          case 'null':
            return utilInspect(null, opts);

          case 'regexp':
            return opts.stylize(this.description, 'regexp');

          default:
            break;
        }
        if (this.preview) {
          const props = ArrayPrototypeMap(
            this.preview.properties,
            (prop, idx) => {
              const value = formatProperty(prop);
              if (prop.name === `${idx}`) return value;
              return `${prop.name}: ${value}`;
            });
          if (this.preview.overflow) {
            ArrayPrototypePush(props, '...');
          }
          const singleLine = ArrayPrototypeJoin(props, ', ');
          const propString =
            singleLine.length > 60 ?
              ArrayPrototypeJoin(props, ',\n  ') :
              singleLine;

          return this.subtype === 'array' ?
            `[ ${propString} ]` : `{ ${propString} }`;
        }
        return this.description;

      default:
        return this.description;
    }
  }

  static fromEvalResult({ result, wasThrown }) {
    if (wasThrown) return convertResultToError(result);
    return new RemoteObject(result);
  }
}

class ScopeSnapshot {
  constructor(scope, properties) {
    ObjectAssign(this, scope);
    this.properties = new SafeMap();
    this.completionGroup = ArrayPrototypeMap(properties, (prop) => {
      const value = new RemoteObject(prop.value);
      this.properties.set(prop.name, value);
      return prop.name;
    });
  }

  [customInspectSymbol](depth, opts) {
    const type = StringPrototypeToUpperCase(this.type[0]) +
                 StringPrototypeSlice(this.type, 1);
    const name = this.name ? `<${this.name}>` : '';
    const prefix = `${type}${name} `;
    return RegExpPrototypeSymbolReplace(/^Map /,
                                        utilInspect(this.properties, opts),
                                        prefix);
  }
}

function copyOwnProperties(target, source) {
  ArrayPrototypeForEach(
    ReflectOwnKeys(source),
    (prop) => {
      const desc = ReflectGetOwnPropertyDescriptor(source, prop);
      ObjectDefineProperty(target, prop, desc);
    });
}

function aliasProperties(target, mapping) {
  ArrayPrototypeForEach(ObjectKeys(mapping), (key) => {
    const desc = ReflectGetOwnPropertyDescriptor(target, key);
    ObjectDefineProperty(target, mapping[key], desc);
  });
}

function createRepl(inspector) {
  const { Debugger, HeapProfiler, Profiler, Runtime } = inspector;

  let repl;

  // Things we want to keep around
  const history = { control: [], debug: [] };
  const watchedExpressions = [];
  const knownBreakpoints = [];
  let heapSnapshotPromise = null;
  let pauseOnExceptionState = 'none';
  let lastCommand;

  // Things we need to reset when the app restarts
  let knownScripts;
  let currentBacktrace;
  let selectedFrame;
  let exitDebugRepl;

  function resetOnStart() {
    knownScripts = {};
    currentBacktrace = null;
    selectedFrame = null;

    if (exitDebugRepl) exitDebugRepl();
    exitDebugRepl = null;
  }
  resetOnStart();

  const INSPECT_OPTIONS = { colors: inspector.stdout.isTTY };
  function inspect(value) {
    return utilInspect(value, INSPECT_OPTIONS);
  }

  function print(value, addNewline = true) {
    const text = typeof value === 'string' ? value : inspect(value);
    return inspector.print(text, addNewline);
  }

  function getCurrentLocation() {
    if (!selectedFrame) {
      throw new ERR_DEBUGGER_ERROR('Requires execution to be paused');
    }
    return selectedFrame.location;
  }

  function isCurrentScript(script) {
    return selectedFrame && getCurrentLocation().scriptId === script.scriptId;
  }

  function formatScripts(displayNatives = false) {
    function isVisible(script) {
      if (displayNatives) return true;
      return !script.isNative || isCurrentScript(script);
    }

    return ArrayPrototypeJoin(ArrayPrototypeMap(
      ArrayPrototypeFilter(ObjectValues(knownScripts), isVisible),
      (script) => {
        const isCurrent = isCurrentScript(script);
        const { isNative, url } = script;
        const name = `${getRelativePath(url)}${isNative ? ' <native>' : ''}`;
        return `${isCurrent ? '*' : ' '} ${script.scriptId}: ${name}`;
      }), '\n');
  }

  function listScripts(displayNatives = false) {
    print(formatScripts(displayNatives));
  }
  listScripts[customInspectSymbol] = function listWithoutInternal() {
    return formatScripts();
  };

  const profiles = [];
  class Profile {
    constructor(data) {
      this.data = data;
    }

    static createAndRegister({ profile }) {
      const p = new Profile(profile);
      ArrayPrototypePush(profiles, p);
      return p;
    }

    [customInspectSymbol](depth, { stylize }) {
      const { startTime, endTime } = this.data;
      const MU = StringFromCharCode(956);
      return stylize(`[Profile ${endTime - startTime}${MU}s]`, 'special');
    }

    save(filename = 'node.cpuprofile') {
      const absoluteFile = Path.resolve(filename);
      const json = JSONStringify(this.data);
      FS.writeFileSync(absoluteFile, json);
      print('Saved profile to ' + absoluteFile);
    }
  }

  class SourceSnippet {
    constructor(location, delta, scriptSource) {
      ObjectAssign(this, location);
      this.scriptSource = scriptSource;
      this.delta = delta;
    }

    [customInspectSymbol](depth, options) {
      const { scriptId, lineNumber, columnNumber, delta, scriptSource } = this;
      const start = MathMax(1, lineNumber - delta + 1);
      const end = lineNumber + delta + 1;

      const lines = StringPrototypeSplit(scriptSource, '\n');
      return ArrayPrototypeJoin(
        ArrayPrototypeMap(
          ArrayPrototypeSlice(lines, start - 1, end),
          (lineText, offset) => {
            const i = start + offset;
            const isCurrent = i === (lineNumber + 1);

            const markedLine = isCurrent ?
              markSourceColumn(lineText, columnNumber, options.colors) :
              lineText;

            let isBreakpoint = false;
            ArrayPrototypeForEach(knownBreakpoints, ({ location }) => {
              if (!location) return;
              if (scriptId === location.scriptId &&
              i === (location.lineNumber + 1)) {
                isBreakpoint = true;
              }
            });

            let prefixChar = ' ';
            if (isCurrent) {
              prefixChar = '>';
            } else if (isBreakpoint) {
              prefixChar = '*';
            }
            return `${leftPad(i, prefixChar, end)} ${markedLine}`;
          }), '\n');
    }
  }

  async function getSourceSnippet(location, delta = 5) {
    const { scriptId } = location;
    const { scriptSource } = await Debugger.getScriptSource({ scriptId });
    return new SourceSnippet(location, delta, scriptSource);
  }

  class CallFrame {
    constructor(callFrame) {
      ObjectAssign(this, callFrame);
    }

    loadScopes() {
      return PromiseAll(
        new SafeArrayIterator(ArrayPrototypeMap(
          ArrayPrototypeFilter(
            this.scopeChain,
            (scope) => scope.type !== 'global'
          ),
          async (scope) => {
            const { objectId } = scope.object;
            const { result } = await Runtime.getProperties({
              objectId,
              generatePreview: true,
            });
            return new ScopeSnapshot(scope, result);
          })
        )
      );
    }

    list(delta = 5) {
      return getSourceSnippet(this.location, delta);
    }
  }

  class Backtrace extends Array {
    [customInspectSymbol]() {
      return ArrayPrototypeJoin(
        ArrayPrototypeMap(this, (callFrame, idx) => {
          const {
            location: { scriptId, lineNumber, columnNumber },
            functionName
          } = callFrame;
          const name = functionName || '(anonymous)';

          const script = knownScripts[scriptId];
          const relativeUrl =
          (script && getRelativePath(script.url)) || '<unknown>';
          const frameLocation =
          `${relativeUrl}:${lineNumber + 1}:${columnNumber}`;

          return `#${idx} ${name} ${frameLocation}`;
        }), '\n');
    }

    static from(callFrames) {
      return FunctionPrototypeCall(
        ArrayFrom,
        this,
        callFrames,
        (callFrame) =>
          (callFrame instanceof CallFrame ?
            callFrame :
            new CallFrame(callFrame))
      );
    }
  }

  function prepareControlCode(input) {
    if (input === '\n') return lastCommand;
    // Add parentheses: exec process.title => exec("process.title");
    const match = RegExpPrototypeSymbolMatch(/^\s*(?:exec|p)\s+([^\n]*)/, input);
    if (match) {
      lastCommand = `exec(${JSONStringify(match[1])})`;
    } else {
      lastCommand = input;
    }
    return lastCommand;
  }

  async function evalInCurrentContext(code) {
    // Repl asked for scope variables
    if (code === '.scope') {
      if (!selectedFrame) {
        throw new ERR_DEBUGGER_ERROR('Requires execution to be paused');
      }
      const scopes = await selectedFrame.loadScopes();
      return ArrayPrototypeMap(scopes, (scope) => scope.completionGroup);
    }

    if (selectedFrame) {
      return PromisePrototypeThen(Debugger.evaluateOnCallFrame({
        callFrameId: selectedFrame.callFrameId,
        expression: code,
        objectGroup: 'node-inspect',
        generatePreview: true,
      }), RemoteObject.fromEvalResult);
    }
    return PromisePrototypeThen(Runtime.evaluate({
      expression: code,
      objectGroup: 'node-inspect',
      generatePreview: true,
    }), RemoteObject.fromEvalResult);
  }

  function controlEval(input, context, filename, callback) {
    debuglog('eval:', input);
    function returnToCallback(error, result) {
      debuglog('end-eval:', input, error);
      callback(error, result);
    }

    try {
      const code = prepareControlCode(input);
      const result = vm.runInContext(code, context, filename);

      const then = result?.then;
      if (typeof then === 'function') {
        FunctionPrototypeCall(
          then, result,
          (result) => returnToCallback(null, result),
          returnToCallback
        );
      } else {
        returnToCallback(null, result);
      }
    } catch (e) {
      returnToCallback(e);
    }
  }

  function debugEval(input, context, filename, callback) {
    debuglog('eval:', input);
    function returnToCallback(error, result) {
      debuglog('end-eval:', input, error);
      callback(error, result);
    }

    PromisePrototypeThen(evalInCurrentContext(input),
                         (result) => returnToCallback(null, result),
                         returnToCallback
    );
  }

  async function formatWatchers(verbose = false) {
    if (!watchedExpressions.length) {
      return '';
    }

    const inspectValue = (expr) =>
      PromisePrototypeCatch(evalInCurrentContext(expr),
                            (error) => `<${error.message}>`);
    const lastIndex = watchedExpressions.length - 1;

    const values = await PromiseAll(new SafeArrayIterator(
      ArrayPrototypeMap(watchedExpressions, inspectValue)));
    const lines = ArrayPrototypeMap(watchedExpressions, (expr, idx) => {
      const prefix = `${leftPad(idx, ' ', lastIndex)}: ${expr} =`;
      const value = inspect(values[idx]);
      if (!StringPrototypeIncludes(value, '\n')) {
        return `${prefix} ${value}`;
      }
      return `${prefix}\n    ${RegExpPrototypeSymbolReplace(/\n/g, value, '\n    ')}`;
    });
    const valueList = ArrayPrototypeJoin(lines, '\n');
    return verbose ? `Watchers:\n${valueList}\n` : valueList;
  }

  function watchers(verbose = false) {
    return PromisePrototypeThen(formatWatchers(verbose), print);
  }

  // List source code
  function list(delta = 5) {
    return selectedFrame.list(delta).then(null, (error) => {
      print("You can't list source code right now");
      throw error;
    });
  }

  function handleBreakpointResolved({ breakpointId, location }) {
    const script = knownScripts[location.scriptId];
    const scriptUrl = script && script.url;
    if (scriptUrl) {
      ObjectAssign(location, { scriptUrl });
    }
    const isExisting = ArrayPrototypeSome(knownBreakpoints, (bp) => {
      if (bp.breakpointId === breakpointId) {
        ObjectAssign(bp, { location });
        return true;
      }
      return false;
    });
    if (!isExisting) {
      ArrayPrototypePush(knownBreakpoints, { breakpointId, location });
    }
  }

  function listBreakpoints() {
    if (!knownBreakpoints.length) {
      print('No breakpoints yet');
      return;
    }

    function formatLocation(location) {
      if (!location) return '<unknown location>';
      const script = knownScripts[location.scriptId];
      const scriptUrl = script ? script.url : location.scriptUrl;
      return `${getRelativePath(scriptUrl)}:${location.lineNumber + 1}`;
    }
    const breaklist = ArrayPrototypeJoin(
      ArrayPrototypeMap(
        knownBreakpoints,
        (bp, idx) => `#${idx} ${formatLocation(bp.location)}`),
      '\n');
    print(breaklist);
  }

  function setBreakpoint(script, line, condition, silent) {
    function registerBreakpoint({ breakpointId, actualLocation }) {
      handleBreakpointResolved({ breakpointId, location: actualLocation });
      if (actualLocation && actualLocation.scriptId) {
        if (!silent) return getSourceSnippet(actualLocation, 5);
      } else {
        print(`Warning: script '${script}' was not loaded yet.`);
      }
      return undefined;
    }

    // setBreakpoint(): set breakpoint at current location
    if (script === undefined) {
      return PromisePrototypeThen(
        Debugger.setBreakpoint({ location: getCurrentLocation(), condition }),
        registerBreakpoint);
    }

    // setBreakpoint(line): set breakpoint in current script at specific line
    if (line === undefined && typeof script === 'number') {
      const location = {
        scriptId: getCurrentLocation().scriptId,
        lineNumber: script - 1,
      };
      return PromisePrototypeThen(
        Debugger.setBreakpoint({ location, condition }),
        registerBreakpoint);
    }

    validateString(script, 'script');

    // setBreakpoint('fn()'): Break when a function is called
    if (StringPrototypeEndsWith(script, '()')) {
      const debugExpr = `debug(${script.slice(0, -2)})`;
      const debugCall = selectedFrame ?
        Debugger.evaluateOnCallFrame({
          callFrameId: selectedFrame.callFrameId,
          expression: debugExpr,
          includeCommandLineAPI: true,
        }) :
        Runtime.evaluate({
          expression: debugExpr,
          includeCommandLineAPI: true,
        });
      return PromisePrototypeThen(debugCall, ({ result, wasThrown }) => {
        if (wasThrown) return convertResultToError(result);
        return undefined; // This breakpoint can't be removed the same way
      });
    }

    // setBreakpoint('scriptname')
    let scriptId = null;
    let ambiguous = false;
    if (knownScripts[script]) {
      scriptId = script;
    } else {
      ArrayPrototypeForEach(ObjectKeys(knownScripts), (id) => {
        const scriptUrl = knownScripts[id].url;
        if (scriptUrl && StringPrototypeIncludes(scriptUrl, script)) {
          if (scriptId !== null) {
            ambiguous = true;
          }
          scriptId = id;
        }
      });
    }

    if (ambiguous) {
      print('Script name is ambiguous');
      return undefined;
    }
    if (line <= 0) {
      print('Line should be a positive value');
      return undefined;
    }

    if (scriptId !== null) {
      const location = { scriptId, lineNumber: line - 1 };
      return PromisePrototypeThen(
        Debugger.setBreakpoint({ location, condition }),
        registerBreakpoint);
    }

    const escapedPath = RegExpPrototypeSymbolReplace(/([/\\.?*()^${}|[\]])/g,
                                                     script, '\\$1');
    const urlRegex = `^(.*[\\/\\\\])?${escapedPath}$`;

    return PromisePrototypeThen(
      Debugger.setBreakpointByUrl({
        urlRegex,
        lineNumber: line - 1,
        condition,
      }),
      (bp) => {
        // TODO: handle bp.locations in case the regex matches existing files
        if (!bp.location) { // Fake it for now.
          ObjectAssign(bp, {
            actualLocation: {
              scriptUrl: `.*/${script}$`,
              lineNumber: line - 1,
            },
          });
        }
        return registerBreakpoint(bp);
      });
  }

  function clearBreakpoint(url, line) {
    const breakpoint = ArrayPrototypeFind(knownBreakpoints, ({ location }) => {
      if (!location) return false;
      const script = knownScripts[location.scriptId];
      if (!script) return false;
      return (
        StringPrototypeIncludes(script.url, url) &&
        (location.lineNumber + 1) === line
      );
    });
    if (!breakpoint) {
      print(`Could not find breakpoint at ${url}:${line}`);
      return PromiseResolve();
    }
    return PromisePrototypeThen(
      Debugger.removeBreakpoint({ breakpointId: breakpoint.breakpointId }),
      () => {
        const idx = ArrayPrototypeIndexOf(knownBreakpoints, breakpoint);
        ArrayPrototypeSplice(knownBreakpoints, idx, 1);
      });
  }

  function restoreBreakpoints() {
    const lastBreakpoints = ArrayPrototypeSplice(knownBreakpoints, 0);
    const newBreakpoints = ArrayPrototypeMap(
      ArrayPrototypeFilter(lastBreakpoints,
                           ({ location }) => !!location.scriptUrl),
      ({ location }) => setBreakpoint(location.scriptUrl,
                                      location.lineNumber + 1));
    if (!newBreakpoints.length) return PromiseResolve();
    return PromisePrototypeThen(
      PromiseAll(new SafeArrayIterator(newBreakpoints)),
      (results) => {
        print(`${results.length} breakpoints restored.`);
      });
  }

  function setPauseOnExceptions(state) {
    return PromisePrototypeThen(
      Debugger.setPauseOnExceptions({ state }),
      () => {
        pauseOnExceptionState = state;
      });
  }

  Debugger.on('paused', ({ callFrames, reason /* , hitBreakpoints */ }) => {
    if (process.env.NODE_INSPECT_RESUME_ON_START === '1' &&
        reason === 'Break on start') {
      debuglog('Paused on start, but NODE_INSPECT_RESUME_ON_START' +
              ' environment variable is set to 1, resuming');
      inspector.client.callMethod('Debugger.resume');
      return;
    }

    // Save execution context's data
    currentBacktrace = Backtrace.from(callFrames);
    selectedFrame = currentBacktrace[0];
    const { scriptId, lineNumber } = selectedFrame.location;

    const breakType = reason === 'other' ? 'break' : reason;
    const script = knownScripts[scriptId];
    const scriptUrl = script ? getRelativePath(script.url) : '[unknown]';

    const header = `${breakType} in ${scriptUrl}:${lineNumber + 1}`;

    inspector.suspendReplWhile(() =>
      PromisePrototypeThen(
        PromiseAll(new SafeArrayIterator(
          [formatWatchers(true), selectedFrame.list(2)])),
        ({ 0: watcherList, 1: context }) => {
          const breakContext = watcherList ?
            `${watcherList}\n${inspect(context)}` :
            inspect(context);
          print(`${header}\n${breakContext}`);
        }));
  });

  function handleResumed() {
    currentBacktrace = null;
    selectedFrame = null;
  }

  Debugger.on('resumed', handleResumed);

  Debugger.on('breakpointResolved', handleBreakpointResolved);

  Debugger.on('scriptParsed', (script) => {
    const { scriptId, url } = script;
    if (url) {
      knownScripts[scriptId] = { isNative: isNativeUrl(url), ...script };
    }
  });

  Profiler.on('consoleProfileFinished', ({ profile }) => {
    Profile.createAndRegister({ profile });
    print(
      'Captured new CPU profile.\n' +
      `Access it with profiles[${profiles.length - 1}]`
    );
  });

  function initializeContext(context) {
    ArrayPrototypeForEach(inspector.domainNames, (domain) => {
      ObjectDefineProperty(context, domain, {
        value: inspector[domain],
        enumerable: true,
        configurable: true,
        writeable: false,
      });
    });

    copyOwnProperties(context, {
      get help() {
        return print(HELP);
      },

      get run() {
        return inspector.run();
      },

      get kill() {
        return inspector.killChild();
      },

      get restart() {
        return inspector.run();
      },

      get cont() {
        handleResumed();
        return Debugger.resume();
      },

      get next() {
        handleResumed();
        return Debugger.stepOver();
      },

      get step() {
        handleResumed();
        return Debugger.stepInto();
      },

      get out() {
        handleResumed();
        return Debugger.stepOut();
      },

      get pause() {
        return Debugger.pause();
      },

      get backtrace() {
        return currentBacktrace;
      },

      get breakpoints() {
        return listBreakpoints();
      },

      exec(expr) {
        return evalInCurrentContext(expr);
      },

      get profile() {
        return Profiler.start();
      },

      get profileEnd() {
        return PromisePrototypeThen(Profiler.stop(),
                                    Profile.createAndRegister);
      },

      get profiles() {
        return profiles;
      },

      takeHeapSnapshot(filename = 'node.heapsnapshot') {
        if (heapSnapshotPromise) {
          print(
            'Cannot take heap snapshot because another snapshot is in progress.'
          );
          return heapSnapshotPromise;
        }
        heapSnapshotPromise = new Promise((resolve, reject) => {
          const absoluteFile = Path.resolve(filename);
          const writer = FS.createWriteStream(absoluteFile);
          let sizeWritten = 0;
          function onProgress({ done, total, finished }) {
            if (finished) {
              print('Heap snapshot prepared.');
            } else {
              print(`Heap snapshot: ${done}/${total}`, false);
            }
          }

          function onChunk({ chunk }) {
            sizeWritten += chunk.length;
            writer.write(chunk);
            print(`Writing snapshot: ${sizeWritten}`, false);
          }

          function onResolve() {
            writer.end(() => {
              teardown();
              print(`Wrote snapshot: ${absoluteFile}`);
              heapSnapshotPromise = null;
              resolve();
            });
          }

          function onReject(error) {
            teardown();
            reject(error);
          }

          function teardown() {
            HeapProfiler.removeListener(
              'reportHeapSnapshotProgress', onProgress);
            HeapProfiler.removeListener('addHeapSnapshotChunk', onChunk);
          }

          HeapProfiler.on('reportHeapSnapshotProgress', onProgress);
          HeapProfiler.on('addHeapSnapshotChunk', onChunk);

          print('Heap snapshot: 0/0', false);
          PromisePrototypeThen(
            HeapProfiler.takeHeapSnapshot({ reportProgress: true }),
            onResolve, onReject);
        });
        return heapSnapshotPromise;
      },

      get watchers() {
        return watchers();
      },

      watch(expr) {
        ArrayPrototypePush(watchedExpressions, expr);
      },

      unwatch(expr) {
        const index = ArrayPrototypeIndexOf(watchedExpressions, expr);

        // Unwatch by expression
        // or
        // Unwatch by watcher number
        ArrayPrototypeSplice(watchedExpressions,
                             index !== -1 ? index : +expr, 1);
      },

      get repl() {
        // Don't display any default messages
        const listeners = ArrayPrototypeSlice(repl.listeners('SIGINT'));
        repl.removeAllListeners('SIGINT');

        const oldContext = repl.context;

        exitDebugRepl = () => {
          // Restore all listeners
          process.nextTick(() => {
            ArrayPrototypeForEach(listeners, (listener) => {
              repl.on('SIGINT', listener);
            });
          });

          // Exit debug repl
          repl.eval = controlEval;

          // Swap history
          history.debug = repl.history;
          repl.history = history.control;

          repl.context = oldContext;
          repl.setPrompt('debug> ');
          repl.displayPrompt();

          repl.removeListener('SIGINT', exitDebugRepl);
          repl.removeListener('exit', exitDebugRepl);

          exitDebugRepl = null;
        };

        // Exit debug repl on SIGINT
        repl.on('SIGINT', exitDebugRepl);

        // Exit debug repl on repl exit
        repl.on('exit', exitDebugRepl);

        // Set new
        repl.eval = debugEval;
        repl.context = {};

        // Swap history
        history.control = repl.history;
        repl.history = history.debug;

        repl.setPrompt('> ');

        print('Press Ctrl+C to leave debug repl');
        return repl.displayPrompt();
      },

      get version() {
        return PromisePrototypeThen(Runtime.evaluate({
          expression: 'process.versions.v8',
          contextId: 1,
          returnByValue: true,
        }), ({ result }) => {
          print(result.value);
        });
      },

      scripts: listScripts,

      setBreakpoint,
      clearBreakpoint,
      setPauseOnExceptions,
      get breakOnException() {
        return setPauseOnExceptions('all');
      },
      get breakOnUncaught() {
        return setPauseOnExceptions('uncaught');
      },
      get breakOnNone() {
        return setPauseOnExceptions('none');
      },

      list,
    });
    aliasProperties(context, SHORTCUTS);
  }

  async function initAfterStart() {
    await Runtime.enable();
    await Profiler.enable();
    await Profiler.setSamplingInterval({ interval: 100 });
    await Debugger.enable();
    await Debugger.setAsyncCallStackDepth({ maxDepth: 0 });
    await Debugger.setBlackboxPatterns({ patterns: [] });
    await Debugger.setPauseOnExceptions({ state: pauseOnExceptionState });
    await restoreBreakpoints();
    return Runtime.runIfWaitingForDebugger();
  }

  return async function startRepl() {
    inspector.client.on('close', () => {
      resetOnStart();
    });
    inspector.client.on('ready', () => {
      initAfterStart();
    });

    // Init once for the initial connection
    await initAfterStart();

    const replOptions = {
      prompt: 'debug> ',
      input: inspector.stdin,
      output: inspector.stdout,
      eval: controlEval,
      useGlobal: false,
      ignoreUndefined: true,
    };

    repl = Repl.start(replOptions);
    initializeContext(repl.context);
    repl.on('reset', initializeContext);

    repl.defineCommand('interrupt', () => {
      // We want this for testing purposes where sending Ctrl+C can be tricky.
      repl.emit('SIGINT');
    });

    return repl;
  };
}
module.exports = createRepl;
