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

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

const core = globalThis.Deno.core;
const internals = globalThis.__bootstrap.internals;
const ops = core.ops;
const primordials = globalThis.__bootstrap.primordials;
const {
  ArrayPrototypeJoin,
  ArrayPrototypeMap,
  decodeURIComponent,
  Error,
  JSONStringify,
  NumberPrototypeToString,
  ObjectPrototypeIsPrototypeOf,
  SafeArrayIterator,
  SafeRegExp,
  String,
  StringPrototypeCharAt,
  StringPrototypeCharCodeAt,
  StringPrototypeMatch,
  StringPrototypePadStart,
  StringPrototypeReplace,
  StringPrototypeSlice,
  StringPrototypeSubstring,
  StringPrototypeToLowerCase,
  StringPrototypeToUpperCase,
  TypeError,
} = primordials;
import { URLPrototype } from "ext:deno_url/00_url.js";

const ASCII_DIGIT = ["\u0030-\u0039"];
const ASCII_UPPER_ALPHA = ["\u0041-\u005A"];
const ASCII_LOWER_ALPHA = ["\u0061-\u007A"];
const ASCII_ALPHA = [
  ...new SafeArrayIterator(ASCII_UPPER_ALPHA),
  ...new SafeArrayIterator(ASCII_LOWER_ALPHA),
];
const ASCII_ALPHANUMERIC = [
  ...new SafeArrayIterator(ASCII_DIGIT),
  ...new SafeArrayIterator(ASCII_ALPHA),
];

const HTTP_TAB_OR_SPACE = ["\u0009", "\u0020"];
const HTTP_WHITESPACE = [
  "\u000A",
  "\u000D",
  ...new SafeArrayIterator(HTTP_TAB_OR_SPACE),
];

const HTTP_TOKEN_CODE_POINT = [
  "\u0021",
  "\u0023",
  "\u0024",
  "\u0025",
  "\u0026",
  "\u0027",
  "\u002A",
  "\u002B",
  "\u002D",
  "\u002E",
  "\u005E",
  "\u005F",
  "\u0060",
  "\u007C",
  "\u007E",
  ...new SafeArrayIterator(ASCII_ALPHANUMERIC),
];
const HTTP_TOKEN_CODE_POINT_RE = new SafeRegExp(
  `^[${regexMatcher(HTTP_TOKEN_CODE_POINT)}]+$`,
);
const HTTP_QUOTED_STRING_TOKEN_POINT = [
  "\u0009",
  "\u0020-\u007E",
  "\u0080-\u00FF",
];
const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new SafeRegExp(
  `^[${regexMatcher(HTTP_QUOTED_STRING_TOKEN_POINT)}]+$`,
);
const HTTP_TAB_OR_SPACE_MATCHER = regexMatcher(HTTP_TAB_OR_SPACE);
const HTTP_TAB_OR_SPACE_PREFIX_RE = new SafeRegExp(
  `^[${HTTP_TAB_OR_SPACE_MATCHER}]+`,
  "g",
);
const HTTP_TAB_OR_SPACE_SUFFIX_RE = new SafeRegExp(
  `[${HTTP_TAB_OR_SPACE_MATCHER}]+$`,
  "g",
);
const HTTP_WHITESPACE_MATCHER = regexMatcher(HTTP_WHITESPACE);
const HTTP_BETWEEN_WHITESPACE = new SafeRegExp(
  `^[${HTTP_WHITESPACE_MATCHER}]*(.*?)[${HTTP_WHITESPACE_MATCHER}]*$`,
);
const HTTP_WHITESPACE_PREFIX_RE = new SafeRegExp(
  `^[${HTTP_WHITESPACE_MATCHER}]+`,
  "g",
);
const HTTP_WHITESPACE_SUFFIX_RE = new SafeRegExp(
  `[${HTTP_WHITESPACE_MATCHER}]+$`,
  "g",
);

/**
 * Turn a string of chars into a regex safe matcher.
 * @param {string[]} chars
 * @returns {string}
 */
function regexMatcher(chars) {
  const matchers = ArrayPrototypeMap(chars, (char) => {
    if (char.length === 1) {
      const a = StringPrototypePadStart(
        NumberPrototypeToString(StringPrototypeCharCodeAt(char, 0), 16),
        4,
        "0",
      );
      return `\\u${a}`;
    } else if (char.length === 3 && char[1] === "-") {
      const a = StringPrototypePadStart(
        NumberPrototypeToString(StringPrototypeCharCodeAt(char, 0), 16),
        4,
        "0",
      );
      const b = StringPrototypePadStart(
        NumberPrototypeToString(StringPrototypeCharCodeAt(char, 2), 16),
        4,
        "0",
      );
      return `\\u${a}-\\u${b}`;
    } else {
      throw TypeError("unreachable");
    }
  });
  return ArrayPrototypeJoin(matchers, "");
}

