/*
 * FlatRender — After Effects template SCANNER (read-only / ExtendScript).
 *
 * Walks an opened .aep project and emits its structure as JSON for the content
 * service to import (scenes, frl_/frd_ elements, frd_/frshare colours).
 *
 * Launch (headless), with the node agent setting env vars first:
 *     SET FR_SCAN_AEP=C:\work\templates\cache\<md5>\extracted\proj\template.aep
 *     SET FR_SCAN_OUT=C:\work\scans\<job>\scan.json
 *     "...\Adobe After Effects <ver>\Support Files\afterfx.exe" -r "...\scan.jsx"
 *
 * Conventions (mirrors the legacy NewBrain/JSX binder, in reverse):
 *   comp "frfinal"  → final render comp (recorded, not a scene)
 *   comp "frshare"  → shared colours: each layer name = key, its "Source Text" = colour value
 *   layer  frl_*    → editable element: TextLayer→Text (font/size/justify/text), else Media/Audio
 *   layer  frd_*    → data/direction layer; if its text looks like a colour → per-scene colour zone
 *   any comp containing frl_/frd_ layers → a scene (key = comp name, duration = comp.duration)
 *
 * Output shape == content-svc ScanResult:
 *   { source, render_comp, scenes:[{key,title,scene_type,default_duration_sec,sort,
 *       elements:[{key,title,type,default_value,font_face,font_size,justify,width,height,video_support,is_hidden,sort}],
 *       colors:[{element_key,title,attr_value,default_color,sort}] }],
 *     shared_colors:[{element_key,title,attr_value,default_color,sort}] }
 */
