#!/usr/bin/env node

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

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

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

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

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

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

/** Prefix for RegExp matching a top-level declaration. */
const declPrefix = '(?:const|let|var|(?:async\\s+)?function(?:\\s*\\*)?|class)';

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} from CJS to ESM...`);

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

  // Migrate from 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.
      moduleName, // Imported module name or path.
    ) {
      if (moduleName[0] === '.') {
        // Relative path.  Could check and add '.mjs' suffix if desired.
      }
      if (name) {
        return `import * as ${name} from '${moduleName}';`;
      } else if (names) {
        names = names.replace(keyValueRE, '$1 as $2');
        return `import ${names} from '${moduleName}';`;
      } else {
        // Side-effect only require.
        return `import '${moduleName}';`;
      }
    },
  );

  // Find and update or remove old-style single-export assignments
  // like:
  //
  // exports.bar = foo;  // becomes export {foo as bar};
  // exports.foo = foo;  // remove the export and export at declaration
  //                     // instead, if possible.
  /** @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 translated 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*)(${declPrefix}\\s+${declName})\\b`,
        'gm',
      );
      if (contents.match(declRE)) {
        easyExports.push({exportName, declRE});
        return ''; // Delete existing export assignment.
      } else {
        return `export ${exportName};\n`; // Safe fallback.
      }
    },
  );

  // Find and update or remove old-style module.exports assignment
  // like:
  //
  // module.exports = {foo, bar: baz, quux};
  //
  // which becomes export {baz as bar}, with foo and quux exported at
  // declaration instead, if possible.
  contents = contents.replace(
    /^module\.exports\s*=\s*\{([^\}]+)\};?(\n?)/m,
    function (
      orig, // Whole statement to be replaced.
      items, // List of items to be exported.
    ) {
      items = items.replace(
        /( *)([$\w]+)\s*(?::\s*([$\w]+)\s*)?,?(\s*?\n?)/gm,
        function (
          origItem, // Whole item being replaced.
          indent, // Optional leading whitespace.
          exportName, // Name to export item as.
          declName, // Already-declared name being exported, if different.
          newline, // Optional trailing whitespace.
        ) {
          if (!declName) declName = exportName;

          // Renamed exports have to be translated as-is.
          if (exportName !== declName) {
            return `${indent}${declName} as ${exportName},${newline}`;
          }
          // OK, this item has no rename.  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 item and later update the
          // declaration into an export.
          const declRE = new RegExp(
            `^(\\s*)(${declPrefix}\\s+${declName})\\b`,
            'gm',
          );
          if (contents.match(declRE)) {
            easyExports.push({exportName, declRE});
            return ''; // Delete existing item.
          } else {
            return `${indent}${exportName},${newline}`; // Safe fallback.
          }
        },
      );
      if (/^\s*$/s.test(items)) {
        // No items left?
        return ''; // Delete entire module.export assignment.
      } else {
        return `export {${items}};\n`;
      }
    },
  );

  // 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(/.c?js$/, '.mjs');
  fs.writeFileSync(newFilename, contents);
  console.log(`Wrote ${newFilename}.`);
}
