import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { abortIfNeeded } from '../utils.js';
let treeSitterModulePromise;
let sharedParser;
const languageCache = new Map();
const tagsQueryCache = new Map();
const tagCapturesByTree = new WeakMap();
const COMPILED_QUERY_CACHE_LIMIT = 32;
class CompiledQuerySlot {
    promise;
    query;
    borrowers = 0;
    evicted = false;
    disposed = false;
    constructor(grammar, source) {
        this.promise = compileTreeSitterQuery(grammar, source).then(query => {
            this.query = query;
            this.disposeIfUnused();
            return query;
        });
    }
    disposeIfUnused() {
        if (!this.evicted || this.borrowers > 0 || this.disposed || !this.query)
            return;
        this.query.delete();
        this.disposed = true;
    }
}
const compiledQueryCache = new Map();
const DEFAULT_PARSE_TREE_CACHE_ENTRIES = 32;
const DEFAULT_PARSE_TREE_CACHE_SOURCE_BYTES = 16 * 1024 * 1024;
class ParseTreeSlot {
    sourceBytes;
    onDelete;
    signal;
    promise;
    tree;
    settled = false;
    borrowers = 0;
    evicted = false;
    disposed = false;
    constructor(sourceBytes, pending, onDelete, signal) {
        this.sourceBytes = sourceBytes;
        this.onDelete = onDelete;
        this.signal = signal;
        this.promise = pending.then(tree => {
            this.settled = true;
            this.tree = tree;
            this.disposeIfUnused();
            return tree;
        }, (error) => {
            this.settled = true;
            throw error;
        });
    }
    disposeIfUnused() {
        if (!this.evicted || this.borrowers > 0 || this.disposed || !this.tree)
            return;
        this.tree.delete();
        this.disposed = true;
        this.onDelete();
    }
}
function positiveCacheLimit(value, fallback, label) {
    const resolved = value ?? fallback;
    if (!Number.isInteger(resolved) || resolved <= 0) {
        throw new Error(`${label} must be a positive integer.`);
    }
    return resolved;
}
function parseTreeCacheKey(grammar, content) {
    const digest = createHash('sha256').update(content, 'utf8').digest('hex');
    return `${grammar}\u0000${digest}`;
}
class ActiveParseTreeCache {
    maxEntries;
    maxSourceBytes;
    onEmpty;
    entries = new Map();
    sourceBytes = 0;
    evictions = 0;
    treesDeleted = 0;
    scopes = 0;
    constructor(maxEntries, maxSourceBytes, onEmpty) {
        this.maxEntries = maxEntries;
        this.maxSourceBytes = maxSourceBytes;
        this.onEmpty = onEmpty;
    }
    open() {
        this.scopes += 1;
    }
    async acquire(grammar, content, signal) {
        abortIfNeeded(signal);
        const key = parseTreeCacheKey(grammar, content);
        let slot = this.entries.get(key);
        if (slot && !slot.settled && slot.signal !== signal) {
            // Cancellation is operation-local. Two overlapping commands with
            // different signals must not share an in-flight parse that either one
            // can cancel; resolved trees are safe to share.
            this.remove(key, slot);
            slot = undefined;
        }
        if (slot) {
            this.entries.delete(key);
            this.entries.set(key, slot);
        }
        else {
            slot = new ParseTreeSlot(Buffer.byteLength(content, 'utf8'), parseTreeSitterContent(grammar, content, signal), () => {
                this.treesDeleted += 1;
            }, signal);
            this.insert(key, slot);
        }
        slot.borrowers += 1;
        this.enforceBounds();
        return this.finishLease(key, slot);
    }
    async adopt(grammar, content, tree) {
        const key = parseTreeCacheKey(grammar, content);
        const existing = this.entries.get(key);
        if (existing)
            this.remove(key, existing);
        const slot = new ParseTreeSlot(Buffer.byteLength(content, 'utf8'), Promise.resolve(tree), () => {
            this.treesDeleted += 1;
        }, undefined);
        slot.borrowers += 1;
        this.insert(key, slot);
        this.enforceBounds();
        const lease = await this.finishLease(key, slot);
        if (!lease)
            throw new Error('An adopted tree-sitter parse tree cannot be unavailable.');
        return lease;
    }
    stats() {
        return {
            entries: this.entries.size,
            sourceBytes: this.sourceBytes,
            evictions: this.evictions,
            treesDeleted: this.treesDeleted,
        };
    }
    close() {
        if (this.scopes <= 0)
            return;
        this.scopes -= 1;
        if (this.scopes > 0)
            return;
        for (const [key, slot] of this.entries)
            this.remove(key, slot);
        this.onEmpty();
    }
    insert(key, slot) {
        this.entries.set(key, slot);
        this.sourceBytes += slot.sourceBytes;
    }
    remove(key, slot) {
        if (this.entries.get(key) === slot) {
            this.entries.delete(key);
            this.sourceBytes -= slot.sourceBytes;
        }
        if (!slot.evicted) {
            slot.evicted = true;
            this.evictions += 1;
        }
        slot.disposeIfUnused();
    }
    enforceBounds() {
        while (this.entries.size > this.maxEntries || this.sourceBytes > this.maxSourceBytes) {
            const oldest = this.entries.entries().next().value;
            if (!oldest)
                return;
            this.remove(oldest[0], oldest[1]);
        }
    }
    async finishLease(key, slot) {
        let tree;
        try {
            tree = await slot.promise;
        }
        catch (error) {
            this.remove(key, slot);
            slot.borrowers -= 1;
            slot.disposeIfUnused();
            throw error;
        }
        if (!tree) {
            this.remove(key, slot);
            slot.borrowers -= 1;
            slot.disposeIfUnused();
            return null;
        }
        let released = false;
        return {
            tree,
            release() {
                if (released)
                    return;
                released = true;
                slot.borrowers -= 1;
                slot.disposeIfUnused();
            },
        };
    }
}
class ParseTreeCacheScope {
    active;
    closed = false;
    constructor(active) {
        this.active = active;
    }
    acquire(grammar, content, signal) {
        if (this.closed)
            throw new Error('Tree-sitter parse cache scope is closed.');
        return this.active.acquire(grammar, content, signal);
    }
    adopt(grammar, content, tree) {
        if (this.closed)
            throw new Error('Tree-sitter parse cache scope is closed.');
        return this.active.adopt(grammar, content, tree);
    }
    stats() {
        return this.active.stats();
    }
    close() {
        if (this.closed)
            return;
        this.closed = true;
        this.active.close();
    }
}
const activeParseTreeCaches = new WeakMap();
/**
 * Open one invocation-scoped parse-tree cache for a CodebaseIndex.
 *
 * Concurrent scopes on the same index share trees and borrower counts. The
 * last scope to close evicts every entry, so a resident index does not retain
 * WASM trees across commands. Eviction is lease-aware: `tree.delete()` is
 * deferred until no borrower can still observe that tree.
 */
