import { Compilation, Compiler, sources } from 'webpack';
import JavaScriptObfuscator, { ObfuscatorOptions } from 'javascript-obfuscator';
import multimatch from 'multimatch';
import { RawSource } from 'webpack-sources';


type CompilationAssets = Pick<Compilation, 'assets'>['assets'];
class WebpackObfuscator {
    // 混淆多个文件，请使用此选项。此选项有助于避免这些文件的全局标识符之间的冲突。每个文件的前缀应该不同。
    private static readonly baseIdentifiersPrefix = 'a';
    public obfuscatorFiles: string[] = [];
    public includes: string[] = [];

    constructor(public options: ObfuscatorOptions = {}, includes: string[]) {
        this.options = options;
        this.includes = this.includes.concat(includes || []);
        this.obfuscatorFiles = [];
    }

    apply(compiler: Compiler) {
        const isDevServer = process.argv.find((v) => v.includes('webpack-dev-server'));

        // dev 直接 return
        if (isDevServer) {
            console.info('JavascriptObfuscator is disabled on webpack-dev-server as the reloading scripts ', 'and the obfuscator can interfere with each other and break the build');
            return;
        }

        const pluginName = this.constructor.name;
        compiler.hooks.emit.tap(pluginName, (compilation) => {
            let identifiersPrefixCounter = 0;

            compilation.chunks.forEach((chunk) => {
                chunk.files.forEach((fileName: string) => {
                    // 非js文件直接 return  或者 满足外面传进来的 inlcude 条件
                    if (!fileName.toLowerCase().endsWith('.js') || !this.shouldInclude(fileName)) {
                        return;
                    }

                    const asset = compilation.assets[fileName];
                    this.obfuscatorFiles.push(fileName);
                    // 拿到 source 和 map
                    // eslint-disable-next-line @typescript-eslint/no-unused-vars
                    const { inputSource, inputSourceMap } = this.extractSourceAndSourceMap(asset);

                    // 拿到加密后的代码和对应的 sourcemap
                    // eslint-disable-next-line @typescript-eslint/no-unused-vars
                    const { obfuscatedSource, obfuscationSourceMap } = this.obfuscate(inputSource as string, fileName, identifiersPrefixCounter);

                    const assets: CompilationAssets = compilation.assets;
                    assets[fileName] = (new RawSource(obfuscatedSource) as any as sources.RawSource);
                    identifiersPrefixCounter++;
                });
            });
        });

        compiler.hooks.done.tap(pluginName, () => {
            console.log('加固的文件是: ', this.obfuscatorFiles);
        });
    }

    shouldInclude(filePath: string) {
        return multimatch(filePath, this.includes).length > 0;
    }

    extractSourceAndSourceMap(asset: sources.Source) {
        if (asset.sourceAndMap) {
            const { source, map } = asset.sourceAndMap();
            return {
                inputSource: source,
                inputSourceMap: map,
            };
        } else {
            return {
                inputSource: asset.source(),
                inputSourceMap: asset.map(),
            };
        }
    }

    obfuscate(javascript: string, fileName: string, identifiersPrefixCounter: number) {
        // 混淆多个文件，请使用此选项。此选项有助于避免这些文件的全局标识符之间的冲突。每个文件的前缀应该不同。
        const obfuscationResult = JavaScriptObfuscator.obfuscate(javascript, {
            identifiersPrefix: `${WebpackObfuscator.baseIdentifiersPrefix}${identifiersPrefixCounter}`,
            sourceMapFileName: fileName + '.map',
            ...this.options,
        });

        return {
            obfuscatedSource: obfuscationResult.getObfuscatedCode(),
            obfuscationSourceMap: obfuscationResult.getSourceMap(),
        };
    }
}

export = WebpackObfuscator