// Copyright 2018-2026 the Deno authors. MIT license.
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
// deno-lint-ignore-file camelcase no-inner-declarations no-this-alias

import { op_get_env_no_permission_check } from "ext:core/ops";

import {
  ERR_INVALID_ARG_TYPE,
  ERR_INVALID_ARG_VALUE,
  ERR_OUT_OF_RANGE,
  ERR_USE_AFTER_CLOSE,
} from "ext:deno_node/internal/errors.ts";
import {
  validateAbortSignal,
  validateArray,
  validateString,
  validateUint32,
} from "ext:deno_node/internal/validators.mjs";
import {
  //   inspect,
  getStringWidth,
  stripVTControlCharacters,
} from "ext:deno_node/internal/util/inspect.mjs";
import EventEmitter from "node:events";
import { kFirstEventParam } from "ext:deno_node/_events.mjs";
import { emitKeypressEvents } from "ext:deno_node/internal/readline/emitKeypressEvents.mjs";
import {
  charLengthAt,
  charLengthLeft,
  commonPrefix,
  kSubstringSearch,
} from "ext:deno_node/internal/readline/utils.mjs";
import {
  clearScreenDown,
  cursorTo,
  moveCursor,
} from "ext:deno_node/internal/readline/callbacks.mjs";
import process from "node:process";

import { StringDecoder } from "node:string_decoder";
import {
  kAddHistory,
  kDecoder,
  kDeleteLeft,
  kDeleteLineLeft,
  kDeleteLineRight,
  kDeleteRight,
  kDeleteWordLeft,
  kDeleteWordRight,
  kGetDisplayPos,
  kHistoryNext,
  kHistoryPrev,
  kInsertString,
  kLine,
  kLine_buffer,
  kMoveCursor,
  kNormalWrite,
  kOldPrompt,
  kOnLine,
  kPreviousKey,
  kPrompt,
  kQuestionCallback,
  kRefreshLine,
  kSawKeyPress,
  kSawReturnAt,
  kSetRawMode,
  kTabComplete,
  kTabCompleter,
  kTtyWrite,
  kWordLeft,
  kWordRight,
  kWriteToOutput,
} from "ext:deno_node/internal/readline/symbols.mjs";
import { primordials } from "ext:core/mod.js";

const {
  ArrayPrototypePush,
  DateNow,
  RegExpPrototypeExec,
  SafeRegExp,
  StringPrototypeEndsWith,
  StringPrototypeSlice,
} = primordials;

const kHistorySize = 30;
const kMincrlfDelay = 100;

/**
 * The end of a line is signaled by either one of the following:
 *  - \r\n
 *  - \n
 *  - \r followed by something other than \n
 *  - \u2028 (Unicode 'LINE SEPARATOR')
 *  - \u2029 (Unicode 'PARAGRAPH SEPARATOR')
 */
const lineEnding = new SafeRegExp(/\r?\n|\r(?!\n)|\u2028|\u2029/g);

const kLineObjectStream = Symbol("line object stream");
export const kQuestionCancel = Symbol("kQuestionCancel");
export const kQuestion = Symbol("kQuestion");
const kKillRing = Symbol("kKillRing");
const kKillRingCursor = Symbol("kKillRingCursor");
const kYank = Symbol("kYank");
const kYankPop = Symbol("kYankPop");
const kUndoStack = Symbol("kUndoStack");
const kRedoStack = Symbol("kRedoStack");
const kUndo = Symbol("kUndo");
const kRedo = Symbol("kRedo");
const kPushToUndoStack = Symbol("kPushToUndoStack");

// GNU readline library - keyseq-timeout is 500ms (default)
const ESCAPE_CODE_TIMEOUT = 500;

export {
  kAddHistory,
  kDecoder,
  kDeleteLeft,
  kDeleteLineLeft,
  kDeleteLineRight,
  kDeleteRight,
  kDeleteWordLeft,
  kDeleteWordRight,
  kGetDisplayPos,
  kHistoryNext,
  kHistoryPrev,
  kInsertString,
  kLine,
  kLine_buffer,
  kMoveCursor,
  kNormalWrite,
  kOldPrompt,
  kOnLine,
  kPreviousKey,
  kPrompt,
  kQuestionCallback,
  kRefreshLine,
  kSawKeyPress,
  kSawReturnAt,
  kSetRawMode,
  kTabComplete,
  kTabCompleter,
  kTtyWrite,
  kWordLeft,
  kWordRight,
  kWriteToOutput,
};

