import path from "path";
import fs from "fs";
import * as acorn from "acorn";
import * as walk from "acorn-walk";
/**
 * MemberExpression이 import.meta.url인지 확인한다.
 */
function isImportMetaUrl(node) {
    return (node.type === "MemberExpression" &&
        node.object.type === "MetaProperty" &&
        node.object.meta.name === "import" &&
        node.object.property.name === "meta" &&
        node.property.type === "Identifier" &&
        node.property.name === "url");
}
/**
 * acorn AST를 사용하여 소스 코드에서 Worker/SharedWorker 및 import.meta.resolve 패턴을 탐지한다.
 *
 * 정규식과 달리 주석, 문자열 리터럴 내부의 패턴을 오탐하지 않는다.
 * 파싱 실패 시 빈 배열을 반환한다.
 *
 * @param content - JavaScript 소스 코드. TypeScript는 상위 `transformWorkerPatterns()`가 사전 변환한다.
 */
export function findWorkerPatterns(content) {
    let ast;
    try {
        ast = acorn.parse(content, {
            ecmaVersion: "latest",
            sourceType: "module",
        });
    }
    catch {
        return [];
    }
    const matches = [];
    walk.simple(ast, {
        NewExpression(node) {
            // new Worker(new URL("path", import.meta.url), opts?)
            // new SharedWorker(new URL("path", import.meta.url), opts?)
            if (node.callee.type !== "Identifier" ||
                (node.callee.name !== "Worker" && node.callee.name !== "SharedWorker")) {
                return;
            }
            const args = node.arguments;
            if (args.length < 1)
                return;
            const urlArg = args[0];
            if (urlArg.type !== "NewExpression" ||
                urlArg.callee.type !== "Identifier" ||
                urlArg.callee.name !== "URL") {
                return;
            }
            const urlArgs = urlArg.arguments;
            if (urlArgs.length < 2)
                return;
            // 첫 번째 인자: 문자열 리터럴 (경로)
            if (urlArgs[0].type !== "Literal" || typeof urlArgs[0].value !== "string")
                return;
            // 두 번째 인자: import.meta.url
            if (!isImportMetaUrl(urlArgs[1]))
                return;
            const match = {
                type: "browser",
                start: node.start,
                end: node.end,
                workerType: node.callee.name,
                urlPath: urlArgs[0].value,
            };
            // 옵션 객체 (두 번째 인자)
            if (args.length >= 2) {
                match.existingOpts = content.slice(args[1].start, args[1].end);
            }
            matches.push(match);
        },
        CallExpression(node) {
            // import.meta.resolve("./relative-path")
            const callee = node.callee;
            if (callee.type !== "MemberExpression" ||
                callee.object.type !== "MetaProperty" ||
                callee.object.meta.name !== "import" ||
                callee.object.property.name !== "meta" ||
                callee.property.type !== "Identifier" ||
                callee.property.name !== "resolve") {
                return;
            }
            const args = node.arguments;
            if (args.length < 1)
                return;
            if (args[0].type !== "Literal" || typeof args[0].value !== "string")
                return;
            const urlPath = args[0].value;
            // 상대 경로만 처리
            if (!urlPath.startsWith("./") && !urlPath.startsWith("../"))
                return;
            matches.push({
                type: "node",
                start: node.start,
                end: node.end,
                urlPath,
            });
        },
    });
    return matches.sort((a, b) => a.start - b.start);
}
/**
 * Worker 파일을 esbuild.buildSync()로 별도 ESM 번들로 빌드한다.
 * esbuild-angular-compiler-plugin.ts의 bundleWebWorker를 기반으로 작성.
 */
function bundleWorker(build, workerFile, platform) {
    const sourcemap = build.initialOptions.sourcemap != null &&
        build.initialOptions.sourcemap !== false;
    const write = build.initialOptions.write !== false;
    try {
        return build.esbuild.buildSync({
            ...build.initialOptions,
            platform,
            write,
            bundle: true,
            metafile: true,
            format: "esm",
            entryNames: "worker-[hash]",
            entryPoints: [workerFile],
            sourcemap,
            // 메인 빌드에서 상속하지 않는 옵션
            external: undefined,
            supported: undefined,
            plugins: undefined,
            outbase: undefined,
            inject: undefined,
        });
    }
    catch (error) {
        if (error != null &&
            typeof error === "object" &&
            "errors" in error &&
            "warnings" in error) {
            return error;
        }
        throw error;
    }
}
/**
 * 파일 내용에서 Worker/SharedWorker 패턴을 감지하여 Worker 파일을 번들링하고
 * URL 경로를 번들된 파일 경로로 치환한다.
 *
 * Angular 컴파일러 플러그인 등 외부에서 직접 호출한다.
 *
 * @returns 변환 결과. 패턴이 없으면 undefined.
 */