/**
 * https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
 * @param {string} input
 * @param {number} position
 * @param {(char: string) => boolean} condition
 * @returns {{result: string, position: number}}
 */
function collectSequenceOfCodepoints(input, position, condition) {
  const start = position;
  for (
    let c = StringPrototypeCharAt(input, position);
    position < input.length && condition(c);
    c = StringPrototypeCharAt(input, ++position)
  );
  return { result: StringPrototypeSlice(input, start, position), position };
}

const LOWERCASE_PATTERN = new SafeRegExp(/[a-z]/g);

/**
 * @param {string} s
 * @returns {string}
 */
function byteUpperCase(s) {
  return StringPrototypeReplace(
    String(s),
    LOWERCASE_PATTERN,
    function byteUpperCaseReplace(c) {
      return StringPrototypeToUpperCase(c);
    },
  );
}

/**
 * @param {string} s
 * @returns {string}
 */
function byteLowerCase(s) {
  // NOTE: correct since all callers convert to ByteString first
  // TODO(@AaronO): maybe prefer a ByteString_Lower webidl converter
  return StringPrototypeToLowerCase(s);
}

/**
 * https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
 * @param {string} input
 * @param {number} position
 * @param {boolean} extractValue
 * @returns {{result: string, position: number}}
 */
function collectHttpQuotedString(input, position, extractValue) {
  // 1.
  const positionStart = position;
  // 2.
  let value = "";
  // 3.
  if (input[position] !== "\u0022") throw new TypeError('must be "');
  // 4.
  position++;
  // 5.
  while (true) {
    // 5.1.
    const res = collectSequenceOfCodepoints(
      input,
      position,
      (c) => c !== "\u0022" && c !== "\u005C",
    );
    value += res.result;
    position = res.position;
    // 5.2.
    if (position >= input.length) break;
    // 5.3.
    const quoteOrBackslash = input[position];
    // 5.4.
    position++;
    // 5.5.
    if (quoteOrBackslash === "\u005C") {
      // 5.5.1.
      if (position >= input.length) {
        value += "\u005C";
        break;
      }
      // 5.5.2.
      value += input[position];
      // 5.5.3.
      position++;
    } else { // 5.6.
      // 5.6.1
      if (quoteOrBackslash !== "\u0022") throw new TypeError('must be "');
      // 5.6.2
      break;
    }
  }
  // 6.
  if (extractValue) return { result: value, position };
  // 7.
  return {
    result: StringPrototypeSubstring(input, positionStart, position + 1),
    position,
  };
}

/**
 * @param {Uint8Array} data
 * @returns {string}
 */
function forgivingBase64Encode(data) {
  return ops.op_base64_encode(data);
}

/**
 * @param {string} data
 * @returns {Uint8Array}
 */
function forgivingBase64Decode(data) {
  return ops.op_base64_decode(data);
}

// Taken from std/encoding/base64url.ts
/*
 * Some variants allow or require omitting the padding '=' signs:
 * https://en.wikipedia.org/wiki/Base64#The_URL_applications
 * @param base64url
 */
/**
 * @param {string} base64url
 * @returns {string}
 */
function addPaddingToBase64url(base64url) {
  if (base64url.length % 4 === 2) return base64url + "==";
  if (base64url.length % 4 === 3) return base64url + "=";
  if (base64url.length % 4 === 1) {
    throw new TypeError("Illegal base64url string!");
  }
  return base64url;
}

/**
 * @param {string} base64url
 * @returns {string}
 */
function convertBase64urlToBase64(base64url) {
  if (!/^[-_A-Z0-9]*?={0,2}$/i.test(base64url)) {
    // Contains characters not part of base64url spec.
    throw new TypeError("Failed to decode base64url: invalid character");
  }
  return addPaddingToBase64url(base64url).replace(/\-/g, "+").replace(
    /_/g,
    "/",
  );
}

/**
 * Encodes a given ArrayBuffer or string into a base64url representation
 * @param {ArrayBuffer | string} data
 * @returns {string}
 */
function forgivingBase64UrlEncode(data) {
  return forgivingBase64Encode(
    typeof data === "string" ? new TextEncoder().encode(data) : data,
  ).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}

/**
 * Converts given base64url encoded data back to original
 * @param {string} b64url
 * @returns {Uint8Array}
 */