export function InterfaceConstructor(input, output, completer, terminal) {
  this[kSawReturnAt] = 0;
  // TODO(BridgeAR): Document this property. The name is not ideal, so we
  // might want to expose an alias and document that instead.
  this.isCompletionEnabled = true;
  this[kSawKeyPress] = false;
  this[kPreviousKey] = null;
  this.escapeCodeTimeout = ESCAPE_CODE_TIMEOUT;
  this.tabSize = 8;
  Function.prototype.call(EventEmitter, this);

  let history;
  let historySize;
  let removeHistoryDuplicates = false;
  let crlfDelay;
  let prompt = "> ";
  let signal;

  if (input?.input) {
    // An options object was given
    output = input.output;
    completer = input.completer;
    terminal = input.terminal;
    history = input.history;
    historySize = input.historySize;
    signal = input.signal;
    if (input.tabSize !== undefined) {
      validateUint32(input.tabSize, "tabSize", true);
      this.tabSize = input.tabSize;
    }
    removeHistoryDuplicates = input.removeHistoryDuplicates;
    if (input.prompt !== undefined) {
      prompt = input.prompt;
    }
    if (input.escapeCodeTimeout !== undefined) {
      if (Number.isFinite(input.escapeCodeTimeout)) {
        this.escapeCodeTimeout = input.escapeCodeTimeout;
      } else {
        throw new ERR_INVALID_ARG_VALUE(
          "input.escapeCodeTimeout",
          this.escapeCodeTimeout,
        );
      }
    }

    if (signal) {
      validateAbortSignal(signal, "options.signal");
    }

    crlfDelay = input.crlfDelay;
    input = input.input;
  }

  if (completer !== undefined && typeof completer !== "function") {
    throw new ERR_INVALID_ARG_VALUE("completer", completer);
  }

  if (history === undefined) {
    history = [];
  } else {
    validateArray(history, "history");
  }

  if (historySize === undefined) {
    historySize = kHistorySize;
  }

  if (typeof historySize !== "number") {
    throw new ERR_INVALID_ARG_TYPE("historySize", "number", historySize);
  }

  if (Number.isNaN(historySize) || historySize < 0) {
    throw new ERR_OUT_OF_RANGE("historySize", ">= 0", historySize);
  }

  // Backwards compat; check the isTTY prop of the output stream
  //  when `terminal` was not specified
  if (terminal === undefined && !(output === null || output === undefined)) {
    terminal = !!output.isTTY;
  }

  const self = this;

  this.line = "";
  this[kSubstringSearch] = null;
  this.output = output;
  this.input = input;
  this.history = history;
  this.historySize = historySize;
  this.removeHistoryDuplicates = !!removeHistoryDuplicates;
  this.crlfDelay = crlfDelay
    ? Math.max(kMincrlfDelay, crlfDelay)
    : kMincrlfDelay;
  this.completer = completer;
  this[kKillRing] = [];
  this[kKillRingCursor] = 0;
  this[kUndoStack] = [];
  this[kRedoStack] = [];

  this.setPrompt(prompt);

  this.terminal = !!terminal;

  function onerror(err) {
    self.emit("error", err);
  }

  function ondata(data) {
    self[kNormalWrite](data);
  }

  function onend() {
    if (
      typeof self[kLine_buffer] === "string" &&
      self[kLine_buffer].length > 0
    ) {
      self.emit("line", self[kLine_buffer]);
    }
    self.close();
  }

  function ontermend() {
    if (typeof self.line === "string" && self.line.length > 0) {
      self.emit("line", self.line);
    }
    self.close();
  }

  function onkeypress(s, key) {
    self[kTtyWrite](s, key);
    if (key && key.sequence) {
      // If the key.sequence is half of a surrogate pair
      // (>= 0xd800 and <= 0xdfff), refresh the line so
      // the character is displayed appropriately.
      const ch = key.sequence.codePointAt(0);
      if (ch >= 0xd800 && ch <= 0xdfff) self[kRefreshLine]();
    }
  }

  function onresize() {
    self[kRefreshLine]();
  }

  this[kLineObjectStream] = undefined;

  input.on("error", onerror);

  if (!this.terminal) {
    function onSelfCloseWithoutTerminal() {
      input.removeListener("data", ondata);
      input.removeListener("error", onerror);
      input.removeListener("end", onend);
    }

    input.on("data", ondata);
    input.on("end", onend);
    self.once("close", onSelfCloseWithoutTerminal);
    this[kDecoder] = new StringDecoder("utf8");
  } else {
    function onSelfCloseWithTerminal() {
      input.removeListener("keypress", onkeypress);
      input.removeListener("error", onerror);
      input.removeListener("end", ontermend);
      if (output !== null && output !== undefined) {
        output.removeListener("resize", onresize);
      }
    }

    emitKeypressEvents(input, this);

    // `input` usually refers to stdin
    input.on("keypress", onkeypress);
    input.on("end", ontermend);

    this[kSetRawMode](true);
    this.terminal = true;

    // Cursor position on the line.
    this.cursor = 0;

    this.historyIndex = -1;

    if (output !== null && output !== undefined) {
      output.on("resize", onresize);
    }

    self.once("close", onSelfCloseWithTerminal);
  }

  if (signal) {
    const onAborted = () => self.close();
    if (signal.aborted) {
      process.nextTick(onAborted);
    } else {
      signal.addEventListener("abort", onAborted, { once: true });
      self.once("close", () => signal.removeEventListener("abort", onAborted));
    }
  }

  // Current line
  this.line = "";

  input.resume();
}

