import { trimTrailingBlank } from './types.js';
/**
 * JSON entities, keyed by RFC 6901 pointer.
 *
 * A character scanner rather than `JSON.parse`, because the entity's *line
 * range* is the point and a parsed value has thrown that away. The scanner also
 * survives trailing commas and comments, which real config files contain and a
 * strict parser rejects outright — losing every entity in the file.
 */
export function extractJsonEntities(content) {
    const lines = content.split(/\r?\n/);
    const entities = [];
    scanContainer(content, lines, findFirstStructural(content), '', undefined, entities, 0);
    return entities;
}
/** Whether JSON/JSONC is syntactically complete enough to prove absence. */
export function isJsonStructureComplete(content) {
    try {
        JSON.parse(content);
        return true;
    }
    catch {
        // JSONC and common trailing-comma config files continue through the
        // normalizer below.
    }
    const withoutComments = [];
    let inString = false;
    let escaped = false;
    let lineComment = false;
    let blockComment = false;
    for (let index = 0; index < content.length; index++) {
        const char = content[index];
        const next = content[index + 1];
        if (lineComment) {
            if (char === '\n') {
                lineComment = false;
                withoutComments.push(char);
            }
            else {
                withoutComments.push(' ');
            }
            continue;
        }
        if (blockComment) {
            if (char === '*' && next === '/') {
                blockComment = false;
                withoutComments.push(' ', ' ');
                index++;
            }
            else {
                withoutComments.push(char === '\n' || char === '\r' ? char : ' ');
            }
            continue;
        }
        if (inString) {
            withoutComments.push(char);
            if (escaped)
                escaped = false;
            else if (char === '\\')
                escaped = true;
            else if (char === '"')
                inString = false;
            continue;
        }
        if (char === '/' && next === '/') {
            lineComment = true;
            withoutComments.push(' ', ' ');
            index++;
        }
        else if (char === '/' && next === '*') {
            blockComment = true;
            withoutComments.push(' ', ' ');
            index++;
        }
        else if (char === '"') {
            inString = true;
            withoutComments.push(char);
        }
        else {
            withoutComments.push(char);
        }
    }
    if (inString || blockComment)
        return false;
    const jsonc = withoutComments.join('');
    const withoutTrailingCommas = [];
    inString = false;
    escaped = false;
    for (let index = 0; index < jsonc.length; index++) {
        const char = jsonc[index];
        if (inString) {
            withoutTrailingCommas.push(char);
            if (escaped)
                escaped = false;
            else if (char === '\\')
                escaped = true;
            else if (char === '"')
                inString = false;
            continue;
        }
        if (char === '"') {
            inString = true;
            withoutTrailingCommas.push(char);
            continue;
        }
        if (char === ',') {
            let lookahead = index + 1;
            while (/\s/u.test(jsonc[lookahead] ?? ''))
                lookahead++;
            if (jsonc[lookahead] === '}' || jsonc[lookahead] === ']') {
                withoutTrailingCommas.push(' ');
                continue;
            }
        }
        withoutTrailingCommas.push(char);
    }
    try {
        JSON.parse(withoutTrailingCommas.join(''));
        return true;
    }
    catch {
        return false;
    }
}
/** RFC 6901: `~` becomes `~0` and `/` becomes `~1`, in that order. */
function escapePointer(key) {
    return key.replace(/~/g, '~0').replace(/\//g, '~1');
}
function findFirstStructural(content) {
    for (let index = 0; index < content.length; index++) {
        const char = content[index];
        if (char === '{' || char === '[')
            return index;
    }
    return -1;
}
function lineAt(content, offset) {
    let line = 1;
    for (let index = 0; index < offset && index < content.length; index++) {
        if (content.charCodeAt(index) === 10)
            line++;
    }
    return line;
}
/** Advance past whitespace and both comment styles, which JSON5/JSONC allow. */
function skipTrivia(content, cursor) {
    while (cursor.index < content.length) {
        const char = content[cursor.index];
        if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
            cursor.index++;
            continue;
        }
        if (char === '/' && content[cursor.index + 1] === '/') {
            while (cursor.index < content.length && content[cursor.index] !== '\n')
                cursor.index++;
            continue;
        }
        if (char === '/' && content[cursor.index + 1] === '*') {
            cursor.index += 2;
            while (cursor.index < content.length && !(content[cursor.index] === '*' && content[cursor.index + 1] === '/')) {
                cursor.index++;
            }
            cursor.index += 2;
            continue;
        }
        return;
    }
}
/** Read a JSON string starting at the opening quote, returning its decoded value. */
function readString(content, cursor) {
    if (content[cursor.index] !== '"')
        return undefined;
    cursor.index++;
    let value = '';
    while (cursor.index < content.length) {
        const char = content[cursor.index];
        if (char === '\\') {
            value += content[cursor.index + 1] ?? '';
            cursor.index += 2;
            continue;
        }
        if (char === '"') {
            cursor.index++;
            return value;
        }
        value += char;
        cursor.index++;
    }
    return value;
}
/** Skip one value, returning where it ended and what kind it was. */
function skipValue(content, cursor) {
    skipTrivia(content, cursor);
    const char = content[cursor.index];
    if (char === '{' || char === '[') {
        const open = char;
        const close = open === '{' ? '}' : ']';
        let depth = 0;
        while (cursor.index < content.length) {
            const current = content[cursor.index];
            if (current === '"') {
                readString(content, cursor);
                continue;
            }
            if (current === '/' && (content[cursor.index + 1] === '/' || content[cursor.index + 1] === '*')) {
                skipTrivia(content, cursor);
                continue;
            }
            if (current === open)
                depth++;
            else if (current === close) {
                depth--;
                cursor.index++;
                if (depth === 0)
                    break;
                continue;
            }
            cursor.index++;
        }
        return open === '{' ? 'object' : 'array';
    }
    if (char === '"') {
        readString(content, cursor);
        return 'property';
    }
    while (cursor.index < content.length && !',}]'.includes(content[cursor.index]))
        cursor.index++;
    return 'property';
}
const MAX_DEPTH = 32;
/**
 * Walk one container, emitting an entity per member and descending into objects.
 *
 * Array elements are not descended into: an element's identity is its index,
 * and an index shifts whenever anything is inserted before it, so treating them
 * as stable entities would report an insertion as a rewrite of everything after.
 */