export function openTreeSitterParseCache(index, options = {}) {
    const maxEntries = positiveCacheLimit(options.maxEntries, DEFAULT_PARSE_TREE_CACHE_ENTRIES, 'Parse-tree cache maxEntries');
    const maxSourceBytes = positiveCacheLimit(options.maxSourceBytes, DEFAULT_PARSE_TREE_CACHE_SOURCE_BYTES, 'Parse-tree cache maxSourceBytes');
    let active = activeParseTreeCaches.get(index);
    if (!active) {
        active = new ActiveParseTreeCache(maxEntries, maxSourceBytes, () => {
            if (activeParseTreeCaches.get(index) === active)
                activeParseTreeCaches.delete(index);
        });
        activeParseTreeCaches.set(index, active);
    }
    active.open();
    return new ParseTreeCacheScope(active);
}
async function loadTreeSitterModule() {
    if (!treeSitterModulePromise) {
        treeSitterModulePromise = import('web-tree-sitter')
            .then(async (module) => {
            await module.Parser.init();
            return module;
        })
            .catch((error) => {
            treeSitterModulePromise = undefined;
            const detail = error instanceof Error ? ` ${error.message}` : '';
            throw new Error(`Failed to load the bundled "web-tree-sitter" dependency.${detail}`);
        });
    }
    return treeSitterModulePromise;
}
/**
 * Complete web-tree-sitter's one-time runtime initialization.
 *
 * Exposed for co-resident consumers of the same module instance (the ast-grep
 * engine): sequencing their init after this one avoids racing the Emscripten
 * module bring-up, which web-tree-sitter caches but does not lock.
 */