Object.setPrototypeOf(InterfaceConstructor.prototype, EventEmitter.prototype);
Object.setPrototypeOf(InterfaceConstructor, EventEmitter);

export class Interface extends InterfaceConstructor {
  // eslint-disable-next-line no-useless-constructor
  constructor(input, output, completer, terminal) {
    super(input, output, completer, terminal);
  }
  get columns() {
    if (this.output && this.output.columns) return this.output.columns;
    return Infinity;
  }

  /**
   * Sets the prompt written to the output.
   * @param {string} prompt
   * @returns {void}
   */
  setPrompt(prompt) {
    this[kPrompt] = prompt;
  }

  /**
   * Returns the current prompt used by `rl.prompt()`.
   * @returns {string}
   */
  getPrompt() {
    return this[kPrompt];
  }

  [kSetRawMode](mode) {
    const wasInRawMode = this.input.isRaw;

    if (typeof this.input.setRawMode === "function") {
      this.input.setRawMode(mode);
    }

    return wasInRawMode;
  }

  /**
   * Writes the configured `prompt` to a new line in `output`.
   * @param {boolean} [preserveCursor]
   * @returns {void}
   */
  prompt(preserveCursor) {
    if (this.paused) this.resume();
    if (this.terminal && op_get_env_no_permission_check("TERM") !== "dumb") {
      if (!preserveCursor) this.cursor = 0;
      this[kRefreshLine]();
    } else {
      this[kWriteToOutput](this[kPrompt]);
    }
  }

  [kQuestion](query, cb) {
    if (this.closed) {
      throw new ERR_USE_AFTER_CLOSE("readline");
    }
    if (this[kQuestionCallback]) {
      this.prompt();
    } else {
      this[kOldPrompt] = this[kPrompt];
      this.setPrompt(query);
      this[kQuestionCallback] = cb;
      this.prompt();
    }
  }

  [kOnLine](line) {
    if (this[kQuestionCallback]) {
      const cb = this[kQuestionCallback];
      this[kQuestionCallback] = null;
      this.setPrompt(this[kOldPrompt]);
      cb(line);
    } else {
      this.emit("line", line);
    }
  }

  [kQuestionCancel]() {
    if (this[kQuestionCallback]) {
      this[kQuestionCallback] = null;
      this.setPrompt(this[kOldPrompt]);
      this.clearLine();
    }
  }

  [kWriteToOutput](stringToWrite) {
    validateString(stringToWrite, "stringToWrite");

    if (this.output !== null && this.output !== undefined) {
      this.output.write(stringToWrite);
    }
  }

  [kAddHistory]() {
    if (this.line.length === 0) return "";

    // If the history is disabled then return the line
    if (this.historySize === 0) return this.line;

    // If the trimmed line is empty then return the line
    if (this.line.trim().length === 0) return this.line;

    if (this.history.length === 0 || this.history[0] !== this.line) {
      if (this.removeHistoryDuplicates) {
        // Remove older history line if identical to new one
        const dupIndex = this.history.indexOf(this.line);
        if (dupIndex !== -1) this.history.splice(dupIndex, 1);
      }

      this.history.unshift(this.line);

      // Only store so many
      if (this.history.length > this.historySize) {
        this.history.pop();
      }
    }

    this.historyIndex = -1;

    // The listener could change the history object, possibly
    // to remove the last added entry if it is sensitive and should
    // not be persisted in the history, like a password
    const line = this.history[0];

    // Emit history event to notify listeners of update
    this.emit("history", this.history);

    return line;
  }

