import { findChild, nodeEndLine } from './helpers.js';
/**
 * Extract symbols from Erlang files.
 *
 * tree-sitter-erlang (WhatsApp) grammar notes:
 * - module_attribute: -module(name).
 * - record_decl: -record(name, {fields}).
 * - fun_decl: contains function_clause children
 * - function_clause: atom expr_args clause_body
 * - call: function calls, with remote child for module:func
 * - expr_args: parenthesized argument lists
 */
export function extractErlangSymbols(tree, _filePath) {
    const ctx = {
        definitions: [],
        calls: [],
        imports: [],
        classes: [],
        exports: [],
        typeMap: new Map(),
    };
    walkErlangNode(tree.rootNode, ctx);
    return ctx;
}
function walkErlangNode(node, ctx) {
    switch (node.type) {
        case 'module_attribute':
            handleModuleAttr(node, ctx);
            break;
        case 'record_decl':
            handleRecordDecl(node, ctx);
            break;
        case 'type_alias':
        case 'opaque':
            handleTypeAlias(node, ctx);
            break;
        case 'fun_decl':
            handleFunDecl(node, ctx);
            break;
        case 'function_clause':
            // Only handle if not inside fun_decl (fun_decl handles its own clauses)
            if (node.parent?.type !== 'fun_decl') {
                handleFunctionClause(node, ctx);
            }
            break;
        case 'pp_define':
            handleDefine(node, ctx);
            break;
        case 'pp_include':
        case 'pp_include_lib':
            handleInclude(node, ctx);
            break;
        case 'import_attribute':
            handleImportAttr(node, ctx);
            break;
        case 'call':
            handleCall(node, ctx);
            break;
    }
    for (let i = 0; i < node.childCount; i++) {
        const child = node.child(i);
        if (child)
            walkErlangNode(child, ctx);
    }
}
function handleModuleAttr(node, ctx) {
    // module_attribute: - module ( atom ) .
    // Prefer the named `name` field exposed by tree-sitter-erlang so we don't
    // accidentally pick up the `module` keyword if a future grammar exposes it
    // as a named `atom` child.
    const nameNode = node.childForFieldName('name') ?? findChild(node, 'atom');
    if (!nameNode)
        return;
    ctx.definitions.push({
        name: nameNode.text,
        kind: 'module',
        line: node.startPosition.row + 1,
        endLine: nodeEndLine(node),
    });
}
function handleRecordDecl(node, ctx) {
    // record_decl: - record ( atom , { record_field, ... } ) .
    // Prefer the named `name` field exposed by tree-sitter-erlang; fall back to
    // the first atom child for grammar versions that don't expose it. Mirrors
    // the Rust `handle_record_decl` defensive pattern.
    const nameNode = node.childForFieldName('name') ?? findChild(node, 'atom');
    if (!nameNode)
        return;
    const children = [];
    for (let i = 0; i < node.childCount; i++) {
        const child = node.child(i);
        if (!child)
            continue;
        if (child.type === 'record_field' || child.type === 'typed_record_field') {
            const fieldName = findChild(child, 'atom');
            if (fieldName) {
                children.push({
                    name: fieldName.text,
                    kind: 'property',
                    line: child.startPosition.row + 1,
                });
            }
        }
    }
    ctx.definitions.push({
        name: nameNode.text,
        kind: 'record',
        line: node.startPosition.row + 1,
        endLine: nodeEndLine(node),
        children: children.length > 0 ? children : undefined,
    });
}
function handleTypeAlias(node, ctx) {
    // type_alias: -type name(...) :: ty.
    // Name is typically wrapped in a `type_name` node containing an `atom`.
    // Mirrors the Rust `handle_type_alias` fallback so the two engines agree
    // even when the grammar nests the name inside `type_name`.
    const directAtom = findChild(node, 'atom');
    const typeNameNode = !directAtom ? findChild(node, 'type_name') : null;
    const wrappedAtom = typeNameNode ? findChild(typeNameNode, 'atom') : null;
    const nameNode = directAtom ?? wrappedAtom;
    if (!nameNode)
        return;
    ctx.definitions.push({
        name: nameNode.text,
        kind: 'type',
        line: node.startPosition.row + 1,
        endLine: nodeEndLine(node),
    });
}
function handleFunDecl(node, ctx) {
    // fun_decl contains one or more function_clause children + dots
    // Extract from the first function_clause
    const clause = findChild(node, 'function_clause');
    if (!clause)
        return;
    handleFunctionClause(clause, ctx);
}
function handleFunctionClause(node, ctx) {
    // function_clause: atom expr_args clause_body
    const nameNode = node.childForFieldName('name') ?? findChild(node, 'atom');
    if (!nameNode)
        return;
    const params = extractErlangParams(node);
    const arity = params.length;
    // Don't duplicate if we already have this function at the same arity.
    // Erlang overloads by arity, so `foo/1` and `foo/2` are distinct definitions.
    if (ctx.definitions.some((d) => d.name === nameNode.text && d.kind === 'function' && (d.children?.length ?? 0) === arity)) {
        return;
    }
    ctx.definitions.push({
        name: nameNode.text,
        kind: 'function',
        line: node.startPosition.row + 1,
        endLine: nodeEndLine(node.parent?.type === 'fun_decl' ? node.parent : node),
        children: params.length > 0 ? params : undefined,
        visibility: 'public',
    });
}
function extractErlangParams(clauseNode) {
    const params = [];
    const argsNode = clauseNode.childForFieldName('args') ?? findChild(clauseNode, 'expr_args');
    if (!argsNode)
        return params;
    // Iterate named children so every argument pattern counts as one parameter,
    // independent of whether it is a bare `var`/`atom` or a complex destructuring
    // pattern (tuple, list, binary, etc.). Punctuation tokens are anonymous and
    // therefore excluded automatically.
    for (let i = 0; i < argsNode.namedChildCount; i++) {
        const child = argsNode.namedChild(i);
        if (!child)
            continue;
        const label = child.type === 'var' || child.type === 'atom'
            ? child.text
            : // Placeholder for complex patterns so arity is preserved.
                `_${i}`;
        params.push({ name: label, kind: 'parameter', line: child.startPosition.row + 1 });
    }
    return params;
}
function handleDefine(node, ctx) {
    // pp_define: -define(NAME, value).
    // For parametric macros, the grammar wraps the name in a `macro_lhs(name, args)`
    // node. Inside `macro_lhs` the name comes first, followed by `(`, the argument
    // `var` children, and `)`. We must therefore try `atom` (lowercase macros,
    // e.g. `-define(foo(X), X+1)`) before `var` (uppercase macros, e.g.
    // `-define(FOO(X), X+1)`) — otherwise `findChild(.., 'var')` skips the leading
    // atom and lands on the first argument variable, mislabeling the definition.
    // Mirrors the Rust `handle_define` so both engines agree.
    const nameNode = findChild(node, 'var') || findChild(node, 'atom') || findChild(node, 'macro_lhs');
    if (!nameNode)
        return;
    const name = nameNode.type === 'macro_lhs'
        ? (findChild(nameNode, 'atom')?.text ?? findChild(nameNode, 'var')?.text ?? nameNode.text)
        : nameNode.text;
    ctx.definitions.push({
        name,
        kind: 'variable',
        line: node.startPosition.row + 1,
        endLine: nodeEndLine(node),
    });
}
function handleInclude(node, ctx) {
    const strNode = findChild(node, 'string');
    if (!strNode)
        return;
    const source = strNode.text.replace(/^"|"$/g, '');
    // Preserve the distinction between local includes (`-include("foo.hrl")`)
    // and OTP library includes (`-include_lib("kernel/include/file.hrl")`) so
    // downstream consumers can apply the correct path-resolution strategy
    // (local: relative to the source file; lib: relative to an OTP app root).
    // Mirrors the Rust `handle_include` so both engines agree.
    const kind = node.type === 'pp_include_lib' ? 'include_lib' : 'include';
    ctx.imports.push({
        source,
        names: [kind],
        line: node.startPosition.row + 1,
    });
}
function handleImportAttr(node, ctx) {
    const moduleNode = findChild(node, 'atom');
    if (!moduleNode)
        return;
    const names = [];
    // Find exported function names
    for (let i = 0; i < node.childCount; i++) {
        const child = node.child(i);
        if (!child)
            continue;
        if (child.type === 'fa') {
            const fnName = findChild(child, 'atom');
            if (fnName)
                names.push(fnName.text);
        }
    }
    ctx.imports.push({
        source: moduleNode.text,
        names: names.length > 0 ? names : [moduleNode.text],
        line: node.startPosition.row + 1,
    });
}
function handleCall(node, ctx) {
    // call: first named child is function ref (atom or remote), then expr_args.
    // Using `namedChild(0)` rather than `child(0)` skips anonymous tokens
    // (punctuation, keywords) so a future grammar revision that inserts a
    // leading anonymous node won't silently drop the call. Mirrors the Rust
    // `handle_call` so both engines emit the same set of calls.
    const funcNode = node.namedChild(0);
    if (!funcNode)
        return;
    if (funcNode.type === 'atom' || funcNode.type === 'identifier') {
        ctx.calls.push({ name: funcNode.text, line: node.startPosition.row + 1 });
    }
    else if (funcNode.type === 'remote') {
        // module:function — remote has atom : atom children
        const atoms = [];
        for (let i = 0; i < funcNode.childCount; i++) {
            const child = funcNode.child(i);
            if (child && (child.type === 'atom' || child.type === 'var')) {
                atoms.push(child.text);
            }
        }
        if (atoms.length >= 2) {
            ctx.calls.push({
                name: atoms[atoms.length - 1],
                receiver: atoms.slice(0, -1).join(':'),
                line: node.startPosition.row + 1,
            });
        }
        else if (atoms.length === 1) {
            ctx.calls.push({ name: atoms[0], line: node.startPosition.row + 1 });
        }
    }
}
//# sourceMappingURL=erlang.js.map