import { symbols } from "../components/symbols.js";
import { displayWidth, graphemes } from "../dashboard/terminal-width.js";
import { getTheme } from "../internal/theme-detect.js";
import { stripAnsi } from "../internal/strip-ansi.js";
import { spacing } from "../tokens/spacing.js";
import { typography } from "../tokens/typography.js";
import { widths } from "../tokens/widths.js";
const lineChar = "─";
export function render(ast, options = {}) {
    const requestedWidth = options.width ?? process.stdout.columns ?? widths.maxLine;
    if (!Number.isFinite(requestedWidth) || requestedWidth <= 0) {
        throw new Error("width must be a positive finite number.");
    }
    const width = Math.max(1, requestedWidth);
    const context = {
        width,
        showFrontmatter: options.showFrontmatter ?? false,
        theme: getTheme(),
        footnotes: ast.type === "root" ? createFootnoteState(ast.children) : undefined
    };
    return renderNode(ast, context);
}
function renderNode(node, context) {
    switch (node.type) {
        case "root":
            return renderRoot(node, context);
        case "heading":
            return renderHeading(node, context);
        case "paragraph":
            return renderParagraph(node, context);
        case "blockquote":
            return renderBlockquote(node, context);
        case "alert":
            return renderAlert(node, context);
        case "code":
            return renderCodeBlock(node, context);
        case "list":
            return renderList(node, context);
        case "table":
            return renderTable(node, context);
        case "html":
            return renderHtml(node, context);
        case "text":
        case "strong":
        case "emphasis":
        case "strikethrough":
        case "inlineCode":
        case "link":
        case "image":
        case "break":
        case "footnoteReference":
            return renderInline([node], context).join("\n");
        case "thematicBreak":
            return `${context.theme.divider(lineChar.repeat(context.width))}\n\n`;
        case "frontmatter":
            return context.showFrontmatter ? renderFrontmatter(node.data, context) : "";
        case "footnoteDefinition":
            return "";
        default:
            return "children" in node ? renderChildren(node.children, context) : "";
    }
}
function renderRoot(node, context) {
    const mainOutput = renderChildren(node.children.filter((child) => child.type !== "footnoteDefinition"), context);
    const footnotesOutput = renderReferencedFootnotes(context);
    return `${mainOutput}${footnotesOutput}`;
}
function renderChildren(children, context) {
    return children.map((child) => renderNode(child, context)).join("");
}
function renderParagraph(node, context) {
    const lines = renderInline(node.children, context);
    if (lines.length === 0) {
        return "";
    }
    return `${lines.join("\n")}\n\n`;
}
function renderHeading(node, context) {
    const lines = renderInline(node.children, context);
    if (lines.length === 0) {
        return "";
    }
    const styledLines = lines.map((line) => styleHeading(line, node.depth, context));
    if (node.depth === 1) {
        const underlineWidth = Math.max(...lines.map((line) => visibleWidth(line)));
        return `${styledLines.join("\n")}\n${context.theme.header(lineChar.repeat(underlineWidth))}\n\n`;
    }
    return `${styledLines.join("\n")}\n\n`;
}
function renderBlockquote(node, context) {
    const prefix = `${symbols.bar} `;
    const body = renderBlockChildren(node.children, reduceWidth(context, prefix));
    if (body.length === 0) {
        return `${symbols.bar}\n\n`;
    }
    const lines = body
        .split("\n")
        .map((line) => (line.length > 0 ? `${prefix}${typography.dim(line)}` : symbols.bar));
    return `${lines.join("\n")}\n\n`;
}
function renderAlert(node, context) {
    const prefix = `${symbols.bar} `;
    const labelLine = `${prefix}${formatAlertLabel(node.kind, context)}`;
    const body = renderBlockChildren(node.children, reduceWidth(context, prefix));
    if (body.length === 0) {
        return `${labelLine}\n\n`;
    }
    const lines = body
        .split("\n")
        .map((line) => (line.length > 0 ? `${prefix}${line}` : symbols.bar));
    return `${labelLine}\n${lines.join("\n")}\n\n`;
}
function renderCodeBlock(node, context) {
    const indent = " ".repeat(spacing.sm);
    const lines = node.value.split("\n").map((line) => stripAnsi(line));
    const longestLine = lines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
    const borderWidth = Math.max(3, Math.min(context.width - indent.length, longestLine));
    const border = context.theme.muted(`${indent}${lineChar.repeat(borderWidth)}`);
    const content = lines.map((line) => `${indent}${line}`).join("\n");
    return `${border}\n${content}\n${border}\n\n`;
}
function renderList(node, context) {
    const items = node.children
        .map((child, index) => {
        if (child.type !== "listItem") {
            return renderNode(child, context).trimEnd();
        }
        return renderListItem(child, node, index, context);
    })
        .filter((item) => item.length > 0);
    if (items.length === 0) {
        return "";
    }
    return `${items.join("\n")}\n\n`;
}
function renderTable(node, context) {
    const rows = node.children.filter((child) => child.type === "tableRow");
    if (rows.length === 0) {
        return "";
    }
    const columnCount = Math.max(node.align.length, ...rows.map((row) => row.children.length));
    if (columnCount === 0) {
        return "";
    }
    const renderedRows = rows.map((row) => Array.from({ length: columnCount }, (_, columnIndex) => {
        const cell = row.children[columnIndex];
        return cell?.type === "tableCell" ? renderTableCell(cell, context) : "";
    }));
    const columnWidths = Array.from({ length: columnCount }, (_, columnIndex) => Math.max(...renderedRows.map((row) => visibleWidth(row[columnIndex] ?? "")), 0));
    if (getRenderedTableWidth(columnWidths) > context.width) {
        return renderStackedTable(rows, columnCount, context);
    }
    const lines = renderedRows.map((row, rowIndex) => row.map((cell, columnIndex) => alignTableCell(rowIndex === 0 ? context.theme.header(typography.bold(cell)) : cell, columnWidths[columnIndex] ?? 0, node.align[columnIndex] ?? null)));
    const outputLines = lines.map((row, rowIndex) => {
        const line = `${symbols.bar}${row
            .map((cell) => `${" ".repeat(spacing.sm)}${cell}${" ".repeat(spacing.sm)}`)
            .join(symbols.bar)}${symbols.bar}`;
        if (rowIndex !== 0 || lines.length === 1) {
            return line;
        }
        const divider = context.theme.muted(`├${columnWidths.map((width) => lineChar.repeat(width + spacing.sm * 2)).join("┼")}┤`);
        return `${line}\n${divider}`;
    });
    return `${outputLines.join("\n")}\n\n`;
}
function renderStackedTable(rows, columnCount, context) {
    const headerRow = rows[0];
    if (headerRow === undefined) {
        return "";
    }
    const dataRows = rows.slice(1);
    if (dataRows.length === 0) {
        const headerLines = Array.from({ length: columnCount }, (_, columnIndex) => tokenizeText(getStackedTableLabel(headerRow.children[columnIndex], columnIndex, context), [
            typography.bold
        ])).flatMap((tokens) => wrapTokens(tokens, context.width));
        return headerLines.length === 0 ? "" : `${headerLines.join("\n")}\n\n`;
    }
    const blocks = dataRows
        .map((row) => Array.from({ length: columnCount }, (_, columnIndex) => renderStackedTableField(headerRow.children[columnIndex], row.children[columnIndex], columnIndex, context))
        .filter((field) => field.length > 0)
        .join("\n"))
        .filter((block) => block.length > 0);
    return blocks.length === 0 ? "" : `${blocks.join("\n\n")}\n\n`;
}
function renderListItem(node, list, index, context) {
    const marker = getListMarker(node, list, index);
    const indent = " ".repeat(spacing.sm);
    const firstPrefix = `${indent}${marker} `;
    const body = renderBlockChildren(node.children, reduceWidth(context, firstPrefix));
    const continuationPrefix = `${indent}${" ".repeat(marker.length + 1)}`;
    if (body.length === 0) {
        return firstPrefix.trimEnd();
    }
    return prefixBlock(body, firstPrefix, continuationPrefix);
}
function styleHeading(value, depth, context) {
    if (depth <= 2) {
        return context.theme.header(typography.bold(value));
    }
    if (depth <= 4) {
        return typography.bold(value);
    }
    return context.theme.muted(typography.bold(value));
}
function renderFrontmatter(data, context) {
    const entries = Object.entries(data);
    if (entries.length === 0) {
        return "";
    }
    const lines = entries.flatMap(([key, value]) => wrapText(`${key}: ${formatFrontmatterValue(value)}`, context.width).map((line) => typography.dim(line)));
    return `${lines.join("\n")}\n\n`;
}
function formatFrontmatterValue(value) {
    if (value === null) {
        return "null";
    }
    if (typeof value === "string") {
        return value;
    }
    if (typeof value === "number" || typeof value === "boolean") {
        return String(value);
    }
    const ancestors = [];
    return JSON.stringify(value, function (_key, nestedValue) {
        if (typeof nestedValue !== "object" || nestedValue === null) {
            return nestedValue;
        }
        while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
            ancestors.pop();
        }
        if (ancestors.includes(nestedValue)) {
            return "[Circular]";
        }
        ancestors.push(nestedValue);
        return nestedValue;
    });
}
function renderHtml(node, context) {
    const value = stripAnsi(stripHtmlTags(node.value)).trim();
    if (value.length === 0) {
        return "";
    }
    const lines = wrapText(value, context.width);
    if (lines.length === 0) {
        return "";
    }
    return `${lines.join("\n")}\n\n`;
}
function renderBlockChildren(children, context) {
    const renderedChildren = children
        .map((child) => ({
        type: child.type,
        value: renderNode(child, context).trimEnd()
    }))
        .filter((child) => child.value.length > 0);
    if (renderedChildren.length === 0) {
        return "";
    }
    let output = renderedChildren[0].value;
    for (let index = 1; index < renderedChildren.length; index += 1) {
        const previous = renderedChildren[index - 1];
        const current = renderedChildren[index];
        const separator = previous.type === "paragraph" && current.type === "list" ? "\n" : "\n\n";
        output += `${separator}${current.value}`;
    }
    return output;
}
function renderReferencedFootnotes(context) {
    const footnotes = context.footnotes;
    if (footnotes === undefined || footnotes.labelsInOrder.length === 0) {
        return "";
    }
    const rendered = [];
    for (let index = 0; index < footnotes.labelsInOrder.length; index += 1) {
        const label = footnotes.labelsInOrder[index];
        const definition = footnotes.definitions.get(label);
        const number = footnotes.numbers.get(label);
        if (definition !== undefined && number !== undefined) {
            rendered.push(renderFootnoteDefinition(definition, number, context));
        }
    }
    if (rendered.length === 0) {
        return "";
    }
    return `${rendered.join("\n")}\n\n`;
}
function renderFootnoteDefinition(node, number, context) {
    const marker = typography.dim(`[${number}]`);
    const firstPrefix = `${" ".repeat(spacing.sm)}${marker} `;
    const continuationPrefix = `${" ".repeat(spacing.sm + visibleWidth(`[${number}] `))}`;
    const body = renderBlockChildren(node.children, reduceWidth(context, firstPrefix));
    if (body.length === 0) {
        return firstPrefix.trimEnd();
    }
    return prefixBlock(body, firstPrefix, continuationPrefix);
}
function getListMarker(node, list, index) {
    if (node.checked === true) {
        return symbols.active;
    }
    if (node.checked === false) {
        return symbols.inactive;
    }
    if (list.ordered) {
        return `${(list.start ?? 1) + index}.`;
    }
    return "•";
}
function prefixBlock(rendered, firstPrefix, restPrefix) {
    return rendered
        .split("\n")
        .map((line, index) => {
        const prefix = index === 0 ? firstPrefix : restPrefix;
        return line.length > 0 ? `${prefix}${line}` : prefix.trimEnd();
    })
        .join("\n");
}
function reduceWidth(context, prefix) {
    return {
        ...context,
        width: Math.max(1, context.width - visibleWidth(prefix))
    };
}
function renderInline(nodes, context) {
    const tokens = tokenizeInline(nodes, context);
    return wrapTokens(tokens, context.width);
}
function tokenizeInline(nodes, context) {
    const tokens = [];
    for (const node of nodes) {
        collectInlineTokens(node, [], context, tokens);
    }
    return tokens;
}
function collectInlineTokens(node, formatters, context, tokens) {
    switch (node.type) {
        case "text":
            tokens.push(...tokenizeText(stripAnsi(node.value), formatters));
            return;
        case "strong":
            collectChildren(node.children, [...formatters, typography.bold], context, tokens);
            return;
        case "emphasis":
            collectChildren(node.children, [...formatters, typography.italic], context, tokens);
            return;
        case "strikethrough":
            collectChildren(node.children, [...formatters, typography.strikethrough], context, tokens);
            return;
        case "inlineCode":
            tokens.push(...tokenizeText(stripAnsi(node.value), [...formatters, context.theme.accent]));
            return;
        case "link":
            collectLinkTokens(node, formatters, context, tokens);
            return;
        case "image":
            tokens.push(createWordToken(formatImagePlaceholder(stripAnsi(node.alt)), [
                ...formatters,
                context.theme.muted
            ]));
            return;
        case "footnoteReference": {
            const footnoteNumber = resolveFootnoteNumber(node.label, context);
            if (footnoteNumber !== null) {
                tokens.push(createWordToken(`[${footnoteNumber}]`, [...formatters, typography.dim]));
            }
            return;
        }
        case "html": {
            const value = stripAnsi(stripHtmlTags(node.value));
            if (value.length > 0) {
                tokens.push(...tokenizeText(value, formatters));
            }
            return;
        }
        case "break":
            tokens.push({ type: "break" });
            return;
        default:
            if ("children" in node) {
                collectChildren(node.children, formatters, context, tokens);
            }
    }
}
function collectChildren(children, formatters, context, tokens) {
    for (const child of children) {
        collectInlineTokens(child, formatters, context, tokens);
    }
}
function collectLinkTokens(node, formatters, context, tokens) {
    if (isAutolink(node)) {
        tokens.push(createWordToken(stripAnsi(node.url), [...formatters, context.theme.accent]));
        return;
    }
    const childTokens = [];
    collectChildren(node.children, formatters, context, childTokens);
    const trimmedChildTokens = trimTrailingSpaces(childTokens);
    tokens.push(...trimmedChildTokens);
    if (trimmedChildTokens.some((token) => token.type === "word")) {
        tokens.push({ type: "space", value: " " });
    }
    tokens.push(createWordToken(`(${stripAnsi(node.url)})`, [...formatters, context.theme.accent]));
}
function isAutolink(node) {
    if (node.children.length !== 1 || node.children[0]?.type !== "text") {
        return false;
    }
    const label = node.children[0].value;
    return (label === node.url ||
        (node.url.startsWith("http://") && label === node.url.slice("http://".length)) ||
        (node.url.startsWith("mailto:") && label === node.url.slice("mailto:".length)));
}
function formatImagePlaceholder(alt) {
    return alt.length > 0 ? `[image: ${alt}]` : "[image]";
}
function createWordToken(value, formatters) {
    return { type: "word", value, formatters };
}
function trimTrailingSpaces(tokens) {
    let end = tokens.length;
    while (end > 0 && tokens[end - 1]?.type === "space") {
        end -= 1;
    }
    return tokens.slice(0, end);
}
function tokenizeText(value, formatters) {
    const lines = value.split("\n");
    return lines.flatMap((line, lineIndex) => {
        const pieces = line
            .split(/([ \t]+)/)
            .filter((piece) => piece.length > 0)
            .map((piece) => /^[ \t]+$/.test(piece)
            ? { type: "space", value: piece }
            : { type: "word", value: piece, formatters });
        return lineIndex < lines.length - 1 ? [...pieces, { type: "break" }] : pieces;
    });
}
function wrapTokens(tokens, width) {
    const lines = [];
    let currentLine = "";
    let currentWidth = 0;
    let pendingSpace = "";
    const flushLine = () => {
        lines.push(currentLine);
        currentLine = "";
        currentWidth = 0;
        pendingSpace = "";
    };
    for (const token of tokens) {
        if (token.type === "break") {
            flushLine();
            continue;
        }
        if (token.type === "space") {
            if (currentWidth > 0) {
                pendingSpace += token.value;
            }
            continue;
        }
        const chunks = splitWord(token.value, width);
        for (let index = 0; index < chunks.length; index++) {
            const chunk = chunks[index];
            const gap = index === 0 ? pendingSpace : "";
            const chunkWidth = visibleWidth(chunk);
            const gapWidth = visibleWidth(gap);
            if (currentWidth > 0 && currentWidth + chunkWidth + gapWidth > width) {
                flushLine();
            }
            if (currentWidth > 0 && gapWidth > 0) {
                currentLine += gap;
                currentWidth += gapWidth;
            }
            currentLine += applyFormatters(chunk, token.formatters);
            currentWidth += chunkWidth;
            pendingSpace = "";
            if (index < chunks.length - 1) {
                flushLine();
            }
        }
    }
    if (currentLine.length > 0) {
        lines.push(currentLine);
    }
    return lines;
}
function splitWord(value, width) {
    if (visibleWidth(value) <= width) {
        return [value];
    }
    const chunks = [];
    let chunk = "";
    let chunkWidth = 0;
    for (const grapheme of graphemes(value)) {
        const graphemeWidth = visibleWidth(grapheme);
        if (chunk.length > 0 && chunkWidth + graphemeWidth > width) {
            chunks.push(chunk);
            chunk = "";
            chunkWidth = 0;
        }
        chunk += grapheme;
        chunkWidth += graphemeWidth;
        if (chunkWidth >= width) {
            chunks.push(chunk);
            chunk = "";
            chunkWidth = 0;
        }
    }
    if (chunk.length > 0) {
        chunks.push(chunk);
    }
    return chunks;
}
function applyFormatters(value, formatters) {
    return formatters.reduce((text, formatter) => formatter(text), value);
}
function wrapText(value, width) {
    const tokens = tokenizeText(value, []);
    return wrapTokens(tokens, Math.max(1, width));
}
function createFootnoteState(children) {
    return {
        definitions: new Map(children.flatMap((child) => child.type === "footnoteDefinition" ? [[child.label, child]] : [])),
        labelsInOrder: [],
        numbers: new Map()
    };
}
function resolveFootnoteNumber(label, context) {
    const footnotes = context.footnotes;
    if (footnotes === undefined || !footnotes.definitions.has(label)) {
        return null;
    }
    const existing = footnotes.numbers.get(label);
    if (existing !== undefined) {
        return existing;
    }
    const number = footnotes.numbers.size + 1;
    footnotes.numbers.set(label, number);
    footnotes.labelsInOrder.push(label);
    return number;
}
function formatAlertLabel(kind, context) {
    switch (kind) {
        case "NOTE":
            return context.theme.info("Note");
        case "TIP":
            return context.theme.success("Tip");
        case "IMPORTANT":
            return context.theme.info("Important");
        case "WARNING":
            return context.theme.warning("Warning");
        case "CAUTION":
            return context.theme.error("Caution");
    }
}
function renderTableCell(node, context) {
    return wrapTokens(tokenizeInline(node.children, context), Number.MAX_SAFE_INTEGER).join(" ");
}
function renderStackedTableField(headerCell, cell, columnIndex, context) {
    const tokens = [
        ...tokenizeText(getStackedTableLabel(headerCell, columnIndex, context), [typography.bold]),
        createWordToken(":", [typography.bold]),
        { type: "space", value: " " },
        ...getStackedTableValueTokens(cell, context)
    ];
    return wrapTokens(tokens, context.width).join("\n");
}
function getStackedTableLabel(headerCell, columnIndex, context) {
    if (headerCell?.type !== "tableCell") {
        return `Column ${columnIndex + 1}`;
    }
    const label = stripAnsi(renderTableCell(headerCell, context)).trim();
    return label.length > 0 ? label : `Column ${columnIndex + 1}`;
}
function getStackedTableValueTokens(cell, context) {
    if (cell?.type !== "tableCell") {
        return [createWordToken("—", [context.theme.muted])];
    }
    const tokens = trimTrailingSpaces(tokenizeInline(cell.children, context));
    return tokens.length > 0 ? tokens : [createWordToken("—", [context.theme.muted])];
}
function getRenderedTableWidth(columnWidths) {
    return columnWidths.reduce((totalWidth, width) => totalWidth + width + spacing.sm * 2, columnWidths.length + 1);
}
function alignTableCell(value, width, align) {
    const extraSpace = Math.max(0, width - visibleWidth(value));
    if (align === "right") {
        return `${" ".repeat(extraSpace)}${value}`;
    }
    if (align === "center") {
        const leftPadding = Math.floor(extraSpace / 2);
        const rightPadding = extraSpace - leftPadding;
        return `${" ".repeat(leftPadding)}${value}${" ".repeat(rightPadding)}`;
    }
    return `${value}${" ".repeat(extraSpace)}`;
}
function stripHtmlTags(value) {
    let output = "";
    let inTag = false;
    for (const char of value) {
        if (char === "<") {
            inTag = true;
            continue;
        }
        if (char === ">" && inTag) {
            inTag = false;
            continue;
        }
        if (!inTag) {
            output += char;
        }
    }
    return output;
}
function visibleWidth(value) {
    return displayWidth(stripAnsi(value));
}
