'use strict';

const fs = require('fs');
const { builtinModules } = require('module');
const path = require('path');
const { debuglog } = require('util');
const getModuleType = require('module-definition');
const Walker = require('node-source-walk');
const detectiveAmd = require('detective-amd');
const detectiveCjs = require('detective-cjs');
const detectiveEs6 = require('detective-es6');
const detectiveLess = require('@dependents/detective-less');
const detectivePostcss = require('detective-postcss');
const detectiveSass = require('detective-sass');
const detectiveScss = require('detective-scss');
const detectiveStylus = require('detective-stylus');
const detectiveTypeScript = require('detective-typescript');
const detectiveVue = require('detective-vue2');

const debug = debuglog('precinct');

/**
 * Finds the list of dependencies for the given file
 *
 * @param {String|Object} content - File's content or AST
 * @param {Object} [options]
 * @param {String} [options.type] - The type of content being passed in. Useful if you want to use a non-JS detective
 * @param {Record<string, any>} [options.walker] - Options to pass to node-source-walk
 * @return {String[]}
 */
function precinct(content, options = {}) {
  debug('options given: %o', options);

  let ast;

  // We assume we're dealing with a JS file
  if (!options.type && typeof content !== 'object') {
    debug('we assume this is JS');
    const walker = new Walker(options.walker);

    try {
      // Parse once and distribute the AST to all detectives
      ast = walker.parse(content);
      debug('parsed the file content into an ast');
      precinct.ast = ast;
    } catch (error) {
      // In case a previous call had it populated
      precinct.ast = null;
      debug('could not parse content: %s', error.message);
      return [];
    }
  // SASS files shouldn't be parsed by Acorn
  } else {
    ast = content;

    if (typeof content === 'object') {
      precinct.ast = content;
    }
  }

  const type = options.type ?? getModuleType.fromSource(ast);
  debug('module type: %s', type);

  const detective = getDetective(type, options);
  let dependencies = [];

  if (detective) {
    dependencies = detective(ast, options[type]);
  } else {
    debug('no detective found for: %s', type);
  }

  // For non-JS files that we don't parse
  if (detective?.ast) {
    precinct.ast = detective.ast;
  }

  return dependencies;
}

/**
 * Returns the dependencies for the given file path
 *
 * @param {String} filename
 * @param {Object} [options]
 * @param {Boolean} [options.includeCore=true] - Whether or not to include core modules in the dependency list
 * @param {Object} [options.fileSystem=undefined] - An alternative fs implementation to use for reading the file path.
 * @param {Record<string, any>} [options.walker] - Options to pass to node-source-walk
 * @return {String[]}
 */
precinct.paperwork = (filename, options = {}) => {
  options = { includeCore: true, ...options };

  const fileSystem = options.fileSystem || fs;
  const content = fileSystem.readFileSync(filename, 'utf8');
  const extension = path.extname(filename);
  let type;

  if (extension === '.styl') {
    debug('paperwork: converting .styl into the stylus type');
    type = 'stylus';
  } else if (extension === '.cjs') {
    debug('paperwork: converting .cjs into the commonjs type');
    type = 'commonjs';
  // We need to sniff the JS module to find its type, not by extension.
  // Other possible types pass through normally
  } else if (!['.js', '.jsx'].includes(extension)) {
    debug('paperwork: stripping the dot from the extension to serve as the type');
    type = extension.replace('.', '');
  }

  if (type) {
    debug('paperwork: setting the module type');
    options.type = type;
  }

  debug('paperwork: invoking precinct');
  const dependencies = precinct(content, options);

  if (!options.includeCore) {
    return dependencies.filter(dependency => {
      if (dependency.startsWith('node:')) return false;

      // In Node.js 18, node:test is a builtin but shows up under natives["test"],
      // but can only be imported by "node:test." We're correcting this so "test"
      // isn't unnecessarily stripped from the imports
      if (dependency === 'test') {
        debug('paperwork: allowing test import to avoid builtin/natives consideration');
        return true;
      }

      return !builtinModules.includes(dependency);
    });
  }

  debug('paperwork: got these results\n', dependencies);
  return dependencies;
};

function getDetective(type, options) {
  const mixedMode = options.es6?.mixedImports;

  switch (type) {
    case 'cjs':
    case 'commonjs': {
      return mixedMode ? detectiveEs6Cjs : detectiveCjs;
    }

    case 'css': {
      return detectivePostcss;
    }

    case 'amd': {
      return detectiveAmd;
    }

    case 'mjs':
    case 'esm':
    case 'es6': {
      return mixedMode ? detectiveEs6Cjs : detectiveEs6;
    }

    case 'sass': {
      return detectiveSass;
    }

    case 'less': {
      return detectiveLess;
    }

    case 'scss': {
      return detectiveScss;
    }

    case 'stylus': {
      return detectiveStylus;
    }

    case 'ts': {
      return detectiveTypeScript;
    }

    case 'tsx': {
      return detectiveTypeScript.tsx;
    }

    case 'vue': {
      return detectiveVue;
    }

    default:
      // nothing
  }
}

function detectiveEs6Cjs(ast, detectiveOptions) {
  return [
    ...detectiveEs6(ast, detectiveOptions),
    ...detectiveCjs(ast, detectiveOptions)
  ];
}

module.exports = precinct;
