// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
const TerserPlugin = require("terser-webpack-plugin");
const minimist = require('minimist');

// commandline args
const args = minimist(process.argv);
const bundleMode = args['bundle-mode'] || 'prod';  // 'prod'|'dev'|'perf'|'node'|undefined;
const useAnalyzer = !!args.a || !!args['use-analyzer'];  // -a, --use-analyzer
const filter = args.f || args.filter;

const VERSION = require(path.join(__dirname, 'package.json')).version;
const COPYRIGHT_BANNER = `/*!
* ONNX Runtime Web v${VERSION}
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/`;

function terserEcmaVersionFromWebpackTarget(target) {
  switch (target) {
    case 'es5':
      return 5;
    case 'es6':
    case 'es2015':
      return 2015;
    case 'es2017':
      return 2017;
    default:
      throw new RangeError(`not supported ECMA version: ${target}`);
  }
}

function defaultTerserPluginOptions(target) {
  return {
    extractComments: false,
    terserOptions: {
      ecma: terserEcmaVersionFromWebpackTarget(target),
      format: {
        comments: false,
      },
      compress: {
        passes: 2
      },
      mangle: {
        reserved: ["_scriptDir"]
      }
    }
  };
}

const DEFAULT_BUILD_DEFS = {
  DISABLE_WEBGL: false,
  DISABLE_WASM: false,
  DISABLE_WASM_PROXY: false,
  DISABLE_WASM_THREAD: false,
};

// common config for release bundle
function buildConfig({ filename, format, target, mode, devtool, build_defs }) {
  const config = {
    target: [format === 'commonjs' ? 'node' : 'web', target],
    entry: path.resolve(__dirname, 'lib/index.ts'),
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename,
      library: {
        type: format
      }
    },
    resolve: {
      extensions: ['.ts', '.js'],
      alias: {
        "util": false,
      },
      fallback: {
        "fs": false,
        "path": false,
        "util": false,
        "os": false,
        "worker_threads": false,
        "perf_hooks": false,
      }
    },
    plugins: [
      new webpack.DefinePlugin({ BUILD_DEFS: build_defs }),
      new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/] })
    ],
    module: {
      rules: [{
        test: /\.ts$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              compilerOptions: { target }
            }
          }
        ]
      }, {
        test: /ort-wasm.*\.worker\.js$/,
        type: 'asset/source'
      }]
    },
    mode,
    devtool
  };

  if (useAnalyzer) {
    config.plugins.unshift(new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: `${filename}.report.html`
    }));
  }

  if (mode === 'production') {
    config.resolve.alias['./binding/ort-wasm-threaded.js'] = './binding/ort-wasm-threaded.min.js';
    config.resolve.alias['./binding/ort-wasm-threaded.worker.js'] = './binding/ort-wasm-threaded.min.worker.js';

    const options = defaultTerserPluginOptions(target);
    options.terserOptions.format.preamble = COPYRIGHT_BANNER;
    config.plugins.push(new TerserPlugin(options));

    // add a custom plugin to check whether code contains 'BUILD_DEFS'
    config.plugins.push({
      apply: (compiler) => {
        compiler.hooks.afterCompile.tap(
          'Check BUILD_DEFS',
          (compilation) => {
            for (const filename of compilation.assetsInfo.keys()) {
              if (filename.endsWith('.js')) {
                const asset = compilation.assets[filename];
                if (asset) {
                  const content = asset.source();
                  if (typeof content !== 'string') {
                    throw new Error(`content for target file '${filename}' is not string.`);
                  }
                  if (content.includes('DISABLE_WEBGL')
                    || content.includes('DISABLE_WASM')
                    || content.includes('DISABLE_WASM_PROXY')
                    || content.includes('DISABLE_WASM_THREAD')) {
                    throw new Error(`target file '${filename}' contains data fields from "BUILD_DEFS".`);
                  }
                }
              }
            }
          });
      }
    });
  } else {
    config.plugins.push(new webpack.BannerPlugin({ banner: COPYRIGHT_BANNER, raw: true }));
  }

  return config;
}

// "ort{.min}.js" config
function buildOrtConfig({
  suffix = '',
  target = 'es2017',
  mode = 'production',
  devtool = 'source-map',
  build_defs = DEFAULT_BUILD_DEFS
}) {
  const config = buildConfig({ filename: `ort${suffix}.js`, format: 'umd', target, mode, devtool, build_defs });
  // set global name 'ort'
  config.output.library.name = 'ort';
  return config;
}

