let cachedCapabilities = null;
// Default cell dimensions - updated by TUI when terminal responds to query
let cellDimensions = { widthPx: 9, heightPx: 18 };
export function getCellDimensions() {
    return cellDimensions;
}
export function setCellDimensions(dims) {
    cellDimensions = dims;
}
export function detectCapabilities() {
    const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
    const term = process.env.TERM?.toLowerCase() || "";
    const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
    // tmux and screen swallow OSC 8 by default (passthrough is opt-in and wraps
    // sequences differently). Force hyperlinks off whenever we detect them, even
    // when the outer terminal would otherwise support OSC 8. Image protocols are
    // also unreliable under tmux/screen, so leave `images: null` for safety.
    const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen");
    if (inTmuxOrScreen) {
        const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
        return { images: null, trueColor, hyperlinks: false };
    }
    if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
        return { images: "kitty", trueColor: true, hyperlinks: true };
    }
    if (termProgram === "ghostty" || term.includes("ghostty") || process.env.GHOSTTY_RESOURCES_DIR) {
        return { images: "kitty", trueColor: true, hyperlinks: true };
    }
    if (process.env.WEZTERM_PANE || termProgram === "wezterm") {
        return { images: "kitty", trueColor: true, hyperlinks: true };
    }
    if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") {
        return { images: "iterm2", trueColor: true, hyperlinks: true };
    }
    if (termProgram === "vscode") {
        return { images: null, trueColor: true, hyperlinks: true };
    }
    if (termProgram === "alacritty") {
        return { images: null, trueColor: true, hyperlinks: true };
    }
    // Unknown terminal: be conservative. OSC 8 is rendered invisibly as "just
    // text" on terminals that swallow it, which means the URL disappears from
    // the rendered output. Default to the legacy `text (url)` behavior unless we
    // have positively identified a hyperlink-capable terminal above.
    const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
    return { images: null, trueColor, hyperlinks: false };
}
export function getCapabilities() {
    if (!cachedCapabilities) {
        cachedCapabilities = detectCapabilities();
    }
    return cachedCapabilities;
}
export function resetCapabilitiesCache() {
    cachedCapabilities = null;
}
/** Override the cached capabilities. Useful in tests to exercise both code paths. */
export function setCapabilities(caps) {
    cachedCapabilities = caps;
}
const KITTY_PREFIX = "\x1b_G";
const ITERM2_PREFIX = "\x1b]1337;File=";
export function isImageLine(line) {
    // Fast path: sequence at line start (single-row images)
    if (line.startsWith(KITTY_PREFIX) || line.startsWith(ITERM2_PREFIX)) {
        return true;
    }
    // Slow path: sequence elsewhere (multi-row images have cursor-up prefix)
    return line.includes(KITTY_PREFIX) || line.includes(ITERM2_PREFIX);
}
/**
 * Generate a random image ID for Kitty graphics protocol.
 * Uses random IDs to avoid collisions between different module instances
 * (e.g., main app vs extensions).
 */
export function allocateImageId() {
    // Use random ID in range [1, 0xffffffff] to avoid collisions
    return Math.floor(Math.random() * 0xfffffffe) + 1;
}
export function encodeKitty(base64Data, options = {}) {
    const CHUNK_SIZE = 4096;
    const params = ["a=T", "f=100", "q=2"];
    if (options.moveCursor === false)
        params.push("C=1");
    if (options.columns)
        params.push(`c=${options.columns}`);
    if (options.rows)
        params.push(`r=${options.rows}`);
    if (options.imageId)
        params.push(`i=${options.imageId}`);
    if (base64Data.length <= CHUNK_SIZE) {
        return `\x1b_G${params.join(",")};${base64Data}\x1b\\`;
    }
    const chunks = [];
    let offset = 0;
    let isFirst = true;
    while (offset < base64Data.length) {
        const chunk = base64Data.slice(offset, offset + CHUNK_SIZE);
        const isLast = offset + CHUNK_SIZE >= base64Data.length;
        if (isFirst) {
            chunks.push(`\x1b_G${params.join(",")},m=1;${chunk}\x1b\\`);
            isFirst = false;
        }
        else if (isLast) {
            chunks.push(`\x1b_Gm=0;${chunk}\x1b\\`);
        }
        else {
            chunks.push(`\x1b_Gm=1;${chunk}\x1b\\`);
        }
        offset += CHUNK_SIZE;
    }
    return chunks.join("");
}
/**
 * Delete a Kitty graphics image by ID.
 * Uses uppercase 'I' to also free the image data.
 */
export function deleteKittyImage(imageId) {
    return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
}
/**
 * Delete all visible Kitty graphics images.
 * Uses uppercase 'A' to also free the image data.
 */