(function () {
    function getenv(name) { try { return $.getenv(name); } catch (e) { return null; } }

    var aepPath = getenv("FR_SCAN_AEP");
    var outPath = getenv("FR_SCAN_OUT") || (Folder.temp.fsName + "/fr_scan.json");
    var mode = String(getenv("FR_SCAN_MODE") || "flexible").toLowerCase(); // fix | flexible | mockup | musicvisualizer

    // ── minimal JSON serializer (older AE has no JSON.stringify) ──────────────
    function esc(s) {
        s = String(s); var out = "", i, c;
        for (i = 0; i < s.length; i++) {
            c = s.charAt(i);
            if (c === '"') out += '\\"';
            else if (c === '\\') out += '\\\\';
            else if (c === '\n') out += '\\n';
            else if (c === '\r') out += '\\r';
            else if (c === '\t') out += '\\t';
            else if (c < ' ') out += '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
            else out += c;
        }
        return out;
    }
    function jstr(v) {
        if (v === null || v === undefined) return "null";
        var t = typeof v;
        if (t === "number") return isFinite(v) ? String(v) : "null";
        if (t === "boolean") return v ? "true" : "false";
        if (t === "string") return '"' + esc(v) + '"';
        if (v instanceof Array) {
            var a = [], i; for (i = 0; i < v.length; i++) a.push(jstr(v[i]));
            return "[" + a.join(",") + "]";
        }
        var props = [], k;
        for (k in v) { if (v.hasOwnProperty(k)) props.push('"' + esc(k) + '":' + jstr(v[k])); }
        return "{" + props.join(",") + "}";
    }

    // ── colour helpers ────────────────────────────────────────────────────────
    function trim(s) { return String(s).replace(/^\s+|\s+$/g, ""); }
    function isColor(s) {
        if (!s) return false; s = trim(s);
        // hex, RGB (3 numbers) or RGBA (4 numbers — the FIX/frshare format, e.g. 253,226,228,255)
        return /^#?[0-9a-fA-F]{6}$/.test(s) || /^\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(\s*,\s*\d{1,3})?$/.test(s);
    }
    function normColor(s) {
        s = trim(s);
        if (/^[0-9a-fA-F]{6}$/.test(s)) return "#" + s;               // bare hex → add #
        var m = s.match(/^(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*\d{1,3})?$/); // r,g,b[,a] → #hex (alpha dropped)
        if (m) {
            function h(n) { n = Math.max(0, Math.min(255, parseInt(n, 10))); var x = n.toString(16); return x.length === 1 ? "0" + x : x; }
            return "#" + h(m[1]) + h(m[2]) + h(m[3]);
        }
        return s;
    }
    // A frshare control layer holds a single integer (e.g. 0-3) that an expression
    // reads to switch a design variant (e.g. logo = image vs fill-colour overlay).
    function isControl(s) { return /^\s*\d+\s*$/.test(String(s)); }

    function justifyName(j) {
        try {
            if (j === ParagraphJustification.LEFT_JUSTIFY) return "LEFT_JUSTIFY";
            if (j === ParagraphJustification.RIGHT_JUSTIFY) return "RIGHT_JUSTIFY";
            if (j === ParagraphJustification.FULL_JUSTIFY) return "FULL_JUSTIFY";
        } catch (e) {}
        return "CENTER_JUSTIFY";
    }

    function readText(layer) {
        try {
            var st = layer.property("Source Text");
            if (!st) return null;
            var td = st.value; // TextDocument
            return { text: td.text, font: td.font, fontSize: Math.round(td.fontSize), justify: justifyName(td.justification) };
        } catch (e) { return null; }
    }

    function pre3(name) { return String(name).substring(0, 3); }

    function scanScene(comp) {
        var elements = [], colors = [];
        for (var i = 1; i <= comp.numLayers; i++) {
            var layer = comp.layer(i), name = layer.name, p = pre3(name);
            if (p === "frl") {
                var el = { key: name, title: name, sort: i };
                var txt = readText(layer);
                if (txt) {
                    el.type = "Text"; el.default_value = txt.text;
                    el.font_face = txt.font; el.font_size = txt.fontSize; el.justify = txt.justify;
                } else {
                    var src = null; try { src = layer.source; } catch (e) {}
                    if (src && src.hasVideo === false && src.hasAudio === true) {
                        el.type = "Audio";
                    } else {
                        el.type = "Media";
                        if (src) { try { el.width = src.width; el.height = src.height; } catch (e) {} }
                        try { el.video_support = !!(src && src.hasVideo); } catch (e) {}
                    }
                }
                elements.push(el);
            } else if (p === "frd") {
                var t2 = readText(layer);
                if (t2 && isColor(t2.text)) {
                    colors.push({ element_key: name, title: name, attr_value: "fill", default_color: normColor(t2.text), sort: i });
                } else {
                    var del = { key: name, title: name, type: "Text", is_hidden: true, sort: i };
                    if (t2) { del.default_value = t2.text; del.font_face = t2.font; del.font_size = t2.fontSize; }
                    elements.push(del);
                }
            }
        }
        return { elements: elements, colors: colors };
    }

    function compHasEditable(comp) {
        for (var k = 1; k <= comp.numLayers; k++) {
            var pp = pre3(comp.layer(k).name);
            if (pp === "frl" || pp === "frd") return true;
        }
        return false;
    }

    // ── frshare: shared colours (RGBA) + shared controls (single 0-3 number) ────
    function readShared(proj) {
        var colors = [], controls = [];
        for (var i = 1; i <= proj.numItems; i++) {
            var item = proj.item(i);
            if (!(item instanceof CompItem) || item.name !== "frshare") continue;
            for (var j = 1; j <= item.numLayers; j++) {
                var cl = item.layer(j), ct = readText(cl);
                if (!ct) continue;
                if (isColor(ct.text)) {
                    colors.push({ element_key: cl.name, title: cl.name, attr_value: "fill", default_color: normColor(ct.text), sort: j });
                } else if (isControl(ct.text)) {
                    controls.push({ element_key: cl.name, title: cl.name, default_value: trim(ct.text), min: 0, max: 3, sort: j });
                }
            }
        }
        return { colors: colors, controls: controls };
    }

    // ── FLEXIBLE / Mockup — each scene IS a comp; layers frl_/frd_ inside ───────
    function scanFlexible(proj) {
        var sh = readShared(proj);
        var result = { source: "ae-jsx", render_comp: null, scenes: [], shared_colors: sh.colors, shared_controls: sh.controls };
        for (var i = 1; i <= proj.numItems; i++) {
            var item = proj.item(i);
            if (!(item instanceof CompItem)) continue;
            var nm = item.name;
            if (nm === "frfinal" || nm === "flatrender") { result.render_comp = nm; continue; }
            if (nm === "frshare") continue;
            if (!compHasEditable(item)) continue;
            var s = scanScene(item);
            result.scenes.push({
                key: nm, title: nm, scene_type: "Normal",
                default_duration_sec: Math.round(item.duration * 100) / 100,
                sort: result.scenes.length, elements: s.elements, colors: s.colors
            });
        }
        return result;
    }

    // ── FIX / MusicVisualizer — scenes encoded in layer names frl_c(x)t/m(y) ────
    function scanFix(proj) {
        var sh = readShared(proj);
        var result = { source: "ae-jsx", render_comp: "frfinal", scenes: [], shared_colors: sh.colors, shared_controls: sh.controls };
        var sceneMap = {}, order = [];
        var re = /^frl_c(\d+)([tm])(\d+)$/;
        for (var i = 1; i <= proj.numItems; i++) {
            var item = proj.item(i);
            if (!(item instanceof CompItem) || item.name === "frshare") continue;
            for (var l = 1; l <= item.numLayers; l++) {
                var layer = item.layer(l), m = String(layer.name).match(re);
                if (!m) continue;
                var sc = parseInt(m[1], 10), typ = m[2], idx = parseInt(m[3], 10);
                if (!sceneMap[sc]) {
                    sceneMap[sc] = { key: "c" + sc, title: "Scene " + sc, scene_type: "Normal", sort: sc, elements: [], colors: [] };
                    order.push(sc);
                }
                var el = { key: layer.name, title: layer.name, sort: idx };
                if (typ === "t") {
                    var txt = readText(layer);
                    el.type = "Text";
                    if (txt) { el.default_value = txt.text; el.font_face = txt.font; el.font_size = txt.fontSize; el.justify = txt.justify; }
                } else {
                    el.type = "Media";
                    var src = null; try { src = layer.source; } catch (e) {}
                    if (src) { try { el.width = src.width; el.height = src.height; } catch (e) {} try { el.video_support = !!src.hasVideo; } catch (e) {} }
                }
                sceneMap[sc].elements.push(el);
            }
        }
        order.sort(function (a, b) { return a - b; });
        for (var k = 0; k < order.length; k++) { var s = sceneMap[order[k]]; s.sort = k; result.scenes.push(s); }
        return result;
    }

    function run() {
        // Silence ALL AE dialogs (missing fonts/footage, version prompts, alerts) so a
        // real project never hangs the headless scan waiting for a click.
        try { app.beginSuppressDialogs(); } catch (e) {}
        try { app.preferences.savePrefAsLong("Misc Section", "Play sound when render finishes", 0, PREFType.PREF_Type_MACHINE_INDEPENDENT); } catch (e) {}
        // AE is usually launched WITH the project as an argument (so the Home screen
        // can't block us); only open it ourselves if nothing is loaded yet.
        var hasProject = false;
        try { hasProject = !!(app.project && app.project.file); } catch (e) {}
        if (!hasProject && aepPath) {
            try { app.open(new File(aepPath)); } catch (e) {}
        }
        var proj = app.project;

        var result = (mode === "fix" || mode === "musicvisualizer") ? scanFix(proj) : scanFlexible(proj);

        var out = new File(outPath);
        out.encoding = "UTF-8";
        out.open("w");
        out.write(jstr(result));
        out.close();
    }

    try {
        run();
    } catch (e) {
        try { var ef = new File(outPath + ".error"); ef.open("w"); ef.write("scan error: " + e.toString()); ef.close(); } catch (e2) {}
    }
    try { app.endSuppressDialogs(false); } catch (e) {}
    // Quit AE without a save prompt so the headless afterfx process returns
    // (set FR_SCAN_QUIT=0 to keep it open while debugging).
    try {
        if (getenv("FR_SCAN_QUIT") !== "0") {
            try { app.project.close(CloseOptions.DO_NOT_SAVE_CHANGES); } catch (e2) {}
            app.quit();
        }
    } catch (e) {}
})();
