// Copyright 2018-2026 the Deno authors. MIT license.

// @ts-check
/// <reference path="../../core/internal.d.ts" />
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../webidl/internal.d.ts" />

(function () {
const { core, primordials } = __bootstrap;
const {
  op_url_get_serialization,
  op_url_parse,
  op_url_parse_search_params,
  op_url_parse_with_base,
  op_url_reparse,
  op_url_stringify_search_params,
} = core.ops;
const {
  ArrayFrom,
  ArrayPrototypeJoin,
  ArrayPrototypeMap,
  ArrayPrototypePush,
  ArrayPrototypeSome,
  ArrayPrototypeSort,
  ArrayPrototypeSplice,
  ObjectGetOwnPropertyDescriptor,
  ObjectKeys,
  ObjectPrototypeIsPrototypeOf,
  ReflectOwnKeys,
  SafeArrayIterator,
  StringPrototypeCharCodeAt,
  StringPrototypeSlice,
  StringPrototypeStartsWith,
  Symbol,
  SymbolFor,
  SymbolIterator,
  TypeError,
  Uint32Array,
} = primordials;

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
const { createFilteredInspectProxy } = core.loadExtScript(
  "ext:deno_web/01_console.js",
);
const { markNotSerializable } = core.loadExtScript(
  "ext:deno_web/13_message_port.js",
);

const _list = Symbol("list");
const _urlObject = Symbol("url object");

// Pre-frozen argument-name arrays used to produce Node-compatible
// `ERR_MISSING_ARGS` messages from `webidl.requiredArguments`.
const NAME_ARG_NAMES = ["name"];
const APPEND_ARG_NAMES = ["name", "value"];

// WARNING: must match rust code's UrlSetter::*
const SET_HASH = 0;
const SET_HOST = 1;
const SET_HOSTNAME = 2;
const SET_PASSWORD = 3;
const SET_PATHNAME = 4;
const SET_PORT = 5;
const SET_PROTOCOL = 6;
const SET_SEARCH = 7;
const SET_USERNAME = 8;

// Helper functions
/**
 * @param {string} href
 * @param {number} setter
 * @param {string} value
 * @returns {string}
 */
function opUrlReparse(href, setter, value) {
  const status = op_url_reparse(
    href,
    setter,
    value,
    componentsBuf,
  );
  return getSerialization(status, href);
}

/**
 * @param {string} href
 * @param {string} [maybeBase]
 * @returns {number}
 */
function opUrlParse(href, maybeBase) {
  if (maybeBase === undefined) {
    return op_url_parse(href, componentsBuf);
  }
  return op_url_parse_with_base(
    href,
    maybeBase,
    componentsBuf,
  );
}

/**
 * @param {number} status
 * @param {string} href
 * @param {string} [maybeBase]
 * @returns {string}
 */
function getSerialization(status, href, maybeBase) {
  if (status === 0) {
    return href;
  } else if (status === 1) {
    return op_url_get_serialization();
  } else {
    throw new TypeError(
      `Invalid URL: '${href}'` +
        (maybeBase ? ` with base '${maybeBase}'` : ""),
    );
  }
}

class URLSearchParams {
  [_list];
  [_urlObject] = null;

  /**
   * @param {string | [string][] | Record<string, string>} init
   */
  constructor(init = undefined) {
    this[webidl.brand] = webidl.brand;
    // `undefined` is the default value of an optional argument, so it means
    // "not passed". `null` is a value, and per WebIDL union resolution it
    // reaches the USVString overload as "null".
    if (init === undefined) {
      this[_list] = [];
      return;
    }

    if (
      init !== null && (typeof init === "object" || typeof init === "function")
    ) {
      // Object overloads: either iterable of pairs or a record.
      const method = init[SymbolIterator];
      if (method !== undefined && method !== null) {
        if (typeof method !== "function") {
          const err = new TypeError("Query pairs must be iterable");
          err.code = "ERR_ARG_NOT_ITERABLE";
          throw err;
        }
        // Sequence<sequence<USVString>>
        const pairs = [];
        // deno-lint-ignore deno-internal/prefer-primordials
        const iter = method.call(init);
        if (iter == null || typeof iter.next !== "function") {
          const err = new TypeError(
            "Each query pair must be an iterable [name, value] tuple",
          );
          err.code = "ERR_INVALID_TUPLE";
          throw err;
        }
        while (true) {
          // deno-lint-ignore deno-internal/prefer-primordials
          const res = iter.next();
          if (res == null) {
            const err = new TypeError(
              "Each query pair must be an iterable [name, value] tuple",
            );
            err.code = "ERR_INVALID_TUPLE";
            throw err;
          }
          if (res.done === true) break;
          const pair = res.value;
          if (
            (typeof pair !== "object" && typeof pair !== "function") ||
            pair === null ||
            typeof pair[SymbolIterator] !== "function"
          ) {
            const err = new TypeError(
              "Each query pair must be an iterable [name, value] tuple",
            );
            err.code = "ERR_INVALID_TUPLE";
            throw err;
          }
          const entry = [];
          for (const v of new SafeArrayIterator(ArrayFrom(pair))) {
            ArrayPrototypePush(
              entry,
              webidl.converters.USVString(v, undefined, undefined),
            );
          }
          if (entry.length !== 2) {
            const err = new TypeError(
              "Each query pair must be an iterable [name, value] tuple",
            );
            err.code = "ERR_INVALID_TUPLE";
            throw err;
          }
          ArrayPrototypePush(pairs, entry);
        }
        this[_list] = pairs;
        return;
      }
      // Record<USVString, USVString>. We iterate own enumerable keys
      // (including Symbol keys so USVString coercion throws on them, like
      // Node does) and dedupe by the USVString-coerced name so that two keys
      // collapsing to U+FFFD overwrite each other instead of appearing twice
      // in the iterator output.
      const result = { __proto__: null };
      const allKeys = ReflectOwnKeys(init);
      for (let i = 0; i < allKeys.length; i++) {
        const key = allKeys[i];
        const desc = ObjectGetOwnPropertyDescriptor(init, key);
        if (desc !== undefined && desc.enumerable === true) {
          const name = webidl.converters.USVString(key, undefined, undefined);
          const value = webidl.converters.USVString(
            init[key],
            undefined,
            undefined,
          );
          result[name] = value;
        }
      }
      const list = [];
      const resultKeys = ObjectKeys(result);
      for (let i = 0; i < resultKeys.length; i++) {
        ArrayPrototypePush(list, [resultKeys[i], result[resultKeys[i]]]);
      }
      this[_list] = list;
      return;
    }

    // USVString overload.
    let str = webidl.converters.USVString(init, undefined, undefined);
    if (str.length === 0) {
      this[_list] = [];
      return;
    }
    if (str[0] === "?") {
      str = StringPrototypeSlice(str, 1);
    }
    this[_list] = op_url_parse_search_params(str);
  }

  #updateUrlSearch() {
    const url = this[_urlObject];
    if (url === null) {
      return;
    }
    // deno-lint-ignore deno-internal/prefer-primordials
    url[_updateUrlSearch](this.toString());
  }

  /**
   * @param {string} name
   * @param {string} value
   */
  append(name, value) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    const prefix = "Failed to execute 'append' on 'URLSearchParams'";
    webidl.requiredArguments(arguments.length, 2, prefix, APPEND_ARG_NAMES);
    name = webidl.converters.USVString(name, prefix, "Argument 1");
    value = webidl.converters.USVString(value, prefix, "Argument 2");
    ArrayPrototypePush(this[_list], [name, value]);
    this.#updateUrlSearch();
  }

  /**
   * @param {string} name
   * @param {string} [value]
   */
  delete(name, value = undefined) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    const prefix = "Failed to execute 'append' on 'URLSearchParams'";
    webidl.requiredArguments(arguments.length, 1, prefix, NAME_ARG_NAMES);
    name = webidl.converters.USVString(name, prefix, "Argument 1");
    const list = this[_list];
    let writeIdx = 0;
    if (value === undefined) {
      for (let i = 0; i < list.length; i++) {
        if (list[i][0] !== name) {
          list[writeIdx++] = list[i];
        }
      }
    } else {
      value = webidl.converters.USVString(value, prefix, "Argument 2");
      for (let i = 0; i < list.length; i++) {
        const entry = list[i];
        if (entry[0] !== name || entry[1] !== value) {
          list[writeIdx++] = entry;
        }
      }
    }
    if (writeIdx !== list.length) {
      ArrayPrototypeSplice(list, writeIdx);
    }
    this.#updateUrlSearch();
  }

  /**
   * @param {string} name
   * @returns {string[]}
   */
  getAll(name) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    const prefix = "Failed to execute 'getAll' on 'URLSearchParams'";
    webidl.requiredArguments(arguments.length, 1, prefix, NAME_ARG_NAMES);
    name = webidl.converters.USVString(name, prefix, "Argument 1");
    const values = [];
    const entries = this[_list];
    for (let i = 0; i < entries.length; ++i) {
      const entry = entries[i];
      if (entry[0] === name) {
        ArrayPrototypePush(values, entry[1]);
      }
    }
    return values;
  }

  /**
   * @param {string} name
   * @return {string | null}
   */
  get(name) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    const prefix = "Failed to execute 'get' on 'URLSearchParams'";
    webidl.requiredArguments(arguments.length, 1, prefix, NAME_ARG_NAMES);
    name = webidl.converters.USVString(name, prefix, "Argument 1");
    const entries = this[_list];
    for (let i = 0; i < entries.length; ++i) {
      const entry = entries[i];
      if (entry[0] === name) {
        return entry[1];
      }
    }
    return null;
  }

  /**
   * @param {string} name
   * @param {string} [value]
   * @return {boolean}
   */
  has(name, value = undefined) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    const prefix = "Failed to execute 'has' on 'URLSearchParams'";
    webidl.requiredArguments(arguments.length, 1, prefix, NAME_ARG_NAMES);
    name = webidl.converters.USVString(name, prefix, "Argument 1");
    if (value !== undefined) {
      value = webidl.converters.USVString(value, prefix, "Argument 2");
      return ArrayPrototypeSome(
        this[_list],
        (entry) => entry[0] === name && entry[1] === value,
      );
    }
    return ArrayPrototypeSome(this[_list], (entry) => entry[0] === name);
  }

  /**
   * @param {string} name
   * @param {string} value
   */
  set(name, value) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    const prefix = "Failed to execute 'set' on 'URLSearchParams'";
    webidl.requiredArguments(arguments.length, 2, prefix, APPEND_ARG_NAMES);
    name = webidl.converters.USVString(name, prefix, "Argument 1");
    value = webidl.converters.USVString(value, prefix, "Argument 2");

    const list = this[_list];

    // If there are any name-value pairs whose name is name, in list,
    // set the value of the first such name-value pair to value
    // and remove the others.
    let writeIdx = 0;
    let found = false;
    for (let i = 0; i < list.length; i++) {
      const entry = list[i];
      if (entry[0] === name) {
        if (!found) {
          entry[1] = value;
          list[writeIdx++] = entry;
          found = true;
        }
      } else {
        list[writeIdx++] = entry;
      }
    }

    // Otherwise, append a new name-value pair whose name is name
    // and value is value, to list.
    if (!found) {
      ArrayPrototypePush(list, [name, value]);
    } else if (writeIdx !== list.length) {
      ArrayPrototypeSplice(list, writeIdx);
    }

    this.#updateUrlSearch();
  }

  sort() {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    ArrayPrototypeSort(
      this[_list],
      (a, b) => (a[0] === b[0] ? 0 : a[0] > b[0] ? 1 : -1),
    );
    this.#updateUrlSearch();
  }

  /**
   * @return {string}
   */
  toString() {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    return op_url_stringify_search_params(this[_list]);
  }

  get size() {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    return this[_list].length;
  }

  // Node exposes this as a method on URLSearchParams.prototype so that
  // `Object.getOwnPropertyDescriptor(URLSearchParams.prototype,
  // Symbol.for("nodejs.util.inspect.custom"))` is not undefined. We delegate
  // to Deno's `privateCustomInspect` to preserve the Map-like formatting
  // (`URLSearchParams { 'a' => 'b' }`) that node compat tests expect.
  [SymbolFor("nodejs.util.inspect.custom")](_depth, inspectOptions, inspect) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    return this[SymbolFor("Deno.privateCustomInspect")](
      inspect,
      inspectOptions,
    );
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    webidl.assertBranded(this, URLSearchParamsPrototype, "URLSearchParams");
    if (
      typeof inspectOptions?.depth === "number" && inspectOptions.depth < 0
    ) {
      return "[Object]";
    }
    const entries = this[_list];
    if (entries.length === 0) return "URLSearchParams {}";
    const pairs = ArrayPrototypeMap(
      entries,
      (e) =>
        `${inspect(e[0], inspectOptions)} => ${inspect(e[1], inspectOptions)}`,
    );
    const inlined = ArrayPrototypeJoin(pairs, ", ");
    const breakLength = inspectOptions?.breakLength;
    const oneLine = `URLSearchParams { ${inlined} }`;
    if (typeof breakLength === "number" && oneLine.length > breakLength) {
      return `URLSearchParams {\n  ${ArrayPrototypeJoin(pairs, ",\n  ")} }`;
    }
    return oneLine;
  }
}

