/**
 * Rust-style TypeScript error formatting
 *
 * Formats TypeScript compiler errors in a style similar to Rust's compiler output,
 * with source code context, error highlighting, and helpful formatting.
 */
import { join } from 'node:path';
import { colorError, colorPrimary, colorInfo, colorMuted, bold, link, getDisplayWidth, getColor, plural, sourceLink, getTerminalWidth, truncateToWidth, } from './tui';
import { symbols } from './tui/symbols';
/**
 * Check if a GrammarItem is a TypeScript error (not a warning or other diagnostic)
 */
function isTsErrorItem(item) {
    return item.type === 'Item' && item.value?.tsError?.value?.type === 'error';
}
/**
 * Parse GrammarItem array into structured TypeScript errors
 */
function parseErrors(items) {
    const errors = [];
    for (const item of items) {
        if (!isTsErrorItem(item) || !item.value)
            continue;
        const val = item.value;
        errors.push({
            path: val.path?.value ?? 'unknown',
            line: val.cursor?.value?.line ?? 0,
            col: val.cursor?.value?.col ?? 0,
            errorCode: val.tsError?.value?.errorString ?? 'TS0000',
            message: (val.message?.value ?? '').trim(),
        });
    }
    return errors;
}
const TAB_WIDTH = 4;
/**
 * Convert tabs to spaces for consistent width calculation and display.
 * Terminals render tabs with variable width (typically 8 spaces), but
 * Bun.stringWidth() returns 0 for tabs, causing alignment issues.
 */
function expandTabs(str, tabWidth = TAB_WIDTH) {
    return str.replace(/\t/g, ' '.repeat(tabWidth));
}
/**
 * Convert a column position from the original source (with tabs) to the
 * expanded position (with tabs converted to spaces).
 */
function expandColumn(originalLine, col, tabWidth = TAB_WIDTH) {
    let expandedCol = 0;
    for (let i = 0; i < col && i < originalLine.length; i++) {
        if (originalLine[i] === '\t') {
            expandedCol += tabWidth;
        }
        else {
            expandedCol += 1;
        }
    }
    return expandedCol;
}
/**
 * Read source lines with context (line before, current, line after)
 */
async function getSourceContext(filePath, lineNumber) {
    try {
        const file = Bun.file(filePath);
        if (!(await file.exists())) {
            return null;
        }
        const content = await file.text();
        const lines = content.split('\n');
        if (lineNumber <= 0 || lineNumber > lines.length) {
            return null;
        }
        const currentOriginal = lines[lineNumber - 1];
        if (currentOriginal === undefined) {
            return null;
        }
        const current = expandTabs(currentOriginal);
        const beforeRaw = lineNumber > 1 ? (lines[lineNumber - 2] ?? null) : null;
        const afterRaw = lineNumber < lines.length ? (lines[lineNumber] ?? null) : null;
        const before = beforeRaw !== null && beforeRaw.trim() !== '' ? expandTabs(beforeRaw) : null;
        const after = afterRaw !== null && afterRaw.trim() !== '' ? expandTabs(afterRaw) : null;
        return {
            before,
            beforeLineNum: lineNumber - 1,
            current,
            currentOriginal,
            after,
            afterLineNum: lineNumber + 1,
            total: lines.length,
        };
    }
    catch {
        return null;
    }
}
/**
 * Prepare error data without rendering (first pass)
 */
