// This plugin exposes a version of require() to the browser console to assist
// debugging.  It also exposes the list of modules it knows about as the keys
// of the require.ids object.

import webpack, { Template } from "webpack";
import path from "path";

export default class DebugRequirePlugin {
    apply(compiler: webpack.Compiler): void {
        const resolved = new Map();
        const nameSymbol = Symbol("DebugRequirePluginName");
        let debugRequirePath: string | undefined;

        (compiler as any).resolverFactory.hooks.resolver
            .for("normal")
            .tap("DebugRequirePlugin", (resolver: any) => {
                resolver.getHook("beforeRawModule").tap("DebugRequirePlugin", (req: any) => {
                    req[nameSymbol] = req[nameSymbol] || req.request;
                });

                resolver.getHook("beforeRelative").tap("DebugRequirePlugin", (req: any) => {
                    const inPath = path.relative(compiler.context, req.path);
                    if (!inPath.startsWith("../")) {
                        req[nameSymbol] = req[nameSymbol] || "./" + inPath;
                    }
                });

                resolver.getHook("beforeResolved").tap("DebugRequirePlugin", (req: any) => {
                    if (req[nameSymbol]) {
                        const names = resolved.get(req.path);
                        if (names) {
                            names.add(req[nameSymbol]);
                        } else {
                            resolved.set(req.path, new Set([req[nameSymbol]]));
                        }
                    }
                });
            });

        compiler.hooks.beforeCompile.tapPromise(
            "DebugRequirePlugin",
            async ({ normalModuleFactory }: any) => {
                const resolver = normalModuleFactory.getResolver("normal");
                debugRequirePath = await new Promise((resolve, reject) =>
                    resolver.resolve(
                        {},
                        __dirname,
                        "./debug-require.js",
                        {},
                        (err?: Error, result?: string) => err ? reject(err) : resolve(result)
                    )
                );
            }
        );

        compiler.hooks.compilation.tap("DebugRequirePlugin", (compilation: any) => {
            compilation.mainTemplate.hooks.beforeStartup.tap(
                "DebugRequirePlugin",
                (source: string, chunk: webpack.compilation.Chunk) => {
                    const ids: [string, string | number][] = [];
                    let debugRequireId;
                    chunk.hasModuleInGraph(
                        ({ resource, rawRequest, id }: any) => {
                            if (resource === debugRequirePath) {
                                debugRequireId = id;
                            }
                            for (const name of resolved.get(resource) || []) {
                                ids.push([
                                    rawRequest.slice(0, rawRequest.lastIndexOf("!") + 1) + name,
                                    id,
                                ]);
                            }
                            return false;
                        },
                        () => true
                    );

                    if (debugRequireId === undefined) {
                        return source;
                    }

                    ids.sort();
                    const { requireFn } = compilation.mainTemplate;
                    return Template.asString([
                        source,
                        `${requireFn}(${JSON.stringify(
                            debugRequireId
                        )}).initialize(${JSON.stringify(Object.fromEntries(ids), null, "\t")});`,
                    ]);
                }
            );
        });
    }
}