export async function initializeTreeSitterRuntime() {
    await loadTreeSitterModule();
}
export function resolveTreeSitterAsset(grammar, fileName) {
    const here = path.dirname(fileURLToPath(import.meta.url));
    const candidates = [
        path.resolve(here, '../../vendor/tree-sitter', grammar, fileName),
        path.resolve(here, '../../../vendor/tree-sitter', grammar, fileName),
        path.resolve(process.cwd(), 'vendor/tree-sitter', grammar, fileName),
    ];
    return candidates.find(candidate => existsSync(candidate)) ?? null;
}
export async function loadTreeSitterLanguage(grammar) {
    const cached = languageCache.get(grammar);
    if (cached)
        return cached;
    const promise = (async () => {
        const wasmPath = resolveTreeSitterAsset(grammar, 'grammar.wasm');
        if (!wasmPath)
            return null;
        const module = await loadTreeSitterModule();
        try {
            return await module.Language.load(wasmPath);
        }
        catch {
            return null;
        }
    })();
    languageCache.set(grammar, promise);
    return promise;
}
async function compileTreeSitterQuery(grammar, source) {
    const language = await loadTreeSitterLanguage(grammar);
    if (!language)
        return null;
    const module = await loadTreeSitterModule();
    return new module.Query(language, source);
}
function evictCompiledQueries() {
    while (compiledQueryCache.size > COMPILED_QUERY_CACHE_LIMIT) {
        const oldest = compiledQueryCache.entries().next().value;
        if (!oldest)
            return;
        const [key, slot] = oldest;
        compiledQueryCache.delete(key);
        slot.evicted = true;
        slot.disposeIfUnused();
    }
}
/**
 * Borrow one compiled caller-supplied query from a bounded LRU.
 *
 * Query text is unbounded user input, so the cache is deliberately capped.
 * Eviction explicitly deletes the WASM handle once the last borrower releases
 * it; an in-flight query is never deleted underneath its caller.
 */