export function transformWorkerPatterns(content, filePath, build, options) {
    // 빠른 사전 필터 — new Worker(), new SharedWorker(), import.meta.resolve()의 단어 경계
    // 호출 형태만 통과시킨다. 식별자·타입·interface로만 등장하는 Worker 키워드는 차단하여
    // TS transformSync 오버헤드를 줄인다. 정확성은 후속 AST 판별(findWorkerPatterns)이 담당한다.
    // new 와 Worker/SharedWorker 사이에 주석이 낀 호출(예: `new /* c */ Worker`)은 의도된
    // 트레이드오프로 탈락한다. 실발생 가능성이 희박하여 현행을 유지한다.
    if (!/\b(new\s+Worker|new\s+SharedWorker|import\.meta\.resolve)\b/.test(content)) {
        return undefined;
    }
    const errors = [];
    const warnings = [];
    // TS(.ts/.cts/.mts)는 JS로 변환한 후 AST 파싱. acorn은 TS 구문을 처리하지 못하므로
    // import type, 타입 어노테이션 등이 있으면 파싱 실패로 Worker 패턴이 조용히 누락된다.
    // skipTsTransform이 true이면 호출자가 content가 이미 JS임을 보장하므로 변환을 스킵한다
    // (예: Angular 컴파일러 플러그인이 ngtsc emit 결과를 전달하는 경로).
    let effectiveContent = content;
    if (options?.skipTsTransform !== true && /\.[cm]?ts$/.test(filePath)) {
        try {
            const transformed = build.esbuild.transformSync(content, {
                loader: "ts",
                sourcemap: false,
            });
            effectiveContent = transformed.code;
            warnings.push(...transformed.warnings);
        }
        catch (e) {
            const failure = e;
            return {
                contents: content,
                errors: failure.errors ?? [
                    { text: `TS transform failed: ${String(e)}`, location: null },
                ],
                warnings: failure.warnings ?? [],
            };
        }
    }
    // AST 기반 패턴 탐지 (변환된 JS 기준)
    const matches = findWorkerPatterns(effectiveContent);
    if (matches.length === 0) {
        return undefined;
    }
    const allOutputFiles = [];
    let mergedMetafile;
    const write = build.initialOptions.write !== false;
    const outdir = build.initialOptions.outdir ?? "";
    const containingDir = path.dirname(filePath);
    /**
     * Worker 번들을 빌드하고, 결과에서 출력 파일 경로를 찾아 반환한다.
     * 에러/경고/outputFiles/metafile을 외부 변수에 수집한다.
     */
    function processWorkerBundle(fullWorkerPath, platform) {
        const workerResult = bundleWorker(build, fullWorkerPath, platform);
        warnings.push(...workerResult.warnings);
        if (workerResult.errors.length > 0) {
            errors.push(...workerResult.errors);
            return undefined;
        }
        if (!write && workerResult.outputFiles != null) {
            allOutputFiles.push(...workerResult.outputFiles);
        }
        if (workerResult.metafile != null) {
            if (mergedMetafile == null) {
                mergedMetafile = { inputs: {}, outputs: {} };
            }
            Object.assign(mergedMetafile.inputs, workerResult.metafile.inputs);
            Object.assign(mergedMetafile.outputs, workerResult.metafile.outputs);
        }
        const workerCodeFile = workerResult.outputFiles?.find((file) => /^worker-[a-z0-9]+\.[cm]?js$/i.test(path.basename(file.path))) ??
            (workerResult.metafile != null
                ? (() => {
                    const outputKey = Object.keys(workerResult.metafile.outputs).find((key) => /worker-[a-z0-9]+\.[cm]?js$/i.test(path.basename(key)));
                    return outputKey != null ? { path: path.resolve(outputKey) } : undefined;
                })()
                : undefined);
        if (workerCodeFile == null) {
            errors.push({
                text: `Worker 번들 출력 파일을 찾을 수 없습니다: ${fullWorkerPath}`,
                location: null,
            });
            return undefined;
        }
        // outdir 루트 기준 상대 경로. bundleWorker에서 outbase: undefined를 설정하여
        // Worker 파일이 항상 outdir 루트에 출력됨을 보장한다.
        return path.relative(outdir, workerCodeFile.path).replaceAll("\\", "/");
    }
    // 정방향 chunks 패턴으로 치환 (esbuild-postcss-plugin.ts 동일 패턴)
    const replacements = [];
    for (const match of matches) {
        if (match.type === "browser") {
            const fullWorkerPath = path.resolve(containingDir, match.urlPath);
            const workerCodePath = processWorkerBundle(fullWorkerPath, "browser");
            if (workerCodePath == null)
                continue;
            const optsStr = match.existingOpts ?? '{ type: "module" }';
            replacements.push({
                start: match.start,
                end: match.end,
                text: `new ${match.workerType}(new URL("${workerCodePath}", import.meta.url), ${optsStr})`,
            });
        }
        else {
            const fullWorkerPath = path.resolve(containingDir, match.urlPath);
            const workerCodePath = processWorkerBundle(fullWorkerPath, build.initialOptions.platform ?? "browser");
            if (workerCodePath == null)
                continue;
            replacements.push({
                start: match.start,
                end: match.end,
                text: `new URL("${workerCodePath}", import.meta.url).href`,
            });
        }
    }
    // chunks 조립 (변환된 JS 기준 — 매치의 start/end는 effectiveContent 오프셋)
    const chunks = [];
    let cursor = 0;
    for (const rep of replacements) {
        chunks.push(effectiveContent.slice(cursor, rep.start));
        chunks.push(rep.text);
        cursor = rep.end;
    }
    chunks.push(effectiveContent.slice(cursor));
    return {
        contents: chunks.join(""),
        errors,
        warnings,
        workerOutputFiles: allOutputFiles.length > 0 ? allOutputFiles : undefined,
        workerMetafile: mergedMetafile,
    };
}
/**
 * esbuild Worker 번들링 플러그인을 생성한다.
 *
 * onLoad에서 .js/.ts 파일의 Worker/SharedWorker 패턴을 감지하여
 * Worker 파일을 별도 ESM 번들로 빌드하고, URL 경로를 번들된 파일로 치환한다.
 */