// "ort-web{.min|.node}.js" config
function buildOrtWebConfig({
  suffix = '',
  format = 'umd',
  target = 'es2017',
  mode = 'production',
  devtool = 'source-map',
  build_defs = DEFAULT_BUILD_DEFS
}) {
  const config = buildConfig({ filename: `ort-web${suffix}.js`, format, target, mode, devtool, build_defs });
  // exclude onnxruntime-common from bundle
  config.externals = {
    'onnxruntime-common': {
      commonjs: "onnxruntime-common",
      commonjs2: "onnxruntime-common",
      root: 'ort'
    }
  };
  // in nodejs, treat as external dependencies
  if (format === 'commonjs') {
    config.externals.path = 'path';
    config.externals.fs = 'fs';
    config.externals.util = 'util';
    config.externals.worker_threads = 'worker_threads';
    config.externals.perf_hooks = 'perf_hooks';
    config.externals.os = 'os';
  }
  return config;
}

function buildTestRunnerConfig({
  suffix = '',
  format = 'umd',
  target = 'es2017',
  mode = 'production',
  devtool = 'source-map'
}) {
  const config = {
    target: ['web', target],
    entry: path.resolve(__dirname, 'test/test-main.ts'),
    output: {
      path: path.resolve(__dirname, 'test'),
      filename: `ort${suffix}.js`,
      library: {
        type: format
      },
      devtoolNamespace: '',
    },
    externals: {
      'onnxruntime-common': 'ort',
      'fs': 'fs',
      'perf_hooks': 'perf_hooks',
      'worker_threads': 'worker_threads',
      '../../node': '../../node'
    },
    resolve: {
      alias: {
        // make sure to refer to original source files instead of generated bundle in test-main.
        '..$': '../lib/index'
      },
      extensions: ['.ts', '.js'],
      fallback: {
        './binding/ort-wasm.js': false,
        './binding/ort-wasm-threaded.js': false,
        './binding/ort-wasm-threaded.worker.js': false
      }
    },
    plugins: [
      new webpack.DefinePlugin({ BUILD_DEFS: DEFAULT_BUILD_DEFS }),
      new webpack.WatchIgnorePlugin({ paths: [/\.js$/, /\.d\.ts$/] }),
      new NodePolyfillPlugin({
        excludeAliases: ["console", "Buffer"]
      }),
    ],
    module: {
      rules: [{
        test: /\.ts$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              compilerOptions: { target }
            }
          }
        ]
      }, {
        test: /ort-wasm.*\.worker\.js$/,
        type: 'asset/source'
      }]
    },
    mode: mode,
    devtool: devtool,
  };

  if (mode === 'production') {
    config.plugins.push(new TerserPlugin(defaultTerserPluginOptions(target)));
  }

  return config;
}

module.exports = () => {
  const builds = [];

  switch (bundleMode) {
    case 'prod':
      builds.push(
        // ort.min.js
        buildOrtConfig({ suffix: '.min' }),
        // ort.js
        buildOrtConfig({ mode: 'development', devtool: 'inline-source-map' }),
        // ort.es6.min.js
        buildOrtConfig({ suffix: '.es6.min', target: 'es6' }),
        // ort.es5.min.js
        buildOrtConfig({ suffix: '.es5.min', target: 'es5' }),

        // ort.wasm.min.js
        buildOrtConfig({
          suffix: '.wasm.min', build_defs: {
            ...DEFAULT_BUILD_DEFS,
            DISABLE_WEBGL: true,
          }
        }),
        // ort.webgl.min.js
        buildOrtConfig({
          suffix: '.webgl.min', build_defs: {
            ...DEFAULT_BUILD_DEFS,
            DISABLE_WASM: true,
          }
        }),
        // ort.wasm-core.min.js
        buildOrtConfig({
          suffix: '.wasm-core.min', build_defs: {
            ...DEFAULT_BUILD_DEFS,
            DISABLE_WEBGL: true,
            DISABLE_WASM_PROXY: true,
            DISABLE_WASM_THREAD: true,
          }
        }),

        // ort-web.min.js
        buildOrtWebConfig({ suffix: '.min' }),
        // ort-web.js
        buildOrtWebConfig({ mode: 'development', devtool: 'inline-source-map' }),
        // ort-web.es6.min.js
        buildOrtWebConfig({ suffix: '.es6.min', target: 'es6' }),
        // ort-web.es5.min.js
        buildOrtWebConfig({ suffix: '.es5.min', target: 'es5' }),
      );

    case 'node':
      builds.push(
        // ort-web.node.js
        buildOrtWebConfig({ suffix: '.node', format: 'commonjs' }),
      );
      break;
    case 'dev':
      builds.push(buildTestRunnerConfig({ suffix: '.dev', mode: 'development', devtool: 'inline-source-map' }));
      break;
    case 'perf':
      builds.push(buildTestRunnerConfig({ suffix: '.perf' }));
      break;
    default:
      throw new Error(`unsupported bundle mode: ${bundleMode}`);
  }

  if (filter) {
    const filterRegex = new RegExp(filter);
    return builds.filter(b => filterRegex.test(b.output.filename));
  }

  return builds;
};
