#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

const filenames = process.argv.slice(2); // Trim off node and script name.

//////////////////////////////////////////////////////////////////////
// Load deps files via require (since they're executalbe .js files).
//////////////////////////////////////////////////////////////////////

/**
 * Dictionary mapping goog.module ID to absolute pathname of the file
 * containing the goog.declareModuleId for that ID.
 * @type {!Object<string, string>}
 */
const modulePaths = {};

/** Absolute path of repository root. */
const repoPath = path.resolve(__dirname, '..', '..');

/**
 * Absolute path of directory containing base.js (the version used as
 * input to tsc, not the one output by it).
 * @type {string}
 */
const closurePath = path.resolve(repoPath, 'closure', 'goog');

globalThis.goog = {};

/**
 * Stub version of addDependency that store mappings in modulePaths.
 * @param {string} relPath The path to the js file.
 * @param {!Array<string>} provides An array of strings with
 *     the names of the objects this file provides.
 * @param {!Array<string>} _requires An array of strings with
 *     the names of the objects this file requires (unused).
 * @param {boolean|!Object<string>=} opt_loadFlags Parameters indicating
 *     how the file must be loaded.  The boolean 'true' is equivalent
 *     to {'module': 'goog'} for backwards-compatibility.  Valid properties
 *     and values include {'module': 'goog'} and {'lang': 'es6'}.
 */
goog.addDependency = function (relPath, provides, _requires, opt_loadFlags) {
  // Ignore any non-ESM files, as they can't be imported.
  if (opt_loadFlags?.module !== 'es6') return;

  // There should be only one "provide" from an ESM, but...
  for (const moduleId of provides) {
    // Store absolute path to source file (i.e., treating relPath
    // relative to closure/goog/, not build/src/closure/goog/).
    modulePaths[moduleId] = path.resolve(closurePath, relPath);
  }
};

// Load deps files relative to this script's location.
require(path.resolve(__dirname, '../../build/deps.js'));

//////////////////////////////////////////////////////////////////////
// Process files mentioned on the command line.
//////////////////////////////////////////////////////////////////////

/** RegExp matching goog.require statements. */
const requireRE =
  /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s+=\s+)?goog.require(Type)?\('([^']+)'\);/gm;

/** RegExp matching key: value pairs in destructuring assignments. */
const keyValueRE = /([$\w]+)\s*:\s*([$\w]+)\s*(?=,|})/g;

for (const filename of filenames) {
  let contents = null;
  try {
    contents = String(fs.readFileSync(filename));
  } catch (e) {
    console.error(`error while reading ${filename}: ${e.message}`);
    continue;
  }
  console.log(`Converting ${filename} to TypeScript...`);

  // Remove "use strict".
  contents = contents.replace(/^\s*["']use strict["']\s*; *\n/m, '');

  // Migrate from goog.module to goog.declareModuleId.
  const closurePathRelative = path.relative(
    path.dirname(path.resolve(filename)),
    closurePath,
  );
  contents = contents.replace(
    /^goog.module\('([$\w.]+)'\);$/m,
    `import * as goog from '${closurePathRelative}/goog.js';\n` +
      `goog.declareModuleId('$1');`,
  );

  // Migrate from goog.require to import.
  contents = contents.replace(
    requireRE,
    function (
      orig, // Whole statement to be replaced.
      name, // Name of named import of whole module (if applicable).
      names, // {}-enclosed list of destructured imports.
      type, // If truthy, it is a requireType not require.
      moduleId, // goog.module ID that was goog.require()d.
    ) {
      const importPath = modulePaths[moduleId];
      type = type ? ' type' : '';
      if (!importPath) {
        console.warn(
          `Unable to migrate goog.require('${moduleId}') as no ES module path known.`,
        );
        return orig;
      }
      let relativePath = path.relative(
        path.dirname(path.resolve(filename)),
        importPath,
      );
      if (relativePath[0] !== '.') relativePath = './' + relativePath;
      if (name) {
        return `import${type} * as ${name} from '${relativePath}';`;
      } else if (names) {
        names = names.replace(keyValueRE, '$1 as $2');
        return `import${type} ${names} from '${relativePath}';`;
      } else {
        // Side-effect only require.
        return `import${type} '${relativePath}';`;
      }
    },
  );

  // Find and update or remove old-style export assignemnts.
  /** @type {!Array<{name: string, re: RegExp>}>} */
  const easyExports = [];
  contents = contents.replace(
    /^\s*exports\.([$\w]+)\s*=\s*([$\w]+)\s*;\n/gm,
    function (
      orig, // Whole statement to be replaced.
      exportName, // Name to export item as.
      declName, // Already-declared name for item being exported.
    ) {
      // Renamed exports have to be transalted as-is.
      if (exportName !== declName) {
        return `export {${declName} as ${exportName}};\n`;
      }
      // OK, we're doing "export.foo = foo;".  Can we update the
      // declaration?  We can't actualy modify it yet as we're in
      // the middle of a search-and-replace on contents already, but
      // we can delete the old export and later update the
      // declaration into an export.
      const declRE = new RegExp(
        `^(\\s*)((?:const|let|var|function|class)\\s+${declName})\\b`,
        'gm',
      );
      if (contents.match(declRE)) {
        easyExports.push({exportName, declRE});
        return ''; // Delete existing export assignment.
      } else {
        return `export ${exportName};\n`; // Safe fallback.
      }
    },
  );
  // Add 'export' to existing declarations where appropriate.
  for (const {exportName, declRE} of easyExports) {
    contents = contents.replace(declRE, '$1export $2');
  }

  // Write converted file with new extension.
  const newFilename = filename.replace(/.js$/, '.ts');
  fs.writeFileSync(newFilename, contents);
  console.log(`Wrote ${newFilename}.`);
}
