const { deepmerge, createDebug, winPath, resolve } = require('@umijs/utils');

const terserOptions = require('./terserOptions');
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const { css, createCSSRule } = require('./css');
const Config = require('webpack-chain');
const pkg = require('../../package.json');
const ASSET_PATH = process.env.ASSET_PATH || '/';
const {
  ThemeColorReplacer,
  themePluginOption,
} = require('../../config/plugin.config');
let defineConfig = require('../../config/config');
const resolveDefine = require('./resolveDefine');
const { getPkgPath, shouldTransform } = require('./pkgMatch');
const {
  TYPE_ALL_EXCLUDE,
  isMatch,
  excludeToPkgs,
  es5ImcompatibleVersionsToPkg,
} = require('./nodeModulesTransform');
const lodash = require('lodash');
const {
  getTargetsAndBrowsersList,
  getBabelPresetOpts,
  getBabelOpts,
} = require('@umijs/bundler-utils');
const { join } = require('lodash');

function getUserLibDir({ library }) {
  if (
    (pkg.dependencies && pkg.dependencies[library]) ||
    (pkg.devDependencies && pkg.devDependencies[library]) ||
    // egg project using `clientDependencies` in ali tnpm
    (pkg.clientDependencies && pkg.clientDependencies[library])
  ) {
    return winPath(
      path.dirname(
        // 通过 resolve 往上找，可支持 lerna 仓库
        // lerna 仓库如果用 yarn workspace 的依赖不一定在 node_modules，可能被提到根目录，并且没有 link
        resolve.sync(`${library}/package.json`, {
          basedir: process.cwd(),
        }),
      ),
    );
  }
  return null;
}
const defaultConfig = {
  autoprefixer: {
    flexbox: 'no-2009',
  },
  cssnano: { mergeRules: false, minifyFontValues: { removeQuotes: false } },
  nodeModulesTransform: {
    type: 'all',
    exclude: [],
  },
  targets: {
    node: true,
    chrome: 49,
    firefox: 64,
    safari: 10,
    edge: 13,
    ios: 10,
    ie: 11,
  },
};
module.exports = options => {
  const chainConfig = new Config();
  chainConfig.mode(options.mode);
  defineConfig = Object.assign(defineConfig, defaultConfig);
  const env = process.env;
  const isDev = process.env.NODE_ENV === 'development';
  const isProd = process.env.NODE_ENV === 'production';
  const disableCompress = process.env.COMPRESS === 'none';
  const isWebpack5 = webpack.version.startsWith('5');

  const devtool = options.devtool || false;

  chainConfig.devtool(
    isDev
      ? devtool === false
        ? false
        : devtool || 'cheap-module-source-map'
      : devtool,
  );

  const useHash = defineConfig.hash && isProd;

  const absOutputPath = process.env.npm_config_releasepath
    ? path.resolve(
        process.env.npm_config_releasepath,
        pkg.name.toLocaleLowerCase(),
      )
    : path.resolve(process.cwd(), pkg.name.toLocaleLowerCase());

  chainConfig.output
    .path(absOutputPath)
    .filename(useHash ? `[name].[contenthash:8].js` : `[name].js`)
    .chunkFilename(
      useHash ? `[name].[contenthash:8].async.js` : `[name].chunk.js`,
    )
    .publicPath(`/${pkg.name.toLocaleLowerCase()}/`)
    .futureEmitAssets(true)
    .crossOriginLoading('anonymous')
    .pathinfo(isDev || disableCompress);

  chainConfig.resolve.modules
    .add('node_modules')
    .add('src')
    .end()
    .extensions.merge(['.js', '.jsx', '.react.js'])
    .end()
    .mainFields.merge(['browser', 'jsnext:main', 'main']);

  const libraries = [
    {
      name: 'react',
      path: path.dirname(require.resolve(`react/package.json`)),
    },
    {
      name: 'react-dom',
      path: path.dirname(require.resolve(`react-dom/package.json`)),
    },
  ];

  libraries.forEach(library => {
    chainConfig.resolve.alias.set(
      library.name,
      getUserLibDir({ library: library.name }) || library.path,
    );
  });

  chainConfig.resolve.alias.set('@', path.join(__dirname, '../../', 'src'));

  if (defineConfig.alias) {
    Object.keys(defineConfig.alias).forEach(key => {
      chainConfig.resolve.alias.set(key, defineConfig.alias[key]);
    });
  }

  chainConfig.target('web');

  const { targets, browserslist } = getTargetsAndBrowsersList({
    config: defineConfig,
    type: 'csr',
  });

  let presetOpts = getBabelPresetOpts({
    config: defineConfig,
    env: process.env.NODE_ENV,
    targets: targets,
  });

  let preset = require('./babel-preset');
  const getPreset = preset({
    ...presetOpts,
    env: {
      useBuiltIns: 'entry',
      corejs: 3,
      modules: false,
    },
    react: {
      development: isDev,
    },
    reactRemovePropTypes: isProd,
    reactRequire: true,
    lockCoreJS3: {},
    import: (presetOpts.import || []).concat([
      { libraryName: 'antd', libraryDirectory: 'es', style: true },
      { libraryName: 'antd-mobile', libraryDirectory: 'es', style: true },
    ]),
  });
  let babelOpts = {
    sourceType: 'unambiguous',
    babelrc: false,
    presets: [...getPreset.presets, ...(defineConfig.extraBabelPresets || [])],
    plugins: [
      ...getPreset.plugins,
      ...(defineConfig.extraBabelPlugins || []),
    ].filter(Boolean),
    env: getPreset.env,
  };

  chainConfig.module
    .rule('js')
    .test(/\.(js|mjs|jsx|ts|tsx)$/)
    .exclude.add(/node_modules/)
    .end()
    .use('babel-loader')
    .loader(require.resolve('babel-loader'))
    .options(babelOpts);

  if (defineConfig.extraBabelIncludes) {
    defineConfig.extraBabelIncludes.forEach((include, index) => {
      const rule = `extraBabelInclude_${index}`;
      chainConfig.module
        .rule(rule)
        .test(/\.(js|mjs|jsx)$/)
        .include.add(a => {
          if (path.isAbsolute(include)) {
            return path.isAbsolute(include);
          }
          if (!a.includes('node_modules')) return false;
          const pkgPath = getPkgPath(a);
          return shouldTransform(pkgPath, include);
        })
        .end()
        .use('babel-loader')
        .loader(require.resolve('babel-loader'))
        .options(babelOpts);
    });
  }

  chainConfig.module
    .rule('images')
    .test(/\.(png|jpe?g|gif|webp|ico)(\?.*)?$/)
    .use('url-loader')
    .loader(require.resolve('url-loader'))
    .options({
      limit: defineConfig.inlineLimit || 10000,
      name: 'static/[name].[hash:8].[ext]',
      // require 图片的时候不用加 .default
      esModule: false,
      fallback: {
        loader: require.resolve('file-loader'),
        options: {
          name: 'static/[name].[hash:8].[ext]',
          esModule: false,
        },
      },
    });

  chainConfig.module
    .rule('svg')
    .test(/\.(svg)(\?.*)?$/)
    .use('file-loader')
    .loader(require.resolve('file-loader'))
    .options({
      name: 'static/[name].[hash:8].[ext]',
      esModule: false,
    })
    .end()
    .use('svg-url-loader')
    .loader(require.resolve('svg-url-loader'))
    .options({
      limit: defineConfig.inlineLimit || 10000,
      // name: '[name].[ext]',
      noquotes: true,
    })
    .end();

  chainConfig.module
    .rule('fonts')
    .test(/\.(eot|woff|woff2|ttf)(\?.*)?$/)
    .use('file-loader')
    .loader(require.resolve('file-loader'))
    .options({
      name: '/static/[name].[hash:8].[ext]',
      esModule: false,
    });

  chainConfig.module
    .rule('mdeia')
    .test(/\.(mp4|webm)$/)
    .use('url-loader')
    .loader(require.resolve('url-loader'))
    .options({
      limit: 10000,
      outputPath: 'static',
    });

  chainConfig.module
    .rule('plaintext')
    .test(/\.(txt|text|md)$/)
    .use('raw-loader')
    .loader(require.resolve('raw-loader'));

  css({
    type: 'csr',
    config: defineConfig,
    webpackConfig: chainConfig,
    isDev,
    disableCompress,
    browserslist,
    miniCSSExtractPluginPath: '',
    miniCSSExtractPluginLoaderPath: '',
  });

  if (defineConfig.externals) {
    chainConfig.externals(defineConfig.externals);
  }

  chainConfig.node.merge({
    setImmediate: false,
    module: 'empty',
    dns: 'mock',
    http2: 'empty',
    process: 'mock',
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty',
    ...options.node,
  });

  if (defineConfig.ignoreMomentLocale) {
    chainConfig.plugin('ignore-moment-locale').use(webpack.IgnorePlugin, [
      {
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
      },
    ]);
  }

  chainConfig.plugin('define').use(webpack.DefinePlugin, [
    resolveDefine({
      define: Object.assign(defineConfig.define || {}, {
        'process.env.ASSET_PATH': JSON.stringify(ASSET_PATH),
        'process.env.MOCK': JSON.stringify(true),
        paths: {
          cwd: process.cwd(),
          absNodeModulesPath: path.join(process.cwd(), 'node_modules'),
          absSrcPath: path.join(process.cwd(), 'src'),
          absPagesPath: path.join(process.cwd(), 'src/pages'),
        },
      }),
    }),
  ]);

  const baseHtmlOptions = {
    inject: true,
    template: 'src/index.ejs',
    favicon: defineConfig.favicon ? defineConfig.favicon : false,
    title:
      defineConfig.title && defineConfig.title !== false
        ? defineConfig.title
        : '',
    chunks: defaultConfig.chunks ? defaultConfig.chunks : 'all',
  };
  const htmlPluginOptions = isDev
    ? {
        ...babelOpts,
      }
    : {
        ...baseHtmlOptions,
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeRedundantAttributes: true,
          useShortDoctype: true,
          removeEmptyAttributes: true,
          removeStyleLinkTypeAttributes: true,
          keepClosingSlash: true,
          minifyJS: true,
          minifyCSS: true,
          minifyURLs: true,
        },
      };

  chainConfig
    .plugin('htmlPlugins')
    .use(require.resolve('html-webpack-plugin'), [htmlPluginOptions]);

  chainConfig
    .plugin('updateHtmlParams')
    .use(require.resolve('./plugins/HtmlGenerator'), [
      {
        config: defineConfig,
      },
    ]);

  chainConfig
    .plugin('replaceTheme')
    .use(ThemeColorReplacer, [themePluginOption]);

  const cwd = process.cwd();
  const copyPatterns = [
    fs.existsSync(path.join(process.cwd(), 'public')) && {
      from: path.join(cwd, 'public'),
      to: path.resolve(
        process.env.npm_config_releasepath || process.cwd(),
        pkg.name.toLocaleLowerCase(),
      ),
    },
    ...(defineConfig.copy
      ? defineConfig.copy.map(item => {
          if (typeof item === 'string') {
            return {
              from: path.join(cwd, item),
              to: path.resolve(
                process.env.npm_config_releasepath || process.cwd(),
                pkg.name.toLocaleLowerCase(),
              ),
            };
          }
          return {
            from: path.join(cwd, item.from),
            to: path.resolve(
              process.env.npm_config_releasepath || process.cwd(),
              pkg.name.toLocaleLowerCase() + '/' + item.to,
            ),
          };
        })
      : []),
  ].filter(Boolean);

  if (copyPatterns.length) {
    chainConfig
      .plugin('copy')
      .use(require.resolve('copy-webpack-plugin'), [copyPatterns]);
  }

  if (isDev && defineConfig.fastRefresh) {
    const debug = createDebug('civ:preset-build-in:fastRefresh');
    chainConfig
      .plugin('fastRefresh')
      .after('hmr')
      .use(require('@pmmmwh/react-refresh-webpack-plugin'), [
        { overlay: false },
      ]);
    debug('FastRefresh webpack loaded');
  }

  if (chainConfig.extraBabelIncludes) {
    chainConfig.extraBabelIncludes.forEach((include, index) => {
      const rule = `extraBabelInclude_${index}`;
      // prettier-ignore
      chainConfig.module
        .rule(rule)
          .test(/\.(js|mjs|jsx)$/)
            .include
            .add((a) => {
              // 支持绝对路径匹配
              if (path.isAbsolute(include)) {
                return path.isAbsolute(include);
              }

              // 支持 node_modules 下的 npm 包
              if (!a.includes('node_modules')) return false;
              const pkgPath = getPkgPath(a);
              return shouldTransform(pkgPath, include);
            })
            .end()
          .use('babel-loader')
            .loader(require.resolve('babel-loader'))
            .options(babelOpts);
    });
  }

  chainConfig.module
    .rule('ts-in-node_modules')
    .test(/\.(jsx|ts|tsx)$/)
    .include.add(/node_modules/)
    .end()
    .use('babel-loader')
    .loader(require.resolve('babel-loader'))
    .options(babelOpts);

  const rule = chainConfig.module
    .rule('js-in-node_modules')
    .test(/\.(js|mjs)$/);
  const nodeModulesTransform = defineConfig.nodeModulesTransform || {
    type: 'all',
    exclude: [],
  };

  if (nodeModulesTransform.type === 'all') {
    const exclude = lodash.uniq([
      ...TYPE_ALL_EXCLUDE,
      ...(nodeModulesTransform.exclude || []),
    ]);
    const pkgs = excludeToPkgs({ exclude });
    rule.include
      .add(/node_modules/)
      .end()
      .exclude.add(path => {
        return isMatch({ path, pkgs });
      })
      .end();
  } else {
    const pkgs = {
      ...es5ImcompatibleVersionsToPkg(),
      ...excludeToPkgs({ exclude: nodeModulesTransform.exclude || [] }),
    };
    rule.include
      .add(path => {
        return isMatch({
          path,
          pkgs,
        });
      })
      .end();
  }

  rule
    .use('babel-loader')
    .loader(require.resolve('babel-loader'))
    .options({});

  if ((isProd && defineConfig.analyze) || process.env.ANALYZE) {
    const mergeAnalyze = Object.assign(
      {
        analyzerMode: process.env.ANALYZE_MODE || 'server',
        analyzerPort: process.env.ANALYZE_PORT || 8888,
        openAnalyzer: process.env.ANALYZE_OPEN !== 'none',
        generateStatsFile: !!process.env.ANALYZE_DUMP,
        statsFilename: process.env.ANALYZE_DUMP || 'stats.json',
        logLevel: process.env.ANALYZE_LOG_LEVEL || 'info',
        defaultSizes: 'parsed', // stat  // gzip
      },
      defineConfig.analyze,
    );
    chainConfig
      .plugin('bundle-analyzer')
      .use(require('umi-webpack-bundle-analyzer').BundleAnalyzerPlugin, [
        mergeAnalyze || {},
      ]);
  }

  if (process.env.PROGRESS !== 'none') {
    chainConfig.plugin('progress').use(require.resolve('webpackbar'));
  }

  if (process.env.FRIENDLY_ERROR !== 'none') {
    chainConfig
      .plugin('friendly-error')
      .use(require.resolve('friendly-errors-webpack-plugin'), [
        {
          clearConsole: false,
        },
      ]);
  }

  if (process.env.WEBPACK_PROFILE) {
    chainConfig.profile(true);
    const statsInclude = ['verbose', 'normal', 'minimal'];
    chainConfig.stats(
      statsInclude.includes(process.env.WEBPACK_PROFILE)
        ? process.env.WEBPACK_PROFILE
        : 'verbose',
    );
    const StatsPlugin = require('stats-webpack-plugin');
    chainConfig.plugin('stats-webpack-plugin').use(
      new StatsPlugin('stats.json', {
        chunkModules: true,
      }),
    );
  }

  chainConfig.when(
    isDev,
    chainConfig => {
      if (isDev) {
        chainConfig.plugin('hmr').use(webpack.HotModuleReplacementPlugin);
      }
    },
    chainConfig => {
      chainConfig.optimization.noEmitOnErrors(true);
      chainConfig.performance.hints(false);

      if (!isWebpack5) {
        chainConfig
          .plugin('hash-module-ids')
          .use(webpack.HashedModuleIdsPlugin, []);
      }

      if (disableCompress) {
        chainConfig.optimization.minimize(false);
      } else if (!options.__disableTerserForTest) {
        chainConfig.optimization
          .minimizer('terser')
          .use(require.resolve('terser-webpack-plugin'), [
            {
              terserOptions: deepmerge(
                terserOptions,
                defineConfig.terserOptions || {},
              ),
              sourceMap: defineConfig.devtool !== false,
              cache: process.env.TERSER_CACHE !== 'none',
              parallel: true,
              extractComments: false,
            },
          ]);
      }
    },
  );

  function createCSSRuleFn(opts) {
    createCSSRule({
      webpackConfig: chainConfig,
      config: defineConfig,
      isDev,
      type: 'csr',
      browserslist,
      miniCSSExtractPluginLoaderPath: '',
      ...opts,
    });
  }

  if (defineConfig.chainWebpack) {
    defineConfig.chainWebpack(chainConfig, {
      type: 'csr',
      env: process.env,
      webpack,
      createCSSRule: createCSSRuleFn,
    });
  }

  // if (defineConfig.chunks) {
  //   chainConfig
  //     .plugin('htmlPlugins')
  //     .use(require('html-webpack-plugin'))
  //     .tap(options => {
  //       options.chunks = defineConfig.chunks;
  //       return options;
  //     });
  // }

  let ret = chainConfig.toConfig();

  // speed-measure-webpack-plugin
  if (process.env.SPEED_MEASURE) {
    const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
    const smpOption =
      process.env.SPEED_MEASURE === 'CONSOLE'
        ? { outputFormat: 'human', outputTarget: console.log }
        : {
            outputFormat: 'json',
            outputTarget: join(process.cwd(), 'speed-measure.json'),
          };
    const smp = new SpeedMeasurePlugin(smpOption);
    ret = smp.wrap(ret);
  }

  let entry = options.entry;
  if (defineConfig.runtimePublicPath) {
    entry.push(require.resolve('./runtimePublicPathEntry'));
  }

  ret = {
    ...ret,
    // mode: options.mode,
    entry: entry,
    plugins: options.plugins.concat([...ret.plugins]),
    optimization: {
      ...options.optimization,
      ...ret.optimization,
    },
    performance: options.performance || {},
  };
  return ret;
};
