import type { BuilderContext } from '@angular-devkit/architect';
import {
  AssetPattern,
  BrowserBuilderOptions,
  KarmaBuilderOptions,
} from '@angular-devkit/build-angular';
import { normalize } from '@angular-devkit/core';
import * as path from 'path';
import { filter } from 'rxjs/operators';
import { Injectable, Injector } from 'static-injector';
import * as webpack from 'webpack';
import { DefinePlugin } from 'webpack';
import { BootstrapAssetsPlugin } from 'webpack-bootstrap-assets-plugin';
import { LIBRARY_OUTPUT_ROOTDIR } from '../library';
import { BuildPlatform } from '../platform/platform';
import type { PlatformType } from '../platform/platform';
import { LibraryTemplateScopeService } from './library-template-scope.service';
import { DynamicLibraryComponentEntryPlugin } from './plugin/dynamic-library-entry.plugin';
import { DynamicWatchEntryPlugin } from './plugin/dynamic-watch-entry.plugin';
import { ExportMiniProgramAssetsPlugin } from './plugin/export-mini-program-assets.plugin';
import { TS_CONFIG_TOKEN } from './token';
import type { PagePattern } from './type';

type OptimizationOptions = NonNullable<webpack.Configuration['optimization']>;
type OptimizationSplitChunksOptions = Exclude<
  OptimizationOptions['splitChunks'],
  false | undefined
>;
type OptimizationSplitChunksCacheGroup = Exclude<
  NonNullable<OptimizationSplitChunksOptions['cacheGroups']>[''],
  false | string | Function | RegExp
>;
@Injectable()
export class WebpackConfigurationChangeService {
  exportMiniProgramAssetsPluginInstance!: ExportMiniProgramAssetsPlugin;
  private buildPlatform!: BuildPlatform;
  private entryList!: PagePattern[];
  constructor(
    private options: (BrowserBuilderOptions | KarmaBuilderOptions) & {
      pages: AssetPattern[];
      components: AssetPattern[];
      platform: PlatformType;
    },
    private context: BuilderContext,
    private config: webpack.Configuration,
    private injector: Injector
  ) {}
  init() {
    this.injector = Injector.create({
      parent: this.injector,
      providers: [
        { provide: ExportMiniProgramAssetsPlugin },
        { provide: LibraryTemplateScopeService },
        {
          provide: TS_CONFIG_TOKEN,
          useValue: path.resolve(
            this.context.workspaceRoot,
            this.options.tsConfig
          ),
        },
        {
          provide: DynamicWatchEntryPlugin,
          deps: [BuildPlatform],
          useFactory: (buildPlatform: BuildPlatform) => {
            return new DynamicWatchEntryPlugin(
              {
                pages: this.options.pages,
                components: this.options.components,
                workspaceRoot: normalize(this.context.workspaceRoot),
                context: this.context,
                config: this.config,
              },
              buildPlatform
            );
          },
        },
        { provide: DynamicLibraryComponentEntryPlugin },
      ],
    });
    this.buildPlatform = this.injector.get(BuildPlatform);
    this.buildPlatform.fileExtname.config =
      this.buildPlatform.fileExtname.config || '.json';
    this.config.output!.globalObject = this.buildPlatform.globalObject;
  }
  async change() {
    this.buildPlatformCompatible();
    this.exportAssets();
    await this.pageHandle();
    this.addLoader();
    this.globalVariableChange();
    this.changeStylesExportSuffix();
    this.config.plugins?.push(
      this.injector.get(DynamicLibraryComponentEntryPlugin)
    );
    this.config.plugins?.push(
      new webpack.NormalModuleReplacementPlugin(
        /^angular-miniprogram\/platform\/wx$/,
        `angular-miniprogram/platform/${this.buildPlatform.packageName}`
      )
    );
  }
  private buildPlatformCompatible() {
    if (this.buildPlatform.packageName == 'zfb') {
      this.config.resolve?.conditionNames?.shift();
      this.config.resolve?.mainFields?.shift();
    }
  }
  private async pageHandle() {
    const dynamicWatchEntryInstance = this.injector.get(
      DynamicWatchEntryPlugin
    );
    await dynamicWatchEntryInstance.init();
    dynamicWatchEntryInstance.entryPattern$
      .pipe(filter((item) => !!item))
      .subscribe((result) => {
        this.entryList = [...result!.pageList, ...result!.componentList];
        this.exportMiniProgramAssetsPluginInstance.setEntry(
          result!.pageList,
          result!.componentList
        );
      });
    this.config.plugins?.push(dynamicWatchEntryInstance);
    // 出口
    const oldFileName = this.config.output!.filename as string;
    this.config.output!.filename = (chunkData) => {
      const page = this.entryList.find(
        (item) => item.entryName === chunkData.chunk!.name
      );
      if (page) {
        return page.outputFiles.logic;
      }
      return oldFileName;
    };
    // 共享依赖
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const oldChunks = (this.config.optimization!.splitChunks as any).cacheGroups
      .defaultVendors.chunks;
    (
      (
        this.config.optimization!
          .splitChunks! as unknown as OptimizationSplitChunksOptions
      ).cacheGroups!.defaultVendors as OptimizationSplitChunksCacheGroup
    ).chunks = (chunk) => {
      if (
        this.entryList.find((item) => item.entryName === chunk.name) ||
        chunk.name.startsWith(`${LIBRARY_OUTPUT_ROOTDIR}/`)
      ) {
        return true;
      }
      return oldChunks(chunk);
    };
    ((this.config.optimization!.splitChunks as OptimizationSplitChunksOptions)
      .cacheGroups!['moduleChunks'] as OptimizationSplitChunksCacheGroup) = {
      test: (module: webpack.NormalModule) => {
        const name = module.nameForCondition();
        return (
          (name &&
            name.endsWith('.ts') &&
            !/[\\/]node_modules[\\/]/.test(name)) ||
          name?.includes('angular-miniprogram\\dist')
        );
      },
      minChunks: 2,
      minSize: 0,
      name: 'module-chunk',
      chunks: 'all',
    };
    // 出口保留必要加载
    const assetsPlugin = new BootstrapAssetsPlugin();
    assetsPlugin.hooks.removeChunk.tap('pageHandle', (chunk) => {
      if (
        this.entryList.some((page) => page.entryName === chunk.name) ||
        [...chunk.files].some((file) =>
          file.endsWith(this.buildPlatform.fileExtname.style)
        ) ||
        chunk.name.startsWith(`${LIBRARY_OUTPUT_ROOTDIR}/`)
      ) {
        return true;
      }
      return false;
    });
    assetsPlugin.hooks.emitAssets.tap('pageHandle', (object, json) => {
      return {
        'app.js':
          this.buildPlatform.importTemplate +
          json.scripts.map((item) => `require('./${item.src}')`).join(';'),
      };
    });
    this.config.plugins!.push(assetsPlugin);
  }
  private exportAssets() {
    this.exportMiniProgramAssetsPluginInstance = this.injector.get(
      ExportMiniProgramAssetsPlugin
    );
    this.config.plugins!.unshift(this.exportMiniProgramAssetsPluginInstance);
  }

