/*************************************************************
 *
 *  Copyright (c) 2018 The MathJax Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * @fileoverview  Creates configurations for webpacking of MathJax components
 *
 * @author dpvc@mathjax.org (Davide Cervone)
 */

const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');

/**************************************************************/

/**
 * @param {string} string  The string whose special characters are to be escaped
 * @return {string}        The string with regex special characters escaped
 */
function quoteRE(string) {
  return string.replace(/([\\.{}[\]()?*^$])/g, '\\$1')
}

/**
 * Creates the plugin needed for including jsdir in the output
 *
 * @param {string} js          The location of the compiled js files
 * @param {string} dir         The directory of the component being built
 * @return {any[]}             The plugin array (empty or with the conversion plugin)
 */
const PLUGINS = function (js, dir) {
  const mjdir = path.resolve(__dirname, '..', 'js');
  const jsdir = path.resolve(dir, js);
  const package = path.resolve(__dirname, '..', 'package.json');

  //
  //  Record the js directory for the pack command
  //
  return [new webpack.DefinePlugin({
    __JSDIR__: jsdir,
    PACKAGE_VERSION: `'${require(package).version}'`
  })];
};

/**
 * Creates the plugin needed for converting mathjax references to component/lib references
 *
 * @param {string} js          The location of the compiled js files
 * @param {string[]} lib       The component library directories to be linked against
 * @param {string} dir         The directory of the component being built
 * @return {any[]}             The plugin array (empty or with the conversion plugin)
 */
const RESOLVE = function (js, libs, dir) {
  const mjdir = path.resolve(__dirname, '..', 'js');
  const jsdir = path.resolve(dir, js);
  const mjRE = new RegExp('^(?:' + quoteRE(jsdir) + '|' + quoteRE(mjdir) + ')' + quoteRE(path.sep));
  const root = path.dirname(mjdir);

  //
  //  Add directory names to libraries
  //
  libs = libs.map(lib => path.join(lib.charAt(0) === '.' ? dir : root, lib) + path.sep);

  //
  // Function replace imported files by ones in the specified component lib directories.
  //
  const replaceLibs = (resource) => {
    //
    // The full file name to check.
    //
    const request = require.resolve(
      resource.request ? 
        resource.request.charAt(0) === '.' ? path.resolve(resource.path, resource.request) : resource.request :
      resource.path
    );
    //
    // Only check files in the MathJax js directory.
    //
    if (!request.match(mjRE)) return;
    //
    // Loop through the libraries and see if the imported file is there.
    //   If so, replace the request with the library version and return.
    //
    for (const lib of libs) {
      const file = request.replace(mjRE, lib);
      if (fs.existsSync(file)) {
        resource.path = file;
        resource.request = undefined;
        return;
      }
    }
  }

  //
  // A plugin that looks for files and modules to see if they need replacing with library versions.
  //
  class ResolveReplacementPlugin {
    apply(compiler) {
      compiler.hooks.file.tap(ResolveReplacementPlugin.name, replaceLibs);
      compiler.hooks.module.tap(ResolveReplacementPlugin.name, replaceLibs);
    }
  }

  return {plugins: [new ResolveReplacementPlugin()]};
}

/**
 * Add babel-loader to appropriate directories
 *
 * @param {string} dir    The directory for the component being built
 * @return {any}          The modules specification for the webpack configuration
 */
const MODULE = function (dir) {
  //
  // Only need to transpile our directory and components directory
  //
  const dirRE = (dir.substr(0, __dirname.length) === __dirname ? quoteRE(__dirname) :
                 '(?:' + quoteRE(__dirname) + '|' + quoteRE(dir) + ')');
  return {
    // NOTE: for babel transpilation
    rules: [{
      test: new RegExp(dirRE + quoteRE(path.sep) + '.*\\.js$'),
      exclude: new RegExp(quoteRE(path.join(path.dirname(__dirname), 'es5') + path.sep)),
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/env']
        }
      }
    }]
  }
};

/**
 * Create a webpack configuration for a distribution file
 *
 * @param {string} name       The name of the component to create
 * @param {string} js         The path to the compiled .js files
 * @param {string[]} libs     Array of paths to component lib directories to link against
 * @param {string} dir        The directory of the component buing built
 * @param {string} dist       The path to the directory where the component .js file will be placed
 *                              (defaults to es5 in the same directory as the js directory)
 */
const PACKAGE = function (name, js, libs, dir, dist) {
  const distDir = dist ? path.resolve(dir, dist) :
                         path.resolve(path.dirname(js), 'es5', path.dirname(name));
  name = path.basename(name);
  return {
    name: name,
    entry: path.join(dir, name + '.js'),
    output: {
      path: distDir,
      filename: name + (dist === '.' ? '.min.js' : '.js')
    },
    target: ['web', 'es5'],  // needed for IE11 and old browsers
    plugins: PLUGINS(js, dir),
    resolve: RESOLVE(js, libs, dir),
    module: MODULE(dir),
    performance: {
      hints: false
    },
    optimization: {
      minimize: true,
      minimizer: [new TerserPlugin({
        extractComments: false,
        terserOptions: {
          output: {
            ascii_only: true
          }
        }
      })]
    },
    mode: 'production'
  };
}

module.exports = PACKAGE;
