// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

// @ts-check
/// <reference path="../webidl/internal.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference path="../web/06_streams_types.d.ts" />
/// <reference path="./lib.deno_fetch.d.ts" />
/// <reference lib="esnext" />
"use strict";

((window) => {
  const webidl = window.__bootstrap.webidl;
  const {
    HTTP_TAB_OR_SPACE_PREFIX_RE,
    HTTP_TAB_OR_SPACE_SUFFIX_RE,
    HTTP_WHITESPACE_PREFIX_RE,
    HTTP_WHITESPACE_SUFFIX_RE,
    HTTP_TOKEN_CODE_POINT_RE,
    byteLowerCase,
    collectSequenceOfCodepoints,
    collectHttpQuotedString,
  } = window.__bootstrap.infra;

  const _headerList = Symbol("header list");
  const _iterableHeaders = Symbol("iterable headers");
  const _guard = Symbol("guard");

  /**
   * @typedef Header
   * @type {[string, string]}
   */

  /**
   * @typedef HeaderList
   * @type {Header[]}
   */

  /**
   * @param {string} potentialValue
   * @returns {string}
   */
  function normalizeHeaderValue(potentialValue) {
    potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_PREFIX_RE, "");
    potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
    return potentialValue;
  }

  /**
   * @param {Headers} headers
   * @param {HeadersInit} object
   */
  function fillHeaders(headers, object) {
    if (Array.isArray(object)) {
      for (const header of object) {
        if (header.length !== 2) {
          throw new TypeError(
            `Invalid header. Length must be 2, but is ${header.length}`,
          );
        }
        appendHeader(headers, header[0], header[1]);
      }
    } else {
      for (const key of Object.keys(object)) {
        appendHeader(headers, key, object[key]);
      }
    }
  }

  /**
   * https://fetch.spec.whatwg.org/#concept-headers-append
   * @param {Headers} headers
   * @param {string} name
   * @param {string} value
   */
  function appendHeader(headers, name, value) {
    // 1.
    value = normalizeHeaderValue(value);

    // 2.
    if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
      throw new TypeError("Header name is not valid.");
    }
    if (
      value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D")
    ) {
      throw new TypeError("Header value is not valid.");
    }

    // 3.
    if (headers[_guard] == "immutable") {
      throw new TypeError("Headers are immutable.");
    }

    // 7.
    const list = headers[_headerList];
    name = byteLowerCase(name);
    list.push([name, value]);
  }

  /**
   * https://fetch.spec.whatwg.org/#concept-header-list-get
   * @param {HeaderList} list
   * @param {string} name
   */
  function getHeader(list, name) {
    const lowercaseName = byteLowerCase(name);
    const entries = list
      .filter((entry) => entry[0] === lowercaseName)
      .map((entry) => entry[1]);
    if (entries.length === 0) {
      return null;
    } else {
      return entries.join("\x2C\x20");
    }
  }

  /**
   * https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
   * @param {HeaderList} list
   * @param {string} name
   * @returns {string[] | null}
   */
  function getDecodeSplitHeader(list, name) {
    const initialValue = getHeader(list, name);
    if (initialValue === null) return null;
    const input = initialValue;
    let position = 0;
    const values = [];
    let value = "";
    while (position < initialValue.length) {
      // 7.1. collect up to " or ,
      const res = collectSequenceOfCodepoints(
        initialValue,
        position,
        (c) => c !== "\u0022" && c !== "\u002C",
      );
      value += res.result;
      position = res.position;

      if (position < initialValue.length) {
        if (input[position] === "\u0022") {
          const res = collectHttpQuotedString(input, position, false);
          value += res.result;
          position = res.position;
          if (position < initialValue.length) {
            continue;
          }
        } else {
          if (input[position] !== "\u002C") throw new TypeError("Unreachable");
          position += 1;
        }
      }

      value = value.replaceAll(HTTP_TAB_OR_SPACE_PREFIX_RE, "");
      value = value.replaceAll(HTTP_TAB_OR_SPACE_SUFFIX_RE, "");

      values.push(value);
      value = "";
    }
    return values;
  }

  class Headers {
    /** @type {HeaderList} */
    [_headerList] = [];
    /** @type {"immutable" | "request" | "request-no-cors" | "response" | "none"} */
    [_guard];

    get [_iterableHeaders]() {
      const list = this[_headerList];

      // The order of steps are not similar to the ones suggested by the
      // spec but produce the same result.
      const headers = {};
      const cookies = [];
      for (const entry of list) {
        const name = entry[0];
        const value = entry[1];
        if (value === null) throw new TypeError("Unreachable");
        // The following if statement is not spec compliant.
        // `set-cookie` is the only header that can not be concatentated,
        // so must be given to the user as multiple headers.
        // The else block of the if statement is spec compliant again.
        if (name === "set-cookie") {
          cookies.push([name, value]);
        } else {
          // The following code has the same behaviour as getHeader()
          // at the end of loop. But it avoids looping through the entire
          // list to combine multiple values with same header name. It
          // instead gradually combines them as they are found.
          let header = headers[name];
          if (header && header.length > 0) {
            header += "\x2C\x20" + value;
          } else {
            header = value;
          }
          headers[name] = header;
        }
      }

      return [...Object.entries(headers), ...cookies].sort((a, b) => {
        const akey = a[0];
        const bkey = b[0];
        if (akey > bkey) return 1;
        if (akey < bkey) return -1;
        return 0;
      });
    }

    /** @param {HeadersInit} [init] */
    constructor(init = undefined) {
      const prefix = "Failed to construct 'Event'";
      if (init !== undefined) {
        init = webidl.converters["HeadersInit"](init, {
          prefix,
          context: "Argument 1",
        });
      }

      this[webidl.brand] = webidl.brand;
      this[_guard] = "none";
      if (init !== undefined) {
        fillHeaders(this, init);
      }
    }

    /**
     * @param {string} name
     * @param {string} value
     */
    append(name, value) {
      webidl.assertBranded(this, Headers);
      const prefix = "Failed to execute 'append' on 'Headers'";
      webidl.requiredArguments(arguments.length, 2, { prefix });
      name = webidl.converters["ByteString"](name, {
        prefix,
        context: "Argument 1",
      });
      value = webidl.converters["ByteString"](value, {
        prefix,
        context: "Argument 2",
      });
      appendHeader(this, name, value);
    }

    /**
     * @param {string} name
     */
    delete(name) {
      const prefix = "Failed to execute 'delete' on 'Headers'";
      webidl.requiredArguments(arguments.length, 1, { prefix });
      name = webidl.converters["ByteString"](name, {
        prefix,
        context: "Argument 1",
      });

      if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
        throw new TypeError("Header name is not valid.");
      }
      if (this[_guard] == "immutable") {
        throw new TypeError("Headers are immutable.");
      }

      const list = this[_headerList];
      name = byteLowerCase(name);
      for (let i = 0; i < list.length; i++) {
        if (list[i][0] === name) {
          list.splice(i, 1);
          i--;
        }
      }
    }

    /**
     * @param {string} name
     */
    get(name) {
      const prefix = "Failed to execute 'get' on 'Headers'";
      webidl.requiredArguments(arguments.length, 1, { prefix });
      name = webidl.converters["ByteString"](name, {
        prefix,
        context: "Argument 1",
      });

      if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
        throw new TypeError("Header name is not valid.");
      }

      const list = this[_headerList];
      return getHeader(list, name);
    }

    /**
     * @param {string} name
     */
    has(name) {
      const prefix = "Failed to execute 'has' on 'Headers'";
      webidl.requiredArguments(arguments.length, 1, { prefix });
      name = webidl.converters["ByteString"](name, {
        prefix,
        context: "Argument 1",
      });

      if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
        throw new TypeError("Header name is not valid.");
      }

      const list = this[_headerList];
      name = byteLowerCase(name);
      for (let i = 0; i < list.length; i++) {
        if (list[i][0] === name) {
          return true;
        }
      }
      return false;
    }

    /**
     * @param {string} name
     * @param {string} value
     */
    set(name, value) {
      webidl.assertBranded(this, Headers);
      const prefix = "Failed to execute 'set' on 'Headers'";
      webidl.requiredArguments(arguments.length, 2, { prefix });
      name = webidl.converters["ByteString"](name, {
        prefix,
        context: "Argument 1",
      });
      value = webidl.converters["ByteString"](value, {
        prefix,
        context: "Argument 2",
      });

      value = normalizeHeaderValue(value);

      // 2.
      if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
        throw new TypeError("Header name is not valid.");
      }
      if (
        value.includes("\x00") || value.includes("\x0A") ||
        value.includes("\x0D")
      ) {
        throw new TypeError("Header value is not valid.");
      }

      if (this[_guard] == "immutable") {
        throw new TypeError("Headers are immutable.");
      }

      const list = this[_headerList];
      name = byteLowerCase(name);
      let added = false;
      for (let i = 0; i < list.length; i++) {
        if (list[i][0] === name) {
          if (!added) {
            list[i][1] = value;
            added = true;
          } else {
            list.splice(i, 1);
            i--;
          }
        }
      }
      if (!added) {
        list.push([name, value]);
      }
    }

    [Symbol.for("Deno.customInspect")](inspect) {
      const headers = {};
      for (const header of this) {
        headers[header[0]] = header[1];
      }
      return `Headers ${inspect(headers)}`;
    }

    get [Symbol.toStringTag]() {
      return "Headers";
    }
  }

  webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1);

  webidl.configurePrototype(Headers);

  webidl.converters["HeadersInit"] = (V, opts) => {
    // Union for (sequence<sequence<ByteString>> or record<ByteString, ByteString>)
    if (webidl.type(V) === "Object" && V !== null) {
      if (V[Symbol.iterator] !== undefined) {
        return webidl.converters["sequence<sequence<ByteString>>"](V, opts);
      }
      return webidl.converters["record<ByteString, ByteString>"](V, opts);
    }
    throw webidl.makeException(
      TypeError,
      "The provided value is not of type '(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'",
      opts,
    );
  };
  webidl.converters["Headers"] = webidl.createInterfaceConverter(
    "Headers",
    Headers,
  );

  /**
   * @param {HeaderList} list
   * @param {"immutable" | "request" | "request-no-cors" | "response" | "none"} guard
   * @returns {Headers}
   */
  function headersFromHeaderList(list, guard) {
    const headers = webidl.createBranded(Headers);
    headers[_headerList] = list;
    headers[_guard] = guard;
    return headers;
  }

  /**
   * @param {Headers}
   * @returns {HeaderList}
   */
  function headerListFromHeaders(headers) {
    return headers[_headerList];
  }

  /**
   * @param {Headers}
   * @returns {"immutable" | "request" | "request-no-cors" | "response" | "none"}
   */
  function guardFromHeaders(headers) {
    return headers[_guard];
  }

  window.__bootstrap.headers = {
    Headers,
    headersFromHeaderList,
    headerListFromHeaders,
    fillHeaders,
    getDecodeSplitHeader,
    guardFromHeaders,
  };
})(this);