const { join, relative, resolve, sep, dirname } = require('path');
const fs = require('fs');

const webpack = require('webpack');
const nsWebpack = require('@nativescript/webpack');
const nativescriptTarget = require('@nativescript/webpack/nativescript-target');
const {
  nsSupportHmrNg
} = require('@nativescript/webpack/transformers/ns-support-hmr-ng');
const { nsTransformNativeClassesNg } = require("@nativescript/webpack/transformers/ns-transform-native-classes-ng");
const {
  parseWorkspaceConfig, hasConfigurations
} = require('@nativescript/webpack/helpers/angular-config-parser');
const {
  getMainModulePath
} = require('@nativescript/webpack/utils/ast-utils');
const { getNoEmitOnErrorFromTSConfig, getCompilerOptionsFromTSConfig } = require("@nativescript/webpack/utils/tsconfig-utils");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const {
  NativeScriptWorkerPlugin
} = require('nativescript-worker-loader/NativeScriptWorkerPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const {
  getAngularCompilerPlugin
} = require('@nativescript/webpack/plugins/NativeScriptAngularCompilerPlugin');
const hashSalt = Date.now().toString();

module.exports = env => {
  // Add your custom Activities, Services and other Android app components here.
  const appComponents = [
    "@nativescript/core/ui/frame", "@nativescript/core/ui/frame/activity"
  ];

  const platform = env && ((env.android && 'android') || (env.ios && 'ios'));
  if (!platform) {
    throw new Error('You need to provide a target platform!');
  }

  const AngularCompilerPlugin = getAngularCompilerPlugin(platform);
  const projectRoot = __dirname;

  // Default destination inside platforms/<platform>/...
  const dist = resolve(
    projectRoot,
    nsWebpack.getAppPath(platform, projectRoot)
  );

  const {
    // The 'appPath' and 'appResourcesPath' values are fetched from
    // the nsconfig.json configuration file
    // when bundling with `tns run android|ios --bundle`.
    appPath = 'src',
    appResourcesPath = 'App_Resources',

    // You can provide the following flags when running 'tns run android|ios'
    snapshot, // --env.snapshot,
    production, // --env.production
    configuration, // --env.configuration (consistent with angular cli usage)
    projectName, // --env.projectName (drive configuration through angular projects)
    uglify, // --env.uglify
    report, // --env.report
    sourceMap, // --env.sourceMap
    hiddenSourceMap, // --env.hiddenSourceMap
    hmr, // --env.hmr,
    unitTesting, // --env.unitTesting
    testing, // --env.testing
    verbose, // --env.verbose
    ci, // --env.ci
    snapshotInDocker, // --env.snapshotInDocker
    skipSnapshotTools, // --env.skipSnapshotTools
    compileSnapshot // --env.compileSnapshot
  } = env;

  const { fileReplacements, copyReplacements } = parseWorkspaceConfig(platform, configuration, projectName);

  const useLibs = compileSnapshot;
  const isAnySourceMapEnabled = !!sourceMap || !!hiddenSourceMap;
  const externals = nsWebpack.getConvertedExternals(env.externals);
  const appFullPath = resolve(projectRoot, appPath);
  const appResourcesFullPath = resolve(projectRoot, appResourcesPath);
  let tsConfigName = 'tsconfig.json';
  let tsConfigPath = resolve(projectRoot, tsConfigName);
  const tsConfigTnsName = 'tsconfig.tns.json';
  const tsConfigTnsPath = resolve(projectRoot, tsConfigTnsName);
  if (fs.existsSync(tsConfigTnsPath)) {
    // support shared angular app configurations 
    tsConfigName = tsConfigTnsName;
    tsConfigPath = tsConfigTnsPath;
  }
  const tsConfigEnvName = 'tsconfig.env.json';
  const tsConfigEnvPath = resolve(projectRoot, tsConfigEnvName);
  if (hasConfigurations(configuration) && fs.existsSync(tsConfigEnvPath)) {
    // when configurations are used, switch to environments supported config
    tsConfigName = tsConfigEnvName;
    tsConfigPath = tsConfigEnvPath;
  }
  const entryModule = `${nsWebpack.getEntryModule(appFullPath, platform)}.ts`;
  const entryPath = `.${sep}${entryModule}`;
  const entries = { bundle: entryPath };
  const areCoreModulesExternal =
    Array.isArray(env.externals) &&
    env.externals.some(e => e.indexOf('@nativescript') > -1);
  if (platform === 'ios' && !areCoreModulesExternal && !testing) {
    entries['tns_modules/@nativescript/core/inspector_modules'] =
      'inspector_modules';
  }

  const compilerOptions = getCompilerOptionsFromTSConfig(tsConfigPath);
  nsWebpack.processTsPathsForScopedModules({ compilerOptions });
  nsWebpack.processTsPathsForScopedAngular({ compilerOptions });

  const ngCompilerTransformers = [nsTransformNativeClassesNg];
  const additionalLazyModuleResources = [];

  const copyIgnore = { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] };
  const copyTargets = [
    { from: 'assets/**', noErrorOnMissing: true, globOptions: { dot: false, ...copyIgnore } },
    { from: 'fonts/**', noErrorOnMissing: true, globOptions: { dot: false, ...copyIgnore } },
    ...copyReplacements
  ];

  if (!production) {
    // for development purposes only
    // for example, include mock json folder
    // copyTargets.push({ from: 'tools/mockdata', to: 'assets/mockdata' });

    if (hmr) {
      ngCompilerTransformers.push(nsSupportHmrNg);
    }
  }

  // when "@angular/core" is external, it's not included in the bundles. In this way, it will be used
  // directly from node_modules and the Angular modules loader won't be able to resolve the lazy routes
  // fixes https://github.com/NativeScript/nativescript-cli/issues/4024
  if (env.externals && env.externals.indexOf('@angular/core') > -1) {
    const appModuleRelativePath = getMainModulePath(
      resolve(appFullPath, entryModule),
      tsConfigName
    );
    if (appModuleRelativePath) {
      const appModuleFolderPath = dirname(
        resolve(appFullPath, appModuleRelativePath)
      );
      // include the new lazy loader path in the allowed ones
      additionalLazyModuleResources.push(appModuleFolderPath);
    }
  }

  const ngCompilerPlugin = new AngularCompilerPlugin({
    hostReplacementPaths: nsWebpack.getResolver([platform, 'tns']),
    platformTransformers: ngCompilerTransformers.map(t =>
      t(() => ngCompilerPlugin, resolve(appFullPath, entryModule), projectRoot)
    ),
    mainPath: join(appFullPath, entryModule),
    tsConfigPath,
    skipCodeGeneration: false,
    sourceMap: !!isAnySourceMapEnabled,
    additionalLazyModuleResources: additionalLazyModuleResources,
    compilerOptions: { paths: compilerOptions.paths }
  });

  let sourceMapFilename = nsWebpack.getSourceMapFilename(
    hiddenSourceMap,
    __dirname,
    dist
  );

  const itemsToClean = [`${dist}/**/*`];
  if (platform === 'android') {
    itemsToClean.push(
      `${join(
        projectRoot,
        'platforms',
        'android',
        'app',
        'src',
        'main',
        'assets',
        'snapshots'
      )}`
    );
    itemsToClean.push(
      `${join(
        projectRoot,
        'platforms',
        'android',
        'app',
        'build',
        'configurations',
        'nativescript-android-snapshot'
      )}`
    );
  }

  const noEmitOnErrorFromTSConfig = getNoEmitOnErrorFromTSConfig(tsConfigName);

  nsWebpack.processAppComponents(appComponents, platform);
  const config = {
    mode: production ? 'production' : 'development',
    context: appFullPath,
    externals,
    watchOptions: {
      ignored: [
        appResourcesFullPath,
        // Don't watch hidden files
        '**/.*'
      ]
    },
    target: nativescriptTarget,
    entry: entries,
    output: {
      pathinfo: false,
      path: dist,
      sourceMapFilename,
      libraryTarget: 'commonjs2',
      filename: '[name].js',
      globalObject: 'global',
      hashSalt
    },
    resolve: {
      extensions: ['.ts', '.js', '.scss', '.css'],
      // Resolve {N} system modules from @nativescript/core
      modules: [
        resolve(__dirname, 'node_modules/@nativescript/core'),
        resolve(__dirname, 'node_modules'),
        'node_modules/@nativescript/core',
        'node_modules'
      ],
      alias: {
        '~/package.json': resolve(projectRoot, 'package.json'),
        '~': appFullPath,
        "tns-core-modules": "@nativescript/core",
        "nativescript-angular": "@nativescript/angular",
        ...fileReplacements
      },
      symlinks: true
    },
    resolveLoader: {
      symlinks: false
    },
    node: {
      // Disable node shims that conflict with NativeScript
      http: false,
      timers: false,
      setImmediate: false,
      fs: 'empty',
      __dirname: false
    },
    devtool: hiddenSourceMap
      ? 'hidden-source-map'
      : sourceMap
      ? 'inline-source-map'
      : 'none',
    optimization: {
      runtimeChunk: 'single',
      noEmitOnErrors: noEmitOnErrorFromTSConfig,
      splitChunks: {
        cacheGroups: {
          vendor: {
            name: 'vendor',
            chunks: 'all',
            test: (module, chunks) => {
              const moduleName = module.nameForCondition
                ? module.nameForCondition()
                : '';
              return (
                /[\\/]node_modules[\\/]/.test(moduleName) ||
                appComponents.some(comp => comp === moduleName)
              );
            },
            enforce: true
          }
        }
      },
      minimize: !!uglify,
      minimizer: [
        new TerserPlugin({
          parallel: true,
          cache: !ci,
          sourceMap: isAnySourceMapEnabled,
          terserOptions: {
            output: {
              comments: false,
              semicolons: !isAnySourceMapEnabled
            },
            compress: {
              // The Android SBG has problems parsing the output
              // when these options are enabled
              collapse_vars: platform !== 'android',
              sequences: platform !== 'android',
              // custom
              drop_console: true,
              drop_debugger: true,
              ecma: 6,
              keep_infinity: platform === 'android', // for Chrome/V8
              reduce_funcs: platform !== 'android', // for Chrome/V8
              global_defs: {
                __UGLIFIED__: true
              }
            },
            // custom
            ecma: 6,
            safari10: platform !== 'android'
          }
        })
      ]
    },
    module: {
      rules: [
        {
          include: join(appFullPath, entryPath),
          use: [
            // Require all Android app components
            platform === 'android' && {
              loader: '@nativescript/webpack/helpers/android-app-components-loader',
              options: { modules: appComponents }
            },

            {
              loader: '@nativescript/webpack/bundle-config-loader',
              options: {
                angular: true,
                loadCss: !snapshot, // load the application css if in debug mode
                unitTesting,
                appFullPath,
                projectRoot,
                ignoredFiles: nsWebpack.getUserDefinedEntries(entries, platform)
              }
            }
          ].filter(loader => !!loader)
        },

        { test: /\.html$|\.xml$/, use: 'raw-loader' },

        {
          test: /[\/|\\]app\.css$/,
          use: [
              '@nativescript/webpack/helpers/style-hot-loader',
              {
                  loader: "@nativescript/webpack/helpers/css2json-loader",
                  options: { useForImports: true }
              },
          ],
        },
        {
          test: /[\/|\\]app\.scss$/,
          use: [
              '@nativescript/webpack/helpers/style-hot-loader',
              {
                  loader: "@nativescript/webpack/helpers/css2json-loader",
                  options: { useForImports: true }
              },
              'sass-loader',
          ],
        },

        // Angular components reference css files and their imports using raw-loader
        { test: /\.css$/, exclude: /[\/|\\]app\.css$/, use: 'raw-loader' },
        {
          test: /\.scss$/,
          exclude: /[\/|\\]app\.scss$/,
          use: ['raw-loader', 'resolve-url-loader', 'sass-loader']
        },

        {
          test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
          use: [
            '@nativescript/webpack/helpers/moduleid-compat-loader',
            '@nativescript/webpack/helpers/lazy-ngmodule-hot-loader',
            '@ngtools/webpack'
          ]
        },

        // Mark files inside `@angular/core` as using SystemJS style dynamic imports.
        // Removing this will cause deprecation warnings to appear.
        {
          test: /[\/\\]@angular[\/\\]core[\/\\].+\.js$/,
          parser: { system: true }
        }
      ]
    },
    plugins: [
      // Define useful constants like TNS_WEBPACK
      new webpack.DefinePlugin({
        'global.TNS_WEBPACK': 'true',
        'global.isAndroid': platform === 'android',
        'global.isIOS': platform === 'ios',
        process: 'global.process'
      }),
      // Remove all files from the out dir.
      new CleanWebpackPlugin({ 
        cleanOnceBeforeBuildPatterns: itemsToClean,
        verbose: !!verbose
      }),
      // Copy assets
      new CopyWebpackPlugin({
          patterns: copyTargets,
      }),
      new nsWebpack.GenerateNativeScriptEntryPointsPlugin('bundle'),
      // For instructions on how to set up workers with webpack
      // check out https://github.com/nativescript/worker-loader
      new NativeScriptWorkerPlugin(),
      ngCompilerPlugin,
      // Does IPC communication with the {N} CLI to notify events when running in watch mode.
      new nsWebpack.WatchStateLoggerPlugin()
    ]
  };

  if (report) {
    // Generate report files for bundles content
    config.plugins.push(
      new BundleAnalyzerPlugin({
        analyzerMode: 'static',
        openAnalyzer: false,
        generateStatsFile: true,
        reportFilename: resolve(projectRoot, 'report', `report.html`),
        statsFilename: resolve(projectRoot, 'report', `stats.json`)
      })
    );
  }

  if (snapshot) {
    config.plugins.push(
      new nsWebpack.NativeScriptSnapshotPlugin({
        chunk: 'vendor',
        angular: true,
        requireModules: [
          'reflect-metadata',
          '@angular/platform-browser',
          '@angular/core',
          '@angular/common',
          '@angular/router',
          '@nativescript/angular'
        ],
        projectRoot,
        webpackConfig: config,
        snapshotInDocker,
        skipSnapshotTools,
        useLibs
      })
    );
  }

  if (!production && hmr) {
    config.plugins.push(new webpack.HotModuleReplacementPlugin());
  }

  return config;
};