export async function acquireTreeSitterQuery(grammar, source) {
    const key = JSON.stringify([grammar, source]);
    let slot = compiledQueryCache.get(key);
    if (slot) {
        compiledQueryCache.delete(key);
        compiledQueryCache.set(key, slot);
    }
    else {
        slot = new CompiledQuerySlot(grammar, source);
        compiledQueryCache.set(key, slot);
        evictCompiledQueries();
    }
    slot.borrowers += 1;
    let query;
    try {
        query = await slot.promise;
    }
    catch (error) {
        if (compiledQueryCache.get(key) === slot)
            compiledQueryCache.delete(key);
        slot.evicted = true;
        slot.borrowers -= 1;
        slot.disposeIfUnused();
        throw error;
    }
    if (!query) {
        slot.borrowers -= 1;
        slot.disposeIfUnused();
        return null;
    }
    let released = false;
    return {
        query,
        release() {
            if (released)
                return;
            released = true;
            slot.borrowers -= 1;
            slot.disposeIfUnused();
        },
    };
}
/** @internal Observable only so the bounded/disposal contract can be regression-tested. */
export function treeSitterQueryCacheSize() {
    return compiledQueryCache.size;
}
async function loadTreeSitterTagsQuery(grammar) {
    const cached = tagsQueryCache.get(grammar);
    if (cached)
        return cached;
    const promise = (async () => {
        const queryPath = resolveTreeSitterAsset(grammar, 'tags.scm');
        if (!queryPath)
            return null;
        const [module, language, source] = await Promise.all([
            loadTreeSitterModule(),
            loadTreeSitterLanguage(grammar),
            readFile(queryPath, 'utf8'),
        ]);
        if (!language || source.trim().length === 0)
            return null;
        try {
            return new module.Query(language, source);
        }
        catch {
            return null;
        }
    })();
    tagsQueryCache.set(grammar, promise);
    return promise;
}
export async function parseTreeSitterContent(grammar, content, signal) {
    abortIfNeeded(signal);
    const language = await loadTreeSitterLanguage(grammar);
    if (!language)
        return null;
    abortIfNeeded(signal);
    const module = await loadTreeSitterModule();
    // web-tree-sitter Parser/Tree are Emscripten handles that JS GC never frees. A
    // single Parser is reused across files (its grammar is reassigned per call) to
    // avoid per-file allocation. setLanguage + parse run with no intervening await,
    // so concurrent callers cannot observe a mismatched grammar. The returned tree
    // is independent of the parser once produced; the caller owns it and must call
    // tree.delete() after copying out the node data it needs.
    if (!sharedParser) {
        sharedParser = new module.Parser();
    }
    sharedParser.setLanguage(language);
    abortIfNeeded(signal);
    const tree = signal
        ? sharedParser.parse(content, null, {
            // web-tree-sitter's declaration types this callback as returning void,
            // but its parser contract cancels when it returns true. A
            // boolean-returning function is assignable to a void callback and
            // preserves that runtime behaviour without a cast.
            progressCallback: () => signal.aborted,
        })
        : sharedParser.parse(content);
    if (!tree && signal?.aborted) {
        // A cancelled parser otherwise resumes the interrupted parse on its next
        // call, which is wrong for this shared cross-file parser.
        sharedParser.reset();
        abortIfNeeded(signal);
    }
    return tree;
}
export async function collectTreeSitterTagCaptures(grammar, root) {
    let capturesByGrammar = tagCapturesByTree.get(root.tree);
    if (!capturesByGrammar) {
        capturesByGrammar = new Map();
        tagCapturesByTree.set(root.tree, capturesByGrammar);
    }
    const cached = capturesByGrammar.get(grammar);
    if (cached)
        return cached;
    const pending = (async () => {
        const query = await loadTreeSitterTagsQuery(grammar);
        if (!query)
            return null;
        try {
            return query.captures(root).map(capture => ({
                name: capture.name,
                node: capture.node,
            }));
        }
        catch {
            return null;
        }
    })().catch((error) => {
        capturesByGrammar?.delete(grammar);
        throw error;
    });
    capturesByGrammar.set(grammar, pending);
    return pending;
}
export function collectTreeSitterErrorNodes(root, options = {}) {
    const errors = [];
    const maxNodes = options.maxNodes ?? 500_000;
    const maxDiagnostics = options.maxDiagnostics ?? 1_000;
    if (!Number.isInteger(maxNodes) || maxNodes <= 0)
        throw new Error('Tree-sitter maxNodes must be a positive integer.');
    if (!Number.isInteger(maxDiagnostics) || maxDiagnostics <= 0) {
        throw new Error('Tree-sitter maxDiagnostics must be a positive integer.');
    }
    // hasError means "is or contains an error", so a subtree without it cannot
    // contribute a diagnostic and is never descended into. This keeps the walk
    // proportional to the recovery region rather than the whole tree.
    if (!root.hasError) {
        abortIfNeeded(options.signal);
        return errors;
    }
    const stack = [root];
    // A node can report hasError while exposing no visible ERROR or MISSING
    // descendant — the recovery happened inside a hidden node. Track the earliest
    // such dead end so an invalid file is never reported as diagnostic-free.
    let hiddenError;
    let visited = 0;
    while (stack.length > 0 && visited < maxNodes && errors.length < maxDiagnostics) {
        if ((visited & 255) === 0)
            abortIfNeeded(options.signal);
        const node = stack.pop();
        if (!node)
            break;
        visited += 1;
        const visible = node.type === 'ERROR' || node.isMissing;
        if (visible) {
            errors.push({
                type: node.type,
                text: node.text,
                line: node.startPosition.row + 1,
                column: node.startPosition.column + 1,
            });
        }
        let descended = false;
        for (let index = node.childCount - 1; index >= 0; index--) {
            const child = node.child(index);
            if (child?.hasError) {
                stack.push(child);
                descended = true;
            }
        }
        if (!visible && !descended && (!hiddenError || node.startIndex < hiddenError.startIndex)) {
            hiddenError = node;
        }
    }
    abortIfNeeded(options.signal);
    if (errors.length === 0 && hiddenError) {
        errors.push({
            type: 'ERROR',
            text: '',
            line: hiddenError.startPosition.row + 1,
            column: hiddenError.startPosition.column + 1,
        });
    }
    return errors;
}
//# sourceMappingURL=tree-sitter.js.map