// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

// Heavily inspired (and slightly tweaked) from:
// https://github.com/jupyterlab/jupyterlab/blob/master/examples/federated/core_package/rspack.config.js

const fs = require('fs-extra');
const path = require('path');
const rspack = require('@rspack/core');
const merge = require('webpack-merge').default;
const Handlebars = require('handlebars');
const { ModuleFederationPlugin } = rspack.container;
const BundleAnalyzerPlugin =
  require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const Build = require('@jupyterlab/builder').Build;
const WPPlugin = require('@jupyterlab/builder').WPPlugin;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const baseConfig = require('@jupyterlab/builder/lib/webpack.config.base');

const data = require('./package.json');

const names = Object.keys(data.dependencies).filter((name) => {
  const packageData = require(path.join(name, 'package.json'));
  return packageData.jupyterlab !== undefined;
});

// Ensure a clear build directory.
const buildDir = path.resolve(__dirname, 'build');
if (fs.existsSync(buildDir)) {
  fs.removeSync(buildDir);
}
fs.ensureDirSync(buildDir);

// Handle the extensions.
const { mimeExtensions, plugins } = data.jupyterlab;

// Create the list of extension packages from the package.json metadata
const extensionPackages = new Set();
Object.keys(plugins).forEach((page) => {
  const pagePlugins = plugins[page];
  Object.keys(pagePlugins).forEach((name) => {
    extensionPackages.add(name);
  });
});

Handlebars.registerHelper('json', function (context) {
  return JSON.stringify(context);
});

// custom help to check if a page corresponds to a value
Handlebars.registerHelper('ispage', function (key, page) {
  return key === page;
});

// custom helper to load the plugins on the index page
Handlebars.registerHelper('list_plugins', function () {
  let str = '';
  const page = this;
  Object.keys(this).forEach((extension) => {
    const plugin = page[extension];
    if (plugin === true) {
      str += `require(\'${extension}\'),\n  `;
    } else if (Array.isArray(plugin)) {
      const plugins = plugin.map((p) => `'${p}',`).join('\n');
      str += `
      require(\'${extension}\').default.filter(({id}) => [
       ${plugins}
      ].includes(id)),
      `;
    }
  });
  return str;
});

// Create the entry point and other assets in build directory.
const source = fs.readFileSync('index.template.js').toString();
const template = Handlebars.compile(source);
const extData = {
  notebook_plugins: plugins,
  notebook_mime_extensions: mimeExtensions,
};
const indexOut = template(extData);
fs.writeFileSync(path.join(buildDir, 'index.js'), indexOut);

// Copy extra files
const cssImports = path.resolve(__dirname, 'style.js');
fs.copySync(cssImports, path.resolve(buildDir, 'extraStyle.js'));

const extras = Build.ensureAssets({
  packageNames: names,
  output: buildDir,
  schemaOutput: path.resolve(__dirname, '..', 'notebook'),
});

/**
 * Create the rspack ``shared`` configuration
 */