export function deleteAllKittyImages() {
    return "\x1b_Ga=d,d=A,q=2\x1b\\";
}
export function encodeITerm2(base64Data, options = {}) {
    const params = [`inline=${options.inline !== false ? 1 : 0}`];
    if (options.width !== undefined)
        params.push(`width=${options.width}`);
    if (options.height !== undefined)
        params.push(`height=${options.height}`);
    if (options.name) {
        const nameBase64 = Buffer.from(options.name).toString("base64");
        params.push(`name=${nameBase64}`);
    }
    if (options.preserveAspectRatio === false) {
        params.push("preserveAspectRatio=0");
    }
    return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
}
export function calculateImageRows(imageDimensions, targetWidthCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
    const targetWidthPx = targetWidthCells * cellDimensions.widthPx;
    const scale = targetWidthPx / imageDimensions.widthPx;
    const scaledHeightPx = imageDimensions.heightPx * scale;
    const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
    return Math.max(1, rows);
}
export function getPngDimensions(base64Data) {
    try {
        const buffer = Buffer.from(base64Data, "base64");
        if (buffer.length < 24) {
            return null;
        }
        if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
            return null;
        }
        const width = buffer.readUInt32BE(16);
        const height = buffer.readUInt32BE(20);
        return { widthPx: width, heightPx: height };
    }
    catch {
        return null;
    }
}
export function getJpegDimensions(base64Data) {
    try {
        const buffer = Buffer.from(base64Data, "base64");
        if (buffer.length < 2) {
            return null;
        }
        if (buffer[0] !== 0xff || buffer[1] !== 0xd8) {
            return null;
        }
        let offset = 2;
        while (offset < buffer.length - 9) {
            if (buffer[offset] !== 0xff) {
                offset++;
                continue;
            }
            const marker = buffer[offset + 1];
            if (marker >= 0xc0 && marker <= 0xc2) {
                const height = buffer.readUInt16BE(offset + 5);
                const width = buffer.readUInt16BE(offset + 7);
                return { widthPx: width, heightPx: height };
            }
            if (offset + 3 >= buffer.length) {
                return null;
            }
            const length = buffer.readUInt16BE(offset + 2);
            if (length < 2) {
                return null;
            }
            offset += 2 + length;
        }
        return null;
    }
    catch {
        return null;
    }
}
export function getGifDimensions(base64Data) {
    try {
        const buffer = Buffer.from(base64Data, "base64");
        if (buffer.length < 10) {
            return null;
        }
        const sig = buffer.slice(0, 6).toString("ascii");
        if (sig !== "GIF87a" && sig !== "GIF89a") {
            return null;
        }
        const width = buffer.readUInt16LE(6);
        const height = buffer.readUInt16LE(8);
        return { widthPx: width, heightPx: height };
    }
    catch {
        return null;
    }
}
export function getWebpDimensions(base64Data) {
    try {
        const buffer = Buffer.from(base64Data, "base64");
        if (buffer.length < 30) {
            return null;
        }
        const riff = buffer.slice(0, 4).toString("ascii");
        const webp = buffer.slice(8, 12).toString("ascii");
        if (riff !== "RIFF" || webp !== "WEBP") {
            return null;
        }
        const chunk = buffer.slice(12, 16).toString("ascii");
        if (chunk === "VP8 ") {
            if (buffer.length < 30)
                return null;
            const width = buffer.readUInt16LE(26) & 0x3fff;
            const height = buffer.readUInt16LE(28) & 0x3fff;
            return { widthPx: width, heightPx: height };
        }
        else if (chunk === "VP8L") {
            if (buffer.length < 25)
                return null;
            const bits = buffer.readUInt32LE(21);
            const width = (bits & 0x3fff) + 1;
            const height = ((bits >> 14) & 0x3fff) + 1;
            return { widthPx: width, heightPx: height };
        }
        else if (chunk === "VP8X") {
            if (buffer.length < 30)
                return null;
            const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;
            const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;
            return { widthPx: width, heightPx: height };
        }
        return null;
    }
    catch {
        return null;
    }
}
export function getImageDimensions(base64Data, mimeType) {
    if (mimeType === "image/png") {
        return getPngDimensions(base64Data);
    }
    if (mimeType === "image/jpeg") {
        return getJpegDimensions(base64Data);
    }
    if (mimeType === "image/gif") {
        return getGifDimensions(base64Data);
    }
    if (mimeType === "image/webp") {
        return getWebpDimensions(base64Data);
    }
    return null;
}
export function renderImage(base64Data, imageDimensions, options = {}) {
    const caps = getCapabilities();
    if (!caps.images) {
        return null;
    }
    const maxWidth = options.maxWidthCells ?? 80;
    const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
    if (caps.images === "kitty") {
        const sequence = encodeKitty(base64Data, {
            columns: maxWidth,
            rows,
            imageId: options.imageId,
            moveCursor: options.moveCursor,
        });
        return { sequence, rows, imageId: options.imageId };
    }
    if (caps.images === "iterm2") {
        const sequence = encodeITerm2(base64Data, {
            width: maxWidth,
            height: "auto",
            preserveAspectRatio: options.preserveAspectRatio ?? true,
        });
        return { sequence, rows };
    }
    return null;
}
/**
 * Wrap text in an OSC 8 hyperlink sequence.
 * The text is rendered as a clickable hyperlink in terminals that support OSC 8
 * (Ghostty, Kitty, WezTerm, iTerm2, VSCode, and others).
 * In terminals that do not support OSC 8, the escape sequences are ignored
 * and only the plain text is displayed.
 *
 * @param text - The visible text to display
 * @param url - The URL to link to
 */
export function hyperlink(text, url) {
    return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
}
export function imageFallback(mimeType, dimensions, filename) {
    const parts = [];
    if (filename)
        parts.push(filename);
    parts.push(`[${mimeType}]`);
    if (dimensions)
        parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`);
    return `[Image: ${parts.join(" ")}]`;
}
//# sourceMappingURL=terminal-image.js.map