  [kRefreshLine]() {
    // line length
    const line = this[kPrompt] + this.line;
    const dispPos = this[kGetDisplayPos](line);
    const lineCols = dispPos.cols;
    const lineRows = dispPos.rows;

    // cursor position
    const cursorPos = this.getCursorPos();

    // First move to the bottom of the current line, based on cursor pos
    const prevRows = this.prevRows || 0;
    if (prevRows > 0) {
      moveCursor(this.output, 0, -prevRows);
    }

    // Cursor to left edge.
    cursorTo(this.output, 0);
    // erase data
    clearScreenDown(this.output);

    // Write the prompt and the current buffer content.
    this[kWriteToOutput](line);

    // Force terminal to allocate a new line
    if (lineCols === 0) {
      this[kWriteToOutput](" ");
    }

    // Move cursor to original position.
    cursorTo(this.output, cursorPos.cols);

    const diff = lineRows - cursorPos.rows;
    if (diff > 0) {
      moveCursor(this.output, 0, -diff);
    }

    this.prevRows = cursorPos.rows;
  }

  /**
   * Closes the `readline.Interface` instance.
   * @returns {void}
   */
  close() {
    if (this.closed) return;
    this.pause();
    if (this.terminal) {
      this[kSetRawMode](false);
    }
    this.closed = true;
    this.emit("close");
  }

  /**
   * Pauses the `input` stream.
   * @returns {void | Interface}
   */
  pause() {
    if (this.closed) {
      throw new ERR_USE_AFTER_CLOSE("readline");
    }
    if (this.paused) return;
    this.input.pause();
    this.paused = true;
    this.emit("pause");
    return this;
  }

  /**
   * Resumes the `input` stream if paused.
   * @returns {void | Interface}
   */
  resume() {
    if (this.closed) {
      throw new ERR_USE_AFTER_CLOSE("readline");
    }
    if (!this.paused) return;
    this.input.resume();
    this.paused = false;
    this.emit("resume");
    return this;
  }

  /**
   * Writes either `data` or a `key` sequence identified by
   * `key` to the `output`.
   * @param {string} d
   * @param {{
   *   ctrl?: boolean;
   *   meta?: boolean;
   *   shift?: boolean;
   *   name?: string;
   *   }} [key]
   * @returns {void}
   */
  write(d, key) {
    if (this.closed) {
      throw new ERR_USE_AFTER_CLOSE("readline");
    }
    if (this.paused) this.resume();
    if (this.terminal) {
      this[kTtyWrite](d, key);
    } else {
      this[kNormalWrite](d);
    }
  }

  [kNormalWrite](b) {
    if (b === undefined) {
      return;
    }
    let string = this[kDecoder].write(b);
    if (
      this[kSawReturnAt] &&
      Date.now() - this[kSawReturnAt] <= this.crlfDelay
    ) {
      string = string.replace(/^\n/, "");
      this[kSawReturnAt] = 0;
    }

    // Run test() on the new string chunk, not on the entire line buffer.
    let newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
    if (newPartContainsEnding !== null) {
      if (this[kLine_buffer]) {
        string = this[kLine_buffer] + string;
        this[kLine_buffer] = null;
        lineEnding.lastIndex = 0; // Start the search from the beginning of the string.
        newPartContainsEnding = RegExpPrototypeExec(lineEnding, string);
      }
      this[kSawReturnAt] = StringPrototypeEndsWith(string, "\r")
        ? DateNow()
        : 0;

      const indexes = [0, newPartContainsEnding.index, lineEnding.lastIndex];
      let nextMatch;
      while ((nextMatch = RegExpPrototypeExec(lineEnding, string)) !== null) {
        ArrayPrototypePush(indexes, nextMatch.index, lineEnding.lastIndex);
      }
      const lastIndex = indexes.length - 1;
      // Either '' or (conceivably) the unfinished portion of the next line
      this[kLine_buffer] = StringPrototypeSlice(string, indexes[lastIndex]);
      for (let i = 1; i < lastIndex; i += 2) {
        this[kOnLine](StringPrototypeSlice(string, indexes[i - 1], indexes[i]));
      }
    } else if (string) {
      // No newlines this time, save what we have for next time
      if (this[kLine_buffer]) {
        this[kLine_buffer] += string;
      } else {
        this[kLine_buffer] = string;
      }
    }
  }

  [kInsertString](c) {
    if (this.cursor < this.line.length) {
      const beg = this.line.slice(0, this.cursor);
      const end = this.line.slice(
        this.cursor,
        this.line.length,
      );
      this.line = beg + c + end;
      this.cursor += c.length;
      this[kRefreshLine]();
    } else {
      this.line += c;
      this.cursor += c.length;

      if (this.getCursorPos().cols === 0) {
        this[kRefreshLine]();
      } else {
        this[kWriteToOutput](c);
      }
    }
  }

  async [kTabComplete](lastKeypressWasTab) {
    this.pause();
    const string = this.line.slice(0, this.cursor);
    let value;
    try {
      value = await this.completer(string);
    } catch (err) {
      // TODO(bartlomieju): inspect is not ported yet
      // this[kWriteToOutput](`Tab completion error: ${inspect(err)}`);
      this[kWriteToOutput](`Tab completion error: ${err}`);
      return;
    } finally {
      this.resume();
    }
    this[kTabCompleter](lastKeypressWasTab, value);
  }