function createShared(packageData) {
  // Set up module federation sharing config
  const shared = {};

  // Make sure any resolutions are shared
  for (let [pkg, requiredVersion] of Object.entries(packageData.resolutions)) {
    shared[pkg] = { requiredVersion };
  }

  // Add any extension packages that are not in resolutions (i.e., installed from npm)
  for (let pkg of extensionPackages) {
    if (!shared[pkg]) {
      shared[pkg] = {
        requiredVersion: require(`${pkg}/package.json`).version,
      };
    }
  }

  // Add dependencies and sharedPackage config from extension packages if they
  // are not already in the shared config. This means that if there is a
  // conflict, the resolutions package version is the one that is shared.
  const extraShared = [];
  for (let pkg of extensionPackages) {
    let pkgShared = {};
    let {
      dependencies = {},
      jupyterlab: { sharedPackages = {} } = {},
    } = require(`${pkg}/package.json`);
    for (let [dep, requiredVersion] of Object.entries(dependencies)) {
      if (!shared[dep]) {
        pkgShared[dep] = { requiredVersion };
      }
    }

    // Overwrite automatic dependency sharing with custom sharing config
    for (let [dep, config] of Object.entries(sharedPackages)) {
      if (config === false) {
        delete pkgShared[dep];
      } else {
        if ('bundled' in config) {
          config.import = config.bundled;
          delete config.bundled;
        }
        pkgShared[dep] = config;
      }
    }
    extraShared.push(pkgShared);
  }

  // Now merge the extra shared config
  const mergedShare = {};
  for (let sharedConfig of extraShared) {
    for (let [pkg, config] of Object.entries(sharedConfig)) {
      // Do not override the basic share config from resolutions
      if (shared[pkg]) {
        continue;
      }

      // Add if we haven't seen the config before
      if (!mergedShare[pkg]) {
        mergedShare[pkg] = config;
        continue;
      }

      // Choose between the existing config and this new config. We do not try
      // to merge configs, which may yield a config no one wants
      let oldConfig = mergedShare[pkg];

      // if the old one has import: false, use the new one
      if (oldConfig.import === false) {
        mergedShare[pkg] = config;
      }
    }
  }

  Object.assign(shared, mergedShare);

  // Transform any file:// requiredVersion to the version number from the
  // imported package. This assumes (for simplicity) that the version we get
  // importing was installed from the file.
  for (let [pkg, { requiredVersion }] of Object.entries(shared)) {
    if (requiredVersion && requiredVersion.startsWith('file:')) {
      shared[pkg].requiredVersion = require(`${pkg}/package.json`).version;
    }
  }

  // Add singleton package information
  for (let pkg of packageData.jupyterlab.singletonPackages) {
    if (shared[pkg]) {
      shared[pkg].singleton = true;
    }
  }

  return shared;
}

// Make a bootstrap entrypoint
const entryPoint = path.join(buildDir, 'bootstrap.js');
const bootstrap = 'import("./index.js");';
fs.writeFileSync(entryPoint, bootstrap);

if (process.env.NODE_ENV === 'production') {
  baseConfig.mode = 'production';
}

if (process.argv.includes('--analyze')) {
  extras.push(new BundleAnalyzerPlugin());
}

const htmlPlugins = [];
['consoles', 'edit', 'error', 'notebooks', 'terminals', 'tree'].forEach(
  (name) => {
    htmlPlugins.push(
      new HtmlWebpackPlugin({
        chunksSortMode: 'none',
        template: path.join(
          path.resolve('./templates'),
          `${name}_template.html`
        ),
        title: name,
        filename: path.join(
          path.resolve(__dirname, '..', 'notebook/templates'),
          `${name}.html`
        ),
      })
    );
  }
);

module.exports = [
  merge(baseConfig, {
    mode: 'development',
    entry: ['./publicpath.js', './' + path.relative(__dirname, entryPoint)],
    output: {
      path: path.resolve(__dirname, '..', 'notebook/static/'),
      publicPath: '{{page_config.fullStaticUrl}}/',
      library: {
        type: 'var',
        name: ['_JUPYTERLAB', 'CORE_OUTPUT'],
      },
      filename: '[name].[contenthash].js',
    },
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          jlab_core: {
            test: /[\\/]node_modules[\\/]@(jupyterlab|jupyter-notebook|lumino(?!\/datagrid))[\\/]/,
            name: 'notebook_core',
          },
        },
      },
    },
    resolve: {
      fallback: { util: false },
    },
    plugins: [
      ...htmlPlugins,
      new WPPlugin.JSONLicenseWebpackPlugin({
        excludedPackageTest: (packageName) =>
          packageName === '@jupyter-notebook/app',
      }),
      new ModuleFederationPlugin({
        library: {
          type: 'var',
          name: ['_JUPYTERLAB', 'CORE_LIBRARY_FEDERATION'],
        },
        name: 'CORE_FEDERATION',
        shared: createShared(data),
      }),
    ],
  }),
].concat(extras);

const logPath = path.join(buildDir, 'build_log.json');
fs.writeFileSync(logPath, JSON.stringify(module.exports, null, '  '));