function scanContainer(content, lines, openIndex, pointer, containerName, entities, depth) {
    if (openIndex === -1 || depth > MAX_DEPTH)
        return;
    const isObject = content[openIndex] === '{';
    const cursor = { index: openIndex + 1 };
    let arrayIndex = 0;
    for (;;) {
        skipTrivia(content, cursor);
        const char = content[cursor.index];
        if (char === undefined || char === '}' || char === ']')
            return;
        if (char === ',') {
            cursor.index++;
            continue;
        }
        const startLine = lineAt(content, cursor.index);
        let name;
        let childPointer;
        if (isObject) {
            const key = readString(content, cursor);
            if (key === undefined)
                return;
            name = key;
            childPointer = `${pointer}/${escapePointer(key)}`;
            skipTrivia(content, cursor);
            if (content[cursor.index] !== ':')
                return;
            cursor.index++;
        }
        else {
            name = String(arrayIndex);
            childPointer = `${pointer}/${arrayIndex}`;
            arrayIndex++;
        }
        skipTrivia(content, cursor);
        const valueStart = cursor.index;
        const valueKind = skipValue(content, cursor);
        const endLine = trimTrailingBlank(lines, lineAt(content, Math.max(valueStart, cursor.index - 1)), startLine);
        entities.push({
            kind: isObject ? valueKind : 'array-item',
            name,
            qualifiedName: childPointer,
            containerName,
            startLine,
            endLine,
        });
        if (isObject && valueKind === 'object') {
            scanContainer(content, lines, valueStart, childPointer, childPointer, entities, depth + 1);
        }
    }
}
//# sourceMappingURL=json.js.map