  private addLoader() {
    this.config.module!.rules!.unshift({
      test: /\.ts$/,
      loader: require.resolve(
        path.join(__dirname, './loader/component-template.loader')
      ),
    });
    this.config.module?.rules?.unshift({
      test: /\.mjs$/,
      loader: require.resolve(path.join(__dirname, './loader/library.loader')),
    });
    this.config.module?.rules?.unshift({
      test: /\.mjs$/,
      loader: require.resolve(
        path.join(__dirname, './loader/library-template.loader')
      ),
    });
  }
  private globalVariableChange() {
    const defineObject: Record<string, string> = {
      global: `${this.buildPlatform.globalObject}.__global`,
      window: `${this.buildPlatform.globalVariablePrefix}`,
      globalThis: `${this.buildPlatform.globalVariablePrefix}`,
      Zone: `${this.buildPlatform.globalVariablePrefix}.Zone`,
      setTimeout: `${this.buildPlatform.globalVariablePrefix}.setTimeout`,
      clearTimeout: `${this.buildPlatform.globalVariablePrefix}.clearTimeout`,
      setInterval: `${this.buildPlatform.globalVariablePrefix}.setInterval`,
      clearInterval: `${this.buildPlatform.globalVariablePrefix}.clearInterval`,
      setImmediate: `${this.buildPlatform.globalVariablePrefix}.setImmediate`,
      clearImmediate: `${this.buildPlatform.globalVariablePrefix}.clearImmediate`,
      Promise: `${this.buildPlatform.globalVariablePrefix}.Promise`,
      Reflect: `${this.buildPlatform.globalVariablePrefix}.Reflect`,
      requestAnimationFrame: `${this.buildPlatform.globalVariablePrefix}.requestAnimationFrame`,
      cancelAnimationFrame: `${this.buildPlatform.globalVariablePrefix}.cancelAnimationFrame`,
      performance: `${this.buildPlatform.globalVariablePrefix}.performance`,
      navigator: `${this.buildPlatform.globalVariablePrefix}.navigator`,
      wx: this.buildPlatform.globalObject,
      miniProgramPlatform: `"${this.buildPlatform.globalObject}"`,
    };
    if (this.config.mode === 'development') {
      defineObject[
        'ngDevMode'
      ] = `${this.buildPlatform.globalObject}.__global.ngDevMode`;
    }
    this.config.plugins!.push(new DefinePlugin(defineObject));
  }
  private changeStylesExportSuffix() {
    const index = this.config.plugins!.findIndex(
      (plugin) =>
        Object.getPrototypeOf(plugin).constructor.name ===
        'MiniCssExtractPlugin'
    );
    if (index > -1) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const pluginInstance = this.config.plugins![index] as any;
      const pluginPrototype = Object.getPrototypeOf(pluginInstance);
      this.config.plugins?.splice(
        index,
        1,
        new pluginPrototype.constructor({
          filename: 'app' + this.buildPlatform.fileExtname.style,
        })
      );
    } else {
      throw new Error('没有找到MiniCssExtractPlugin插件,无法修改生成style');
    }
  }
}