"use strict";
// ============================================================
// Bento — Figma-to-Email Compiler (Plugin Sandbox)
// Converts Auto Layout frames into nested HTML <table> email markup.
// ============================================================
figma.showUI(__html__, { width: 480, height: 680, themeColors: true, title: 'Bento - Email Generator - Figma to HTML Email' });
// Current backend identifier — bump this when the backend URL changes
const CURRENT_BACKEND = 'bentoplugin.com';
// Load stored API token (with backend migration check)
(async () => {
    const storedBackend = await figma.clientStorage.getAsync('backend_id');
    if (storedBackend !== CURRENT_BACKEND) {
        // Backend has changed — clear old token to force fresh login
        await figma.clientStorage.deleteAsync('api_token');
        await figma.clientStorage.setAsync('backend_id', CURRENT_BACKEND);
        return; // No token to restore — user will see "Login"
    }
    const token = await figma.clientStorage.getAsync('api_token');
    if (token) {
        figma.ui.postMessage({ type: 'init-auth', token });
    }
})();
// --------------- Helpers ---------------
function rgbToHex(r, g, b) {
    const toHex = (v) => {
        const h = Math.round(v * 255).toString(16);
        return h.length === 1 ? '0' + h : h;
    };
    return '#' + toHex(r) + toHex(g) + toHex(b);
}
function getBackgroundColor(node) {
    if (!('fills' in node))
        return null;
    const fills = node.fills;
    if (!Array.isArray(fills))
        return null;
    for (const fill of fills) {
        if (fill.visible === false)
            continue;
        if (fill.type === 'SOLID') {
            return rgbToHex(fill.color.r, fill.color.g, fill.color.b);
        }
        // For gradients, use the first color stop as a fallback background
        if (fill.type === 'GRADIENT_LINEAR' || fill.type === 'GRADIENT_RADIAL' ||
            fill.type === 'GRADIENT_ANGULAR' || fill.type === 'GRADIENT_DIAMOND') {
            const stops = fill.gradientStops;
            if (stops && stops.length > 0) {
                const c = stops[0].color;
                return rgbToHex(c.r, c.g, c.b);
            }
        }
    }
    return null;
}
function getBorderRadius(node) {
    if (!('cornerRadius' in node))
        return '';
    const cr = node.cornerRadius;
    if (typeof cr === 'number' && cr > 0) {
        return `border-radius:${cr}px;`;
    }
    // Mixed radii
    const n = node;
    if (n.topLeftRadius || n.topRightRadius || n.bottomRightRadius || n.bottomLeftRadius) {
        return `border-radius:${n.topLeftRadius || 0}px ${n.topRightRadius || 0}px ${n.bottomRightRadius || 0}px ${n.bottomLeftRadius || 0}px;`;
    }
    return '';
}
function getBorder(node) {
    if (!('strokes' in node))
        return '';
    const strokes = node.strokes;
    if (!Array.isArray(strokes) || strokes.length === 0)
        return '';
    const stroke = strokes[0];
    if (stroke.type === 'SOLID' && stroke.visible !== false) {
        const color = rgbToHex(stroke.color.r, stroke.color.g, stroke.color.b);
        const n = node;
        // Check for individual stroke edges (Frame, Rectangle, etc)
        if (typeof n.strokeTopWeight === 'number') {
            let css = '';
            if (n.strokeTopWeight > 0)
                css += `border-top:${n.strokeTopWeight}px solid ${color};`;
            if (n.strokeRightWeight > 0)
                css += `border-right:${n.strokeRightWeight}px solid ${color};`;
            if (n.strokeBottomWeight > 0)
                css += `border-bottom:${n.strokeBottomWeight}px solid ${color};`;
            if (n.strokeLeftWeight > 0)
                css += `border-left:${n.strokeLeftWeight}px solid ${color};`;
            return css;
        }
        // Fallback for uniform strokes (Vectors, Lines, etc)
        const weight = typeof n.strokeWeight === 'number' ? n.strokeWeight : 1;
        if (weight > 0) {
            return `border:${weight}px solid ${color};`;
        }
    }
    return '';
}
function escapeHtml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/\n/g, '<br>');
}
function getTextAlign(node) {
    const align = node.textAlignHorizontal;
    if (align === 'CENTER')
        return 'center';
    if (align === 'RIGHT')
        return 'right';
    if (align === 'JUSTIFIED')
        return 'justify';
    return 'left';
}
function getCounterAxisAlign(align) {
    if (align === 'CENTER')
        return 'center';
    if (align === 'MAX')
        return 'right';
    return 'left';
}
function getCounterAxisVAlign(align) {
    if (align === 'CENTER')
        return 'middle';
    if (align === 'MAX')
        return 'bottom';
    return 'top';
}
function hasImageFill(node) {
    if (!('fills' in node))
        return false;
    const fills = node.fills;
    if (!Array.isArray(fills))
        return false;
    return fills.some(f => f.type === 'IMAGE' && f.visible !== false);
}
function isAutoLayoutFrame(node) {
    return ((node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE') &&
        'layoutMode' in node &&
        node.layoutMode !== 'NONE');
}
function isContainerNode(node) {
    return (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'INSTANCE' || node.type === 'GROUP');
}
// --------------- Smart Rasterization Helpers ---------------
function hasGradientFill(node) {
    if (!('fills' in node))
        return false;
    const fills = node.fills;
    if (!Array.isArray(fills))
        return false;
    return fills.some((f) => f.visible !== false &&
        (f.type === 'GRADIENT_LINEAR' ||
            f.type === 'GRADIENT_RADIAL' ||
            f.type === 'GRADIENT_ANGULAR' ||
            f.type === 'GRADIENT_DIAMOND'));
}
function hasComplexEffects(node) {
    if (!('effects' in node))
        return false;
    const effects = node.effects;
    if (!Array.isArray(effects) || effects.length === 0)
        return false;
    return effects.some((e) => e.visible !== false);
}
function isCircularClip(node) {
    if (node.type !== 'FRAME' && node.type !== 'COMPONENT' && node.type !== 'INSTANCE')
        return false;
    const frame = node;
    if (!frame.clipsContent)
        return false;
    const cr = frame.cornerRadius;
    if (typeof cr === 'number' && cr >= Math.min(frame.width, frame.height) * 0.4) {
        return true;
    }
    return false;
}
function hasTextDescendant(node) {
    if (node.type === 'TEXT')
        return true;
    if ('children' in node) {
        const children = node.children;
        for (const child of children) {
            if ('visible' in child && !child.visible)
                continue;
            if (hasTextDescendant(child))
                return true;
        }
    }
    return false;
}
function shouldRasterize(node, isRoot) {
    // Never rasterize the root email frame
    if (isRoot)
        return false;
    // Never rasterize text nodes — they're always compiled to HTML
    if (node.type === 'TEXT')
        return false;
    // If this node contains ANY text descendants, do NOT rasterize —
    // preserve text editability for buttons, headings, paragraphs, etc.
    if (hasTextDescendant(node))
        return false;
    // 1) Graphical elements (Icons): instances, components, or small non-text frames.
    // Because they lack text, they act as purely graphical components. Rasterizing them
    // perfectly maintains their vector alignments and inner logic.
    if (node.type === 'INSTANCE' || node.type === 'COMPONENT')
        return true;
    if ((node.type === 'FRAME' || node.type === 'GROUP') && node.width <= 100 && node.height <= 100)
        return true;
    // 2) Gradient fills → can't be reproduced in email HTML
    if (hasGradientFill(node))
        return true;
    // 3) Effects (shadows, blurs) → not supported in most email clients
    if (hasComplexEffects(node))
        return true;
    // 3) Circular / heavily-rounded clips
    if (isCircularClip(node))
        return true;
    // 5) Auto-layout frame with absolutely-positioned children
    if (isAutoLayoutFrame(node)) {
        const frameChildren = node.children;
        if (frameChildren.some((c) => 'layoutPositioning' in c && c.layoutPositioning === 'ABSOLUTE'))
            return true;
    }
    // 6) Auto-layout with wrap (grid layout not representable in email tables)
    if (isAutoLayoutFrame(node)) {
        const wrap = node.layoutWrap;
        if (wrap && wrap === 'WRAP')
            return true;
    }
    // 7) Container with image fill background and children
    if (isContainerNode(node) && hasImageFill(node)) {
        if ('children' in node && node.children.length > 0)
            return true;
    }
    return false;
}
// --------------- Image Export ---------------
let imageCounter = 0;
const imageAssets = [];
async function exportNodeAsImage(node) {
    imageCounter++;
    const fileName = `img_${imageCounter}_${node.name.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 30)}.png`;
    const bytes = await node.exportAsync({
        format: 'PNG',
        constraint: { type: 'SCALE', value: 2 },
    });
    const base64 = figma.base64Encode(bytes);
    const asset = {
        id: `image_${imageCounter}`,
        name: fileName,
        base64: base64,
        width: Math.round(node.width),
        height: Math.round(node.height),
    };
    imageAssets.push(asset);
    return asset.id;
}
// --------------- Text Compiler ---------------
function compileTextNode(node) {
    const segments = node.getStyledTextSegments([
        'fontSize',
        'fontName',
        'fontWeight',
        'fills',
        'letterSpacing',
        'lineHeight',
        'textDecoration',
        'textCase',
        'hyperlink',
    ]);
    if (segments.length === 0) {
        return escapeHtml(node.characters);
    }
    let html = '';
    for (const seg of segments) {
        let style = '';
        style += `font-size:${seg.fontSize}px;`;
        style += `font-family:'${seg.fontName.family}', Arial, sans-serif;`;
        style += `font-weight:${seg.fontWeight};`;
        // Color
        if (seg.fills && Array.isArray(seg.fills)) {
            for (const fill of seg.fills) {
                if (fill.type === 'SOLID' && fill.visible !== false) {
                    style += `color:${rgbToHex(fill.color.r, fill.color.g, fill.color.b)};`;
                    break;
                }
            }
        }
        // Letter spacing
        if (seg.letterSpacing && typeof seg.letterSpacing === 'object') {
            const ls = seg.letterSpacing;
            if (ls.unit === 'PIXELS') {
                style += `letter-spacing:${ls.value}px;`;
            }
            else if (ls.unit === 'PERCENT') {
                style += `letter-spacing:${ls.value / 100}em;`;
            }
        }
        // Line height
        if (seg.lineHeight && typeof seg.lineHeight === 'object') {
            const lh = seg.lineHeight;
            if (lh.unit === 'PIXELS') {
                style += `line-height:${Math.round(lh.value)}px;mso-line-height-rule:exactly;`;
            }
            else if (lh.unit === 'PERCENT') {
                style += `line-height:${Math.round(lh.value)}%;`;
            }
        }
        // Text decoration
        if (seg.textDecoration === 'UNDERLINE') {
            style += 'text-decoration:underline;';
        }
        else if (seg.textDecoration === 'STRIKETHROUGH') {
            style += 'text-decoration:line-through;';
        }
        // Text case
        if (seg.textCase === 'UPPER') {
            style += 'text-transform:uppercase;';
        }
        else if (seg.textCase === 'LOWER') {
            style += 'text-transform:lowercase;';
        }
        else if (seg.textCase === 'TITLE') {
            style += 'text-transform:capitalize;';
        }
        let text = escapeHtml(seg.characters);
        // Hyperlink
        if (seg.hyperlink && seg.hyperlink.type === 'URL') {
            text = `<a href="${seg.hyperlink.value}" target="_blank" style="${style}text-decoration:underline;">${text}</a>`;
        }
        else {
            text = `<span style="${style}">${text}</span>`;
        }
        html += text;
    }
    return html;
}
// --------------- Main Compiler ---------------
async function compileNode(node, isRoot = false) {
    // Skip invisible nodes
    if ('visible' in node && !node.visible)
        return '';
    // --- TEXT NODE ---
    if (node.type === 'TEXT') {
        const textAlign = getTextAlign(node);
        const textHtml = compileTextNode(node);
        let isHug = false;
        let noWrap = false;
        if ('layoutSizingHorizontal' in node && node.layoutSizingHorizontal === 'HUG')
            isHug = true;
        if (node.textAutoResize === 'WIDTH_AND_HEIGHT') {
            isHug = true;
            noWrap = true;
        }
        let isFill = false;
        if ('layoutSizingHorizontal' in node && node.layoutSizingHorizontal === 'FILL')
            isFill = true;
        const w = Math.round(node.width);
        let wAttr = '';
        if (isFill) {
            wAttr = ' width="100%"';
        }
        else if (!isHug) {
            wAttr = ` width="${w}"`;
        }
        let tdStyle = `text-align:${textAlign};margin:0;padding:0;word-break:break-word;`;
        if (noWrap)
            tdStyle += 'white-space:nowrap;';
        return `<table${wAttr} cellpadding="0" cellspacing="0" border="0" role="presentation"${wAttr ? ' style="table-layout:fixed;"' : ''}><tr><td align="${textAlign}" style="${tdStyle}">${textHtml}</td></tr></table>`;
    }
    // --- SMART RASTERIZATION ---
    // Export complex visual elements as images for pixel-perfect email rendering
    if (shouldRasterize(node, isRoot)) {
        const imageId = await exportNodeAsImage(node);
        const w = Math.round(node.width);
        const h = Math.round(node.height);
        return `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(node.name)}">`;
    }
    // --- VECTOR / BOOLEAN / STAR / LINE / POLYGON / ELLIPSE ---
    // Export as image
    if (node.type === 'VECTOR' ||
        node.type === 'STAR' ||
        node.type === 'LINE' ||
        node.type === 'POLYGON' ||
        node.type === 'ELLIPSE' ||
        node.type === 'BOOLEAN_OPERATION') {
        const imageId = await exportNodeAsImage(node);
        const w = Math.round(node.width);
        const h = Math.round(node.height);
        return `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(node.name)}">`;
    }
    // --- RECTANGLE with image fill ---
    if (node.type === 'RECTANGLE' && hasImageFill(node)) {
        const imageId = await exportNodeAsImage(node);
        const w = Math.round(node.width);
        const h = Math.round(node.height);
        return `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(node.name)}">`;
    }
    // --- RECTANGLE (solid color) ---
    if (node.type === 'RECTANGLE' && !hasImageFill(node)) {
        const bg = getBackgroundColor(node);
        const w = Math.round(node.width);
        const h = Math.round(node.height);
        const radius = getBorderRadius(node);
        const border = getBorder(node);
        if (bg) {
            const inner = `<table width="${w}" cellpadding="0" cellspacing="0" border="0" role="presentation"><tr><td style="background-color:${bg};${radius}${border}font-size:0;line-height:0;height:${h}px;" height="${h}">&nbsp;</td></tr></table>`;
            if (isRoot)
                return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;"><tr><td align="center" valign="top">${inner}</td></tr></table>`;
            return inner;
        }
        return '';
    }
    // --- GROUP ---
    if (node.type === 'GROUP') {
        if ('children' in node && node.children.length > 0) {
            const w = Math.round(node.width);
            const h = Math.round(node.height);
            const gx = Math.round(node.x);
            const gy = Math.round(node.y);
            try {
                const boxes = node.children
                    .filter(c => !('visible' in c) || c.visible)
                    .map(c => ({
                    node: c,
                    x: Math.round(c.x) - gx,
                    y: Math.round(c.y) - gy,
                    w: Math.round(c.width),
                    h: Math.round(c.height)
                }));
                let innerHtml = await compileChildrenBSP(boxes, { x: 0, y: 0, w, h });
                const outerTable = `<table width="${w}" cellpadding="0" cellspacing="0" border="0" role="presentation"><tr><td valign="top">${innerHtml}</td></tr></table>`;
                if (isRoot) {
                    return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;"><tr><td align="center" valign="top">${outerTable}</td></tr></table>`;
                }
                return outerTable;
            }
            catch (e) {
                if (e.message === 'OVERLAP') {
                    const imageId = await exportNodeAsImage(node);
                    const outerTable = `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(node.name)}">`;
                    if (isRoot) {
                        return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;"><tr><td align="center" valign="top">${outerTable}</td></tr></table>`;
                    }
                    return outerTable;
                }
                throw e;
            }
        }
        // Empty group: export as image
        const imageId = await exportNodeAsImage(node);
        const w = Math.round(node.width);
        const h = Math.round(node.height);
        return `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(node.name)}">`;
    }
    // --- AUTO LAYOUT FRAME / COMPONENT / INSTANCE ---
    if (isAutoLayoutFrame(node)) {
        return await compileAutoLayoutFrame(node, isRoot);
    }
    // --- FRAME without Auto Layout (or COMPONENT/INSTANCE without auto layout) ---
    if (isContainerNode(node)) {
        const containerNode = node;
        // If it has an image fill, export the whole thing
        if (hasImageFill(containerNode)) {
            const imageId = await exportNodeAsImage(containerNode);
            const w = Math.round(containerNode.width);
            const h = Math.round(containerNode.height);
            return `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(containerNode.name)}">`;
        }
        // Try to compile children if it has any
        if ('children' in containerNode && containerNode.children.length > 0) {
            const bg = getBackgroundColor(containerNode);
            const radius = getBorderRadius(containerNode);
            const border = getBorder(containerNode);
            const w = Math.round(containerNode.width);
            const h = Math.round(containerNode.height);
            let tableStyle = '';
            if (bg)
                tableStyle += `background-color:${bg};`;
            tableStyle += radius + border;
            try {
                const boxes = containerNode.children
                    .filter(c => !('visible' in c) || c.visible)
                    .map(c => ({
                    node: c,
                    x: Math.round(c.x),
                    y: Math.round(c.y),
                    w: Math.round(c.width),
                    h: Math.round(c.height)
                }));
                let innerHtml = await compileChildrenBSP(boxes, { x: 0, y: 0, w, h });
                const outerTable = `<table width="${w}" cellpadding="0" cellspacing="0" border="0" role="presentation"${tableStyle ? ` style="${tableStyle}"` : ''}><tr><td valign="top">${innerHtml}</td></tr></table>`;
                if (isRoot) {
                    return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;${bg ? `background-color:${bg};` : ''}"><tr><td align="center" valign="top">${outerTable}</td></tr></table>`;
                }
                return outerTable;
            }
            catch (e) {
                if (e.message === 'OVERLAP') {
                    const imageId = await exportNodeAsImage(containerNode);
                    const outerTable = `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="${escapeHtml(containerNode.name)}">`;
                    if (isRoot) {
                        return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;${bg ? `background-color:${bg};` : ''}"><tr><td align="center" valign="top">${outerTable}</td></tr></table>`;
                    }
                    return outerTable;
                }
                throw e;
            }
        }
        // Single solid-fill frame without children
        const bg = getBackgroundColor(containerNode);
        if (bg) {
            const w = Math.round(containerNode.width);
            const h = Math.round(containerNode.height);
            const inner = `<table width="${w}" cellpadding="0" cellspacing="0" border="0" role="presentation"><tr><td style="background-color:${bg};${getBorderRadius(containerNode)}${getBorder(containerNode)}font-size:0;line-height:0;height:${h}px;" height="${h}">&nbsp;</td></tr></table>`;
            if (isRoot)
                return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;"><tr><td align="center" valign="top">${inner}</td></tr></table>`;
            return inner;
        }
        return '';
    }
    // --- FALLBACK: export as image ---
    try {
        const imageId = await exportNodeAsImage(node);
        const w = Math.round(node.width);
        const h = Math.round(node.height);
        return `<img data-bento-image="${imageId}" width="${w}" height="${h}" style="display:block;border:0;outline:none;max-width:100%;height:auto;" alt="">`;
    }
    catch (_e) {
        return `<!-- Unsupported node: ${node.type} -->`;
    }
}
function getProjectedIntervals(boxes, axis) {
    const intervals = boxes.map(b => axis === 'y' ? [b.y, b.y + b.h] : [b.x, b.x + b.w]);
    intervals.sort((a, b) => a[0] - b[0]);
    const merged = [];
    if (intervals.length === 0)
        return merged;
    let current = [intervals[0][0], intervals[0][1]];
    for (let i = 1; i < intervals.length; i++) {
        // allow a 1px tolerance for rounding errors in Figma coordinates
        if (intervals[i][0] <= current[1] + 1) {
            current[1] = Math.max(current[1], intervals[i][1]);
        }
        else {
            merged.push(current);
            current = [intervals[i][0], intervals[i][1]];
        }
    }
    merged.push(current);
    return merged;
}
async function compileChildrenBSP(boxes, bounds) {
    if (boxes.length === 0) {
        return ``;
    }
    // Try horizontal split (split into rows)
    const mergedY = getProjectedIntervals(boxes, 'y');
    // We only trigger Y split if there are multiple rows OR a top gap
    if (mergedY.length > 1 || mergedY[0][0] > bounds.y) {
        let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation">`;
        let currentY = bounds.y;
        for (const interval of mergedY) {
            if (interval[0] > currentY) {
                const gapH = Math.round(interval[0] - currentY);
                html += `<tr><td height="${gapH}" style="height:${gapH}px;font-size:0;line-height:0;">&nbsp;</td></tr>`;
            }
            const rowH = Math.round(interval[1] - interval[0]);
            const rowBoxes = boxes.filter(b => b.y >= interval[0] && b.y + b.h <= interval[1]);
            const rowHtml = await compileChildrenBSP(rowBoxes, { x: bounds.x, y: interval[0], w: bounds.w, h: rowH });
            html += `<tr><td valign="top">${rowHtml}</td></tr>`;
            currentY = interval[1];
        }
        // Intentionally omitted bottom gap to allow email to "hug" contents vertically
        html += `</table>`;
        return html;
    }
    // Try vertical split (split into columns)
    const mergedX = getProjectedIntervals(boxes, 'x');
    // Trigger X split if there are multiple cols, a left gap, OR a right gap
    if (mergedX.length > 1 || mergedX[0][0] > bounds.x || mergedX[mergedX.length - 1][1] < bounds.x + bounds.w) {
        let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="table-layout:fixed;"><tr>`;
        let currentX = bounds.x;
        for (const interval of mergedX) {
            if (interval[0] > currentX) {
                const gapW = Math.round(interval[0] - currentX);
                html += `<td width="${gapW}" style="width:${gapW}px;min-width:${gapW}px;max-width:${gapW}px;font-size:0;line-height:0;">&nbsp;</td>`;
            }
            const colW = Math.round(interval[1] - interval[0]);
            const colBoxes = boxes.filter(b => b.x >= interval[0] && b.x + b.w <= interval[1]);
            const colHtml = await compileChildrenBSP(colBoxes, { x: interval[0], y: bounds.y, w: colW, h: bounds.h });
            html += `<td width="${colW}" valign="top" style="width:${colW}px;min-width:${colW}px;max-width:${colW}px;">${colHtml}</td>`;
            currentX = interval[1];
        }
        if (currentX < bounds.x + bounds.w) {
            const gapW = Math.round(bounds.x + bounds.w - currentX);
            html += `<td width="${gapW}" style="width:${gapW}px;min-width:${gapW}px;max-width:${gapW}px;font-size:0;line-height:0;">&nbsp;</td>`;
        }
        html += `</tr></table>`;
        return html;
    }
    // If we reach here, there are no gaps and no splits.
    if (boxes.length === 1) {
        return await compileNode(boxes[0].node);
    }
    // If there are multiple boxes that cannot be split and have no gaps, they overlap.
    throw new Error('OVERLAP');
}
// --------------- Auto Layout Frame Compiler ---------------
async function compileAutoLayoutFrame(frame, isRoot) {
    const isVertical = frame.layoutMode === 'VERTICAL';
    const w = Math.round(frame.width);
    const bg = getBackgroundColor(frame);
    const radius = getBorderRadius(frame);
    const border = getBorder(frame);
    const gap = frame.itemSpacing || 0;
    const padTop = frame.paddingTop || 0;
    const padRight = frame.paddingRight || 0;
    const padBottom = frame.paddingBottom || 0;
    const padLeft = frame.paddingLeft || 0;
    const hasPadding = padTop > 0 || padRight > 0 || padBottom > 0 || padLeft > 0;
    const primaryAlign = frame.primaryAxisAlignItems || 'MIN';
    const counterAlign = frame.counterAxisAlignItems || 'MIN';
    const tdAlign = getCounterAxisAlign(counterAlign);
    const tdVAlign = getCounterAxisVAlign(counterAlign);
    let sizingW = 'HUG';
    if ('layoutSizingHorizontal' in frame) {
        sizingW = frame.layoutSizingHorizontal;
    }
    else if ('primaryAxisSizingMode' in frame) {
        sizingW = isVertical ? frame.counterAxisSizingMode : frame.primaryAxisSizingMode;
        if (sizingW === 'AUTO')
            sizingW = 'HUG';
    }
    let sizingH = 'HUG';
    if ('layoutSizingVertical' in frame) {
        sizingH = frame.layoutSizingVertical;
    }
    else if ('primaryAxisSizingMode' in frame) {
        sizingH = isVertical ? frame.primaryAxisSizingMode : frame.counterAxisSizingMode;
        if (sizingH === 'AUTO')
            sizingH = 'HUG';
    }
    // Build outer table style
    let tableStyle = '';
    if (bg)
        tableStyle += `background-color:${bg};`;
    tableStyle += radius + border;
    // Collect visible children
    const children = [];
    if ('children' in frame) {
        for (const child of frame.children) {
            if ('visible' in child && !child.visible)
                continue;
            children.push(child);
        }
    }
    // Build inner content
    let innerHtml = '';
    if (isVertical) {
        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            const childContent = await compileNode(child);
            let childAlign = tdAlign;
            if ('layoutAlign' in child) {
                if (child.layoutAlign === 'MIN')
                    childAlign = 'left';
                if (child.layoutAlign === 'MAX')
                    childAlign = 'right';
                if (child.layoutAlign === 'CENTER')
                    childAlign = 'center';
            }
            innerHtml += `<tr><td align="${childAlign}" valign="top">${childContent}</td></tr>`;
            if (primaryAlign === 'SPACE_BETWEEN' && i < children.length - 1) {
                innerHtml += `<tr><td height="100%" style="font-size:0;line-height:0;">&nbsp;</td></tr>`;
            }
            else if (gap > 0 && i < children.length - 1) {
                innerHtml += `<tr><td style="height:${gap}px;font-size:${gap}px;line-height:${gap}px;" height="${gap}">&nbsp;</td></tr>`;
            }
        }
    }
    else {
        innerHtml += '<tr>';
        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            const childContent = await compileNode(child);
            const childW = Math.round(child.width);
            let childValign = tdVAlign;
            if ('layoutAlign' in child) {
                if (child.layoutAlign === 'MIN')
                    childValign = 'top';
                if (child.layoutAlign === 'MAX')
                    childValign = 'bottom';
                if (child.layoutAlign === 'CENTER')
                    childValign = 'middle';
            }
            let childSizingW = 'HUG';
            if ('layoutSizingHorizontal' in child)
                childSizingW = child.layoutSizingHorizontal;
            let tdStyle = `vertical-align:${childValign};`;
            let childWAttr = '';
            if (childSizingW === 'FILL') {
                childWAttr = ` width="100%"`;
            }
            else if (childSizingW !== 'HUG') {
                childWAttr = ` width="${childW}"`;
                tdStyle += `width:${childW}px;min-width:${childW}px;max-width:${childW}px;`;
            }
            innerHtml += `<td${childWAttr} align="left" valign="${childValign}" style="${tdStyle}">${childContent}</td>`;
            if (primaryAlign === 'SPACE_BETWEEN' && i < children.length - 1) {
                innerHtml += `<td width="100%" style="font-size:0;line-height:0;">&nbsp;</td>`;
            }
            else if (gap > 0 && i < children.length - 1) {
                innerHtml += `<td style="width:${gap}px;min-width:${gap}px;max-width:${gap}px;font-size:0;line-height:0;" width="${gap}">&nbsp;</td>`;
            }
        }
        innerHtml += '</tr>';
    }
    let tableAlign = 'left';
    if (!isVertical) {
        if (primaryAlign === 'CENTER')
            tableAlign = 'center';
        if (primaryAlign === 'MAX')
            tableAlign = 'right';
    }
    else {
        if (tdAlign === 'center')
            tableAlign = 'center';
        if (tdAlign === 'right')
            tableAlign = 'right';
    }
    let hasFillChild = false;
    if (!isVertical) {
        for (const child of children) {
            if ('layoutSizingHorizontal' in child && child.layoutSizingHorizontal === 'FILL')
                hasFillChild = true;
        }
    }
    let innerTableWidth = ' width="100%"';
    let innerTableStyle = 'table-layout:fixed;';
    if (!isVertical && primaryAlign !== 'SPACE_BETWEEN' && !hasFillChild) {
        // Let horizontal tables without stretched children shrink-wrap so `align` dictates their exact position within the outer table
        innerTableWidth = '';
        innerTableStyle = '';
    }
    const innerTable = `<table${innerTableWidth} align="${tableAlign}" cellpadding="0" cellspacing="0" border="0" role="presentation"${innerTableStyle ? ` style="${innerTableStyle}"` : ''}>${innerHtml}</table>`;
    let paddedValign = 'top';
    if (isVertical) {
        if (primaryAlign === 'CENTER')
            paddedValign = 'middle';
        if (primaryAlign === 'MAX')
            paddedValign = 'bottom';
    }
    let paddedContent = innerTable;
    if (hasPadding) {
        paddedContent = `<td valign="${paddedValign}" align="${tableAlign}" style="padding:${padTop}px ${padRight}px ${padBottom}px ${padLeft}px;">${innerTable}</td>`;
    }
    else {
        paddedContent = `<td valign="${paddedValign}" align="${tableAlign}">${innerTable}</td>`;
    }
    let widthAttr = '';
    if (isRoot) {
        widthAttr = ` width="${w}"`;
    }
    else if (sizingW === 'FILL') {
        widthAttr = ' width="100%"';
    }
    else if (sizingW === 'HUG') {
        // Let HUG containers shrink-wrap to their content
        widthAttr = '';
    }
    else {
        // FIXED width gets explicitly sized widths mapping Figma specs to trap flexbox inflation
        widthAttr = ` width="${w}"`;
    }
    // Also enforce height inline so web fonts don't blow up container height
    let enforcedHeightStyle = tableStyle;
    if (!isRoot && sizingH === 'FIXED') {
        enforcedHeightStyle += `height:${Math.round(frame.height)}px;`;
    }
    const outerTable = `<table${widthAttr} cellpadding="0" cellspacing="0" border="0" role="presentation" style="${enforcedHeightStyle}"><tr>${paddedContent}</tr></table>`;
    if (isRoot) {
        return `<table width="100%" cellpadding="0" cellspacing="0" border="0" role="presentation" style="margin:0;padding:0;${bg ? `background-color:${bg};` : ''}"><tr><td align="center" valign="top">${outerTable}</td></tr></table>`;
    }
    return outerTable;
}
// --------------- Message Handler ---------------
figma.on('selectionchange', () => {
    figma.ui.postMessage({ type: 'selectionchange' });
});
figma.ui.onmessage = async (msg) => {
    if (msg.type === 'generate-html') {
        const selection = figma.currentPage.selection;
        if (selection.length !== 1) {
            figma.ui.postMessage({ type: 'error', message: 'Please select exactly one frame.' });
            return;
        }
        const node = selection[0];
        if (!isAutoLayoutFrame(node)) {
            figma.ui.postMessage({ type: 'error', message: 'Please select an Auto Layout frame. Regular frames or groups are not supported as the root design.' });
            return;
        }
        // Reset image counter
        imageCounter = 0;
        imageAssets.length = 0;
        figma.ui.postMessage({ type: 'status', message: 'Compiling…' });
        try {
            const bodyHtml = await compileNode(node, true);
            figma.ui.postMessage({
                type: 'html-ready',
                html: bodyHtml,
                images: imageAssets,
                frameName: node.name,
            });
        }
        catch (err) {
            figma.ui.postMessage({
                type: 'error',
                message: `Compilation error: ${err.message || err}`,
            });
        }
    }
    else if (msg.type === 'set-api-token') {
        await figma.clientStorage.setAsync('api_token', msg.token);
    }
    else if (msg.type === 'clear-api-token') {
        await figma.clientStorage.deleteAsync('api_token');
    }
};
