// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import { parse, print, types, visit } from 'recast';
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import { getMappings } from './get-mappings.js';

const readDir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const b = types.builders;

const FRONT_END_FOLDER = path.join(__dirname, '..', '..', 'front_end');

// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function rewriteSource(pathName: string, srcFile: string, mappings:Map<string, any>, useExternalRefs = false) {
  const filePath = path.join(pathName, srcFile);
  const srcFileContents = await readFile(filePath, { encoding: 'utf-8' });
  const ast = parse(srcFileContents);

  const importsRequired = new Set<{file: string, replacement: string, sameFolderReplacement: string}>();

  visit(ast, {
    visitComment(path) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const comments = (path.node as any).comments;
      for (const comment of comments) {

        if (comment.loc) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          (comment.loc as any).indent = 0;
        }

        for (const [str, value] of mappings.entries()) {
          const containsString = new RegExp(`${str}([^\\.\\w])`, 'g');
          const stringMatches = containsString.exec(comment.value);

          if (!stringMatches) {
            continue;
          }

          const replacement = useExternalRefs ? value.replacement : value.sameFolderReplacement;

          importsRequired.add(value);
          comment.value = comment.value.replace(stringMatches[0], replacement + stringMatches[0].slice(-1));
        }
      }

      this.traverse(path);
    },

    visitMemberExpression(path) {
      const node = path.node;
      const nodeCopy = b.memberExpression.from({...node, comments: []});
      const nodeAsCode = print(nodeCopy).code;

      for (const [str, value] of mappings.entries()) {
        if (nodeAsCode !== str) {
          continue;
        }

        const name = useExternalRefs ? value.replacement : value.sameFolderReplacement;

        importsRequired.add(value);
        return b.identifier.from({ name, comments: node.comments || [] });
      }

      this.traverse(path);
    },
  });

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const importMap = new Map<string, any[]>();
  for (const { file, sameFolderReplacement, replacement } of importsRequired) {
    if (filePath === file) {
      continue;
    }

    const src = path.dirname(filePath);
    const dst = path.dirname(file);

    let replacementIdentifier = '';
    let relativePath = path.relative(src, dst);
    const isSameFolder = relativePath === '';
    if (isSameFolder) {
      relativePath = './';
      replacementIdentifier = sameFolderReplacement;
    } else {
      relativePath += '/';
      replacementIdentifier = replacement;
    }

    const targetImportFile = relativePath + path.basename(file);

    if (!importMap.has(targetImportFile)) {
      importMap.set(targetImportFile, []);
    }

    const imports = importMap.get(targetImportFile);
    if (!imports) {
      throw new Error(`Expected to find imports for ${targetImportFile} but found none.`);
    }
    if (useExternalRefs) {
      if (imports.length === 0) {
        // We are creating statements like import * as Foo from '../foo/foo.js' so
        // here we take the first part of the identifier, e.g. Foo.Bar.Bar gives us
        // Foo so we can make import * as Foo from that.
        const namespaceIdentifier = replacementIdentifier.split('.')[0];
        imports.push(b.importNamespaceSpecifier(b.identifier(namespaceIdentifier)));
      }

      // Make sure there is only one import * from Foo import added.
      continue;
    }

    imports.push(b.importSpecifier(b.identifier(replacementIdentifier)));
  }

  // Add missing imports.
  for (const [targetImportFile, specifiers] of importMap) {
    const newImport = b.importDeclaration.from({
      specifiers,
      comments: ast.program.body[0].comments,
      source: b.literal(targetImportFile),
    });

    // Remove any file comments.
    ast.program.body[0].comments = [];

    // Add the import statements.
    ast.program.body.unshift(newImport);
  }

  return print(ast).code;
}

async function main(folder: string, namespaces?: string[]) {
  const pathName = path.join(FRONT_END_FOLDER, folder);
  const srcDir = await readDir(pathName);
  const useExternalRefs = namespaces !== undefined && (namespaces[0] !== folder);
  let mappings = new Map();
  if (namespaces && namespaces.length) {
    for (const namespace of namespaces) {
      mappings = await getMappings(namespace, mappings, useExternalRefs);
    }
  } else {
    mappings = await getMappings(folder, mappings, useExternalRefs);
  }

  for (const srcFile of srcDir) {
    if (srcFile === `${folder}.js` || srcFile === `${folder}-legacy.js` || !srcFile.endsWith('.js')) {
      continue;
    }

    const distFileContents = await rewriteSource(pathName, srcFile, mappings, useExternalRefs);
    await writeFile(path.join(pathName, `${srcFile}`), distFileContents);
  }
}

if (!process.argv[2]) {
  console.error('No arguments specified. Run this script with "<folder-name>". For example: "ui"');
  process.exit(1);
}

main(process.argv[2], process.argv[3] && process.argv[3].split(',') || undefined);