  [kTabCompleter](lastKeypressWasTab, { 0: completions, 1: completeOn }) {
    // Result and the text that was completed.

    if (!completions || completions.length === 0) {
      return;
    }

    // If there is a common prefix to all matches, then apply that portion.
    const prefix = commonPrefix(
      completions.filter((e) => e !== ""),
    );
    if (
      prefix.startsWith(completeOn) &&
      prefix.length > completeOn.length
    ) {
      this[kInsertString](prefix.slice(completeOn.length));
      return;
    } else if (!completeOn.startsWith(prefix)) {
      this.line = this.line.slice(0, this.cursor - completeOn.length) +
        prefix +
        this.line.slice(this.cursor, this.line.length);
      this.cursor = this.cursor - completeOn.length + prefix.length;
      this._refreshLine();
      return;
    }

    if (!lastKeypressWasTab) {
      return;
    }

    // Apply/show completions.
    const completionsWidth = completions.map(
      (e) => getStringWidth(e),
    );
    const width = Math.max(...completionsWidth) + 2; // 2 space padding
    let maxColumns = Math.floor(this.columns / width) || 1;
    if (maxColumns === Infinity) {
      maxColumns = 1;
    }
    let output = "\r\n";
    let lineIndex = 0;
    let whitespace = 0;
    for (let i = 0; i < completions.length; i++) {
      const completion = completions[i];
      if (completion === "" || lineIndex === maxColumns) {
        output += "\r\n";
        lineIndex = 0;
        whitespace = 0;
      } else {
        output += " ".repeat(whitespace);
      }
      if (completion !== "") {
        output += completion;
        whitespace = width - completionsWidth[i];
        lineIndex++;
      } else {
        output += "\r\n";
      }
    }
    if (lineIndex !== 0) {
      output += "\r\n\r\n";
    }
    this[kWriteToOutput](output);
    this[kRefreshLine]();
  }

  [kWordLeft]() {
    if (this.cursor > 0) {
      // Reverse the string and match a word near beginning
      // to avoid quadratic time complexity
      const leading = this.line.slice(0, this.cursor);
      const reversed = Array.from(leading).reverse().join("");
      const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
      this[kMoveCursor](-match[0].length);
    }
  }

  [kWordRight]() {
    if (this.cursor < this.line.length) {
      const trailing = this.line.slice(this.cursor);
      const match = trailing.match(/^(?:\s+|[^\w\s]+|\w+)\s*/);
      this[kMoveCursor](match[0].length);
    }
  }

  [kDeleteLeft]() {
    if (this.cursor > 0 && this.line.length > 0) {
      this[kPushToUndoStack]();
      // The number of UTF-16 units comprising the character to the left
      const charSize = charLengthLeft(this.line, this.cursor);
      this.line = this.line.slice(0, this.cursor - charSize) +
        this.line.slice(this.cursor, this.line.length);

      this.cursor -= charSize;
      this[kRefreshLine]();
    }
  }

  [kDeleteRight]() {
    if (this.cursor < this.line.length) {
      this[kPushToUndoStack]();
      // The number of UTF-16 units comprising the character to the left
      const charSize = charLengthAt(this.line, this.cursor);
      this.line = this.line.slice(0, this.cursor) +
        this.line.slice(
          this.cursor + charSize,
          this.line.length,
        );
      this[kRefreshLine]();
    }
  }

  [kDeleteWordLeft]() {
    if (this.cursor > 0) {
      this[kPushToUndoStack]();
      // Reverse the string and match a word near beginning
      // to avoid quadratic time complexity
      let leading = this.line.slice(0, this.cursor);
      const reversed = Array.from(leading).reverse().join("");
      const match = reversed.match(/^\s*(?:[^\w\s]+|\w+)?/);
      leading = leading.slice(
        0,
        leading.length - match[0].length,
      );
      const killed = this.line.slice(leading.length, this.cursor);
      this.line = leading +
        this.line.slice(this.cursor, this.line.length);
      this.cursor = leading.length;
      this[kKillRing].push(killed);
      this[kKillRingCursor] = this[kKillRing].length - 1;
      this[kRefreshLine]();
    }
  }

  [kDeleteWordRight]() {
    if (this.cursor < this.line.length) {
      this[kPushToUndoStack]();
      const trailing = this.line.slice(this.cursor);
      const match = trailing.match(/^(?:\s+|\W+|\w+)\s*/);
      const killed = trailing.slice(0, match[0].length);
      this.line = this.line.slice(0, this.cursor) +
        trailing.slice(match[0].length);
      this[kKillRing].push(killed);
      this[kKillRingCursor] = this[kKillRing].length - 1;
      this[kRefreshLine]();
    }
  }