webidl.mixinPairIterable("URLSearchParams", URLSearchParams, _list, 0, 1);

webidl.configureInterface(URLSearchParams);
const URLSearchParamsPrototype = URLSearchParams.prototype;
markNotSerializable(URLSearchParamsPrototype);

webidl.converters["URLSearchParams"] = webidl.createInterfaceConverter(
  "URLSearchParams",
  URLSearchParamsPrototype,
);

const _updateUrlSearch = Symbol("updateUrlSearch");

function trim(s) {
  if (s.length === 1) return "";
  return s;
}

// Represents a "no port" value. A port in URL cannot be greater than 2^16 - 1
const NO_PORT = 65536;

const skipInit = Symbol();
const componentsBuf = new Uint32Array(8);

function isAsciiLowerAlpha(code) {
  return code >= 0x61 && code <= 0x7a;
}

function isAsciiUpperAlpha(code) {
  return code >= 0x41 && code <= 0x5a;
}

function isAsciiDigit(code) {
  return code >= 0x30 && code <= 0x39;
}

function isSimpleSpecialHostCanonical(host, start, end) {
  if (start === end) return false;
  if (
    StringPrototypeCharCodeAt(host, start) === 0x2e ||
    StringPrototypeCharCodeAt(host, end - 1) === 0x2e
  ) {
    return false;
  }

  let allNumeric = true;
  for (let i = start; i < end; i++) {
    const code = StringPrototypeCharCodeAt(host, i);
    if (!isAsciiDigit(code) && code !== 0x2e) {
      allNumeric = false;
      break;
    }
  }
  if (allNumeric) {
    let dots = 0;
    let part = 0;
    let partLen = 0;
    for (let i = start; i < end; i++) {
      const code = StringPrototypeCharCodeAt(host, i);
      if (code === 0x2e) {
        if (
          partLen === 0 || part > 255 ||
          (partLen > 1 &&
            StringPrototypeCharCodeAt(host, i - partLen) === 0x30)
        ) {
          return false;
        }
        dots++;
        part = 0;
        partLen = 0;
        continue;
      }
      part = part * 10 + code - 0x30;
      partLen++;
      if (partLen > 3) return false;
    }
    return dots === 3 && partLen !== 0 && part <= 255 &&
      (partLen === 1 ||
        StringPrototypeCharCodeAt(host, end - partLen) !== 0x30);
  }

  if (isAsciiDigit(StringPrototypeCharCodeAt(host, start))) {
    return false;
  }

  let labelStart = start;
  let labelLen = 0;
  let finalLabelAllDigits = true;
  for (let i = start; i < end; i++) {
    const code = StringPrototypeCharCodeAt(host, i);
    if (
      !isAsciiLowerAlpha(code) && !isAsciiDigit(code) &&
      code !== 0x2e && code !== 0x2d
    ) {
      return false;
    }
    if (code === 0x2e) {
      if (labelLen === 0) return false;
      labelStart = i + 1;
      labelLen = 0;
      finalLabelAllDigits = true;
      continue;
    }
    if (!isAsciiDigit(code)) {
      finalLabelAllDigits = false;
    }
    labelLen++;
    if (
      i === labelStart + 3 &&
      StringPrototypeCharCodeAt(host, labelStart) === 0x78 &&
      StringPrototypeCharCodeAt(host, labelStart + 1) === 0x6e &&
      StringPrototypeCharCodeAt(host, labelStart + 2) === 0x2d &&
      code === 0x2d
    ) {
      return false;
    }
  }
  return labelLen !== 0 && !finalLabelAllDigits &&
    !(labelLen >= 2 &&
      StringPrototypeCharCodeAt(host, labelStart) === 0x30 &&
      StringPrototypeCharCodeAt(host, labelStart + 1) === 0x78);
}