async function prepareError(error, projectDir, maxAvailableWidth) {
    const fullPath = error.path.startsWith('/') ? error.path : `${projectDir}/${error.path}`;
    // Error header
    const url = link(`https://typescript.tv/errors/#${error.errorCode}`, `error[${error.errorCode}]`, getColor('error'));
    const header = '  ' + url + colorMuted(': ') + bold(error.message);
    // File location
    const vscodelink = sourceLink(join(projectDir, error.path), error.line, error.col, `${error.path}:${error.line}:${error.col}`, getColor('info'));
    const location = colorInfo(`  ${vscodelink}`);
    const codeLines = [];
    // Get source code context
    const context = await getSourceContext(fullPath, error.line);
    if (context !== null) {
        const maxLineNum = Math.max(context.before !== null ? context.beforeLineNum : error.line, error.line, context.after !== null ? context.afterLineNum : error.line);
        const lineNumWidth = String(maxLineNum).length;
        const formatLineNum = (num) => String(num).padStart(lineNumWidth);
        const padding = ' '.repeat(lineNumWidth);
        const linePrefix = lineNumWidth + 3;
        const truncateCodeLine = (lineNum, separator, code) => {
            const prefixWidth = getDisplayWidth(lineNum) + getDisplayWidth(separator) + 1;
            const availableForCode = maxAvailableWidth - prefixWidth;
            if (availableForCode > 10 && getDisplayWidth(code) > availableForCode) {
                return `${lineNum}${separator} ${truncateToWidth(code, availableForCode, '…')}`;
            }
            return `${lineNum}${separator} ${code}`;
        };
        if (context.beforeLineNum > 1) {
            const dots = '.'.repeat(String(context.beforeLineNum).length);
            const content = colorMuted(`${dots} ${symbols.bar}`);
            codeLines.push({ content, rawWidth: getDisplayWidth(content) });
        }
        // Context line before (muted)
        if (context.before !== null) {
            const lineContent = truncateCodeLine(formatLineNum(context.beforeLineNum), ` ${symbols.bar}`, context.before);
            const content = colorMuted(lineContent);
            codeLines.push({ content, rawWidth: getDisplayWidth(content) });
        }
        // Error line (primary color)
        const truncatedCurrent = getDisplayWidth(context.current) > maxAvailableWidth - linePrefix
            ? truncateToWidth(context.current, maxAvailableWidth - linePrefix, '…')
            : context.current;
        const errorLineContent = `${colorInfo(formatLineNum(error.line))} ${colorMuted(symbols.bar)} ${colorPrimary(truncatedCurrent)}`;
        codeLines.push({ content: errorLineContent, rawWidth: getDisplayWidth(errorLineContent) });
        // Error pointer line with carets
        // Convert the original column (which may include tabs) to the expanded column position
        const originalCol = Math.max(0, error.col - 1);
        const expandedCol = expandColumn(context.currentOriginal, originalCol);
        // Find identifier length in the expanded (displayed) content
        let underlineLength = 1;
        const restOfLine = context.current.slice(expandedCol);
        const identifierMatch = restOfLine.match(/^[a-zA-Z_$][a-zA-Z0-9_$]*/);
        if (identifierMatch) {
            underlineLength = identifierMatch[0].length;
        }
        else {
            const tokenMatch = restOfLine.match(/^\S+/);
            if (tokenMatch) {
                underlineLength = Math.min(tokenMatch[0].length, 20);
            }
        }
        const maxCaretWidth = maxAvailableWidth - linePrefix;
        const caretStart = Math.min(expandedCol, maxCaretWidth - 1);
        const caretLen = Math.min(underlineLength, maxCaretWidth - caretStart);
        const carets = caretLen > 0 ? '^'.repeat(caretLen) : '^';
        const caretPadding = ' '.repeat(caretStart);
        const caretLine = `${padding} ${colorMuted(symbols.bar)} ${caretPadding}${colorError(carets)}`;
        codeLines.push({ content: caretLine, rawWidth: getDisplayWidth(caretLine) });
        // Context line after (muted)
        if (context.after !== null) {
            const lineContent = truncateCodeLine(formatLineNum(context.afterLineNum), ` ${symbols.bar}`, context.after);
            const content = colorMuted(lineContent);
            codeLines.push({ content, rawWidth: getDisplayWidth(content) });
        }
        if (context.afterLineNum + 1 < context.total) {
            const dots = '.'.repeat(String(context.afterLineNum).length);
            const content = colorMuted(`${dots} ${symbols.bar}`);
            codeLines.push({ content, rawWidth: getDisplayWidth(content) });
        }
    }
    const maxContentWidth = codeLines.length > 0 ? Math.max(...codeLines.map((l) => l.rawWidth)) : 0;
    return {
        error,
        header,
        location,
        codeLines,
        maxContentWidth,
    };
}
/**
 * Render a prepared error with a specific box width
 */