  [kDeleteLineLeft]() {
    this[kPushToUndoStack]();
    const killed = this.line.slice(0, this.cursor);
    this.line = this.line.slice(this.cursor);
    this.cursor = 0;
    this[kKillRing].push(killed);
    this[kKillRingCursor] = this[kKillRing].length - 1;
    this[kRefreshLine]();
  }

  [kDeleteLineRight]() {
    this[kPushToUndoStack]();
    const killed = this.line.slice(this.cursor);
    this.line = this.line.slice(0, this.cursor);
    this[kKillRing].push(killed);
    this[kKillRingCursor] = this[kKillRing].length - 1;
    this[kRefreshLine]();
  }

  [kYank]() {
    if (this[kKillRing].length > 0) {
      this[kKillRingCursor] = this[kKillRing].length - 1;
      const killed = this[kKillRing][this[kKillRingCursor]];
      this[kInsertString](killed);
    }
  }

  [kYankPop]() {
    if (this[kKillRing].length > 1) {
      // Remove previously yanked text
      const prev = this[kKillRing][this[kKillRingCursor]];
      this.line = this.line.slice(0, this.cursor - prev.length) +
        this.line.slice(this.cursor);
      this.cursor -= prev.length;

      // Cycle to previous entry in kill ring
      this[kKillRingCursor]--;
      if (this[kKillRingCursor] < 0) {
        this[kKillRingCursor] = this[kKillRing].length - 1;
      }
      const killed = this[kKillRing][this[kKillRingCursor]];
      this[kInsertString](killed);
    }
  }

  [kPushToUndoStack]() {
    this[kUndoStack].push({ line: this.line, cursor: this.cursor });
    this[kRedoStack] = [];
  }

  [kUndo]() {
    if (this[kUndoStack].length > 0) {
      this[kRedoStack].push({ line: this.line, cursor: this.cursor });
      const state = this[kUndoStack].pop();
      this.line = state.line;
      this.cursor = state.cursor;
      this[kRefreshLine]();
    }
  }

  [kRedo]() {
    if (this[kRedoStack].length > 0) {
      this[kUndoStack].push({ line: this.line, cursor: this.cursor });
      const state = this[kRedoStack].pop();
      this.line = state.line;
      this.cursor = state.cursor;
      this[kRefreshLine]();
    }
  }

  clearLine() {
    this[kMoveCursor](+Infinity);
    this[kWriteToOutput]("\r\n");
    this.line = "";
    this.cursor = 0;
    this.prevRows = 0;
  }

  [kLine]() {
    const line = this[kAddHistory]();
    this.clearLine();
    this[kOnLine](line);
  }

  // TODO(BridgeAR): Add underscores to the search part and a red background in
  // case no match is found. This should only be the visual part and not the
  // actual line content!
  // TODO(BridgeAR): In case the substring based search is active and the end is
  // reached, show a comment how to search the history as before. E.g., using
  // <ctrl> + N. Only show this after two/three UPs or DOWNs, not on the first
  // one.
  [kHistoryNext]() {
    if (this.historyIndex >= 0) {
      const search = this[kSubstringSearch] || "";
      let index = this.historyIndex - 1;
      while (
        index >= 0 &&
        (!this.history[index].startsWith(search) ||
          this.line === this.history[index])
      ) {
        index--;
      }
      if (index === -1) {
        this.line = search;
      } else {
        this.line = this.history[index];
      }
      this.historyIndex = index;
      this.cursor = this.line.length; // Set cursor to end of line.
      this[kRefreshLine]();
    }
  }

  [kHistoryPrev]() {
    if (this.historyIndex < this.history.length && this.history.length) {
      const search = this[kSubstringSearch] || "";
      let index = this.historyIndex + 1;
      while (
        index < this.history.length &&
        (!this.history[index].startsWith(search) ||
          this.line === this.history[index])
      ) {
        index++;
      }
      if (index === this.history.length) {
        this.line = search;
      } else {
        this.line = this.history[index];
      }
      this.historyIndex = index;
      this.cursor = this.line.length; // Set cursor to end of line.
      this[kRefreshLine]();
    }
  }

