/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/* Shorthand constructors to construct an nsI(Local)File and zip reader: */
const LocalFile = new Components.Constructor(
  "@mozilla.org/file/local;1",
  Ci.nsIFile,
  "initWithPath"
);
const ZipReader = new Components.Constructor(
  "@mozilla.org/libjar/zip-reader;1",
  "nsIZipReader",
  "open"
);

const IS_ALPHA = /^[a-z]+$/i;

var { PerfTestHelpers } = ChromeUtils.importESModule(
  "resource://testing-common/PerfTestHelpers.sys.mjs"
);

const kESModuleList = new Set([
  /browser\/lockwise-card.js$/,
  /browser\/monitor-card.js$/,
  /browser\/proxy-card.js$/,
  /browser\/vpn-card.js$/,
  /toolkit\/content\/global\/certviewer\/components\/.*\.js$/,
  /toolkit\/content\/global\/certviewer\/.*\.js$/,
  /toolkit\/content\/global\/ml\/transformers.*\.js$/,
  /chrome\/pdfjs\/content\/web\/.*\.js$/,
]);

/**
 * Check if a URI should be parsed as an ES module.
 *
 * @param uri the uri to check against the ES module list
 * @return true if the uri should be parsed as a module, otherwise parse it as a script.
 */
function uriIsESModule(uri) {
  if (uri.filePath.endsWith(".mjs")) {
    return true;
  }

  for (let allowlistItem of kESModuleList) {
    if (allowlistItem.test(uri.spec)) {
      return true;
    }
  }
  return false;
}

/**
 * Returns a promise that is resolved with a list of files that have one of the
 * extensions passed, represented by their nsIURI objects, which exist inside
 * the directory passed.
 *
 * @param dir the directory which to scan for files (nsIFile)
 * @param extensions the extensions of files we're interested in (Array).
 */
function generateURIsFromDirTree(dir, extensions) {
  if (!Array.isArray(extensions)) {
    extensions = [extensions];
  }
  let dirQueue = [dir.path];
  return (async function () {
    let rv = [];
    while (dirQueue.length) {
      let nextDir = dirQueue.shift();
      let { subdirs, files } = await iterateOverPath(nextDir, extensions);
      dirQueue.push(...subdirs);
      rv.push(...files);
    }
    return rv;
  })();
}

/**
 * Iterate over the children of |path| and find subdirectories and files with
 * the given extension.
 *
 * This function recurses into ZIP and JAR archives as well.
 *
 * @param {string} path The path to check.
 * @param {string[]} extensions The file extensions we're interested in.
 *
 * @returns {Promise<object>}
 *           A promise that resolves to an object containing the following
 *           properties:
 *           - files: an array of nsIURIs corresponding to
 *             files that match the extensions passed
 *           - subdirs: an array of paths for subdirectories we need to recurse
 *             into (handled by generateURIsFromDirTree above)
 */
async function iterateOverPath(path, extensions) {
  const children = await IOUtils.getChildren(path);

  const files = [];
  const subdirs = [];

  for (const entry of children) {
    let stat;
    try {
      stat = await IOUtils.stat(entry);
    } catch (error) {
      if (error.name === "NotFoundError") {
        // Ignore symlinks from prior builds to subsequently removed files
        continue;
      }
      throw error;
    }

    if (stat.type === "directory") {
      subdirs.push(entry);
    } else if (extensions.some(extension => entry.endsWith(extension))) {
      if (await IOUtils.exists(entry)) {
        const spec = PathUtils.toFileURI(entry);
        files.push(Services.io.newURI(spec));
      }
    } else if (
      entry.endsWith(".ja") ||
      entry.endsWith(".jar") ||
      entry.endsWith(".zip") ||
      entry.endsWith(".xpi")
    ) {
      const file = new LocalFile(entry);
      for (const extension of extensions) {
        files.push(...generateEntriesFromJarFile(file, extension));
      }
    }
  }

  return { files, subdirs };
}

/* Helper function to generate a URI spec (NB: not an nsIURI yet!)
 * given an nsIFile object */
function getURLForFile(file) {
  let fileHandler = Services.io.getProtocolHandler("file");
  fileHandler = fileHandler.QueryInterface(Ci.nsIFileProtocolHandler);
  return fileHandler.getURLSpecFromActualFile(file);
}

/**
 * A generator that generates nsIURIs for particular files found in jar files
 * like omni.ja.
 *
 * @param jarFile an nsIFile object for the jar file that needs checking.
 * @param extension the extension we're interested in.
 */
function* generateEntriesFromJarFile(jarFile, extension) {
  let zr = new ZipReader(jarFile);
  const kURIStart = getURLForFile(jarFile);

  for (let entry of zr.findEntries("*" + extension + "$")) {
    // Ignore the JS cache which is stored in omni.ja
    if (entry.startsWith("jsloader") || entry.startsWith("jssubloader")) {
      continue;
    }
    let entryURISpec = "jar:" + kURIStart + "!/" + entry;
    yield Services.io.newURI(entryURISpec);
  }
  zr.close();
}

function fetchFile(uri) {
  return new Promise(resolve => {
    let xhr = new XMLHttpRequest();
    xhr.responseType = "text";
    xhr.open("GET", uri, true);
    xhr.onreadystatechange = function () {
      if (this.readyState != this.DONE) {
        return;
      }
      try {
        resolve(this.responseText);
      } catch (ex) {
        ok(false, `Script error reading ${uri}: ${ex}`);
        resolve("");
      }
    };
    xhr.onerror = error => {
      ok(false, `XHR error reading ${uri}: ${error}`);
      resolve("");
    };
    xhr.send(null);
  });
}

/**
 * Returns whether or not a word (presumably in en-US) is capitalized per
 * expectations.
 *
 * @param {string} word The single word to check.
 * @param {boolean} expectCapitalized True if the word should be capitalized.
 * @returns {boolean} True if the word matches the expected capitalization.
 */
function hasExpectedCapitalization(word, expectCapitalized) {
  let firstChar = word[0];
  if (!IS_ALPHA.test(firstChar)) {
    return true;
  }

  let isCapitalized = firstChar == firstChar.toLocaleUpperCase("en-US");
  return isCapitalized == expectCapitalized;
}
