// Plain-text + markdown adapter (Epic #189, Issue #266). Pure: takes bytes, returns a
// ParserResult. Plain text emits a single section unit covering the whole document; markdown
// is split on ATX-style headings (`^#{1,6}\\s`) into one section per heading run with the
// hierarchical sectionPath populated from heading text.
//
// Source code (.ts/.js/.py/.yaml/etc) routes here too via `languageHint`. The lexical scan
// runs once over the decoded string and stops emitting units the moment any limit (deadline
// / cancellation / unit count) trips.
import { decodeUtf8, diagnostic, emptyResult, oversizeDiagnostic, shouldStop, } from "./_internal.js";
const PARSER_ID = "text";
const PARSER_VERSION = "1";
// Extensions accepted by the text adapter. The list is intentionally explicit so unknown
// binary extensions fall through to the unsupported adapter rather than being mis-parsed as
// "plain text". `languageHint` is emitted for source / config files so #195 (chunker) can
// route them through code-aware splitters when those land.
const TEXT_EXTENSIONS = new Set([
    "txt",
    "log",
    "md",
    "markdown",
    "rst",
    "adoc",
    "asciidoc",
    "ts",
    "tsx",
    "js",
    "jsx",
    "mjs",
    "cjs",
    "py",
    "rb",
    "go",
    "rs",
    "java",
    "kt",
    "swift",
    "c",
    "cc",
    "cpp",
    "h",
    "hpp",
    "cs",
    "php",
    "sh",
    "bash",
    "zsh",
    "fish",
    "ps1",
    "yaml",
    "yml",
    "toml",
    "ini",
    "cfg",
    "conf",
    "env",
    "properties",
    "sql",
    "graphql",
    "gql",
]);
const TEXT_MEDIA_PREFIXES = ["text/"];
function isMarkdown(input) {
    const ext = input.extension.toLowerCase();
    if (ext === "md" || ext === "markdown")
        return true;
    return input.mediaType.toLowerCase() === "text/markdown";
}
function isTextLike(input) {
    const ext = input.extension.toLowerCase();
    if (TEXT_EXTENSIONS.has(ext))
        return true;
    const media = input.mediaType.toLowerCase();
    for (const prefix of TEXT_MEDIA_PREFIXES) {
        if (media.startsWith(prefix))
            return true;
    }
    return false;
}
function scanHeadings(text) {
    const out = [];
    let cursor = 0;
    while (cursor < text.length) {
        const newline = text.indexOf("\n", cursor);
        const lineEnd = newline === -1 ? text.length : newline;
        const line = text.slice(cursor, lineEnd);
        const heading = matchAtxHeading(line);
        if (heading !== null) {
            out.push({ ...heading, characterStart: cursor });
        }
        cursor = lineEnd + 1;
    }
    return out;
}
const HASH = 0x23;
const SPACE = 0x20;
const TAB = 0x09;
function isHSpace(code) {
    return code === SPACE || code === TAB;
}
function countHashes(line) {
    let i = 0;
    while (i < line.length && line.charCodeAt(i) === HASH && i < 6)
        i += 1;
    return i;
}
function skipHSpace(line, from) {
    let i = from;
    while (i < line.length && isHSpace(line.charCodeAt(i)))
        i += 1;
    return i;
}
function trimAtxTrailing(line, start) {
    let end = line.length;
    while (end > start && isHSpace(line.charCodeAt(end - 1)))
        end -= 1;
    while (end > start && line.charCodeAt(end - 1) === HASH)
        end -= 1;
    while (end > start && isHSpace(line.charCodeAt(end - 1)))
        end -= 1;
    return end;
}
function matchAtxHeading(line) {
    const level = countHashes(line);
    if (level < 1 || level > 6)
        return null;
    if (level >= line.length || !isHSpace(line.charCodeAt(level)))
        return null;
    const textStart = skipHSpace(line, level + 1);
    const textEnd = trimAtxTrailing(line, textStart);
    if (textEnd <= textStart)
        return null;
    return { level, text: line.slice(textStart, textEnd) };
}
function sectionUnit(input, path, start, end) {
    return {
        kind: "section",
        documentId: input.documentId,
        sectionPath: path,
        characterStart: start,
        characterEnd: end,
    };
}
function pushHeading(stack, current, next, text, input) {
    while (stack.length >= current.level)
        stack.pop();
    stack.push(current.text);
    const sectionEnd = next === undefined ? text.length : next.characterStart;
    return sectionUnit(input, [...stack], current.characterStart, sectionEnd);
}
function emitMarkdownSections(text, input, options, startedAt) {
    const headings = scanHeadings(text);
    if (headings.length === 0) {
        return emitPlainSection(text, input, options, startedAt);
    }
    const units = [];
    const diagnostics = [];
    const stack = [];
    const firstStart = headings[0]?.characterStart ?? 0;
    if (firstStart > 0)
        units.push(sectionUnit(input, [], 0, firstStart));
    for (let i = 0; i < headings.length; i += 1) {
        const limit = shouldStop(startedAt, options, units.length);
        if (limit.stop && limit.code !== undefined && limit.message !== undefined) {
            diagnostics.push(diagnostic(limit.code, limit.message, input.documentId, "info"));
            break;
        }
        const current = headings[i];
        if (current === undefined)
            break;
        units.push(pushHeading(stack, current, headings[i + 1], text, input));
    }
    return { units, diagnostics };
}
function emitPlainSection(text, input, options, startedAt) {
    const limit = shouldStop(startedAt, options, 0);
    if (limit.stop && limit.code !== undefined && limit.message !== undefined) {
        return {
            units: [],
            diagnostics: [diagnostic(limit.code, limit.message, input.documentId, "info")],
        };
    }
    return {
        units: [
            {
                kind: "section",
                documentId: input.documentId,
                sectionPath: [],
                characterStart: 0,
                characterEnd: text.length,
            },
        ],
        diagnostics: [],
    };
}
export const textParser = Object.freeze({
    capability: Object.freeze({
        parserId: PARSER_ID,
        parserVersion: PARSER_VERSION,
        matches: (input) => isTextLike(input),
    }),
    parse: (input, options) => {
        if (input.bytes.byteLength > options.maxBytes) {
            return emptyResult(textParser.capability, input.documentId, options, [
                oversizeDiagnostic(input.documentId, input.bytes.byteLength, options.maxBytes),
            ]);
        }
        const startedAt = options.now();
        const decoded = decodeUtf8(input.bytes);
        const emission = isMarkdown(input)
            ? emitMarkdownSections(decoded.text, input, options, startedAt)
            : emitPlainSection(decoded.text, input, options, startedAt);
        return emptyResult(textParser.capability, input.documentId, options, emission.diagnostics, emission.units);
    },
});