function renderError(prepared, boxContentWidth) {
    const lines = [];
    lines.push(prepared.header);
    lines.push(prepared.location);
    if (prepared.codeLines.length > 0) {
        const boxWidth = boxContentWidth + 2;
        // Draw box
        lines.push(colorMuted(`  ${symbols.cornerTL}${symbols.barH.repeat(boxWidth)}${symbols.cornerTR}`));
        for (const codeLine of prepared.codeLines) {
            const rightPad = ' '.repeat(Math.max(0, boxContentWidth - codeLine.rawWidth));
            lines.push(colorMuted(`  ${symbols.bar}`) +
                ` ${codeLine.content}${rightPad} ` +
                colorMuted(symbols.bar));
        }
        lines.push(colorMuted(`  ${symbols.cornerBL}${symbols.barH.repeat(boxWidth)}${symbols.cornerBR}`));
    }
    else {
        lines.push(colorMuted('  (source not available)'));
    }
    return lines.join('\n') + '\n';
}
/**
 * Group errors by file for better organization
 */
function groupErrorsByFile(errors) {
    const grouped = new Map();
    for (const error of errors) {
        const existing = grouped.get(error.path) ?? [];
        existing.push(error);
        grouped.set(error.path, existing);
    }
    for (const errors of grouped.values()) {
        errors.sort((a, b) => a.line - b.line || a.col - b.col);
    }
    return grouped;
}
/**
 * Format TypeScript errors in Rust-style output
 */
export async function formatTypeScriptErrors(items, options) {
    const errors = parseErrors(items);
    if (errors.length === 0) {
        return '';
    }
    // Calculate terminal constraints
    const terminalWidth = getTerminalWidth(80);
    const boxChrome = 6;
    const maxAvailableWidth = terminalWidth - boxChrome;
    // First pass: prepare all errors and collect their widths
    const groupedErrors = groupErrorsByFile(errors);
    const maxErrors = options.maxErrors ?? errors.length;
    const preparedErrors = [];
    let displayedCount = 0;
    for (const [, fileErrors] of groupedErrors) {
        for (const error of fileErrors) {
            if (displayedCount >= maxErrors)
                break;
            const prepared = await prepareError(error, options.projectDir, maxAvailableWidth);
            preparedErrors.push(prepared);
            displayedCount++;
        }
        if (displayedCount >= maxErrors)
            break;
    }
    // Calculate uniform box width (max content width across all errors, capped at terminal width)
    const globalMaxContentWidth = Math.max(...preparedErrors.map((p) => p.maxContentWidth));
    const uniformBoxContentWidth = Math.min(globalMaxContentWidth, maxAvailableWidth);
    // Second pass: render all errors with uniform width
    const output = [];
    for (const prepared of preparedErrors) {
        output.push(renderError(prepared, uniformBoxContentWidth));
    }
    // Summary line
    const totalErrors = errors.length;
    const hiddenCount = totalErrors - displayedCount;
    let msg = bold(colorPrimary(`aborting due to ${totalErrors} TypeScript compiler ${plural(totalErrors, 'error', 'errors')}`));
    if (hiddenCount > 0) {
        msg += colorMuted(` (${hiddenCount} not shown)`);
    }
    output.push(`${bold(colorError('error'))}: ${msg}`);
    return output.join('\n');
}
/**
 * Check if the parsed result contains any errors
 */
export function hasErrors(items) {
    return items.some(isTsErrorItem);
}
/**
 * Get the count of errors
 */
export function getErrorCount(items) {
    return items.filter(isTsErrorItem).length;
}
//# sourceMappingURL=typescript-errors.js.map