  // Returns the last character's display position of the given string
  [kGetDisplayPos](str) {
    let offset = 0;
    const col = this.columns;
    let rows = 0;
    str = stripVTControlCharacters(str);
    for (const char of str[Symbol.iterator]()) {
      if (char === "\n") {
        // Rows must be incremented by 1 even if offset = 0 or col = +Infinity.
        rows += Math.ceil(offset / col) || 1;
        offset = 0;
        continue;
      }
      // Tabs must be aligned by an offset of the tab size.
      if (char === "\t") {
        offset += this.tabSize - (offset % this.tabSize);
        continue;
      }
      const width = getStringWidth(char);
      if (width === 0 || width === 1) {
        offset += width;
      } else {
        // width === 2
        if ((offset + 1) % col === 0) {
          offset++;
        }
        offset += 2;
      }
    }
    const cols = offset % col;
    rows += (offset - cols) / col;
    return { cols, rows };
  }

  /**
   * Returns the real position of the cursor in relation
   * to the input prompt + string.
   * @returns {{
   *   rows: number;
   *   cols: number;
   *   }}
   */
  getCursorPos() {
    const strBeforeCursor = this[kPrompt] +
      this.line.slice(0, this.cursor);
    return this[kGetDisplayPos](strBeforeCursor);
  }

  // This function moves cursor dx places to the right
  // (-dx for left) and refreshes the line if it is needed.
  [kMoveCursor](dx) {
    if (dx === 0) {
      return;
    }
    const oldPos = this.getCursorPos();
    this.cursor += dx;

    // Bounds check
    if (this.cursor < 0) {
      this.cursor = 0;
    } else if (this.cursor > this.line.length) {
      this.cursor = this.line.length;
    }

    const newPos = this.getCursorPos();

    // Check if cursor stayed on the line.
    if (oldPos.rows === newPos.rows) {
      const diffWidth = newPos.cols - oldPos.cols;
      moveCursor(this.output, diffWidth, 0);
    } else {
      this[kRefreshLine]();
    }
  }