function forgivingBase64UrlDecode(b64url) {
  return forgivingBase64Decode(convertBase64urlToBase64(b64url));
}

/**
 * @param {string} char
 * @returns {boolean}
 */
function isHttpWhitespace(char) {
  switch (char) {
    case "\u0009":
    case "\u000A":
    case "\u000D":
    case "\u0020":
      return true;
    default:
      return false;
  }
}

/**
 * @param {string} s
 * @returns {string}
 */
function httpTrim(s) {
  if (!isHttpWhitespace(s[0]) && !isHttpWhitespace(s[s.length - 1])) {
    return s;
  }
  return StringPrototypeMatch(s, HTTP_BETWEEN_WHITESPACE)?.[1] ?? "";
}

class AssertionError extends Error {
  constructor(msg) {
    super(msg);
    this.name = "AssertionError";
  }
}

/**
 * @param {unknown} cond
 * @param {string=} msg
 * @returns {asserts cond}
 */
function assert(cond, msg = "Assertion failed.") {
  if (!cond) {
    throw new AssertionError(msg);
  }
}

/**
 * @param {unknown} value
 * @returns {string}
 */
function serializeJSValueToJSONString(value) {
  const result = JSONStringify(value);
  if (result === undefined) {
    throw new TypeError("Value is not JSON serializable.");
  }
  return result;
}

const PATHNAME_WIN_RE = new SafeRegExp(/^\/*([A-Za-z]:)(\/|$)/);
const SLASH_WIN_RE = new SafeRegExp(/\//g);
const PERCENT_RE = new SafeRegExp(/%(?![0-9A-Fa-f]{2})/g);

// Keep in sync with `fromFileUrl()` in `std/path/win32.ts`.
/**
 * @param {URL} url
 * @returns {string}
 */
function pathFromURLWin32(url) {
  let p = StringPrototypeReplace(
    url.pathname,
    PATHNAME_WIN_RE,
    "$1/",
  );
  p = StringPrototypeReplace(
    p,
    SLASH_WIN_RE,
    "\\",
  );
  p = StringPrototypeReplace(
    p,
    PERCENT_RE,
    "%25",
  );
  let path = decodeURIComponent(p);
  if (url.hostname != "") {
    // Note: The `URL` implementation guarantees that the drive letter and
    // hostname are mutually exclusive. Otherwise it would not have been valid
    // to append the hostname and path like this.
    path = `\\\\${url.hostname}${path}`;
  }
  return path;
}

// Keep in sync with `fromFileUrl()` in `std/path/posix.ts`.
/**
 * @param {URL} url
 * @returns {string}
 */
function pathFromURLPosix(url) {
  if (url.hostname !== "") {
    throw new TypeError(`Host must be empty.`);
  }

  return decodeURIComponent(
    StringPrototypeReplace(
      url.pathname,
      PERCENT_RE,
      "%25",
    ),
  );
}

function pathFromURL(pathOrUrl) {
  if (ObjectPrototypeIsPrototypeOf(URLPrototype, pathOrUrl)) {
    if (pathOrUrl.protocol != "file:") {
      throw new TypeError("Must be a file URL.");
    }

    return core.build.os == "windows"
      ? pathFromURLWin32(pathOrUrl)
      : pathFromURLPosix(pathOrUrl);
  }
  return pathOrUrl;
}

// NOTE(bartlomieju): this is exposed on `internals` so we can test
// it in unit tests
internals.pathFromURL = pathFromURL;

export {
  ASCII_ALPHA,
  ASCII_ALPHANUMERIC,
  ASCII_DIGIT,
  ASCII_LOWER_ALPHA,
  ASCII_UPPER_ALPHA,
  assert,
  AssertionError,
  byteLowerCase,
  byteUpperCase,
  collectHttpQuotedString,
  collectSequenceOfCodepoints,
  forgivingBase64Decode,
  forgivingBase64Encode,
  forgivingBase64UrlDecode,
  forgivingBase64UrlEncode,
  HTTP_QUOTED_STRING_TOKEN_POINT,
  HTTP_QUOTED_STRING_TOKEN_POINT_RE,
  HTTP_TAB_OR_SPACE,
  HTTP_TAB_OR_SPACE_PREFIX_RE,
  HTTP_TAB_OR_SPACE_SUFFIX_RE,
  HTTP_TOKEN_CODE_POINT,
  HTTP_TOKEN_CODE_POINT_RE,
  HTTP_WHITESPACE,
  HTTP_WHITESPACE_PREFIX_RE,
  HTTP_WHITESPACE_SUFFIX_RE,
  httpTrim,
  pathFromURL,
  regexMatcher,
  serializeJSValueToJSONString,
};