"use strict";

const execa = require("execa");
const path = require("path");
const { rollup } = require("rollup");
const webpack = require("webpack");
const resolve = require("rollup-plugin-node-resolve");
const alias = require("@rollup/plugin-alias");
const commonjs = require("rollup-plugin-commonjs");
const nodeGlobals = require("rollup-plugin-node-globals");
const json = require("rollup-plugin-json");
const replace = require("@rollup/plugin-replace");
const { terser } = require("rollup-plugin-terser");
const babel = require("rollup-plugin-babel");
const nativeShims = require("./rollup-plugins/native-shims");
const executable = require("./rollup-plugins/executable");
const evaluate = require("./rollup-plugins/evaluate");
const externals = require("./rollup-plugins/externals");

const EXTERNALS = [
  "assert",
  "buffer",
  "constants",
  "crypto",
  "events",
  "fs",
  "module",
  "os",
  "path",
  "stream",
  "url",
  "util",
  "readline",

  // See comment in jest.config.js
  "graceful-fs"
];

function getBabelConfig(bundle) {
  const config = {
    babelrc: false,
    plugins: bundle.babelPlugins || [],
    compact: bundle.type === "plugin" ? false : "auto"
  };
  if (bundle.type === "core") {
    config.plugins.push(
      require.resolve("./babel-plugins/transform-custom-require")
    );
  }
  const targets = { node: 4 };
  if (bundle.target === "universal") {
    // From https://jamie.build/last-2-versions
    targets.browsers = [">0.25%", "not ie 11", "not op_mini all"];
  }
  config.presets = [
    [require.resolve("@babel/preset-env"), { targets, modules: false }]
  ];
  return config;
}

function getRollupConfig(bundle) {
  const config = {
    input: bundle.input,

    onwarn(warning) {
      if (
        // We use `eval("require")` to enable dynamic requires in the
        // custom parser API
        warning.code === "EVAL" ||
        // ignore `MIXED_EXPORTS` warn
        warning.code === "MIXED_EXPORTS" ||
        (warning.code === "CIRCULAR_DEPENDENCY" &&
          warning.importer.startsWith("node_modules"))
      ) {
        return;
      }

      // web bundle can't have external requires
      if (
        warning.code === "UNRESOLVED_IMPORT" &&
        bundle.target === "universal"
      ) {
        throw new Error(
          `Unresolved dependency in universal bundle: ${warning.source}`
        );
      }

      console.warn(warning);
    }
  };

  const replaceStrings = {
    "process.env.PRETTIER_TARGET": JSON.stringify(bundle.target),
    "process.env.NODE_ENV": JSON.stringify("production")
  };
  if (bundle.target === "universal") {
    // We can't reference `process` in UMD bundles and this is
    // an undocumented "feature"
    replaceStrings["process.env.PRETTIER_DEBUG"] = "global.PRETTIER_DEBUG";
  }
  Object.assign(replaceStrings, bundle.replace);

  const babelConfig = getBabelConfig(bundle);

  config.plugins = [
    replace({
      values: replaceStrings,
      delimiters: ["", ""]
    }),
    executable(),
    evaluate(),
    json(),
    bundle.alias && alias(bundle.alias),
    bundle.target === "universal" &&
      nativeShims(path.resolve(__dirname, "shims")),
    resolve({
      extensions: [".js", ".json"],
      preferBuiltins: bundle.target === "node"
    }),
    commonjs(
      Object.assign(
        bundle.target === "node" ? { ignoreGlobal: true } : {},
        bundle.commonjs
      )
    ),
    externals(bundle.externals),
    bundle.target === "universal" && nodeGlobals(),
    babelConfig && babel(babelConfig),
    bundle.type === "plugin" && terser()
  ].filter(Boolean);

  if (bundle.target === "node") {
    config.external = EXTERNALS;
  }

  return config;
}

function getRollupOutputOptions(bundle) {
  const options = {
    file: `dist/${bundle.output}`,
    strict: typeof bundle.strict === "undefined" ? true : bundle.strict,
    paths: [{ "graceful-fs": "fs" }]
  };

  if (bundle.target === "node") {
    options.format = "cjs";
  } else if (bundle.target === "universal") {
    options.format = "umd";
    options.name =
      bundle.type === "plugin" ? `prettierPlugins.${bundle.name}` : bundle.name;
  }
  return options;
}

function getWebpackConfig(bundle) {
  if (bundle.type !== "plugin" || bundle.target !== "universal") {
    throw new Error("Must use rollup for this bundle");
  }

  const root = path.resolve(__dirname, "..", "..");
  const config = {
    entry: path.resolve(root, bundle.input),
    module: {
      rules: [
        {
          test: /\.js$/,
          use: {
            loader: "babel-loader",
            options: getBabelConfig(bundle)
          }
        }
      ]
    },
    output: {
      path: path.resolve(root, "dist"),
      filename: bundle.output,
      library: ["prettierPlugins", bundle.name],
      libraryTarget: "umd",
      // https://github.com/webpack/webpack/issues/6642
      globalObject: 'new Function("return this")()'
    }
  };

  if (bundle.terserOptions) {
    const TerserPlugin = require("terser-webpack-plugin");

    config.optimization = {
      minimizer: [new TerserPlugin(bundle.terserOptions)]
    };
  }

  return config;
}

function runWebpack(config) {
  return new Promise((resolve, reject) => {
    webpack(config, err => {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
}

module.exports = async function createBundle(bundle, cache) {
  const inputOptions = getRollupConfig(bundle);
  const outputOptions = getRollupOutputOptions(bundle);

  const useCache = await cache.checkBundle(
    bundle.output,
    inputOptions,
    outputOptions
  );
  if (useCache) {
    try {
      await execa("cp", [
        path.join(cache.cacheDir, "files", bundle.output),
        "dist"
      ]);
      return { cached: true };
    } catch (err) {
      // Proceed to build
    }
  }

  if (bundle.bundler === "webpack") {
    await runWebpack(getWebpackConfig(bundle));
  } else {
    const result = await rollup(inputOptions);
    await result.write(outputOptions);
  }

  return { bundled: true };
};
