const SingleEntryPlugin = require("webpack/lib/SingleEntryPlugin");

class BootloaderPlugin {
    constructor(htmlWebpackPlugin, options) {
        this.htmlWebpackPlugin = htmlWebpackPlugin;
        this.options = Object.assign({
            name: "bootloader",
        }, options);
    }

    isBootloaderScriptTag(tag, bootloaderFiles) {
        if (tag.tagName !== "script" || !(tag.attributes && tag.attributes.src)) {
            return true;
        }
        return bootloaderFiles.has(tag.attributes.src);
    }

    isBootloaderStyleTag(tag, bootloaderFiles) {
        if (tag.tagName !== "link" || !(tag.attributes && tag.attributes.href)) {
            return true;
        }
        return bootloaderFiles.has(tag.attributes.href);
    }

    inlineScriptTag(publicPath, assets, tag) {
        if (tag.tagName !== "script" || !(tag.attributes && tag.attributes.src)) {
            return tag;
        }
        const scriptName = publicPath ?
            tag.attributes.src.replace(publicPath, "") :
            tag.attributes.src;
        const asset = assets[scriptName];
        if (asset == null) {
            return tag;
        }
        return {tagName: "script", innerHTML: asset.source(), closeTag: true};
    }

    inlineStyleTag(publicPath, assets, tag) {
        if (tag.tagName !== "link" || !(tag.attributes && tag.attributes.href)) {
            return tag;
        }
        const scriptName = publicPath ?
            tag.attributes.href.replace(publicPath, "") :
            tag.attributes.href;
        const asset = assets[scriptName];
        if (asset == null) {
            return tag;
        }
        return {tagName: "style", innerHTML: asset.source(), closeTag: true};
    }

    processHtmlAsset(publicPath, src, assets, excludeFiles, result) {
        const scriptName = publicPath ? src.replace(publicPath, "") : src;
        if (excludeFiles.has(scriptName)) return;
        const asset = assets[scriptName];
        if (!asset) return;
        result.push({
            file: src,
            size: asset.size(),
        });
    }

    apply(compiler) {
        const isProductionLikeMode = compiler.options.mode === "production" || !compiler.options.mode;

        if (!isProductionLikeMode) return;

        let publicPath = compiler.options.output.publicPath || "";
        if (publicPath && !publicPath.endsWith("/")) {
            publicPath += "/";
        }

        const htmlAssets = {
            js: [],
            css: [],
        };

        compiler.hooks.entryOption.tap("BootloaderPlugin", (context) => {
            compiler.hooks.make.tapAsync("BootloaderPlugin", (compilation, callback) => {
                const entry = SingleEntryPlugin.createDependency(this.options.script, this.options.name);
                compilation.addEntry(context, entry, this.options.name, callback);
            });
        });

        compiler.hooks.thisCompilation.tap("BootloaderPlugin", (compilation) => {
            compilation.hooks.afterOptimizeChunks.tap("BootloaderPlugin", () => {
                const entrypoint = compilation.entrypoints.get(this.options.name);
                if (entrypoint) {
                    // create a new chunk for the entire bootloader
                    // most likely webpack will return an existing one
                    const newChunk = compilation.addChunk(this.options.name);

                    for (const chunk of Array.from(entrypoint.chunks)) {
                        if (chunk === newChunk) continue;
                        // move all modules to new chunk
                        for (const module of chunk.getModules()) {
                            chunk.moveModule(module, newChunk);
                        }
                        entrypoint.removeChunk(chunk);
                        const index = compilation.chunks.indexOf(chunk);
                        if (index > -1) {
                            compilation.chunks.splice(index, 1);
                        }
                        compilation.namedChunks.delete(chunk.name);
                    }
                    entrypoint.pushChunk(newChunk);
                    entrypoint.setRuntimeChunk(newChunk);
                }
            });
            const hooks = this.htmlWebpackPlugin.getHooks(compilation);
            hooks.beforeAssetTagGeneration.tap("BootloaderPlugin", ({assets}) => {
                const entrypoint = compilation.entrypoints.get(this.options.name);
                if (entrypoint) {
                    const bootloaderFiles = new Set(entrypoint.getFiles());
                    assets.js.forEach((src) => this.processHtmlAsset(assets.publicPath, src, compilation.assets, bootloaderFiles, htmlAssets.js));
                    assets.css.forEach((src) => this.processHtmlAsset(assets.publicPath, src, compilation.assets, bootloaderFiles, htmlAssets.css));
                }
            });
            hooks.alterAssetTags.tap("BootloaderPlugin", ({assetTags}) => {
                const entrypoint = compilation.entrypoints.get(this.options.name);
                if (entrypoint) {
                    const bootloaderFiles = new Set(entrypoint.getFiles().map((filename) => publicPath + filename));
                    assetTags.scripts = assetTags.scripts
                        .filter((tag) => this.isBootloaderScriptTag(tag, bootloaderFiles))
                        .map((tag) => this.inlineScriptTag(publicPath, compilation.assets, tag));
                    assetTags.styles = assetTags.styles
                        .filter((tag) => this.isBootloaderStyleTag(tag, bootloaderFiles))
                        .map((tag) => this.inlineStyleTag(publicPath, compilation.assets, tag));
                    // inject assets
                    const assetSource = `!function(){var bootloader={};bootloader.assets=${JSON.stringify(htmlAssets)};window.$bootloader=bootloader;}();`;
                    assetTags.scripts.unshift({tagName: "script", innerHTML: assetSource, closeTag: true});
                    // removing bootloader files from assets so webpack will not emit them
                    entrypoint.getFiles().forEach((filename) => delete compilation.assets[filename]);
                }
            });
        });
    }
}

module.exports = BootloaderPlugin;
