"use strict";
// ---------------------------------------------------------------------------
// Whole-Program Analyzer
//
// Top-level orchestrator for cross-module constant propagation.
// Caches per-module environments and provides demand-driven import resolution.
// ---------------------------------------------------------------------------
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WholeProgramAnalyzer = void 0;
const typescript_1 = __importDefault(require("typescript"));
const diagnosticCodes_js_1 = require("../diagnosticCodes.js");
const lattice_js_1 = require("./lattice.js");
const moduleAnalyzer_js_1 = require("./moduleAnalyzer.js");
const expressionEvaluator_js_1 = require("./expressionEvaluator.js");
// ---------------------------------------------------------------------------
// WholeProgramAnalyzer
// ---------------------------------------------------------------------------
class WholeProgramAnalyzer {
    program;
    checker;
    context;
    serviceRegistry;
    inliningDepth;
    /** Cached module environments keyed by file path. */
    cache = new Map();
    /** Files currently being analyzed (cycle detection). */
    inProgress = new Set();
    /** Track which diagnostics we've already emitted to avoid duplicates. */
    emittedDiagnostics = new Set();
    constructor(context, serviceRegistry, options) {
        this.context = context;
        this.program = context.program;
        this.checker = context.checker;
        this.serviceRegistry = serviceRegistry;
        this.inliningDepth = options?.inliningDepth ?? 1;
    }
    // -----------------------------------------------------------------------
    // Public API
    // -----------------------------------------------------------------------
    /**
     * Analyze a source file and return its module environment.
     * Results are cached — calling this twice for the same file is free.
     */
    analyzeFile(sourceFile) {
        const key = sourceFile.fileName;
        const cached = this.cache.get(key);
        if (cached)
            return cached;
        // Cycle detection
        if (this.inProgress.has(key)) {
            // Return a placeholder environment — imports from this file will Bottom
            const placeholder = new moduleAnalyzer_js_1.ModuleEnvironment(sourceFile, this.checker);
            this.cache.set(key, placeholder);
            return placeholder;
        }
        this.inProgress.add(key);
        const importResolver = (fromFile, moduleSpec, importedName) => {
            return this.resolveImport(fromFile, moduleSpec, importedName);
        };
        const namespaceResolver = (fromFile, moduleSpec) => {
            return this.resolveNamespaceImport(fromFile, moduleSpec);
        };
        const env = (0, moduleAnalyzer_js_1.analyzeModule)(sourceFile, this.checker, importResolver, namespaceResolver, this.inliningDepth);
        this.cache.set(key, env);
        this.inProgress.delete(key);
        return env;
    }
    /**
     * Resolve a symbol to its lattice value, analyzing the declaring module if needed.
     */
    resolveSymbol(symbol) {
        const decls = symbol.getDeclarations();
        if (!decls || decls.length === 0) {
            return (0, lattice_js_1.bottom)(`Symbol '${symbol.getName()}' has no declarations`);
        }
        const sourceFile = decls[0].getSourceFile();
        if (sourceFile.isDeclarationFile) {
            return (0, lattice_js_1.bottom)(`Symbol '${symbol.getName()}' is from a declaration file`);
        }
        const env = this.analyzeFile(sourceFile);
        return env.resolve(symbol);
    }
    /**
     * Evaluate an arbitrary expression using the module's environment.
     * Supports pure function inlining and cross-module resolution.
     */
    evaluateExpression(expr, sourceFile) {
        const env = this.analyzeFile(sourceFile);
        const callResolver = this.inliningDepth > 0
            ? (callExpr, evaluator) => {
                return (0, moduleAnalyzer_js_1.tryInlineCall)(callExpr, evaluator, env, this.checker, this.inliningDepth);
            }
            : undefined;
        const evaluator = new expressionEvaluator_js_1.ExpressionEvaluator(this.checker, (sym) => env.resolve(sym), callResolver);
        return evaluator.evaluate(expr);
    }
    /**
     * Resolve an import across modules.
     */
    resolveImport(fromFile, moduleSpecifier, importedName) {
        const resolved = this.resolveModulePath(fromFile, moduleSpecifier);
        if (!resolved) {
            this.emitImportDiagnostic(fromFile, moduleSpecifier, `Cannot resolve module '${moduleSpecifier}'`, diagnosticCodes_js_1.ErrorCodes.DataFlow.UnresolvableImport);
            return (0, lattice_js_1.bottom)(`Cannot resolve module '${moduleSpecifier}'`);
        }
        const targetFile = this.program.getSourceFile(resolved);
        if (!targetFile) {
            this.emitImportDiagnostic(fromFile, moduleSpecifier, `Module '${moduleSpecifier}' not found in program`, diagnosticCodes_js_1.ErrorCodes.DataFlow.UnresolvableImport);
            return (0, lattice_js_1.bottom)(`Module '${moduleSpecifier}' not found in program`);
        }
        if (targetFile.isDeclarationFile) {
            // Not an error — .d.ts modules just aren't analyzable for constant values
            return (0, lattice_js_1.bottom)(`Module '${moduleSpecifier}' is a declaration file — values not analyzable`);
        }
        // Cycle detection
        if (this.inProgress.has(targetFile.fileName)) {
            this.emitImportDiagnostic(fromFile, moduleSpecifier, `Circular import dependency: ${fromFile.fileName} → ${targetFile.fileName}`, diagnosticCodes_js_1.ErrorCodes.DataFlow.CircularImport);
            return (0, lattice_js_1.bottom)(`Circular import dependency: ${fromFile.fileName} → ${targetFile.fileName}`);
        }
        const env = this.analyzeFile(targetFile);
        if (importedName === 'default') {
            return env.getExport('default');
        }
        return env.getExport(importedName);
    }
    /**
     * Resolve a namespace import (import * as X) — returns an object of all exports.
     */
    resolveNamespaceImport(fromFile, moduleSpecifier) {
        const resolved = this.resolveModulePath(fromFile, moduleSpecifier);
        if (!resolved) {
            return (0, lattice_js_1.bottom)(`Cannot resolve module '${moduleSpecifier}'`);
        }
        const targetFile = this.program.getSourceFile(resolved);
        if (!targetFile) {
            return (0, lattice_js_1.bottom)(`Module '${moduleSpecifier}' not found in program`);
        }
        if (targetFile.isDeclarationFile) {
            return (0, lattice_js_1.bottom)(`Module '${moduleSpecifier}' is a declaration file`);
        }
        if (this.inProgress.has(targetFile.fileName)) {
            this.emitImportDiagnostic(fromFile, moduleSpecifier, `Circular import dependency`, diagnosticCodes_js_1.ErrorCodes.DataFlow.CircularImport);
            return (0, lattice_js_1.bottom)(`Circular import dependency`);
        }
        const env = this.analyzeFile(targetFile);
        const allExports = env.getAllExports();
        // Build a constant object from exports that are all constant
        const nsObj = {};
        let allConstant = true;
        for (const [name, value] of allExports) {
            if ((0, lattice_js_1.isConstant)(value)) {
                nsObj[name] = value.value;
            }
            else {
                allConstant = false;
                break;
            }
        }
        if (allConstant) {
            return (0, lattice_js_1.constant)(nsObj);
        }
        return (0, lattice_js_1.bottom)(`Namespace import from '${moduleSpecifier}' contains non-constant exports`);
    }
    // -----------------------------------------------------------------------
    // Diagnostic helpers
    // -----------------------------------------------------------------------
    /**
     * Find the import declaration node for a given module specifier and emit a diagnostic.
     * Deduplicates by file+specifier+code to avoid noisy repeated errors.
     */
    emitImportDiagnostic(fromFile, moduleSpecifier, message, code) {
        const key = `${fromFile.fileName}:${moduleSpecifier}:${code.code}`;
        if (this.emittedDiagnostics.has(key))
            return;
        this.emittedDiagnostics.add(key);
        // Find the import declaration node for accurate source location
        const node = this.findImportNode(fromFile, moduleSpecifier);
        if (!node)
            return; // Can't emit without a node
        if (code.severity === 'warning') {
            this.context.addWarning(node, message, code);
        }
        else {
            this.context.addError(node, message, code);
        }
    }
    findImportNode(sourceFile, moduleSpecifier) {
        for (const stmt of sourceFile.statements) {
            if (typescript_1.default.isImportDeclaration(stmt) &&
                typescript_1.default.isStringLiteral(stmt.moduleSpecifier) &&
                stmt.moduleSpecifier.text === moduleSpecifier) {
                return stmt.moduleSpecifier;
            }
            if (typescript_1.default.isExportDeclaration(stmt) &&
                stmt.moduleSpecifier &&
                typescript_1.default.isStringLiteral(stmt.moduleSpecifier) &&
                stmt.moduleSpecifier.text === moduleSpecifier) {
                return stmt.moduleSpecifier;
            }
        }
        return undefined;
    }
    // -----------------------------------------------------------------------
    // Module resolution
    // -----------------------------------------------------------------------
    resolveModulePath(fromFile, moduleSpecifier) {
        const compilerOptions = this.program.getCompilerOptions();
        // Try standard module resolution first (works with real file systems)
        if (typescript_1.default.sys) {
            const result = typescript_1.default.resolveModuleName(moduleSpecifier, fromFile.fileName, compilerOptions, typescript_1.default.sys);
            if (result.resolvedModule) {
                return result.resolvedModule.resolvedFileName;
            }
        }
        // Fallback: scan program source files for a match.
        // Handles virtual file systems (browser environments, tests) where
        // ts.sys doesn't know about the files.
        return this.resolveModulePathFromProgram(fromFile, moduleSpecifier);
    }
    resolveModulePathFromProgram(fromFile, moduleSpecifier) {
        if (!moduleSpecifier.startsWith('.'))
            return undefined;
        // Resolve the relative path from the importing file's directory
        const fromDir = fromFile.fileName.substring(0, fromFile.fileName.lastIndexOf('/'));
        const candidates = this.buildResolutionCandidates(fromDir, moduleSpecifier);
        for (const candidate of candidates) {
            if (this.program.getSourceFile(candidate)) {
                return candidate;
            }
        }
        return undefined;
    }
    buildResolutionCandidates(fromDir, specifier) {
        // Normalize the path: resolve './' and '../'
        const rawPath = `${fromDir}/${specifier}`;
        const parts = rawPath.split('/');
        const resolved = [];
        for (const part of parts) {
            if (part === '.')
                continue;
            if (part === '..') {
                if (resolved.length > 1)
                    resolved.pop();
                continue;
            }
            resolved.push(part);
        }
        const base = resolved.join('/');
        // If specifier already has an extension, try it directly
        if (/\.\w+$/.test(specifier)) {
            return [base];
        }
        // Try common extensions
        return [
            `${base}.ts`,
            `${base}.tsx`,
            `${base}/index.ts`,
            `${base}/index.tsx`,
            base,
        ];
    }
}
exports.WholeProgramAnalyzer = WholeProgramAnalyzer;
//# sourceMappingURL=wholeProgramAnalyzer.js.map