  // Handle a write from the tty
  [kTtyWrite](s, key) {
    const previousKey = this[kPreviousKey];
    key = key || {};
    this[kPreviousKey] = key;

    // Activate or deactivate substring search.
    if (
      (key.name === "up" || key.name === "down") &&
      !key.ctrl &&
      !key.meta &&
      !key.shift
    ) {
      if (this[kSubstringSearch] === null) {
        this[kSubstringSearch] = this.line.slice(
          0,
          this.cursor,
        );
      }
    } else if (this[kSubstringSearch] !== null) {
      this[kSubstringSearch] = null;
      // Reset the index in case there's no match.
      if (this.history.length === this.historyIndex) {
        this.historyIndex = -1;
      }
    }

    // Undo (Ctrl+_) and Redo (Ctrl+^)
    if (key.sequence === "\x1F") {
      this[kUndo]();
      return;
    }
    if (key.sequence === "\x1E") {
      this[kRedo]();
      return;
    }

    // Ignore escape key, fixes
    // https://github.com/nodejs/node-v0.x-archive/issues/2876.
    if (key.name === "escape") return;

    if (key.ctrl && key.shift) {
      /* Control and shift pressed */
      switch (key.name) {
        // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
        // identical to <ctrl>-h. It should have a unique escape sequence.
        case "backspace":
          this[kDeleteLineLeft]();
          break;

        case "delete":
          this[kDeleteLineRight]();
          break;
      }
    } else if (key.ctrl) {
      /* Control key pressed */

      switch (key.name) {
        case "c":
          if (this.listenerCount("SIGINT") > 0) {
            this.emit("SIGINT");
          } else {
            // This readline instance is finished
            this.close();
          }
          break;

        case "h": // delete left
          this[kDeleteLeft]();
          break;

        case "d": // delete right or EOF
          if (this.cursor === 0 && this.line.length === 0) {
            // This readline instance is finished
            this.close();
          } else if (this.cursor < this.line.length) {
            this[kDeleteRight]();
          }
          break;

        case "u": // Delete from current to start of line
          this[kDeleteLineLeft]();
          break;

        case "k": // Delete from current to end of line
          this[kDeleteLineRight]();
          break;

        case "a": // Go to the start of the line
          this[kMoveCursor](-Infinity);
          break;

        case "e": // Go to the end of the line
          this[kMoveCursor](+Infinity);
          break;

        case "b": // back one character
          this[kMoveCursor](-charLengthLeft(this.line, this.cursor));
          break;

        case "f": // Forward one character
          this[kMoveCursor](+charLengthAt(this.line, this.cursor));
          break;

        case "l": // Clear the whole screen
          cursorTo(this.output, 0, 0);
          clearScreenDown(this.output);
          this[kRefreshLine]();
          break;

        case "n": // next history item
          this[kHistoryNext]();
          break;

        case "p": // Previous history item
          this[kHistoryPrev]();
          break;

        case "y": // Yank killed text
          this[kYank]();
          break;

        case "z":
          if (process.platform === "win32") break;
          if (this.listenerCount("SIGTSTP") > 0) {
            this.emit("SIGTSTP");
          } else {
            process.once("SIGCONT", () => {
              // Don't raise events if stream has already been abandoned.
              if (!this.paused) {
                // Stream must be paused and resumed after SIGCONT to catch
                // SIGINT, SIGTSTP, and EOF.
                this.pause();
                this.emit("SIGCONT");
              }
              // Explicitly re-enable "raw mode" and move the cursor to
              // the correct position.
              // See https://github.com/joyent/node/issues/3295.
              this[kSetRawMode](true);
              this[kRefreshLine]();
            });
            this[kSetRawMode](false);
            process.kill(process.pid, "SIGTSTP");
          }
          break;

        case "w": // Delete backwards to a word boundary
        // TODO(BridgeAR): The transmitted escape sequence is `\b` and that is
        // identical to <ctrl>-h. It should have a unique escape sequence.
        // Falls through
        case "backspace":
          this[kDeleteWordLeft]();
          break;

        case "delete": // Delete forward to a word boundary
          this[kDeleteWordRight]();
          break;

        case "left":
          this[kWordLeft]();
          break;

        case "right":
          this[kWordRight]();
          break;
      }
    } else if (key.meta) {
      /* Meta key pressed */

      switch (key.name) {
        case "b": // backward word
          this[kWordLeft]();
          break;

        case "f": // forward word
          this[kWordRight]();
          break;

        case "d": // delete forward word
        case "delete":
          this[kDeleteWordRight]();
          break;

        case "backspace": // Delete backwards to a word boundary
          this[kDeleteWordLeft]();
          break;

        case "y": // Yank pop
          this[kYankPop]();
          break;
      }
    } else {
      /* No modifier keys used */

      // \r bookkeeping is only relevant if a \n comes right after.
      if (this[kSawReturnAt] && key.name !== "enter") this[kSawReturnAt] = 0;

      switch (key.name) {
        case "return": // Carriage return, i.e. \r
          this[kSawReturnAt] = Date.now();
          this[kLine]();
          break;

        case "enter":
          // When key interval > crlfDelay
          if (
            this[kSawReturnAt] === 0 ||
            Date.now() - this[kSawReturnAt] > this.crlfDelay
          ) {
            this[kLine]();
          }
          this[kSawReturnAt] = 0;
          break;

        case "backspace":
          this[kDeleteLeft]();
          break;

        case "delete":
          this[kDeleteRight]();
          break;

        case "left":
          // Obtain the code point to the left
          this[kMoveCursor](-charLengthLeft(this.line, this.cursor));
          break;

        case "right":
          this[kMoveCursor](+charLengthAt(this.line, this.cursor));
          break;

        case "home":
          this[kMoveCursor](-Infinity);
          break;

        case "end":
          this[kMoveCursor](+Infinity);
          break;

        case "up":
          this[kHistoryPrev]();
          break;

        case "down":
          this[kHistoryNext]();
          break;

        case "tab":
          // If tab completion enabled, do that...
          if (
            typeof this.completer === "function" &&
            this.isCompletionEnabled
          ) {
            const lastKeypressWasTab = previousKey &&
              previousKey.name === "tab";
            this[kTabComplete](lastKeypressWasTab);
            break;
          }
        // falls through
        default:
          if (typeof s === "string" && s) {
            // Erase state of previous searches.
            lineEnding.lastIndex = 0;
            let nextMatch;
            // Keep track of the end of the last match.
            let lastIndex = 0;
            while ((nextMatch = RegExpPrototypeExec(lineEnding, s)) !== null) {
              this[kInsertString](
                StringPrototypeSlice(s, lastIndex, nextMatch.index),
              );
              ({ lastIndex } = lineEnding);
              this[kLine]();
              // Restore lastIndex as the call to kLine could have mutated it.
              lineEnding.lastIndex = lastIndex;
            }
            // This ensures that the last line is written if it doesn't end in a newline.
            // Note that the last line may be the first line, in which case this still works.
            this[kInsertString](StringPrototypeSlice(s, lastIndex));
          }
      }
    }
  }

  /**
   * Creates an `AsyncIterator` object that iterates through
   * each line in the input stream as a string.
   * @typedef {{
   *   [Symbol.asyncIterator]: () => InterfaceAsyncIterator,
   *   next: () => Promise<string>
   * }} InterfaceAsyncIterator
   * @returns {InterfaceAsyncIterator}
   */
  [Symbol.asyncIterator]() {
    if (this[kLineObjectStream] === undefined) {
      this[kLineObjectStream] = EventEmitter.on(
        this,
        "line",
        {
          close: ["close"],
          highWaterMark: 1024,
          [kFirstEventParam]: true,
        },
      );
    }
    return this[kLineObjectStream];
  }
}