function isSimpleSpecialPathCode(code) {
  return isAsciiLowerAlpha(code) ||
    isAsciiUpperAlpha(code) ||
    isAsciiDigit(code) ||
    code === 0x2f ||
    code === 0x2e ||
    code === 0x5f ||
    code === 0x7e ||
    code === 0x2d ||
    code === 0x21 ||
    code === 0x24 ||
    code === 0x26 ||
    code === 0x27 ||
    code === 0x28 ||
    code === 0x29 ||
    code === 0x2a ||
    code === 0x2b ||
    code === 0x2c ||
    code === 0x3b ||
    code === 0x3d ||
    code === 0x3a ||
    code === 0x40;
}

// Keep in sync with parse_simple_special_url() in ext/web/url.rs.
function parseSimpleSpecialUrl(href) {
  let schemeEnd;
  let defaultPort;
  if (StringPrototypeStartsWith(href, "http://")) {
    schemeEnd = 4;
    defaultPort = 80;
  } else if (StringPrototypeStartsWith(href, "https://")) {
    schemeEnd = 5;
    defaultPort = 443;
  } else {
    return false;
  }

  const hostStart = schemeEnd + 3;
  const len = href.length;
  let pathStart = hostStart;
  while (pathStart < len) {
    const code = StringPrototypeCharCodeAt(href, pathStart);
    if (code === 0x2f) break;
    if (
      isAsciiLowerAlpha(code) || isAsciiDigit(code) ||
      code === 0x2e || code === 0x2d || code === 0x3a
    ) {
      pathStart++;
      continue;
    }
    return false;
  }
  if (pathStart === hostStart || pathStart === len) return false;

  let hostEnd = pathStart;
  let port = NO_PORT;
  for (let i = hostStart; i < pathStart; i++) {
    if (StringPrototypeCharCodeAt(href, i) !== 0x3a) continue;
    if (i === hostStart || i + 1 === pathStart) return false;
    hostEnd = i;
    port = 0;
    if (
      i + 2 < pathStart && StringPrototypeCharCodeAt(href, i + 1) === 0x30
    ) {
      return false;
    }
    for (let j = i + 1; j < pathStart; j++) {
      const code = StringPrototypeCharCodeAt(href, j);
      if (!isAsciiDigit(code)) return false;
      port = port * 10 + code - 0x30;
      if (port > 65535) return false;
    }
    if (port === defaultPort) return false;
    break;
  }
  if (!isSimpleSpecialHostCanonical(href, hostStart, hostEnd)) {
    return false;
  }

  let queryStart = 0;
  for (let i = pathStart; i < len; i++) {
    const code = StringPrototypeCharCodeAt(href, i);
    if (
      code === 0x2e && i > pathStart &&
      StringPrototypeCharCodeAt(href, i - 1) === 0x2f &&
      (i + 1 === len ||
        StringPrototypeCharCodeAt(href, i + 1) === 0x2f ||
        StringPrototypeCharCodeAt(href, i + 1) === 0x3f ||
        StringPrototypeCharCodeAt(href, i + 1) === 0x2e)
    ) {
      return false;
    }
    if (queryStart !== 0 && code === 0x27) return false;
    if (isSimpleSpecialPathCode(code)) continue;
    if (code === 0x3f && queryStart === 0) {
      queryStart = i;
      continue;
    }
    return false;
  }

  componentsBuf[0] = schemeEnd;
  componentsBuf[1] = hostStart;
  componentsBuf[2] = hostStart;
  componentsBuf[3] = hostEnd;
  componentsBuf[4] = port;
  componentsBuf[5] = pathStart;
  componentsBuf[6] = queryStart;
  componentsBuf[7] = 0;
  return true;
}

