// IDexport.jsx — runs inside Adobe InDesign
// Exports the active document as idconvert_export.json
// Upload the resulting JSON to IDconvert to generate an editable Word document.
// Version: 1.0

#target indesign

// ── btoa polyfill (ExtendScript / ES3 does not have btoa built-in) ───────────

var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';

function btoa(input) {
    var output = '';
    var i = 0;
    var len = input.length;
    while (i < len) {
        var b0 = input.charCodeAt(i++) & 0xff;
        var b1 = (i < len) ? (input.charCodeAt(i++) & 0xff) : 0;
        var b2 = (i < len) ? (input.charCodeAt(i++) & 0xff) : 0;
        output += _b64chars.charAt(b0 >> 2);
        output += _b64chars.charAt(((b0 & 3) << 4) | (b1 >> 4));
        output += _b64chars.charAt(((b1 & 15) << 2) | (b2 >> 6));
        output += _b64chars.charAt(b2 & 63);
    }
    var pad = len % 3;
    if (pad === 1) output = output.slice(0, -2) + '==';
    else if (pad === 2) output = output.slice(0, -1) + '=';
    return output;
}

// ── JSON polyfill (ExtendScript / ES3 does not have JSON built-in) ────────────

var JSON = (function () {
    function stringify(val) {
        if (val === null) return 'null';
        if (val === undefined) return 'null';
        var t = typeof val;
        if (t === 'boolean') return val ? 'true' : 'false';
        if (t === 'number') {
            if (!isFinite(val)) return 'null';
            return String(val);
        }
        if (t === 'string') {
            return '"' + val
                .replace(/\\/g, '\\\\')
                .replace(/"/g, '\\"')
                .replace(/\n/g, '\\n')
                .replace(/\r/g, '\\r')
                .replace(/\t/g, '\\t') + '"';
        }
        if (t === 'object') {
            if (val instanceof Array) {
                var items = [];
                for (var i = 0; i < val.length; i++) {
                    items.push(stringify(val[i]));
                }
                return '[' + items.join(',') + ']';
            }
            var pairs = [];
            for (var k in val) {
                if (val.hasOwnProperty(k)) {
                    var v = stringify(val[k]);
                    if (v !== undefined) {
                        pairs.push('"' + k + '":' + v);
                    }
                }
            }
            return '{' + pairs.join(',') + '}';
        }
        return 'null';
    }
    return { stringify: stringify };
}());

function exportDocument() {
    if (app.documents.length === 0) {
        alert('No document is open. Please open an InDesign document first.');
        return;
    }

    var doc = app.activeDocument;

    if (!doc.saved) {
        alert('Please save your document before exporting.');
        return;
    }

    // Force all scripting measurements to points for the duration of this export.
    // page.bounds and geometricBounds respect app.scriptPreferences.measurementUnit,
    // so without this the values would be in whatever the document ruler is set to
    // (inches, mm, picas, etc.) and page dimensions in Word would be wrong.
    var originalUnit = app.scriptPreferences.measurementUnit;
    app.scriptPreferences.measurementUnit = MeasurementUnits.POINTS;

    var output;
    try {
        output = {
            version: '1.0',
            generator: 'IDexport',
            document: extractDocumentInfo(doc),
            styles: extractStyles(doc),
            colors: extractColors(doc),
            fonts_used: extractFonts(doc),
            pages: extractPages(doc)
        };
    } finally {
        // Always restore the original unit even if an error occurs
        app.scriptPreferences.measurementUnit = originalUnit;
    }

    var exportPath = doc.filePath + '/idconvert_export.json';
    var file = new File(exportPath);
    file.encoding = 'UTF-8';
    file.open('w');
    file.write(JSON.stringify(output));
    file.close();

    alert('IDexport complete.\nFile saved to:\n' + exportPath +
          '\n\nUpload idconvert_export.json to IDconvert to generate your Word document.');
}

// ── Document info ─────────────────────────────────────────────────────────────

function extractDocumentInfo(doc) {
    var page = doc.pages[0];
    var bounds = page.bounds; // [top, left, bottom, right]
    return {
        name: doc.name.replace('.indd', ''),
        page_width_pt:  bounds[3] - bounds[1],
        page_height_pt: bounds[2] - bounds[0],
        page_count: doc.pages.length,
        facing_pages: doc.documentPreferences.facingPages
    };
}

// ── Styles ────────────────────────────────────────────────────────────────────

function extractStyles(doc) {
    var paragraphStyles = [];
    for (var i = 0; i < doc.paragraphStyles.length; i++) {
        var s = doc.paragraphStyles[i];
        if (s.name === '[No Paragraph Style]' || s.name === '[Basic Paragraph]') continue;
        try {
            paragraphStyles.push({
                name: s.name,
                font: safeGet(s, 'appliedFont') ? s.appliedFont.name : null,
                size_pt: safeGet(s, 'pointSize', null),
                leading_pt: safeGet(s, 'leading', null),
                space_before_pt: safeGet(s, 'spaceBefore', 0),
                space_after_pt: safeGet(s, 'spaceAfter', 0),
                alignment: alignmentToString(safeGet(s, 'justification', null)),
                color_hex: resolveColor(safeGet(s, 'fillColor', null)),
                bold: safeGet(s, 'fontStyle', '').toLowerCase().indexOf('bold') >= 0,
                italic: safeGet(s, 'fontStyle', '').toLowerCase().indexOf('italic') >= 0
            });
        } catch(e) {}
    }

    var characterStyles = [];
    for (var j = 0; j < doc.characterStyles.length; j++) {
        var cs = doc.characterStyles[j];
        if (cs.name === '[No Character Style]') continue;
        try {
            characterStyles.push({
                name: cs.name,
                font: safeGet(cs, 'appliedFont') ? cs.appliedFont.name : null,
                bold: safeGet(cs, 'fontStyle', '').toLowerCase().indexOf('bold') >= 0,
                italic: safeGet(cs, 'fontStyle', '').toLowerCase().indexOf('italic') >= 0,
                color_hex: resolveColor(safeGet(cs, 'fillColor', null))
            });
        } catch(e) {}
    }

    return { paragraph: paragraphStyles, character: characterStyles };
}

// ── Colors ────────────────────────────────────────────────────────────────────

function extractColors(doc) {
    var colors = {};
    for (var i = 0; i < doc.colors.length; i++) {
        var c = doc.colors[i];
        try { colors[c.name] = colorToHex(c); } catch(e) {}
    }
    return colors;
}

// ── Fonts ─────────────────────────────────────────────────────────────────────

function extractFonts(doc) {
    var fonts = [];
    var seen = {};
    for (var i = 0; i < doc.fonts.length; i++) {
        var f = doc.fonts[i];
        if (!seen[f.name]) {
            seen[f.name] = true;
            fonts.push({
                name: f.name,
                postscript: safeGet(f, 'postscriptName', f.name)
            });
        }
    }
    return fonts;
}

// ── Pages ─────────────────────────────────────────────────────────────────────

function extractPages(doc) {
    var pages = [];
    for (var i = 0; i < doc.pages.length; i++) {
        var page = doc.pages[i];
        var bounds = page.bounds;
        pages.push({
            page_number: page.name,
            width_pt:  bounds[3] - bounds[1],
            height_pt: bounds[2] - bounds[0],
            items: extractPageItems(page, doc)
        });
    }
    return pages;
}

function extractPageItems(page, doc) {
    var items = [];
    var allItems = page.allPageItems;

    for (var i = 0; i < allItems.length; i++) {
        var item = allItems[i];
        try {
            var extracted = extractItem(item, page, doc);
            if (extracted) items.push(extracted);
        } catch(e) {}
    }

    // Sort top-to-bottom, left-to-right (reading order)
    items.sort(function(a, b) {
        if (Math.abs(a.y_pt - b.y_pt) < 5) return a.x_pt - b.x_pt;
        return a.y_pt - b.y_pt;
    });

    return items;
}

function extractItem(item, page, doc) {
    var bounds = item.geometricBounds; // [top, left, bottom, right] in spread coords
    var pageBounds = page.bounds;       // [top, left, bottom, right] of page
    var x = bounds[1] - pageBounds[1];
    var y = bounds[0] - pageBounds[0];
    var width  = bounds[3] - bounds[1];
    var height = bounds[2] - bounds[0];

    var typeName = item.constructor.name;

    if (typeName === 'TextFrame') {
        return extractTextFrame(item, x, y, width, height);
    }

    if (typeName === 'Rectangle' || typeName === 'Oval' || typeName === 'GraphicFrame') {
        if (item.graphics && item.graphics.length > 0) {
            return extractImageFrame(item, x, y, width, height);
        }
        return extractShapeFrame(item, x, y, width, height);
    }

    return null;
}

// ── Text frames ───────────────────────────────────────────────────────────────

function extractTextFrame(frame, x, y, width, height) {
    if (hasAutoPageNumber(frame)) {
        return extractPageNumber(frame, x, y);
    }

    var paragraphs = [];
    try {
        // Use frame.texts[0].paragraphs — only the text visible in THIS frame,
        // not frame.parentStory.paragraphs which returns the entire threaded story.
        var frameText = frame.texts[0];
        for (var i = 0; i < frameText.paragraphs.length; i++) {
            var para = frameText.paragraphs[i];
            paragraphs.push(extractParagraph(para));
        }
    } catch(e) {}

    var threadId = '';
    var threadPos = 1;
    try {
        threadId = frame.parentStory.id.toString();
        var prev = frame.previousTextFrame;
        while (prev !== null) {
            threadPos++;
            prev = prev.previousTextFrame;
        }
    } catch(e) {}

    return {
        type: 'text_frame',
        id: frame.id.toString(),
        thread_id: threadId,
        thread_position: threadPos,
        x_pt: x,
        y_pt: y,
        width_pt: width,
        height_pt: height,
        column_count: safeGet(frame.textFramePreferences, 'textColumnCount', 1),
        paragraphs: paragraphs
    };
}

function extractParagraph(para) {
    var styleName = '[No Paragraph Style]';
    try { styleName = para.appliedParagraphStyle.name; } catch(e) {}

    var runs = [];
    var characters = para.characters;
    for (var i = 0; i < characters.length; i++) {
        var run = buildRun(characters[i]);
        if (runs.length > 0 && runsMatch(runs[runs.length - 1], run)) {
            runs[runs.length - 1].text += run.text;
        } else {
            runs.push(run);
        }
    }

    return {
        style: styleName,
        text: para.contents,
        runs: runs
    };
}

function buildRun(ch) {
    var styleName = null;
    try {
        if (ch.appliedCharacterStyle.name !== '[No Character Style]') {
            styleName = ch.appliedCharacterStyle.name;
        }
    } catch(e) {}

    var fontStyle = safeGet(ch, 'fontStyle', '').toLowerCase();
    return {
        text: ch.contents,
        style: styleName,
        bold: fontStyle.indexOf('bold') >= 0,
        italic: fontStyle.indexOf('italic') >= 0,
        color_hex: resolveColor(safeGet(ch, 'fillColor', null)),
        font: safeGet(ch, 'appliedFont') ? ch.appliedFont.name : null,
        size_pt: safeGet(ch, 'pointSize', null)
    };
}

function runsMatch(a, b) {
    return a.style === b.style &&
           a.bold === b.bold &&
           a.italic === b.italic &&
           a.color_hex === b.color_hex &&
           a.font === b.font &&
           a.size_pt === b.size_pt;
}

// ── Images ────────────────────────────────────────────────────────────────────

function extractImageFrame(frame, x, y, width, height) {
    var imageData = null;
    var imageName = 'image';
    try {
        var graphic = frame.graphics[0];
        imageName = graphic.itemLink.name;
        var tempFile = new File(Folder.temp + '/idconvert_temp.jpg');
        frame.exportFile(ExportFormat.JPG, tempFile, false);
        imageData = fileToBase64(tempFile);
        tempFile.remove();
    } catch(e) {}

    return {
        type: 'image_frame',
        id: frame.id.toString(),
        x_pt: x, y_pt: y,
        width_pt: width, height_pt: height,
        image_name: imageName,
        image_data_b64: imageData,
        fit_mode: 'fill_proportionally'
    };
}

// ── Shapes ────────────────────────────────────────────────────────────────────

function extractShapeFrame(frame, x, y, width, height) {
    var fillColor = null;
    try {
        if (frame.fillColor.name !== 'None' && frame.fillColor.name !== '[None]') {
            fillColor = colorToHex(frame.fillColor);
        }
    } catch(e) {}

    if (!fillColor) return null;

    return {
        type: 'shape',
        id: frame.id.toString(),
        x_pt: x, y_pt: y,
        width_pt: width, height_pt: height,
        fill_hex: fillColor
    };
}

// ── Page numbers ──────────────────────────────────────────────────────────────

function extractPageNumber(frame, x, y) {
    var run = null;
    try { run = frame.parentStory.characters[0]; } catch(e) {}

    return {
        type: 'page_number',
        id: frame.id.toString(),
        x_pt: x, y_pt: y,
        font: run ? (run.appliedFont ? run.appliedFont.name : null) : null,
        size_pt: run ? safeGet(run, 'pointSize', 9) : 9,
        color_hex: run ? resolveColor(safeGet(run, 'fillColor', null)) : '000000',
        alignment: run ? alignmentToString(safeGet(run, 'justification', null)) : 'center'
    };
}

function hasAutoPageNumber(frame) {
    try {
        var characters = frame.parentStory.texts[0].characters;
        for (var i = 0; i < characters.length; i++) {
            if (characters[i].contents === SpecialCharacters.AUTO_PAGE_NUMBER) return true;
        }
    } catch(e) {}
    return false;
}

// ── Utilities ─────────────────────────────────────────────────────────────────

function resolveColor(colorObj) {
    if (!colorObj) return null;
    try {
        if (colorObj.name === 'None' || colorObj.name === '[None]') return null;
        return colorToHex(colorObj);
    } catch(e) { return null; }
}

function colorToHex(colorObj) {
    try {
        var vals = colorObj.colorValue;
        if (colorObj.space === ColorSpace.CMYK) {
            var c = vals[0]/100, m = vals[1]/100, y = vals[2]/100, k = vals[3]/100;
            return toHex(Math.round(255*(1-c)*(1-k))) +
                   toHex(Math.round(255*(1-m)*(1-k))) +
                   toHex(Math.round(255*(1-y)*(1-k)));
        }
        if (colorObj.space === ColorSpace.RGB) {
            return toHex(vals[0]) + toHex(vals[1]) + toHex(vals[2]);
        }
    } catch(e) {}
    return '000000';
}

function toHex(n) {
    var h = Math.max(0, Math.min(255, Math.round(n))).toString(16);
    return h.length === 1 ? '0' + h : h;
}

function alignmentToString(justification) {
    if (!justification) return 'left';
    var map = {
        1: 'left', 2: 'center', 3: 'right', 4: 'justify',
        1514227313: 'left', 1514731619: 'center',
        1514731618: 'right', 1514599026: 'justify'
    };
    return map[justification] || 'left';
}

function fileToBase64(file) {
    file.open('r');
    file.encoding = 'BINARY';
    var content = file.read();
    file.close();
    return btoa(content);
}

function safeGet(obj, prop, fallback) {
    try {
        var val = obj[prop];
        return (val !== undefined && val !== null) ? val : fallback;
    } catch(e) {
        return (fallback !== undefined) ? fallback : null;
    }
}

// ── Run ───────────────────────────────────────────────────────────────────────

exportDocument();
