/* host.jsx — runs inside Premiere Pro's ExtendScript engine.
 * Exposes MotionVault.* functions invoked from the panel via CSInterface.evalScript.
 *
 * Returns JSON-serialized strings (panel parses them). All errors come back as
 * { "error": "..." } so the JS side can show them as toasts.
 */
$.global.MotionVault = (function () {

    function ok(o)  { return JSON.stringify(o == null ? { ok: true } : o); }
    function err(m) { return JSON.stringify({ error: String(m) }); }

    function ping() {
        try {
            return ok({
                ok: true,
                host: (app && app.name) || "?",
                version: (app && app.version) || "?",
                hasProject: !!(app && app.project),
                hasSequence: !!(app && app.project && app.project.activeSequence)
            });
        } catch (e) { return err(e.message); }
    }

    function getActiveSequenceInfo() {
        try {
            if (!app || !app.project) return ok({ hasSequence: false, reason: "no_project" });
            var seq = app.project.activeSequence;
            if (!seq) return ok({ hasSequence: false, reason: "no_sequence" });
            return ok({
                hasSequence: true,
                name: seq.name,
                videoTracks: seq.videoTracks.numTracks,
                audioTracks: seq.audioTracks.numTracks,
                cti: seq.getPlayerPosition().seconds
            });
        } catch (e) { return err(e.message); }
    }

    /* Pick the best video track for inserting the .mogrt.
     * Priority:
     *   1. The first video track that is empty at the playhead, starting from V1.
     *   2. If all are occupied, return -1 (host will keep V1, Premiere stacks above).
     */
    function pickTargetTrack(seq, ticks) {
        try {
            var t = Number(ticks);
            for (var i = 0; i < seq.videoTracks.numTracks; i++) {
                var tr = seq.videoTracks[i];
                if (tr.isLocked && tr.isLocked()) continue;
                if (!isOccupiedAt(tr, t)) return i;
            }
        } catch (e) {}
        return -1;
    }

    function isOccupiedAt(track, t) {
        try {
            for (var i = 0; i < track.clips.numItems; i++) {
                var c = track.clips[i];
                var s = Number(c.start.ticks);
                var e = Number(c.end.ticks);
                if (t >= s && t < e) return true;
            }
            return false;
        } catch (e) { return false; }
    }

    /* Primeiro audio track livre no playhead (analogo a pickTargetTrack). */
    function pickFreeAudioTrack(seq, ticks) {
        try {
            var t = Number(ticks);
            for (var i = 0; i < seq.audioTracks.numTracks; i++) {
                var tr = seq.audioTracks[i];
                if (tr.isLocked && tr.isLocked()) continue;
                if (!isOccupiedAt(tr, t)) return i;
            }
        } catch (e) {}
        return -1;
    }

    /* Normaliza path Windows pra comparação case-insensitive. */
    function normPath(p) {
        return String(p || "").toLowerCase().replace(/\//g, "\\");
    }

    /* Item é "file-like" (clip importável), NÃO um bin/folder.
     * ProjectItemType: 0=root, 1=bin, 2=clip, 3=file, 4=sequence. */
    function isFileLikeItem(it) {
        try {
            var t = Number(it.type);
            if (t === 1 || t === 0) return false; // bin ou root
            // Sequências (type 4) também não importam pra audio overwrite
            if (t === 4) return false;
            return true;
        } catch (e) {
            // Sem .type? Olha se tem children — se tem, é bin
            try {
                if (it.children && it.children.numItems > 0) return false;
            } catch (e2) {}
            return true;
        }
    }

    /* Chave estável de um projectItem pra diff antes/depois da importação.
     * nodeId é o ideal (único e estável); treePath é fallback; nome+tipo é último recurso. */
    function getItemKey(it) {
        try { var n = it.nodeId; if (n) return "n:" + n; } catch (e) {}
        try { var tp = it.treePath; if (tp) return "p:" + tp; } catch (e) {}
        var nm = ""; try { nm = String(it.name || ""); } catch (e) {}
        var tt = "?"; try { tt = String(it.type); } catch (e) {}
        return "k:" + nm + ":" + tt;
    }

    /* Path do mídia file (pra match exato após import). */
    function getMediaPathSafe(it) {
        try { if (typeof it.getMediaPath === "function") return String(it.getMediaPath() || ""); } catch (e) {}
        return "";
    }

    /* Coleta TODOS os items recursivamente em um array flat. */
    function collectAllItems(bin, out) {
        try {
            for (var i = 0; i < bin.children.numItems; i++) {
                var it = bin.children[i];
                out.push(it);
                var hasKids = false;
                try { hasKids = it.children && it.children.numItems > 0; } catch (e) {}
                if (hasKids) collectAllItems(it, out);
            }
        } catch (e) {}
    }

    /* Busca recursiva por nome, IGNORANDO bins (só file-like items).
     * Tolerante a extensão e a sufixo de duplicata. */
    function findItemByName(bin, name) {
        try {
            for (var i = 0; i < bin.children.numItems; i++) {
                var it = bin.children[i];
                var nm = String(it.name || "");
                var hasKids = false;
                try { hasKids = it.children && it.children.numItems > 0; } catch (e) {}
                if (hasKids) {
                    var found = findItemByName(it, name);
                    if (found) return found;
                    continue; // bin não é alvo
                }
                if (!isFileLikeItem(it)) continue;
                if (nm === name) return it;
                if (nm === name + ".wav" || nm === name + ".mp3" || nm === name + ".aac" || nm === name + ".m4a") return it;
                if (nm.indexOf(name) === 0) return it;
            }
        } catch (e) {}
        return null;
    }

    /**
     * Imports an audio file (.wav/.mp3) onto the first free audio track at CTI.
     * Sound FX da aba Motion FX. Returns { ok, audioTrack, name } ou { error }.
     *
     * Estratégia de localização (do mais confiável pro menos):
     *   1) Match por getMediaPath() — exato (case-insensitive Windows)
     *   2) Novo nodeId/treePath/key entre antes e depois (file-like preferido)
     *   3) Recursivo por nome (ignora bins)
     *   4) FALLBACK FINAL: qualquer item novo, sem filtro isFileLike
     */
    /* Executa pprsequencer.exe DENTRO do processo Premiere (in-process).
     *
     * CRÍTICO: precisa ser FIRE-AND-FORGET (assíncrono). Se rodarmos síncrono
     * com system.callSystem direto, o Premiere fica BLOQUEADO esperando o exe
     * terminar, mas o exe tenta enviar Ctrl+V pra janela do Premiere bloqueada
     * → DEADLOCK clássico.
     *
     * Solução: usar `cmd /c start "" /B "exe" "args"`:
     *   - `cmd /c` envolve o comando
     *   - `start "" /B` dispara em background sem nova janela e retorna IMEDIATAMENTE
     *   - O Premiere não fica bloqueado → pprsequencer consegue focar a janela
     *     e fazer SendInput Ctrl+V
     *
     * Vantagem vs child_process.spawn JS-side:
     *   - O processo filho é gerado dentro do contexto do Premiere → herda
     *     permissão de foreground window pro SetForegroundWindow funcionar
     */
    function runPprSequencer(binPath, filePath, hash) {
        try {
            if (!binPath || !filePath) return err("Args inválidos pra runPprSequencer");
            var binFile = new File(binPath);
            if (!binFile.exists) return err("pprsequencer.exe não encontrado: " + binPath);
            var srcFile = new File(filePath);
            if (!srcFile.exists) return err("Arquivo não encontrado: " + filePath);

            // FIRE-AND-FORGET via `cmd /c start /B`:
            //   - aspas vazias depois de start = título da janela (obrigatório quando o
            //     próximo arg tem aspas)
            //   - /B = sem nova janela de console
            // Sintaxe final: cmd /c start "" /B "binPath" "filePath" "hash"
            var cmd = 'cmd /c start "" /B "' + binPath + '" "' + filePath + '" "' + (hash || "motionvault-2026") + '"';

            var t0 = (new Date()).getTime();
            try {
                system.callSystem(cmd);
            } catch (eC) {
                return err("system.callSystem throw: " + (eC.message || eC));
            }
            var dt = (new Date()).getTime() - t0;

            return ok({
                ok: true,
                applied: true,
                strategy: "system.callSystem fire-and-forget",
                cmd: cmd,
                elapsed_ms: dt
            });
        } catch (e) {
            return err("runPprSequencer: " + (e.message || e));
        }
    }

    function importAudio(audioPath, niceName) {
        var diag = [];
        function log(s) { diag.push(s); }
        try {
            if (!app)                      return err("Premiere Pro indisponível");
            if (!app.project)              return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq)                      return err("Abra uma sequência antes de importar");

            var f = new File(audioPath);
            if (!f.exists)                 return err("Arquivo não encontrado:\n" + audioPath);

            var base = f.name.replace(/\.[^.]+$/, "");
            var displayName = String(niceName || base);
            log("audioPath=" + audioPath);
            log("f.name=" + f.name + " base=" + base + " nice=" + displayName);

            var bin = app.project.rootItem;
            try { if (app.project.getInsertionBin) { var ib = app.project.getInsertionBin(); if (ib) bin = ib; } } catch (e) {}

            var allBefore = [];
            collectAllItems(app.project.rootItem, allBefore);
            var beforeKeys = {};
            for (var s = 0; s < allBefore.length; s++) beforeKeys[getItemKey(allBefore[s])] = true;
            log("allBefore=" + allBefore.length);

            app.project.importFiles([f.fsName], true, bin, false);

            var allAfter = [];
            collectAllItems(app.project.rootItem, allAfter);
            log("allAfter=" + allAfter.length);

            var item = null;
            var targetPath = normPath(f.fsName);

            // 1) Match por media path
            for (var i1 = 0; i1 < allAfter.length; i1++) {
                var it1 = allAfter[i1];
                if (!isFileLikeItem(it1)) continue;
                if (normPath(getMediaPathSafe(it1)) === targetPath) { item = it1; log("strategy1 mediaPath match"); break; }
            }

            // 2) Item novo (key diff) — file-like preferido, com preferência a nome casando
            var newOnes = [];
            if (!item) {
                for (var i2 = 0; i2 < allAfter.length; i2++) {
                    var it2 = allAfter[i2];
                    if (!beforeKeys[getItemKey(it2)]) newOnes.push(it2);
                }
                log("newOnes=" + newOnes.length);
                // Prioridade 1: file-like com nome casando
                for (var k = 0; k < newOnes.length; k++) {
                    if (!isFileLikeItem(newOnes[k])) continue;
                    var nm = String(newOnes[k].name || "");
                    if (nm === base || nm.indexOf(base) === 0 || nm === f.name) { item = newOnes[k]; log("strategy2 name match: " + nm); break; }
                }
                // Prioridade 2: qualquer file-like novo (último)
                if (!item) {
                    for (var k2 = newOnes.length - 1; k2 >= 0; k2--) {
                        if (isFileLikeItem(newOnes[k2])) { item = newOnes[k2]; log("strategy2 last file-like: " + String(item.name || "")); break; }
                    }
                }
            }

            // 3) Recursivo por nome
            if (!item) {
                item = findItemByName(app.project.rootItem, base);
                if (item) log("strategy3 findItemByName: " + String(item.name || ""));
            }

            // 4) FALLBACK FINAL: qualquer item NOVO mesmo se isFileLike falha
            //    (versões novas do PP podem retornar type undefined → isFileLike rejeita áudio válido)
            if (!item && newOnes.length > 0) {
                item = newOnes[newOnes.length - 1];
                log("strategy4 ANY new item (no filter): " + String(item.name || ""));
            }

            if (!item) return err("Áudio importado mas não localizado: " + displayName + " | diag: " + diag.join(" | "));

            // NÃO faz pre-check de bin — no Premiere 26.2+ TODOS projectItems
            // têm método createBin, então essa checagem rejeitava clips legítimos.
            // Em vez disso, tenta overwriteClip direto e captura o throw se for bin de verdade.

            var cti     = seq.getPlayerPosition();
            var aPick   = pickFreeAudioTrack(seq, cti.ticks);
            var aTarget = aPick >= 0 ? aPick : 0;
            var track   = seq.audioTracks[aTarget];
            log("targetTrack=A" + (aTarget + 1) + " cti=" + cti.seconds);

            try {
                track.overwriteClip(item, cti.seconds);
                log("overwriteClip OK");
            } catch (eOC) {
                return err("Falha ao inserir áudio na timeline: " + (eOC.message || eOC) + " | diag: " + diag.join(" | "));
            }

            // Renomeia o item importado com o nome bonito do catalog (Buttons 11 em vez de hash)
            // Apenas se niceName foi fornecido e diferente do nome atual
            if (niceName && niceName !== String(item.name || "")) {
                try { item.name = displayName; log("renamed → " + displayName); } catch (eRn) { log("rename failed: " + eRn.message); }
            }

            return ok({ ok: true, audioTrack: aTarget, name: displayName, pickedEmpty: aPick >= 0, diag: diag.join(" | ") });
        } catch (e) {
            return err((e && e.message) ? e.message : String(e));
        }
    }

    /* Helpers de project tree pra applyAtomxAsset */

    function collectAllProjectItems(bin, out) {
        try {
            for (var i = 0; i < bin.children.numItems; i++) {
                var it = bin.children[i];
                out.push(it);
                var hasKids = false;
                try { hasKids = it.children && it.children.numItems > 0; } catch (e) {}
                if (hasKids) collectAllProjectItems(it, out);
            }
        } catch (e) {}
    }

    function itemKey(it) {
        try { if (it.nodeId) return "n:" + it.nodeId; } catch (e) {}
        try { if (it.treePath) return "p:" + it.treePath; } catch (e) {}
        var nm = ""; try { nm = String(it.name || ""); } catch (e) {}
        var tt = "?"; try { tt = String(it.type); } catch (e) {}
        return "k:" + nm + ":" + tt;
    }

    function isSequenceItem(it) {
        try { if (it.isSequence && it.isSequence()) return true; } catch (e) {}
        try { if (Number(it.type) === 4) return true; } catch (e) {}
        return false;
    }

    /* Procura o clip no V1 da seq ativa que cobre o playhead. Retorna { start, end, dur } em segundos,
     * ou null se o playhead estiver vazio. */
    function clipAtPlayhead(seq) {
        try {
            var ctiTicks = Number(seq.getPlayerPosition().ticks);
            for (var i = 0; i < seq.videoTracks.numTracks; i++) {
                var tr = seq.videoTracks[i];
                for (var j = 0; j < tr.clips.numItems; j++) {
                    var c = tr.clips[j];
                    var s = Number(c.start.ticks);
                    var e = Number(c.end.ticks);
                    if (ctiTicks >= s && ctiTicks < e) {
                        return {
                            track: i,
                            startTicks: s, endTicks: e,
                            startSec: Number(c.start.seconds),
                            endSec:   Number(c.end.seconds),
                            dur:      Number(c.end.seconds) - Number(c.start.seconds)
                        };
                    }
                }
            }
        } catch (e) {}
        return null;
    }

    /* Garante que a sequência tem pelo menos N tracks de vídeo. PP cria sob demanda
     * só via addTracks() em algumas versões; fallback ignora se não suportar. */
    function ensureVideoTracks(seq, n) {
        try {
            while (seq.videoTracks.numTracks < n) {
                try { seq.videoTracks.addTracks(1); } catch (e) { return false; }
            }
            return seq.videoTracks.numTracks >= n;
        } catch (e) { return false; }
    }

    /* ═══════════════ TRANSITIONS BUILT-IN (estratégia revisada) ═══════════════
     *
     * Os .atomxasset de Transition referenciam efeitos AE (Circle, Paint Bucket,
     * Magnify, Grid) que NÃO existem no Premiere por padrão — importar o .prproj
     * causa popup "Filtro de vídeo ausente" e crash no render.
     *
     * Estratégia nova: mapear cada CATEGORIA de transition AtomX pra uma TRANSITION
     * BUILT-IN do Premiere (sempre presente, nunca crasha). Aplicação via QE DOM.
     */

    var BUILTIN_TRANSITION_BY_CATEGORY = {
        "Zoom":         "Cross Zoom",
        "Blur":         "Cross Dissolve",
        "Fade":         "Cross Dissolve",
        "3D Flip":      "Cube Spin",
        "Perspective":  "Cube Spin",
        "Glitch":       "Cross Dissolve",   // PP não tem glitch nativo
        "Pan":          "Push",
        "Shake":        "Cross Dissolve",
        "Shape":        "Iris Round",
        "Split":        "Split",
        "Stretch":      "Stretch In",
        "Light Leaks":  "Dip to White",
        "Cinematic":    "Dip to Black",
        "Bounce":       "Cross Dissolve",
        "Distortion":   "Cross Dissolve"
    };

    /* Aplica uma transição BUILT-IN do Premiere no V1, perto do CTI.
     *
     * Estratégia híbrida (mais robusta):
     *   - ExtendScript regular pra NAVEGAR os clips do V1 (confiável, sempre funciona)
     *   - QE DOM SÓ pra: pegar transition pelo nome + aplicar via addVideoTransition
     *
     * Lógica de decisão:
     *   - Pega o índice do clip do V1 cujo intervalo contém o CTI (ou mais próximo)
     *   - Se o próximo clip é adjacente → adiciona transition no END (entre eles)
     *   - Se o anterior é adjacente → no START
     *   - Se nenhum é adjacente → fade out no END (default seguro)
     */
    function applyBuiltInTransition(category, niceName) {
        var step = "init";
        var diag = [];
        try {
            if (!app || !app.project) return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq)                  return err("Abra uma sequência antes de aplicar");

            var transitionName = BUILTIN_TRANSITION_BY_CATEGORY[String(category)] || "Cross Dissolve";
            diag.push("category=" + category + " → built-in=" + transitionName);

            // ── 1) Navega clips do V1 via ExtendScript regular (CONFIÁVEL) ──
            step = "scan-v1";
            var v1 = seq.videoTracks[0];
            if (!v1) return err("Sequência sem V1");
            var nClips = v1.clips.numItems;
            diag.push("v1.clips.numItems=" + nClips);
            if (nClips === 0) return err("V1 está vazio");

            var ctiTicks = Number(seq.getPlayerPosition().ticks);
            var ctiSec   = Number(seq.getPlayerPosition().seconds);
            diag.push("cti=" + ctiSec.toFixed(3) + "s ticks=" + ctiTicks);

            // Coleta info dos clips
            var clips = [];
            for (var i = 0; i < nClips; i++) {
                var c = v1.clips[i];
                clips.push({
                    idx: i,
                    startTicks: Number(c.start.ticks),
                    endTicks:   Number(c.end.ticks),
                    startSec:   Number(c.start.seconds),
                    endSec:     Number(c.end.seconds),
                    name:       String(c.name || "")
                });
            }
            // log primeiros 3
            for (var li = 0; li < Math.min(clips.length, 3); li++) {
                diag.push("clip[" + li + "] " + clips[li].name + " " + clips[li].startSec.toFixed(2) + "→" + clips[li].endSec.toFixed(2));
            }

            // ── 2) Decide qual clip alvo + lado da transition ──
            step = "decide-target";
            // a) Tenta clip que CONTÉM o CTI
            var currentIdx = -1;
            for (var j = 0; j < clips.length; j++) {
                if (ctiTicks >= clips[j].startTicks && ctiTicks < clips[j].endTicks) { currentIdx = j; break; }
            }
            // b) CTI exatamente no start do primeiro clip
            if (currentIdx === -1 && clips.length > 0 && Math.abs(ctiTicks - clips[0].startTicks) < 5000) {
                currentIdx = 0;
            }
            // c) Mais próximo (CTI antes/depois de tudo)
            if (currentIdx === -1) {
                var bestDist = Infinity;
                for (var k = 0; k < clips.length; k++) {
                    var d = Math.min(Math.abs(clips[k].startTicks - ctiTicks), Math.abs(clips[k].endTicks - ctiTicks));
                    if (d < bestDist) { bestDist = d; currentIdx = k; }
                }
            }
            if (currentIdx < 0) return err("Não consegui determinar clip alvo no V1");
            diag.push("currentIdx=" + currentIdx + " (" + clips[currentIdx].name + ")");

            // Detecta vizinhos adjacentes (tolerância 1000 ticks = ~0.004ms)
            var TOL = 1000;
            var hasNext = (currentIdx + 1 < clips.length) && (clips[currentIdx + 1].startTicks - clips[currentIdx].endTicks <= TOL);
            var hasPrev = (currentIdx > 0) && (clips[currentIdx].startTicks - clips[currentIdx - 1].endTicks <= TOL);
            diag.push("hasNext=" + hasNext + " hasPrev=" + hasPrev);

            // Lógica de alinhamento
            //   hasNext → transition no END do clip atual (cobre o corte → next)
            //   hasPrev → transition no START do clip atual (vem do prev → cobre o corte)
            //   nenhum  → END como fade out (default seguro pra clip solo)
            var addAtEnd, applyIdx = currentIdx;
            if (hasNext)       addAtEnd = true;
            else if (hasPrev)  addAtEnd = false;
            else               addAtEnd = true;

            diag.push("addAtEnd=" + addAtEnd + " applyIdx=" + applyIdx);

            // ── 3) Habilita QE ──
            step = "enableQE";
            try { app.enableQE(); } catch (eq) { diag.push("enableQE throw: " + eq.message); }
            var qeSeq = null;
            try { qeSeq = qe.project.getActiveSequence(); } catch (eq2) { diag.push("qe.getActiveSeq throw: " + eq2.message); }
            if (!qeSeq) return err("QE DOM indisponível (qe.project.getActiveSequence retornou null)");

            // ── 4) Pega transition object pelo nome ──
            step = "find-transition";
            var transitionObj = null;
            try {
                if (qe.project.getVideoTransitionByName) transitionObj = qe.project.getVideoTransitionByName(transitionName);
                diag.push("byName('" + transitionName + "') → " + (transitionObj ? String(transitionObj.name || "found") : "null"));
            } catch (eFt) { diag.push("byName throw: " + eFt.message); }

            if (!transitionObj) {
                try {
                    transitionObj = qe.project.getVideoTransitionByName("Cross Dissolve");
                    diag.push("fallback Cross Dissolve → " + (transitionObj ? "found" : "null"));
                } catch (eDef) { diag.push("byName CD throw: " + eDef.message); }
            }
            if (!transitionObj) return err("Não achei nenhuma transição built-in (nem default Cross Dissolve)");

            // ── 5) Pega o clip QE pelo mesmo índice da regular API ──
            step = "qe-clip";
            var qeV1 = null;
            try { qeV1 = qeSeq.getVideoTrackAt(0); } catch (eQT) { diag.push("getVideoTrackAt throw: " + eQT.message); }
            if (!qeV1) return err("QE: V1 indisponível");

            var qeNumItems = -1;
            try { qeNumItems = qeV1.numItems; } catch (e) {}
            diag.push("qeV1.numItems=" + qeNumItems);

            var qeClip = null;
            try { qeClip = qeV1.getItemAt(applyIdx); } catch (eGI) { diag.push("getItemAt(" + applyIdx + ") throw: " + eGI.message); }

            // Se índice direto não bateu, varre o QE procurando clip não-vazio com start similar
            if (!qeClip || (function () { try { return String(qeClip.type) === "Empty"; } catch (e) { return false; } })()) {
                diag.push("qeClip direto inválido, varrendo...");
                for (var qi = 0; qi < (qeNumItems > 0 ? qeNumItems : 30); qi++) {
                    var qc;
                    try { qc = qeV1.getItemAt(qi); } catch (eQc) { continue; }
                    if (!qc) continue;
                    var qcType = "";
                    try { qcType = String(qc.type); } catch (e) {}
                    if (qcType === "Empty") continue;
                    var qStart = -1;
                    try { qStart = Number(qc.start.seconds); } catch (e) {}
                    if (Math.abs(qStart - clips[applyIdx].startSec) < 0.1) { qeClip = qc; diag.push("QE match @ idx=" + qi + " start=" + qStart); break; }
                }
            }
            if (!qeClip) return err("QE: não consegui obter o clip[" + applyIdx + "] no V1");

            // ── 6) DIAGNÓSTICO: lista métodos disponíveis em qeClip/qeV1/qeSeq
            //     porque sabemos que addVideoTransition não existe em PP 26.2+
            step = "list-qe-methods";
            function listMethods(obj, label) {
                if (!obj) return label + "=null";
                var methods = [];
                try {
                    for (var k in obj) {
                        try {
                            if (typeof obj[k] === "function") methods.push(k);
                        } catch (e) {}
                    }
                } catch (e) { return label + " enum threw: " + e.message; }
                return label + ".methods=[" + methods.join(",") + "]";
            }
            diag.push(listMethods(qeClip, "qeClip"));
            diag.push(listMethods(qeV1, "qeV1"));
            diag.push(listMethods(qeSeq, "qeSeq"));

            // ── 7) Tenta TODOS os nomes possíveis (na clip E no track) ──
            step = "try-many-apis";
            var added = false;
            var lastErr = null;
            var tries = [];

            // Variantes possíveis no QE clip
            ["addVideoTransition", "addTransition", "applyTransition", "appendTransition", "insertTransition"]
                .forEach(function (m) {
                    if (qeClip && typeof qeClip[m] === "function") {
                        tries.push({ label: "qeClip." + m + "(t,end,dur,0,true)", fn: function () { qeClip[m](transitionObj, addAtEnd, "00:00:01:00", 0, true); } });
                        tries.push({ label: "qeClip." + m + "(t,end,dur)",        fn: function () { qeClip[m](transitionObj, addAtEnd, "00:00:01:00"); } });
                        tries.push({ label: "qeClip." + m + "(t,end)",            fn: function () { qeClip[m](transitionObj, addAtEnd); } });
                        tries.push({ label: "qeClip." + m + "(t)",                fn: function () { qeClip[m](transitionObj); } });
                    }
                });

            // Variantes possíveis no QE track
            ["addVideoTransition", "addTransition", "applyTransition", "appendTransition", "insertTransition"]
                .forEach(function (m) {
                    if (qeV1 && typeof qeV1[m] === "function") {
                        var ctiStr = String(ctiSec); // alguns aceitam time como string
                        tries.push({ label: "qeV1." + m + "(t,time,end,dur,0)", fn: function () { qeV1[m](transitionObj, ctiStr, addAtEnd, "00:00:01:00", 0); } });
                        tries.push({ label: "qeV1." + m + "(t,time,end,dur)",  fn: function () { qeV1[m](transitionObj, ctiStr, addAtEnd, "00:00:01:00"); } });
                        tries.push({ label: "qeV1." + m + "(t,time,end)",      fn: function () { qeV1[m](transitionObj, ctiStr, addAtEnd); } });
                        tries.push({ label: "qeV1." + m + "(t,time)",          fn: function () { qeV1[m](transitionObj, ctiStr); } });
                        tries.push({ label: "qeV1." + m + "(t)",               fn: function () { qeV1[m](transitionObj); } });
                    }
                });

            diag.push("trying " + tries.length + " API variants");

            for (var ti = 0; ti < tries.length; ti++) {
                try {
                    tries[ti].fn();
                    added = true;
                    diag.push("✓ " + tries[ti].label);
                    break;
                } catch (eT) {
                    diag.push("✗ " + tries[ti].label + " :: " + eT.message);
                    lastErr = eT;
                }
            }

            if (!added) {
                // último recurso: app.executeCommand pra "Apply Default Video Transition"
                step = "executeCommand-fallback";
                try {
                    if (app.executeCommand) {
                        // O cmdId pra "Apply Default Video Transition" varia; tentamos alguns conhecidos
                        var cmdIds = [3784, 3785, 3786, 4000];
                        for (var ci = 0; ci < cmdIds.length; ci++) {
                            try {
                                app.executeCommand(cmdIds[ci]);
                                added = true;
                                diag.push("✓ app.executeCommand(" + cmdIds[ci] + ")");
                                break;
                            } catch (eCmd) {
                                diag.push("✗ executeCommand(" + cmdIds[ci] + ") :: " + eCmd.message);
                            }
                        }
                    }
                } catch (eEC) { diag.push("executeCommand outer throw: " + eEC.message); }
            }

            if (!added) {
                return err("Premiere recusou aplicar a transition em todas as " + tries.length + " variantes da API. Veja diag pra ver quais métodos QE existem. diag: " + diag.join(" | "));
            }

            return ok({
                ok: true,
                applied: true,
                strategy: "builtin_transition",
                category: category,
                transitionName: transitionName,
                hint: "✓ Transição '" + transitionName + "' aplicada no V1 (equivalente built-in pra '" + (niceName || category) + "')",
                applyMsg: "transition=" + transitionName + " atEnd=" + addAtEnd + " applyIdx=" + applyIdx,
                diag: diag.join(" | ")
            });
        } catch (e) {
            return err("[" + step + "] " + ((e && e.message) ? e.message : String(e)) + " | diag=" + diag.join(" | "));
        }
    }

    /* Garante uma bin chamada "MotionVault Imports" na raiz do projeto.
     * Retorna o projectItem da bin (ou root como fallback).
     *
     * Tolerante a versões onde it.type não retorna 1 — testamos por NAME e por
     * presença do método createBin/children (estilo duck-typing) pra não criar bin
     * duplicada. Versão anterior comparava só name === ... && type === 1 e em
     * algumas versões do PP o type vinha undefined → criava bin duplicada toda vez. */
    function ensureImportBin() {
        var name = "MotionVault Imports";
        var root = app.project.rootItem;
        try {
            for (var i = 0; i < root.children.numItems; i++) {
                var it = root.children[i];
                if (String(it.name || "") !== name) continue;
                // É bin se tem método createBin OU tem children e NÃO é sequence
                var looksLikeBin = false;
                try { looksLikeBin = !!it.createBin; } catch (e1) {}
                if (!looksLikeBin) {
                    try { looksLikeBin = it.children && it.children.numItems !== undefined && !(it.isSequence && it.isSequence()); } catch (e2) {}
                }
                if (looksLikeBin) return it;
            }
        } catch (e) {}
        try { return root.createBin(name); } catch (e) {}
        return root;
    }

    /**
     * Aplica um .atomxasset descomprimido (.prproj) na timeline ativa.
     *
     * ESTRATÉGIA REVISADA (após descobrir que Transitions referenciam efeitos AE
     * não-built-in que crasham o Premiere):
     *
     *   transition    → NÃO importa o .prproj (causa crash). Em vez disso, aplica
     *                   transição BUILT-IN do Premiere mapeada por categoria
     *                   (Zoom→Cross Zoom, Blur→Cross Dissolve, ...)
     *
     *   color_grading → Importa o .prproj e insere como nested no V2 (Lumetri é
     *                   built-in, sem dependências externas — funciona)
     *
     *   effect        → SÓ importa na bin com hint manual (auto-insert desligado
     *                   por segurança — muitos effects usam Paint Bucket/Grid/etc
     *                   que não estão no Premiere por padrão)
     *
     * Parâmetro `category` (novo) vem do catalog — usado pelo branch transition.
     */
    function applyAtomxAsset(prprojPath, subType, niceName, category) {
        var sub = String(subType || "").toLowerCase();
        // NOVA ESTRATÉGIA pós-RE do pprsequencer.exe:
        //   pprsequencer.exe NÃO tem filtro de license/Id (confirmado por análise PE).
        //   O segredo do paste do AtomX original está no JSXBIN dele — opaco sem decoder.
        //   Plano B funcional: pra TODOS sub-types, tenta import + overwriteClip.
        //     - color_grading → V2 (Lumetri built-in, 100% seguro)
        //     - transition    → V3 (pode falhar se asset usa effect AE custom)
        //     - effect        → V3 (mesma situação)
        //   Try/catch agressivo no host.jsx — se Premiere recusar o import, devolvemos
        //   "imported_only" e o user arrasta da bin manualmente.
        return applyAtomxAssetViaImport(prprojPath, sub, niceName);
    }

    function applyAtomxAssetViaImport(prprojPath, subType, niceName) {
        var step = "init";
        var diag = [];   // diagnóstico humano detalhado
        function log(s) { diag.push(s); }
        try {
            if (!app || !app.project) return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq)                  return err("Abra uma sequência antes de aplicar");
            var f = new File(prprojPath);
            if (!f.exists)             return err("Preset não encontrado:\n" + prprojPath);

            var sub = String(subType || "").toLowerCase();
            if (sub !== "transition" && sub !== "color_grading" && sub !== "effect") sub = "color_grading";
            log("sub=" + sub + " nice=" + (niceName || "(none)"));

            // ─── ATOMX-STYLE: enableQE() ANTES de qualquer operação na sequence
            // O AtomX faz isso (decompilado de ATOMX_DHEFCJRSOGLANKIBQP em
            // ATOMXBKFHEACDMIGJ.jsx) pra liberar a API QuickEdit que dá acesso
            // a flushCache, sequence refresh e outras operações low-level.
            try { app.enableQE(); log("enableQE ok"); } catch (eQE) { log("enableQE noop: " + (eQE.message || eQE)); }

            step = "ensure-bin";
            var importBin = ensureImportBin();
            log("bin=" + String(importBin.name || "?"));

            step = "snapshot";
            var allBefore = [];
            collectAllProjectItems(app.project.rootItem, allBefore);
            var beforeKeys = {};
            for (var s = 0; s < allBefore.length; s++) beforeKeys[itemKey(allBefore[s])] = true;
            log("before=" + allBefore.length);

            step = "importFiles";
            // Importa NA SUB-BIN — não polui raiz e fica fácil pro user achar
            try {
                app.project.importFiles([f.fsName], true, importBin, false);
            } catch (eImp) {
                return err("Falha ao importar .prproj: " + (eImp.message || eImp));
            }

            step = "locate-sequence";
            var allAfter = [];
            collectAllProjectItems(app.project.rootItem, allAfter);
            log("after=" + allAfter.length);

            // Coleta TODOS os novos items
            var newItems = [];
            for (var i = 0; i < allAfter.length; i++) {
                if (!beforeKeys[itemKey(allAfter[i])]) newItems.push(allAfter[i]);
            }
            log("new=" + newItems.length);

            // ATOMX-STYLE: procura a sequence WRAPPER (a master template), NÃO as
            // sub-sequences internas. Heurística:
            //   1) Sequence cujo nome bate o basename do .prproj
            //   2) Sequence cujo nome NÃO começa com "Adjustment Layer" (internas)
            //   3) Como último recurso, a primeira nova sequence
            var foundSeq = null;
            var allNewSeqs = [];
            for (var i3 = 0; i3 < newItems.length; i3++) {
                if (isSequenceItem(newItems[i3])) allNewSeqs.push(newItems[i3]);
            }
            log("new sequences=" + allNewSeqs.length);
            if (allNewSeqs.length === 1) {
                foundSeq = allNewSeqs[0];
            } else if (allNewSeqs.length > 1) {
                var base = String(prprojPath).replace(/^.*[\\\/]/, "").replace(/\.prproj$/i, "").toLowerCase();
                // normaliza separadores: "Zoom_IN_CW" vs "Zoom IN CW" vs "Zoom-IN-CW"
                var baseNorm = base.replace(/[\s_\-]+/g, "");
                // Pass 1: nome bate basename
                for (var iN = 0; iN < allNewSeqs.length; iN++) {
                    var nm = String(allNewSeqs[iN].name).toLowerCase();
                    var nmNorm = nm.replace(/[\s_\-]+/g, "");
                    if (nm === base || nmNorm === baseNorm || nmNorm.indexOf(baseNorm) >= 0 || baseNorm.indexOf(nmNorm) >= 0) {
                        foundSeq = allNewSeqs[iN];
                        log("pick wrapper by name: " + allNewSeqs[iN].name);
                        break;
                    }
                }
                // Pass 2: pula sequences "Adjustment Layer..." (são internas do AtomX)
                if (!foundSeq) {
                    for (var iN2 = 0; iN2 < allNewSeqs.length; iN2++) {
                        var nm2 = String(allNewSeqs[iN2].name);
                        if (nm2.toLowerCase().indexOf("adjustment layer") < 0) {
                            foundSeq = allNewSeqs[iN2];
                            log("pick non-adjustment: " + nm2);
                            break;
                        }
                    }
                }
                // Pass 3: primeira
                if (!foundSeq) { foundSeq = allNewSeqs[0]; log("pick first fallback: " + foundSeq.name); }
            }
            if (!foundSeq) {
                // Sem sequência? Devolve sucesso parcial — assets estão na bin
                return ok({
                    ok: true,
                    applied: false,
                    sub_type: sub,
                    hint: "Importado na bin '" + String(importBin.name) + "' — sem sequência aplicável (verifique o Project panel)",
                    applyMsg: "imported=" + newItems.length + " no_sequence=true",
                    diag: diag.join(" | ")
                });
            }
            log("seq=" + String(foundSeq.name));

            // ─── A partir daqui é OPCIONAL — se algo falhar, retornamos "imported"
            //     em vez de "applied" e o user puxa da bin manualmente. NUNCA quebra. ───

            // ─── TRACK por sub-type:
            //     color_grading → V2 (Lumetri built-in, sempre seguro)
            //     transition    → V3 (pode falhar pra effects AE custom)
            //     effect        → V3 (pode falhar pra effects AE custom)
            step = "decide-track";
            var targetIdx = (sub === "color_grading") ? 1 : 2;   // V2 ou V3
            try { ensureVideoTracks(seq, targetIdx + 1); } catch (e) {}
            if (!seq.videoTracks[targetIdx]) {
                targetIdx = Math.max(0, seq.videoTracks.numTracks - 1);
            }
            var targetTrack = seq.videoTracks[targetIdx];
            log("targetTrack=V" + (targetIdx + 1));

            step = "compute-time";
            var ctiSec = Number(seq.getPlayerPosition().seconds);
            var insertSec = ctiSec;
            log("cti=" + ctiSec.toFixed(3));
            // Alinha ao início do clip do V1 — mas SÓ pra color_grading (que cobre
            // o clip inteiro). Pra transition/effect, mantém o CTI exato pra colar
            // EM CIMA do playhead (que é o que o usuário espera).
            if (sub === "color_grading") {
                var below = clipAtPlayhead(seq);
                if (below) { insertSec = below.startSec; log("aligned_to_v1=" + insertSec.toFixed(3)); }
            }
            // Se posição é 0 mas existem clips no V1, salta pro start do primeiro clip
            // (raro, mas evita inserir antes da timeline visível).
            if (insertSec <= 0.001) {
                try {
                    var v1 = seq.videoTracks[0];
                    if (v1 && v1.clips && v1.clips.numItems > 0) {
                        var firstClipStart = Number(v1.clips[0].start.seconds);
                        if (firstClipStart > insertSec) {
                            insertSec = firstClipStart;
                            log("snap_to_first_v1_clip=" + insertSec.toFixed(3));
                        }
                    }
                } catch (e) {}
            }

            // ATOMX-STYLE: destrava TODAS as tracks antes do overwrite
            step = "unlock-tracks";
            var lockedSnapshot = { v: [], a: [] };
            try {
                for (var lv = 0; lv < seq.videoTracks.numTracks; lv++) {
                    var tv = seq.videoTracks[lv];
                    if (tv.isLocked && tv.isLocked()) { lockedSnapshot.v.push(lv); try { tv.setLock(false); } catch (e) {} }
                }
                for (var la = 0; la < seq.audioTracks.numTracks; la++) {
                    var ta = seq.audioTracks[la];
                    if (ta.isLocked && ta.isLocked()) { lockedSnapshot.a.push(la); try { ta.setLock(false); } catch (e) {} }
                }
                log("unlocked v=" + lockedSnapshot.v.length + " a=" + lockedSnapshot.a.length);
            } catch (eUnlock) { log("unlock-noop: " + (eUnlock.message || eUnlock)); }

            // SNAP FREE SLOT: se posição alvo tem clip no V3 ali, avança pro gap.
            // Sem isso, overwriteClip substitui o clip que estava ali.
            step = "find-free-slot";
            try {
                var insertTicks = Math.round(insertSec * 254016000000);
                var moved = false;
                for (var attempts = 0; attempts < 50; attempts++) {
                    var conflict = null;
                    for (var ic = 0; ic < targetTrack.clips.numItems; ic++) {
                        var existing = targetTrack.clips[ic];
                        var es = Number(existing.start.ticks);
                        var ee = Number(existing.end.ticks);
                        if (insertTicks >= es && insertTicks < ee) { conflict = existing; break; }
                    }
                    if (!conflict) break;
                    insertTicks = Number(conflict.end.ticks) + 1;
                    moved = true;
                }
                if (moved) {
                    var newSec = insertTicks / 254016000000;
                    log("snap_free_slot " + insertSec.toFixed(3) + "→" + newSec.toFixed(3));
                    insertSec = newSec;
                }
            } catch (eFree) { log("find-free-slot noop: " + (eFree.message || eFree)); }

            step = "overwrite";
            var beforeCount = -1, afterCount = -1;
            var beforeIds = {};
            var insertedOk = false;
            var insertedClipName = null;
            try {
                beforeCount = targetTrack.clips.numItems;
                for (var ib = 0; ib < beforeCount; ib++) {
                    var bc = targetTrack.clips[ib];
                    try { beforeIds[String(bc.start.ticks) + ":" + String(bc.name || "")] = true; } catch (e) {}
                }
            } catch (e) {}
            try {
                targetTrack.overwriteClip(foundSeq, insertSec);
                afterCount = targetTrack.clips.numItems;
                // Detecção robusta: procura clip NOVO no V3
                for (var ia = 0; ia < afterCount; ia++) {
                    var ac = targetTrack.clips[ia];
                    var key = "";
                    try { key = String(ac.start.ticks) + ":" + String(ac.name || ""); } catch (e) {}
                    if (key && !beforeIds[key]) {
                        insertedOk = true;
                        insertedClipName = String(ac.name || "");
                        break;
                    }
                }
                log("clips " + beforeCount + "→" + afterCount + " new_clip=" + (insertedClipName || "(none)") + " ok=" + insertedOk);
            } catch (eOw) {
                log("overwrite-throw: " + (eOw.message || eOw));
            }

            // UX: move o playhead pra cima do clip que acabei de inserir.
            // Sem isso, o user nao vê o clip novo porque a timeline pode estar
            // scrollada/zoomada longe de insertSec.
            if (insertedOk) {
                try {
                    var seekTicks = Math.round(insertSec * 254016000000);
                    seq.setPlayerPosition(seekTicks);
                    log("playhead → " + insertSec.toFixed(3) + "s");
                } catch (eSeek) { log("playhead-noop: " + (eSeek.message || eSeek)); }
            }

            // ATOMX-STYLE: força flush da timeline pra render imediato
            try {
                if (typeof qe !== "undefined" && qe.project && qe.project.getActiveSequence) {
                    var qSeq = qe.project.getActiveSequence();
                    if (qSeq && qSeq.flushCache) { qSeq.flushCache(); log("qe.flushCache ok"); }
                }
            } catch (eFC) { log("qe.flushCache noop"); }

            // Re-trava tracks que estavam travadas
            try {
                for (var rv = 0; rv < lockedSnapshot.v.length; rv++) {
                    try { seq.videoTracks[lockedSnapshot.v[rv]].setLock(true); } catch (e) {}
                }
                for (var ra = 0; ra < lockedSnapshot.a.length; ra++) {
                    try { seq.audioTracks[lockedSnapshot.a[ra]].setLock(true); } catch (e) {}
                }
            } catch (e) {}

            var trackLabel = "V" + (targetIdx + 1);

            if (insertedOk) {
                var subLabel = (sub === "color_grading") ? "Color Grading"
                             : (sub === "transition")     ? "Transition"
                             : (sub === "effect")         ? "Effect"
                             : sub;
                return ok({
                    ok: true,
                    applied: true,
                    sub_type: sub,
                    track: targetIdx,
                    seqName: String(foundSeq.name),
                    hint: "✓ " + subLabel + " inserido no " + trackLabel,
                    applyMsg: "sequence=" + foundSeq.name + " track=" + trackLabel + " at=" + insertSec.toFixed(2) + "s",
                    diag: diag.join(" | ")
                });
            }

            return ok({
                ok: true,
                applied: false,
                sub_type: sub,
                seqName: String(foundSeq.name),
                hint: "Importado na bin '" + String(importBin.name) + "' — arraste '" + String(foundSeq.name) + "' pro " + trackLabel,
                applyMsg: "no_insert track=" + trackLabel + " at=" + insertSec.toFixed(2) + "s",
                diag: diag.join(" | ")
            });
        } catch (e) {
            return err("[" + step + "] " + ((e && e.message) ? e.message : String(e)) + " | diag=" + diag.join(" | "));
        }
    }

    /* Mantém compat com chamadas legacy: importPresetProject(path) aplica como
     * color_grading por padrão. */
    function importPresetProject(prprojPath) {
        return applyAtomxAsset(prprojPath, "color_grading", null, null);
    }

    /**
     * Imports a .mogrt onto the active sequence at the current CTI.
     * Returns JSON { ok, track, name } on success or { error } on failure.
     */
    function importMogrt(mogrtPath) {
        try {
            if (!app)                      return err("Premiere Pro indisponível");
            if (!app.project)              return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq)                      return err("Abra uma sequência antes de importar");

            var f = new File(mogrtPath);
            if (!f.exists)                 return err("Arquivo não encontrado:\n" + mogrtPath);

            var cti     = seq.getPlayerPosition();
            var ticks   = cti.ticks;
            var pick    = pickTargetTrack(seq, ticks);
            var target  = pick >= 0 ? pick : 0;   // fallback V1 if everything occupied

            // importMGT(path, ticksIn, vidTrackOffset, audTrackOffset)
            var clip = seq.importMGT(f.fsName, String(ticks), target, 0);
            if (!clip) {
                // try once again on V1 in case track index was invalid
                clip = seq.importMGT(f.fsName, String(ticks), 0, 0);
            }
            if (!clip)                     return err("Premiere recusou a importação. Verifique se a versão suporta .mogrt e se a sequência não está bloqueada.");

            // advance CTI to the end of the new clip so successive imports stack in time
            try {
                var dur     = Number(clip.end.ticks) - Number(clip.start.ticks);
                var endStr  = String(Number(ticks) + dur);
                seq.setPlayerPosition(endStr);
            } catch (e) {}

            return ok({ ok: true, track: target, name: clip.name, pickedEmpty: pick >= 0 });
        } catch (e) {
            return err((e && e.message) ? e.message : String(e));
        }
    }

    /* ============================================================
     * PRE-RENDER PIPELINE
     * Lê manifest JSON, abre cada .prproj, manda pro AME render como
     * ProRes 4444 with alpha. Renderiza só os 910 transitions/effects
     * (color_grading não precisa).
     *
     * Chamado pelo botão "Pré-Render" do painel. Retorna estado pra UI
     * mostrar progresso.
     * ============================================================ */
    var PRE_RENDER_STATE = { running: false, idx: 0, total: 0, queued: 0, skipped: 0, failed: 0, log: [] };

    function preRenderStatus() {
        return ok({
            running: PRE_RENDER_STATE.running,
            idx: PRE_RENDER_STATE.idx,
            total: PRE_RENDER_STATE.total,
            queued: PRE_RENDER_STATE.queued,
            skipped: PRE_RENDER_STATE.skipped,
            failed: PRE_RENDER_STATE.failed,
            log: PRE_RENDER_STATE.log.slice(-20)
        });
    }

    function _prLog(line) {
        PRE_RENDER_STATE.log.push("[" + new Date().toTimeString().slice(0,8) + "] " + line);
        try {
            var lf = new File("C:\\Users\\Gabriel\\MotionVault-Renders\\logs\\render-batch.log");
            lf.encoding = "UTF-8";
            lf.open("a");
            lf.write("[" + new Date().toISOString() + "] " + line + "\n");
            lf.close();
        } catch (e) {}
    }

    function _prFindMainSequence(name) {
        var allSeqs = [];
        function collect(item) {
            try {
                if (item.children) {
                    for (var i = 0; i < item.children.numItems; i++) {
                        var c = item.children[i];
                        if (isSequenceItem(c)) allSeqs.push(c);
                        if (c.children) collect(c);
                    }
                }
            } catch (e) {}
        }
        collect(app.project.rootItem);
        if (!allSeqs.length) return null;
        if (allSeqs.length === 1) return allSeqs[0];
        var baseNorm = String(name).toLowerCase().replace(/[\s_\-]+/g, "");
        for (var i = 0; i < allSeqs.length; i++) {
            var nm = String(allSeqs[i].name).toLowerCase().replace(/[\s_\-]+/g, "");
            if (nm === baseNorm || nm.indexOf(baseNorm) >= 0 || baseNorm.indexOf(nm) >= 0) return allSeqs[i];
        }
        for (var j = 0; j < allSeqs.length; j++) {
            if (String(allSeqs[j].name).toLowerCase().indexOf("adjustment layer") < 0) return allSeqs[j];
        }
        return allSeqs[0];
    }

    function _prEnsureDir(p) {
        var folder = new Folder(p);
        if (!folder.exists) folder.create();
    }

    function startPreRender(batchLimit) {
        if (PRE_RENDER_STATE.running) return err("Pré-render já em andamento");
        var MANIFEST = "C:\\Users\\Gabriel\\MotionVault-Renders\\render-manifest.json";
        var OUT_ROOT = "C:\\Users\\Gabriel\\MotionVault-Renders\\renders";
        var PRESET = "C:\\Program Files\\Adobe\\Adobe Media Encoder 2026\\MediaIO\\systempresets\\3F3F3F3F_4D6F6F56\\Apple ProRes 4444 with alpha.epr";

        var manifestFile = new File(MANIFEST);
        if (!manifestFile.exists) return err("Manifest não encontrado: " + MANIFEST + "\n\nRode 'node 1-decompress.js' primeiro.");
        if (!new File(PRESET).exists) return err("Preset ProRes 4444 not found:\n" + PRESET);

        manifestFile.encoding = "UTF-8";
        manifestFile.open("r");
        var raw = manifestFile.read();
        manifestFile.close();

        var manifest;
        try { manifest = JSON.parse(raw); } catch (e) { return err("Erro parsing manifest: " + e.message); }

        var limit = Math.min(manifest.length, Number(batchLimit) || 999999);
        PRE_RENDER_STATE = { running: true, idx: 0, total: limit, queued: 0, skipped: 0, failed: 0, log: [] };

        _prLog("=== PRE-RENDER START === total=" + limit);

        // Abre AME
        try { if (app.encoder && app.encoder.launchEncoder) { app.encoder.launchEncoder(); _prLog("AME launched"); } }
        catch (eLE) { _prLog("launchEncoder err: " + eLE.message); }

        for (var i = 0; i < limit; i++) {
            var m = manifest[i];
            PRE_RENDER_STATE.idx = i + 1;

            var outDir = OUT_ROOT + "\\" + m.catSlug;
            _prEnsureDir(outDir);
            var outPath = outDir + "\\" + m.slug + ".mov";

            if (new File(outPath).exists) { PRE_RENDER_STATE.skipped++; continue; }

            _prLog("[" + (i+1) + "/" + limit + "] " + m.slug);

            try {
                var openOk = app.openDocument(m.prproj);
                if (!openOk) { _prLog("  ✗ openDocument false"); PRE_RENDER_STATE.failed++; continue; }
                $.sleep(400);

                var seq = _prFindMainSequence(m.name);
                if (!seq) { _prLog("  ✗ sem sequence"); PRE_RENDER_STATE.failed++; try { app.project.closeDocument(); } catch (e) {} continue; }

                var jobId = null;
                try {
                    jobId = app.encoder.encodeSequence(seq, outPath, PRESET, 0, 1);
                    _prLog("  + queued seq=" + seq.name);
                    PRE_RENDER_STATE.queued++;
                } catch (eEnc) { _prLog("  ✗ encodeSequence: " + eEnc.message); PRE_RENDER_STATE.failed++; }

                try { app.project.closeDocument(0, 0); } catch (e) {}
                $.sleep(200);

                if (PRE_RENDER_STATE.queued > 0 && PRE_RENDER_STATE.queued % 50 === 0) {
                    _prLog("  ... pausa de 5s a cada 50 jobs ...");
                    $.sleep(5000);
                }
            } catch (eOuter) { _prLog("  ✗ outer: " + eOuter.message); PRE_RENDER_STATE.failed++; }
        }

        try { if (PRE_RENDER_STATE.queued > 0 && app.encoder && app.encoder.startBatch) { app.encoder.startBatch(); _prLog("AME batch started"); } }
        catch (eSB) { _prLog("startBatch err: " + eSB.message); }

        _prLog("=== PRE-RENDER FIM === queued=" + PRE_RENDER_STATE.queued + " skipped=" + PRE_RENDER_STATE.skipped + " failed=" + PRE_RENDER_STATE.failed);
        PRE_RENDER_STATE.running = false;

        return ok({
            ok: true,
            total: limit,
            queued: PRE_RENDER_STATE.queued,
            skipped: PRE_RENDER_STATE.skipped,
            failed: PRE_RENDER_STATE.failed,
            outputDir: OUT_ROOT,
            logFile: "C:\\Users\\Gabriel\\MotionVault-Renders\\logs\\render-batch.log"
        });
    }

    /* ============================================================
     * BUILT-IN TRANSITIONS — apply effects in nato no clip do V1 usando
     * APENAS efeitos built-in do Premiere (Transform, Gaussian Blur, etc).
     *
     * Funciona em 100% das maquinas Premiere — sem dependencia de plugins
     * AE de terceiros. Baseado em RE dos .prproj do AtomX que mostraram
     * que 95% das transitions usam apenas built-in effects.
     *
     * Implementacao conservadora: SO 4 categorias confiaveis testadas:
     *   - zoom-in       (Transform scale 100->200% nos ultimos 0.6s)
     *   - zoom-out      (Transform scale 200->100% nos primeiros 0.6s)
     *   - blur-in       (Gaussian Blur 50->0 nos primeiros 0.6s)
     *   - blur-out      (Gaussian Blur 0->50 nos ultimos 0.6s)
     * ============================================================ */

    function _bitFindClipAtPlayhead(seq) {
        try {
            var pos = Number(seq.getPlayerPosition().ticks);
            var v1 = seq.videoTracks[0];
            if (!v1 || !v1.clips || !v1.clips.numItems) return null;
            for (var i = 0; i < v1.clips.numItems; i++) {
                var c = v1.clips[i];
                if (pos >= Number(c.start.ticks) && pos < Number(c.end.ticks)) return c;
            }
        } catch (e) {}
        return null;
    }

    function _bitFindComponentByMatchName(clip, matchName) {
        try {
            for (var i = 0; i < clip.components.numItems; i++) {
                var c = clip.components[i];
                if (String(c.matchName) === matchName) return c;
            }
        } catch (e) {}
        return null;
    }

    function _bitFindProperty(component, displayNameContains) {
        try {
            for (var i = 0; i < component.properties.numItems; i++) {
                var p = component.properties[i];
                var n = String(p.displayName || "").toLowerCase();
                if (n.indexOf(String(displayNameContains).toLowerCase()) >= 0) return p;
            }
        } catch (e) {}
        return null;
    }

    function applyBuiltInTransition(kind) {
        var step = "init";
        var diag = [];
        function log(s) { diag.push(s); }
        try {
            if (!app || !app.project) return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq) return err("Abra uma sequencia antes de aplicar");
            try { app.enableQE(); } catch (e) {}

            var clip = _bitFindClipAtPlayhead(seq);
            if (!clip) return err("Coloque o playhead em cima de um clip no V1 antes de aplicar");

            var k = String(kind || "").toLowerCase();
            log("kind=" + k + " clip=" + clip.name);

            var clipStartSec = Number(clip.start.seconds);
            var clipEndSec   = Number(clip.end.seconds);
            var clipDurSec   = clipEndSec - clipStartSec;
            var transitionSec = Math.min(0.6, clipDurSec * 0.3);
            log("dur=" + clipDurSec.toFixed(2) + "s trans=" + transitionSec.toFixed(2) + "s");

            // ─── ZOOM IN / ZOOM OUT ───
            if (k === "zoom-in" || k === "zoom-out") {
                step = "add-transform";
                try { clip.components.addItem("AE.ADBE Geometry"); } catch (eA) { log("addItem Geometry: " + eA.message); }
                var transform = _bitFindComponentByMatchName(clip, "AE.ADBE Geometry");
                if (!transform) return err("Falha ao adicionar Transform (AE.ADBE Geometry)");

                step = "find-scale";
                var scale = _bitFindProperty(transform, "scale");
                if (!scale) return err("Property Scale nao encontrada no Transform");

                step = "set-keyframes";
                scale.setTimeVarying(true);
                var t0 = (k === "zoom-in") ? (clipEndSec - transitionSec) : clipStartSec;
                var t1 = (k === "zoom-in") ? clipEndSec : (clipStartSec + transitionSec);
                var v0 = (k === "zoom-in") ? 100 : 200;
                var v1 = (k === "zoom-in") ? 200 : 100;

                scale.addKey(t0); scale.setValueAtKey(t0, v0, true);
                scale.addKey(t1); scale.setValueAtKey(t1, v1, true);
                log("keys @" + t0.toFixed(2) + "s=" + v0 + " @" + t1.toFixed(2) + "s=" + v1);

                return ok({
                    ok: true, applied: true, kind: k,
                    hint: "✓ " + (k === "zoom-in" ? "Zoom In" : "Zoom Out") + " aplicado em '" + String(clip.name) + "'",
                    applyMsg: "scale " + v0 + "→" + v1 + "% durante " + transitionSec.toFixed(2) + "s",
                    diag: diag.join(" | ")
                });
            }

            // ─── BLUR IN / BLUR OUT ───
            if (k === "blur-in" || k === "blur-out") {
                step = "add-blur";
                try { clip.components.addItem("AE.ADBE Gaussian Blur 2"); } catch (eB) { log("addItem GaussianBlur: " + eB.message); }
                var blur = _bitFindComponentByMatchName(clip, "AE.ADBE Gaussian Blur 2");
                if (!blur) return err("Falha ao adicionar Gaussian Blur");

                step = "find-blur-prop";
                var blurProp = _bitFindProperty(blur, "blurriness");
                if (!blurProp) blurProp = _bitFindProperty(blur, "blur");
                if (!blurProp) return err("Property Blurriness nao encontrada");

                step = "set-keyframes";
                blurProp.setTimeVarying(true);
                var bt0 = (k === "blur-in") ? clipStartSec : (clipEndSec - transitionSec);
                var bt1 = (k === "blur-in") ? (clipStartSec + transitionSec) : clipEndSec;
                var bv0 = (k === "blur-in") ? 50 : 0;
                var bv1 = (k === "blur-in") ? 0 : 50;

                blurProp.addKey(bt0); blurProp.setValueAtKey(bt0, bv0, true);
                blurProp.addKey(bt1); blurProp.setValueAtKey(bt1, bv1, true);
                log("keys @" + bt0.toFixed(2) + "s=" + bv0 + " @" + bt1.toFixed(2) + "s=" + bv1);

                return ok({
                    ok: true, applied: true, kind: k,
                    hint: "✓ " + (k === "blur-in" ? "Blur In" : "Blur Out") + " aplicado em '" + String(clip.name) + "'",
                    applyMsg: "blur " + bv0 + "→" + bv1 + " durante " + transitionSec.toFixed(2) + "s",
                    diag: diag.join(" | ")
                });
            }

            return err("Tipo de transition built-in nao suportado: " + k);
        } catch (e) {
            return err("[" + step + "] " + (e.message || e) + " | diag=" + diag.join(" | "));
        }
    }

    /* ============================================================
     * applyLUT — adiciona Lumetri Color no clip do V1 e seta LUT do .cube
     * ============================================================ */
    /* Procura clip no playhead em QUALQUER track de video — não só V1 */
    function _findClipAtPlayheadAnyTrack(seq) {
        try {
            var pos = Number(seq.getPlayerPosition().ticks);
            // Procura de cima pra baixo (V4 → V1) — adjustment layers no V2/V3 têm prioridade
            for (var t = seq.videoTracks.numTracks - 1; t >= 0; t--) {
                var tr = seq.videoTracks[t];
                if (!tr || !tr.clips || !tr.clips.numItems) continue;
                for (var j = 0; j < tr.clips.numItems; j++) {
                    var c = tr.clips[j];
                    if (pos >= Number(c.start.ticks) && pos < Number(c.end.ticks)) return c;
                }
            }
        } catch (e) {}
        return null;
    }

    /* Cria Adjustment Layer no projeto via QE e insere no V2 sobre playhead.
     * Retorna o clip inserido (TrackItem) ou null. */
    function _createAdjustmentLayerAtPlayhead(seq, niceName) {
        try {
            app.enableQE();
            if (typeof qe === "undefined" || !qe.project) return null;

            var qeSeq = qe.project.getActiveSequence();
            if (!qeSeq) return null;

            // Tenta criar adjustment layer via QE (newAdjustmentLayer existe em PR 14+)
            var adjItem = null;
            try {
                // Method 1: newAdjustmentLayer no project QE
                adjItem = qe.project.newAdjustmentLayer();
            } catch (e1) {
                try {
                    // Method 2: getActiveSequence().newAdjustmentLayer
                    adjItem = qeSeq.newAdjustmentLayer && qeSeq.newAdjustmentLayer();
                } catch (e2) {}
            }
            if (!adjItem) return null;

            // O adjItem é um QE projectItem. Pra inserir na timeline, precisa achar
            // o equivalente no app.project.rootItem (regular API).
            // Procura o item mais novo no rootItem (recém-criado deveria ser o último).
            var newProjItem = null;
            function findAdj(item) {
                if (newProjItem) return;
                if (item.children) {
                    for (var i = item.children.numItems - 1; i >= 0; i--) {
                        var c = item.children[i];
                        var n = String(c.name || "").toLowerCase();
                        if (n.indexOf("adjustment") >= 0 || n.indexOf("camada de ajuste") >= 0) {
                            newProjItem = c;
                            return;
                        }
                        if (c.children) findAdj(c);
                    }
                }
            }
            findAdj(app.project.rootItem);
            if (!newProjItem) return null;

            // Renomeia pra ficar identificável
            try { newProjItem.name = "LUT · " + (niceName || "Adjustment"); } catch (e) {}

            // Inserir no V2 (track index 1) sobre playhead
            var targetIdx = Math.min(1, seq.videoTracks.numTracks - 1);
            var track = seq.videoTracks[targetIdx];
            if (!track) return null;

            var insertSec = Number(seq.getPlayerPosition().seconds);
            // Snap pra gap livre no track
            var insertTicks = Math.round(insertSec * 254016000000);
            for (var att = 0; att < 50; att++) {
                var conflict = null;
                for (var ic = 0; ic < track.clips.numItems; ic++) {
                    var ex = track.clips[ic];
                    var es = Number(ex.start.ticks);
                    var ee = Number(ex.end.ticks);
                    if (insertTicks >= es && insertTicks < ee) { conflict = ex; break; }
                }
                if (!conflict) break;
                insertTicks = Number(conflict.end.ticks) + 1;
            }
            insertSec = insertTicks / 254016000000;

            var before = track.clips.numItems;
            try { track.overwriteClip(newProjItem, insertSec); }
            catch (eOw) { return null; }
            var after = track.clips.numItems;
            if (after <= before) return null;

            // Acha o clip recém-inserido
            for (var ai = 0; ai < after; ai++) {
                var ac = track.clips[ai];
                if (Math.abs(Number(ac.start.seconds) - insertSec) < 0.05) return ac;
            }
            return null;
        } catch (e) {
            return null;
        }
    }

    function applyLUT(lutPath) {
        var diag = [];
        try {
            if (!app || !app.project) return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq) return err("Abra uma sequência antes de aplicar");
            try { app.enableQE(); } catch (e) {}

            var lutFile = new File(lutPath);
            if (!lutFile.exists) return err("LUT não encontrado: " + lutPath);
            var lutName = String(lutFile.name).replace(/\.(cube|3dl|look)$/i, "");

            // 1. Procura clip em qualquer track de video no playhead
            var clip = _findClipAtPlayheadAnyTrack(seq);
            var createdAdj = false;

            // 2. Se não achou, CRIA Adjustment Layer automaticamente
            if (!clip) {
                diag.push("sem clip no playhead — criando Adjustment Layer");
                clip = _createAdjustmentLayerAtPlayhead(seq, lutName);
                if (clip) {
                    createdAdj = true;
                    diag.push("Adjustment Layer criada e inserida no V" + 2);
                }
            }

            if (!clip) {
                return err(
                    "Não foi possível criar Adjustment Layer automaticamente.\n\n" +
                    "Faça assim:\n" +
                    "1. Project Panel → New Item → Adjustment Layer (Premiere cria 1)\n" +
                    "2. Arraste a Adjustment Layer pro V2 sobre os clipes\n" +
                    "3. Posicione o playhead em cima dela\n" +
                    "4. Clica de novo no LUT no plugin\n\n" +
                    "diag=" + diag.join("|")
                );
            }
            diag.push("clip=" + clip.name);

            // Procura Lumetri Color existente (TODO clip do Premiere pode ter)
            var lumetri = _bitFindComponentByMatchName(clip, "AE.ADBE Lumetri Color");

            // Se não tem, tenta múltiplas formas de adicionar
            if (!lumetri) {
                diag.push("Lumetri não existe — tentando adicionar");
                var added = false;
                // Tentativa 1: addItem na collection (API antiga)
                try {
                    if (clip.components && typeof clip.components.addItem === "function") {
                        clip.components.addItem("AE.ADBE Lumetri Color");
                        added = true; diag.push("via components.addItem");
                    }
                } catch (e1) { diag.push("components.addItem throw: " + e1.message); }
                // Tentativa 2: clip.addVideoEffect (API moderna PR 23.6+)
                if (!added) {
                    try {
                        var qProjectItem = app.project.rootItem;
                        // Procura ou cria Lumetri Color preset
                        var effectName = "Lumetri Color";
                        if (typeof clip.addVideoEffect === "function") {
                            clip.addVideoEffect(effectName);
                            added = true; diag.push("via clip.addVideoEffect");
                        }
                    } catch (e2) { diag.push("addVideoEffect throw: " + e2.message); }
                }
                // Tentativa 3: via QE (QuickEdit)
                if (!added) {
                    try {
                        if (typeof qe !== "undefined" && qe.project) {
                            var qeSeq = qe.project.getActiveSequence();
                            var v1 = qeSeq.getVideoTrackAt(0);
                            for (var qi = 0; qi < v1.numItems; qi++) {
                                var qClip = v1.getItemAt(qi);
                                if (qClip && qClip.name === String(clip.name)) {
                                    qClip.addVideoEffect("Lumetri Color");
                                    added = true; diag.push("via qe.addVideoEffect");
                                    break;
                                }
                            }
                        }
                    } catch (e3) { diag.push("qe addVideoEffect throw: " + e3.message); }
                }

                // Re-procura
                lumetri = _bitFindComponentByMatchName(clip, "AE.ADBE Lumetri Color");
            }

            if (!lumetri) {
                return err("Não foi possível adicionar Lumetri Color. ABRA o panel Lumetri Color manualmente (Effect Controls do clip) e selecione o LUT: " + lutFile.fsName + " | diag=" + diag.join("|"));
            }

            diag.push("Lumetri OK");

            // Procura a property "Input LUT" e seta
            var lutPropNames = ["Input LUT", "Input LUT (Premiere)", "LUT", "ImportLUT", "LUT Setting"];
            var setOk = false;
            for (var pn = 0; pn < lutPropNames.length && !setOk; pn++) {
                var prop = _bitFindProperty(lumetri, lutPropNames[pn]);
                if (prop) {
                    try {
                        prop.setValue(lutFile.fsName, true);
                        setOk = true;
                        diag.push("LUT set via '" + lutPropNames[pn] + "'");
                    } catch (eS) {
                        diag.push("setValue throw " + lutPropNames[pn] + ": " + (eS.message || eS));
                    }
                }
            }

            var prefix = createdAdj ? "✓ Adjustment Layer criada + " : "✓ ";
            return ok({
                ok: true,
                applied: true,
                hint: setOk
                    ? (prefix + "LUT '" + lutName + "' aplicado em '" + String(clip.name) + "'")
                    : (prefix + "Lumetri adicionado — abra Effect Controls > Lumetri Color > Basic Correction > Input LUT e selecione: " + lutFile.fsName),
                clipName: String(clip.name),
                lutPath: lutFile.fsName,
                createdAdjustmentLayer: createdAdj,
                setOk: setOk,
                diag: diag.join(" | ")
            });
        } catch (e) {
            return err("applyLUT throw: " + (e.message || e) + " | " + diag.join(" | "));
        }
    }

    /* ============================================================
     * applyOverlay — importa MP4 + insere no V3 sobre playhead + blend mode Screen
     * ============================================================ */
    function applyOverlay(mediaPath, blendMode) {
        var diag = [];
        try {
            if (!app || !app.project) return err("Nenhum projeto aberto");
            var seq = app.project.activeSequence;
            if (!seq) return err("Abra uma sequência antes de aplicar");
            try { app.enableQE(); } catch (e) {}

            var mf = new File(mediaPath);
            if (!mf.exists) return err("Arquivo não encontrado: " + mediaPath);

            // Importa o MP4 (ou reusa se já está no projeto)
            var allBefore = [];
            collectAllProjectItems(app.project.rootItem, allBefore);
            var beforeKeys = {};
            for (var s = 0; s < allBefore.length; s++) beforeKeys[itemKey(allBefore[s])] = true;

            var importBin = ensureImportBin();
            try { app.project.importFiles([mf.fsName], true, importBin, false); }
            catch (eI) { return err("Falha import MP4: " + (eI.message || eI)); }

            var allAfter = [];
            collectAllProjectItems(app.project.rootItem, allAfter);
            var imported = null;
            for (var i = 0; i < allAfter.length; i++) {
                if (!beforeKeys[itemKey(allAfter[i])]) { imported = allAfter[i]; break; }
            }
            if (!imported) {
                // Talvez já existia — busca por nome (ExtendScript: mf.name é o filename)
                var wantedName = String(mf.name).replace(/\.(mp4|mov)$/i, "");
                for (var j = 0; j < allAfter.length; j++) {
                    if (String(allAfter[j].name) === wantedName) {
                        imported = allAfter[j]; break;
                    }
                }
            }
            if (!imported) return err("Item importado não encontrado");

            diag.push("imported=" + imported.name);

            // Target V3 (acima do clip + color grading no V2)
            var targetIdx = 2;   // V3
            try { ensureVideoTracks(seq, targetIdx + 1); } catch (e) {}
            if (!seq.videoTracks[targetIdx]) targetIdx = Math.max(0, seq.videoTracks.numTracks - 1);
            var targetTrack = seq.videoTracks[targetIdx];

            // Snap a um gap livre no V3
            var insertSec = Number(seq.getPlayerPosition().seconds);
            var insertTicks = Math.round(insertSec * 254016000000);
            for (var att = 0; att < 50; att++) {
                var conflict = null;
                for (var ic = 0; ic < targetTrack.clips.numItems; ic++) {
                    var ex = targetTrack.clips[ic];
                    var es = Number(ex.start.ticks);
                    var ee = Number(ex.end.ticks);
                    if (insertTicks >= es && insertTicks < ee) { conflict = ex; break; }
                }
                if (!conflict) break;
                insertTicks = Number(conflict.end.ticks) + 1;
            }
            insertSec = insertTicks / 254016000000;

            var before = targetTrack.clips.numItems;
            try { targetTrack.overwriteClip(imported, insertSec); }
            catch (eOw) { return err("overwriteClip falhou: " + (eOw.message || eOw)); }
            var after = targetTrack.clips.numItems;

            // Acha o clip recém-inserido e seta blend mode
            var insertedClip = null;
            for (var ai = 0; ai < after; ai++) {
                var ac = targetTrack.clips[ai];
                if (Math.abs(Number(ac.start.seconds) - insertSec) < 0.05) { insertedClip = ac; break; }
            }
            var blendSetOk = false;
            if (insertedClip) {
                // Opacity component tem propriedade "Blend Mode" — int (Screen=10, Add=4)
                var BLEND_MODE_MAP = { Screen: 10, Add: 4, Lighten: 7, Multiply: 1, Overlay: 5, "Soft Light": 16 };
                var bm = blendMode || "Screen";
                var bmVal = BLEND_MODE_MAP[bm];
                if (bmVal !== undefined) {
                    try {
                        var opacity = _bitFindComponentByMatchName(insertedClip, "AE.ADBE Opacity");
                        if (!opacity) opacity = insertedClip.components[1]; // Opacity geralmente é o 2o
                        if (opacity) {
                            for (var p2 = 0; p2 < opacity.properties.numItems; p2++) {
                                var prop2 = opacity.properties[p2];
                                if (String(prop2.displayName || "").toLowerCase().indexOf("blend") >= 0) {
                                    prop2.setValue(bmVal, true);
                                    blendSetOk = true;
                                    diag.push("blend set to " + bm + "(" + bmVal + ")");
                                    break;
                                }
                            }
                        }
                    } catch (eB) { diag.push("blend set throw: " + (eB.message || eB)); }
                }
            }

            // Move playhead pra cima do clip novo
            try { seq.setPlayerPosition(insertTicks); } catch (e) {}

            return ok({
                ok: true,
                applied: true,
                track: targetIdx,
                insertSec: insertSec.toFixed(2),
                blendSetOk: blendSetOk,
                hint: "✓ Overlay '" + String(imported.name) + "' inserido no V" + (targetIdx+1) + (blendSetOk ? " (blend Screen)" : ""),
                diag: diag.join(" | ")
            });
        } catch (e) {
            return err("applyOverlay throw: " + (e.message || e) + " | " + diag.join(" | "));
        }
    }

    return {
        ping:                  ping,
        importMogrt:           importMogrt,
        importAudio:           importAudio,
        importPresetProject:   importPresetProject,
        applyAtomxAsset:       applyAtomxAsset,
        runPprSequencer:       runPprSequencer,
        getActiveSequenceInfo: getActiveSequenceInfo,
        startPreRender:        startPreRender,
        preRenderStatus:       preRenderStatus,
        applyBuiltInTransition: applyBuiltInTransition,
        applyLUT:              applyLUT,
        applyOverlay:          applyOverlay
    };
})();