export function createWorkerBundlePlugin() {
    return {
        name: "sd-worker-bundle",
        setup(build) {
            const pendingWorkerResults = [];
            build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
                const content = fs.readFileSync(args.path, "utf-8");
                const result = transformWorkerPatterns(content, args.path, build);
                if (result == null)
                    return undefined;
                // write: false일 때 Worker outputFiles를 모아뒀다가 onEnd에서 병합
                if (result.workerOutputFiles != null || result.workerMetafile != null) {
                    pendingWorkerResults.push({
                        outputFiles: result.workerOutputFiles,
                        metafile: result.workerMetafile,
                    });
                }
                // TS(.ts/.cts/.mts)는 transformWorkerPatterns 내부에서 JS로 변환되어 반환되므로
                // loader는 "js". .tsx/.jsx는 변환하지 않으므로 esbuild가 JSX를 처리하도록 "tsx".
                const isJsx = /\.[cm]?tsx$/.test(args.path) || args.path.endsWith(".jsx");
                return {
                    contents: result.contents,
                    loader: isJsx ? "tsx" : "js",
                    errors: result.errors.length > 0 ? result.errors : undefined,
                    warnings: result.warnings.length > 0 ? result.warnings : undefined,
                };
            });
            build.onEnd((result) => {
                for (const wr of pendingWorkerResults) {
                    if (wr.outputFiles != null && wr.outputFiles.length > 0) {
                        result.outputFiles?.push(...wr.outputFiles);
                    }
                    if (result.metafile != null && wr.metafile != null) {
                        Object.assign(result.metafile.inputs, wr.metafile.inputs);
                        Object.assign(result.metafile.outputs, wr.metafile.outputs);
                    }
                }
                pendingWorkerResults.length = 0;
            });
        },
    };
}
//# sourceMappingURL=esbuild-worker-plugin.js.map