class URL {
  /** @type {URLSearchParams|null} */
  #queryObject = null;
  /** @type {string} */
  #serialization;
  /** @type {number} */
  #schemeEnd;
  /** @type {number} */
  #usernameEnd;
  /** @type {number} */
  #hostStart;
  /** @type {number} */
  #hostEnd;
  /** @type {number} */
  #port;
  /** @type {number} */
  #pathStart;
  /** @type {number} */
  #queryStart;
  /** @type {number} */
  #fragmentStart;

  [_updateUrlSearch](value) {
    this.#serialization = opUrlReparse(
      this.#serialization,
      SET_SEARCH,
      value,
    );
    this.#updateComponents();
  }

  /**
   * @param {string} url
   * @param {string} [base]
   */
  constructor(url, base = undefined) {
    // skip initialization for URL.parse
    if (url === skipInit) {
      return;
    }
    const prefix = "Failed to construct 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    url = webidl.converters.DOMString(url, prefix, "Argument 1");
    if (base !== undefined) {
      base = webidl.converters.DOMString(base, prefix, "Argument 2");
    }
    this[webidl.brand] = webidl.brand;
    if (base === undefined && parseSimpleSpecialUrl(url)) {
      this.#serialization = url;
    } else {
      const status = opUrlParse(url, base);
      this.#serialization = getSerialization(status, url, base);
    }
    this.#updateComponents();
  }

  /**
   * @param {string} url
   * @param {string} [base]
   */
  static parse(url, base = undefined) {
    const prefix = "Failed to execute 'URL.parse'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    url = webidl.converters.DOMString(url, prefix, "Argument 1");
    if (base !== undefined) {
      base = webidl.converters.DOMString(base, prefix, "Argument 2");
    }
    const status = opUrlParse(url, base);
    if (status !== 0 && status !== 1) {
      return null;
    }
    // If initialized with webidl.createBranded, private properties are not be accessible,
    // so it is passed through the constructor
    const self = new this(skipInit);
    self[webidl.brand] = webidl.brand;
    self.#serialization = getSerialization(status, url, base);
    self.#updateComponents();
    return self;
  }

  /**
   * @param {string} url
   * @param {string} [base]
   */
  static canParse(url, base = undefined) {
    const prefix = "Failed to execute 'URL.canParse'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    url = webidl.converters.DOMString(url, prefix, "Argument 1");
    if (base !== undefined) {
      base = webidl.converters.DOMString(base, prefix, "Argument 2");
    }
    const status = opUrlParse(url, base);
    return status === 0 || status === 1;
  }

  #updateComponents() {
    ({
      0: this.#schemeEnd,
      1: this.#usernameEnd,
      2: this.#hostStart,
      3: this.#hostEnd,
      4: this.#port,
      5: this.#pathStart,
      6: this.#queryStart,
      7: this.#fragmentStart,
    } = componentsBuf);
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      createFilteredInspectProxy({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(URLPrototype, this),
        keys: [
          "href",
          "origin",
          "protocol",
          "username",
          "password",
          "host",
          "hostname",
          "port",
          "pathname",
          "hash",
          "search",
        ],
      }),
      inspectOptions,
    );
  }

  // See URLSearchParams: Node exposes this as a method so that the descriptor
  // lookup is not undefined. Deno's own inspector still prefers
  // Deno.privateCustomInspect, so this is effectively the same code path.
  [SymbolFor("nodejs.util.inspect.custom")](_depth, inspectOptions, inspect) {
    return this[SymbolFor("Deno.privateCustomInspect")](
      inspect,
      inspectOptions,
    );
  }

  #updateSearchParams() {
    if (this.#queryObject !== null) {
      const params = this.#queryObject[_list];
      const newParams = op_url_parse_search_params(
        StringPrototypeSlice(this.search, 1),
      );
      ArrayPrototypeSplice(
        params,
        0,
        params.length,
        ...new SafeArrayIterator(newParams),
      );
    }
  }

  #hasAuthority() {
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L824
    return StringPrototypeStartsWith(
      StringPrototypeSlice(this.#serialization, this.#schemeEnd),
      "://",
    );
  }

  /** @return {string} */
  get hash() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L263
    return this.#fragmentStart
      ? trim(StringPrototypeSlice(this.#serialization, this.#fragmentStart))
      : "";
  }

  /** @param {string} value */
  set hash(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'hash' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_HASH,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get host() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L101
    return StringPrototypeSlice(
      this.#serialization,
      this.#hostStart,
      this.#pathStart,
    );
  }

  /** @param {string} value */
  set host(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'host' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_HOST,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get hostname() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L988
    return StringPrototypeSlice(
      this.#serialization,
      this.#hostStart,
      this.#hostEnd,
    );
  }

  /** @param {string} value */
  set hostname(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'hostname' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_HOSTNAME,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get href() {
    webidl.assertBranded(this, URLPrototype);
    return this.#serialization;
  }

  /** @param {string} value */
  set href(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'href' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    const status = opUrlParse(value);
    this.#serialization = getSerialization(status, value);
    this.#updateComponents();
    this.#updateSearchParams();
  }

  /** @return {string} */
  get origin() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/origin.rs#L14
    const scheme = StringPrototypeSlice(
      this.#serialization,
      0,
      this.#schemeEnd,
    );
    if (
      scheme === "http" || scheme === "https" || scheme === "ftp" ||
      scheme === "ws" || scheme === "wss"
    ) {
      return `${scheme}://${this.host}`;
    }

    if (scheme === "blob") {
      // TODO(@littledivy): Fast path.
      try {
        return new URL(this.pathname).origin;
      } catch {
        return "null";
      }
    }

    return "null";
  }

  /** @return {string} */
  get password() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L914
    if (
      this.#hasAuthority() &&
      this.#usernameEnd !== this.#serialization.length &&
      this.#serialization[this.#usernameEnd] === ":"
    ) {
      return StringPrototypeSlice(
        this.#serialization,
        this.#usernameEnd + 1,
        this.#hostStart - 1,
      );
    }
    return "";
  }

  /** @param {string} value */
  set password(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'password' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_PASSWORD,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get pathname() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L1203
    if (!this.#queryStart && !this.#fragmentStart) {
      return StringPrototypeSlice(this.#serialization, this.#pathStart);
    }

    const nextComponentStart = this.#queryStart || this.#fragmentStart;
    return StringPrototypeSlice(
      this.#serialization,
      this.#pathStart,
      nextComponentStart,
    );
  }

  /** @param {string} value */
  set pathname(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'pathname' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_PATHNAME,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get port() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L196
    if (this.#port === NO_PORT) {
      return StringPrototypeSlice(
        this.#serialization,
        this.#hostEnd,
        this.#pathStart,
      );
    } else {
      return StringPrototypeSlice(
        this.#serialization,
        this.#hostEnd + 1, /* : */
        this.#pathStart,
      );
    }
  }

  /** @param {string} value */
  set port(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'port' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_PORT,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get protocol() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L56
    return StringPrototypeSlice(
      this.#serialization,
      0,
      this.#schemeEnd + 1, /* : */
    );
  }

  /** @param {string} value */
  set protocol(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'protocol' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_PROTOCOL,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get search() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/quirks.rs#L249
    const afterPath = this.#queryStart || this.#fragmentStart ||
      this.#serialization.length;
    const afterQuery = this.#fragmentStart || this.#serialization.length;
    return trim(
      StringPrototypeSlice(this.#serialization, afterPath, afterQuery),
    );
  }

  /** @param {string} value */
  set search(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'search' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_SEARCH,
        value,
      );
      this.#updateComponents();
      this.#updateSearchParams();
    } catch {
      /* pass */
    }
  }

  /** @return {string} */
  get username() {
    webidl.assertBranded(this, URLPrototype);
    // https://github.com/servo/rust-url/blob/1d307ae51a28fecc630ecec03380788bfb03a643/url/src/lib.rs#L881
    const schemeSeparatorLen = 3; /* :// */
    if (
      this.#hasAuthority() &&
      this.#usernameEnd > this.#schemeEnd + schemeSeparatorLen
    ) {
      return StringPrototypeSlice(
        this.#serialization,
        this.#schemeEnd + schemeSeparatorLen,
        this.#usernameEnd,
      );
    } else {
      return "";
    }
  }

  /** @param {string} value */
  set username(value) {
    webidl.assertBranded(this, URLPrototype);
    const prefix = "Failed to set 'username' on 'URL'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    value = webidl.converters.DOMString(value, prefix, "Argument 1");
    try {
      this.#serialization = opUrlReparse(
        this.#serialization,
        SET_USERNAME,
        value,
      );
      this.#updateComponents();
    } catch {
      /* pass */
    }
  }

  /** @return {URLSearchParams} */
  get searchParams() {
    if (this.#queryObject == null) {
      this.#queryObject = new URLSearchParams(this.search);
      this.#queryObject[_urlObject] = this;
    }
    return this.#queryObject;
  }

  /** @return {string} */
  toString() {
    webidl.assertBranded(this, URLPrototype);
    return this.#serialization;
  }

  /** @return {string} */
  toJSON() {
    webidl.assertBranded(this, URLPrototype);
    return this.#serialization;
  }
}

webidl.configureInterface(URL);
const URLPrototype = URL.prototype;
markNotSerializable(URLPrototype);

/**
 * This function implements application/x-www-form-urlencoded parsing.
 * https://url.spec.whatwg.org/#concept-urlencoded-parser
 * @param {Uint8Array} bytes
 * @returns {[string, string][]}
 */
function parseUrlEncoded(bytes) {
  return op_url_parse_search_params(null, bytes);
}

webidl
  .converters[
    "sequence<sequence<USVString>> or record<USVString, USVString> or USVString"
  ] = (V, prefix, context, opts) => {
    // Union for (sequence<sequence<USVString>> or record<USVString, USVString> or USVString)
    if (webidl.type(V) === "Object" && V !== null) {
      if (V[SymbolIterator] !== undefined) {
        return webidl.converters["sequence<sequence<USVString>>"](
          V,
          prefix,
          context,
          opts,
        );
      }
      return webidl.converters["record<USVString, USVString>"](
        V,
        prefix,
        context,
        opts,
      );
    }
    return webidl.converters.USVString(V, prefix, context, opts);
  };

return {
  parseUrlEncoded,
  URL,
  URLPrototype,
  URLSearchParams,
  URLSearchParamsPrototype,
};
})();
