/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

/**
 * Build: <srcdir>/tools/@types/generated/tspaths.json, and
 *        <srcdir>/tools/@types/generated/lib.gecko.modules.d.ts
 *
 * from:  various import forms in all *.js and *.mjs sources.
 */

const fs = require("fs");
const path = require("path");
const { fixed } = require("./config/fixed_paths.js");

const HEADER = `/**
 * NOTE: Do not modify this file by hand.
 * Content was generated by running "mach ts paths".
 */
`;

const IGNORE = [/\.git/, /\.hg/, /node_modules/, /^obj.*/, /test262/];
const IMPORT =
  /(\bimport |import\(|require\(|\.(importESModule|defineESModuleGetters?|declareLazy|defineLazy)\()[^;]+/gm;
const URI = /("|')((resource|chrome|moz-src):\/\/[\w\d\/_.-]+\.m?js)\1/gm;

function ignore(filePath) {
  return IGNORE.some(re => filePath.match(re));
}

// Scan the root dir for *.js and *.mjs files, recursivelly.
function scan(root, dir, files) {
  for (let file of fs.readdirSync(`${root}/${dir}`, { withFileTypes: true })) {
    if (file.isDirectory() && !ignore(dir + file.name)) {
      scan(root, `${dir}${file.name}/`, files);
    } else if (file.name.match(/\.(x?html|m?js)$/)) {
      files.push(dir + file.name);
    }
  }
}

// Emit path mapping for all found module URIs.
function emitPaths(files, uris, modules, relativeBasePath) {
  let paths = {};
  for (let uri of [...uris].sort()) {
    if (uri in fixed) {
      continue;
    }
    let parts = uri.split("/");
    let filePath = "";
    let matches;

    // Check for a substitution .d.ts file from processed/generated sources.
    let sub = parts.at(-1).replace(/\.(m)?js$/, ".d.$1ts");
    if (fs.existsSync(`${__dirname}/../@types/subs/${sub}`)) {
      paths[uri] = [`tools/@types/subs/${sub}`];
      continue;
    }

    // Starting with just the file name, add each path part going backwards.
    do {
      filePath = "/" + parts.pop() + filePath;
      matches = files.filter(f => f.endsWith(filePath));
      // While multiple files match, add parts used for filtering.
    } while (matches.length > 1 && parts.length);

    // Unique match is almost certainy correct.
    if (matches.length === 1) {
      paths[uri] = [matches[0]];
    } else {
      // URI matched more than one, or failed to match any file.
      console.warn("[WARN]", uri);
      modules.delete(uri);
    }
  }

  Object.assign(paths, fixed);
  let tspaths = { compilerOptions: { baseUrl: relativeBasePath, paths } };
  return JSON.stringify(tspaths, null, 2) + "\n";
}

// Emit type mapping for all modules imported via lazy getters.
function emitLazy(modules) {
  let lines = [HEADER];
  lines.push("export interface Modules {");
  for (let uri of [...modules].sort()) {
    lines.push(`  "${uri}": typeof import("${uri}"),`);
  }
  lines.push("}\n");
  return lines.join("\n");
}

function main(root_dir, paths_json, lib_lazy) {
  let files = [];
  scan(root_dir, "", files);

  let uris = new Set();
  let modules = new Set();

  for (let file of files) {
    let src = fs.readFileSync(`${root_dir}/${file}`, "utf-8");

    for (let [exp, , method] of src.matchAll(IMPORT)) {
      for (let [, , uri, proto] of exp.matchAll(URI)) {
        if (proto !== "moz-src") {
          uris.add(uri);
        }
        if (method?.match(/importESModule|ModuleGetter|Lazy/)) {
          modules.add(uri);
        }
      }
    }
  }

  let json = emitPaths(
    files,
    uris,
    modules,
    path.relative(path.dirname(paths_json), root_dir).replaceAll("\\", "/")
  );
  console.log(`[INFO] ${paths_json} (${json.length.toLocaleString()} bytes)`);
  fs.writeFileSync(paths_json, json);

  let lib = emitLazy(modules);
  console.log(`[INFO] ${lib_lazy} (${lib.length.toLocaleString()} bytes)`);
  fs.writeFileSync(lib_lazy, lib);
}

main(...process.argv.slice(2));
