import { execSync } from "node:child_process";
import { homedir } from "node:os";
import { isAbsolute } from "node:path";
import { pathToFileURL } from "node:url";
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;
}
/**
 * Checks whether the attached tmux client forwards OSC 8 hyperlinks to the
 * outer terminal. tmux only re-emits them when its `client_termfeatures` lists
 * `hyperlinks`, and strips them otherwise. On any error fallbacks `false`.
 */
function probeTmuxHyperlinks() {
    try {
        const termfeatures = execSync("tmux display-message -p '#{client_termfeatures}'", {
            encoding: "utf8",
            timeout: 250,
            stdio: ["ignore", "pipe", "ignore"],
        });
        return termfeatures
            .split(",")
            .map((feature) => feature.trim())
            .includes("hyperlinks");
    }
    catch {
        return false;
    }
}
export function detectCapabilities(tmuxForwardsHyperlink = probeTmuxHyperlinks) {
    const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
    const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
    const term = process.env.TERM?.toLowerCase() || "";
    const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
    const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit";
    const isWindowsConsole = process.platform === "win32";
    // Emit OSC 8 hyperlinks only when tmux confirms it forwards.
    // Image protocols are unreliable under tmux, so leave `images: null`.
    if (process.env.TMUX || term.startsWith("tmux")) {
        return { images: null, trueColor: hasTrueColorHint, hyperlinks: tmuxForwardsHyperlink() };
    }
    // screen does not forward OSC 8 hyperlinks, so keep them off there.
    if (term.startsWith("screen")) {
        return { images: null, trueColor: hasTrueColorHint, 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 };
    }
    // Warp supports the Kitty graphics protocol and OSC 8 hyperlinks.
    if (termProgram === "warpterminal" || process.env.WARP_SESSION_ID || process.env.WARP_TERMINAL_SESSION_UUID) {
        return { images: "kitty", trueColor: true, hyperlinks: true };
    }
    if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") {
        return { images: "iterm2", trueColor: true, hyperlinks: true };
    }
    if (process.env.WT_SESSION) {
        return { images: null, trueColor: true, hyperlinks: true };
    }
    if (termProgram === "vscode") {
        return { images: null, trueColor: true, hyperlinks: true };
    }
    if (termProgram === "alacritty") {
        return { images: null, trueColor: true, hyperlinks: true };
    }
    if (terminalEmulator === "jetbrains-jediterm") {
        return { images: null, trueColor: true, hyperlinks: false };
    }
    // Windows Terminal does not always set WT_SESSION, for example when it hosts
    // a cmd.exe launched directly from Win+R. Modern Windows consoles support
    // truecolor; keep hyperlinks off unless we positively detected support above.
    if (isWindowsConsole) {
        return { images: null, trueColor: true, hyperlinks: false };
    }
    // 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.
    return { images: null, trueColor: hasTrueColorHint, 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\\";
}
/** Delete all visible Kitty placements while retaining their uploaded image data. */
export function deleteAllKittyPlacements() {
    return "\x1b_Ga=d,d=a,q=2\x1b\\";
}
export function encodeITerm2(base64Data, options = {}) {
    const params = [
        `inline=${options.inline !== false ? 1 : 0}`,
        `size=${Buffer.byteLength(base64Data, "base64")}`,
    ];
    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`;
}
const kittyImageMetadata = new Map();
let kittyTransmissionGeneration = 0;
export function registerKittyImageMetadata(metadata) {
    kittyTransmissionGeneration += 1;
    kittyImageMetadata.delete(metadata.imageId);
    kittyImageMetadata.set(metadata.imageId, { ...metadata, transmissionGeneration: kittyTransmissionGeneration });
    if (kittyImageMetadata.size > 1000) {
        const oldestImageId = kittyImageMetadata.keys().next().value;
        if (oldestImageId !== undefined)
            kittyImageMetadata.delete(oldestImageId);
    }
}
function getRegisteredKittyImageMetadata(line) {
    const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
    if (!controls)
        return undefined;
    const imageId = /(?:^|,)i=(\d+)(?:,|$)/.exec(controls)?.[1];
    return imageId === undefined ? undefined : kittyImageMetadata.get(Number.parseInt(imageId, 10));
}
export function getKittyImageMetadata(line) {
    const metadata = getRegisteredKittyImageMetadata(line);
    if (!metadata)
        return undefined;
    return {
        imageId: metadata.imageId,
        columns: metadata.columns,
        rows: metadata.rows,
        widthPx: metadata.widthPx,
        heightPx: metadata.heightPx,
    };
}
const KITTY_PLACEMENT_CONTROL_KEYS = new Set([
    "i",
    "p",
    "x",
    "y",
    "w",
    "h",
    "X",
    "Y",
    "c",
    "r",
    "C",
    "U",
    "z",
    "P",
    "Q",
    "H",
    "V",
]);
/** Build a placement-only command for an image line emitted by {@link renderImage}. */
export function getKittyImagePlacement(line) {
    const match = /\x1b_G([^;]*);/.exec(line);
    const metadata = getRegisteredKittyImageMetadata(line);
    if (!match || !metadata)
        return undefined;
    let commandStart = match.index;
    let commandControls = match[1];
    let transmissionEnd;
    while (true) {
        const terminator = line.indexOf("\x1b\\", commandStart + KITTY_PREFIX.length);
        if (terminator === -1)
            return undefined;
        transmissionEnd = terminator + 2;
        if (!/(?:^|,)m=1(?:,|$)/.test(commandControls))
            break;
        commandStart = transmissionEnd;
        if (!line.startsWith(KITTY_PREFIX, commandStart))
            return undefined;
        const controlsEnd = line.indexOf(";", commandStart + KITTY_PREFIX.length);
        if (controlsEnd === -1)
            return undefined;
        commandControls = line.slice(commandStart + KITTY_PREFIX.length, controlsEnd);
    }
    const controls = match[1]
        .split(",")
        .filter((control) => KITTY_PLACEMENT_CONTROL_KEYS.has(control.split("=", 1)[0] ?? ""));
    const sequence = `\x1b_Ga=p,q=2,${controls.join(",")}\x1b\\`;
    return {
        imageId: metadata.imageId,
        transmissionGeneration: metadata.transmissionGeneration,
        transmissionBytes: transmissionEnd - match.index,
        estimatedDecodedBytes: metadata.widthPx * metadata.heightPx * 4,
        sequence,
        replacementLine: `${line.slice(0, match.index)}${sequence}${line.slice(transmissionEnd)}`,
    };
}
export function cropKittyImageLine(line, hiddenRows, visibleRows) {
    const metadata = getKittyImageMetadata(line);
    const match = /\x1b_G([^;]*);/.exec(line);
    if (!metadata || !match || hiddenRows < 0 || hiddenRows >= metadata.rows || visibleRows <= 0)
        return line;
    const croppedRows = Math.min(visibleRows, metadata.rows - hiddenRows);
    if (hiddenRows === 0 && croppedRows === metadata.rows)
        return line;
    const sourceY = Math.floor((metadata.heightPx * hiddenRows) / metadata.rows);
    const sourceEnd = Math.ceil((metadata.heightPx * (hiddenRows + croppedRows)) / metadata.rows);
    const sourceHeight = Math.max(1, Math.min(metadata.heightPx, sourceEnd) - sourceY);
    const controls = match[1].split(",").filter((control) => !/^[yhr]=/.test(control));
    controls.push(`y=${sourceY}`, `h=${sourceHeight}`, `r=${croppedRows}`);
    return `${line.slice(0, match.index)}\x1b_G${controls.join(",")};${line.slice(match.index + match[0].length)}`;
}
export function calculateImageCellSize(imageDimensions, maxWidthCells, maxHeightCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
    const maxWidth = Math.max(1, Math.floor(maxWidthCells));
    const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
    const imageWidth = Math.max(1, imageDimensions.widthPx);
    const imageHeight = Math.max(1, imageDimensions.heightPx);
    const widthScale = (maxWidth * cellDimensions.widthPx) / imageWidth;
    const heightScale = maxHeight === undefined ? widthScale : (maxHeight * cellDimensions.heightPx) / imageHeight;
    const scale = Math.min(widthScale, heightScale);
    const scaledWidthPx = imageWidth * scale;
    const scaledHeightPx = imageHeight * scale;
    const columns = Math.ceil(scaledWidthPx / cellDimensions.widthPx);
    const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
    return {
        columns: Math.max(1, Math.min(maxWidth, columns)),
        rows: Math.max(1, maxHeight === undefined ? rows : Math.min(maxHeight, rows)),
    };
}
export function calculateImageRows(imageDimensions, targetWidthCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
    return calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).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 size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions());
    if (caps.images === "kitty") {
        if (options.imageId !== undefined) {
            registerKittyImageMetadata({
                imageId: options.imageId,
                columns: size.columns,
                rows: size.rows,
                widthPx: imageDimensions.widthPx,
                heightPx: imageDimensions.heightPx,
            });
        }
        const sequence = encodeKitty(base64Data, {
            columns: size.columns,
            rows: size.rows,
            imageId: options.imageId,
            moveCursor: options.moveCursor,
        });
        return { sequence, columns: size.columns, rows: size.rows, imageId: options.imageId };
    }
    if (caps.images === "iterm2") {
        const sequence = encodeITerm2(base64Data, {
            width: size.columns,
            height: "auto",
            preserveAspectRatio: options.preserveAspectRatio ?? true,
        });
        return { sequence, columns: size.columns, rows: size.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\\`;
}
/** Shorten home-prefixed absolute paths to ~/... for compact display. */
function shortenImagePath(filename) {
    const home = homedir();
    if (home && (filename === home || filename.startsWith(`${home}/`) || filename.startsWith(`${home}\\`))) {
        return `~${filename.slice(home.length)}`;
    }
    return filename;
}
/**
 * Text fallback when the terminal cannot render inline images.
 * Absolute paths are shown shortened (~/...) and, when OSC 8 hyperlinks are
 * available, linked to file:// so the full path remains openable.
 */
export function imageFallback(mimeType, dimensions, filename) {
    const parts = [];
    if (filename) {
        const display = shortenImagePath(filename);
        if (getCapabilities().hyperlinks && isAbsolute(filename)) {
            parts.push(hyperlink(display, pathToFileURL(filename).href));
        }
        else {
            parts.push(display);
        }
    }
    parts.push(`[${mimeType}]`);
    if (dimensions)
        parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`);
    return `[Image: ${parts.join(" ")}]`;
}
//# sourceMappingURL=terminal-image.js.map