/*
    vfxr_build_preview.jsx — VFXReview render pipeline, preview build stage.

    Runs headlessly:  AfterFX.com -noui -r <wrapper>.jsx
    where the wrapper sets  var VFXR_CONTEXT_PATH = "...json";  then
    $.evalFile()s this script.

    It does NOT render. It opens the studio slate/overlay template, rebuilds
    the shot around the *rendered* EXR sequence (OCIO → show LUT → overlay),
    duplicates the export template into a preview comp with the slate filled
    in, queues the MOV and MP4 output modules, and saves a throwaway AEP.
    The worker then runs `aerender -project <that aep>` with no -comp, which
    renders the whole queue in one launch.

    Build logic mirrors the VFXReviewConnector panel functions
    buildShotFromCode / updateOverlayForComp / buildPreviewForShot so the
    farm and the artist produce identical slates and burn-ins.

    Everything is reported back through the result JSON — never a dialog.
*/

(function vfxrBuildPreview() {
    var ctx = null;
    var result = {
        ok: false,
        error: null,
        warnings: [],
        tempAep: null,
        previewComp: null,
        queued: [],
        frameCount: null,
        width: null,
        height: null
    };
    var resultPath = null;

    // ── tiny helpers (ES3 / ExtendScript safe) ───────────────────────────────

    function readTextFile(path) {
        var f = new File(path);
        var text;
        if (!f.exists) {
            return null;
        }
        f.encoding = "UTF-8";
        if (!f.open("r")) {
            return null;
        }
        text = f.read();
        f.close();
        return text;
    }

    function writeTextFile(path, text) {
        var f = new File(path);
        try {
            f.encoding = "UTF-8";
            if (!f.open("w")) {
                return false;
            }
            f.write(text);
            f.close();
            return true;
        } catch (writeError) {
            return false;
        }
    }

    function jsonEscape(value) {
        var text = String(value);
        var out = "";
        var i;
        var ch;
        var code;

        for (i = 0; i < text.length; i += 1) {
            ch = text.charAt(i);
            code = text.charCodeAt(i);
            if (ch === "\"") {
                out += "\\\"";
            } else if (ch === "\\") {
                out += "\\\\";
            } else if (ch === "\n") {
                out += "\\n";
            } else if (ch === "\r") {
                out += "\\r";
            } else if (ch === "\t") {
                out += "\\t";
            } else if (code < 32) {
                out += "\\u" + ("000" + code.toString(16)).slice(-4);
            } else {
                out += ch;
            }
        }
        return out;
    }

    function toJSON(value) {
        var parts = [];
        var i;
        var key;

        if (value === null || value === undefined) {
            return "null";
        }
        if (typeof value === "number") {
            return isFinite(value) ? String(value) : "null";
        }
        if (typeof value === "boolean") {
            return String(value);
        }
        if (typeof value === "string") {
            return "\"" + jsonEscape(value) + "\"";
        }
        if (value instanceof Array) {
            for (i = 0; i < value.length; i += 1) {
                parts.push(toJSON(value[i]));
            }
            return "[" + parts.join(",") + "]";
        }
        for (key in value) {
            if (value.hasOwnProperty(key) && value[key] !== undefined) {
                parts.push("\"" + jsonEscape(key) + "\":" + toJSON(value[key]));
            }
        }
        return "{" + parts.join(",") + "}";
    }

    function warn(message) {
        result.warnings.push(String(message));
    }

    function findComp(name) {
        var i;
        var item;

        for (i = 1; i <= app.project.numItems; i += 1) {
            item = app.project.item(i);
            if (item instanceof CompItem && item.name === name) {
                return item;
            }
        }
        return null;
    }

    function findFolder(name) {
        var i;
        var item;

        for (i = 1; i <= app.project.numItems; i += 1) {
            item = app.project.item(i);
            if (item instanceof FolderItem && item.name === name) {
                return item;
            }
        }
        return null;
    }

    // Mirrors the panel's addOCIOToLayer: effect added but disabled, so the
    // artist/farm chain matches exactly.
    function addOCIOToLayer(layer) {
        var ocio;
        var outputProp;

        try {
            ocio = layer.property("Effects").addProperty("OCIO Color Space Transform");
            ocio.enabled = false;
        } catch (ocioError) {
            warn("OCIO effect unavailable: " + ocioError.toString());
            return;
        }

        try {
            outputProp = ocio.property("Output Color Space");
        } catch (byNameError) {
            outputProp = null;
        }
        if (!outputProp) {
            try {
                outputProp = ocio.property(2);
            } catch (byIndexError) {
                outputProp = null;
            }
        }
        if (outputProp) {
            try {
                outputProp.setValue(93);
            } catch (setError) {
                warn("Could not set OCIO output colour space: " + setError.toString());
            }
        }
    }

    function setEssentialProperty(layer, propName, value) {
        if (value === null || value === undefined || value === "") {
            return false;
        }
        try {
            layer.property("Essential Properties").property(propName).setValue(value);
            return true;
        } catch (setError) {
            return false;
        }
    }

    function setEssentialPropertyByIndex(layer, index, value) {
        try {
            layer.property("Essential Properties").property(index).setValue(value);
            return true;
        } catch (setError) {
            return false;
        }
    }

    function listEssentialPropertyNames(layer) {
        var names = [];
        var group;
        var i;

        try {
            group = layer.property("Essential Properties");
            for (i = 1; i <= group.numProperties; i += 1) {
                try {
                    names.push(i + ":" + group.property(i).name);
                } catch (oneError) {
                }
            }
        } catch (groupError) {
        }
        return names;
    }

    // Per-submission fields (VFX Scope / Submission Note). The property names
    // come from config; if one does not exist, report the template's actual
    // property names so it can be corrected without another build.
    function setSlateField(layer, propName, value, label) {
        if (!propName) {
            return;
        }
        if (value === null || value === undefined || value === "") {
            return; // nothing to write; leave the template's own default
        }
        if (!setEssentialProperty(layer, propName, value)) {
            warn(label + ": Essential Property \"" + propName + "\" not found or not settable. " +
                "Available: " + listEssentialPropertyNames(layer).join(", "));
        }
    }

    function setCompStartFrame(comp, frameNumber) {
        try {
            comp.displayStartFrame = frameNumber;
            return;
        } catch (frameError) {
        }
        try {
            comp.displayStartTime = frameNumber / comp.frameRate;
        } catch (timeError) {
        }
    }

    function applyOutputModuleTemplate(outputModule, templateName) {
        var templates;
        var i;

        try {
            templates = outputModule.templates;
            for (i = 0; i < templates.length; i += 1) {
                if (templates[i] === templateName) {
                    outputModule.applyTemplate(templateName);
                    return true;
                }
            }
        } catch (templateError) {
        }
        return false;
    }

    function importExrSequence(dirPath) {
        var folder = new Folder(dirPath);
        var files;
        var importOptions;
        var footage;

        if (!folder.exists) {
            throw new Error("Render output folder not found: " + dirPath);
        }

        files = folder.getFiles("*.exr");
        if (!files || files.length < 1) {
            throw new Error("No EXR files found in " + dirPath);
        }
        files.sort();

        importOptions = new ImportOptions(files[0]);
        importOptions.sequence = true;
        footage = app.project.importFile(importOptions);
        try {
            footage.mainSource.conformFrameRate = ctx.fps || 24;
        } catch (conformError) {
            warn("Could not conform frame rate: " + conformError.toString());
        }
        return footage;
    }

    function queueOutput(comp, templateName, outputPath, label) {
        var item;
        var outputModule;

        if (!templateName || !outputPath) {
            warn("Skipping " + label + ": no template or output path configured");
            return false;
        }

        try {
            item = app.project.renderQueue.items.add(comp);
            outputModule = item.outputModule(1);
            if (!applyOutputModuleTemplate(outputModule, templateName)) {
                item.remove();
                throw new Error("Output module template not found: \"" + templateName + "\"");
            }
            outputModule.file = new File(outputPath);
            result.queued.push({ label: label, template: templateName, output: outputPath });
            return true;
        } catch (queueError) {
            try {
                if (item) {
                    item.remove();
                }
            } catch (removeError) {
            }
            throw queueError;
        }
    }

    // ── main ─────────────────────────────────────────────────────────────────

    try {
        if (typeof VFXR_CONTEXT_PATH === "undefined" || !VFXR_CONTEXT_PATH) {
            throw new Error("VFXR_CONTEXT_PATH was not set by the wrapper script");
        }

        var contextText = readTextFile(VFXR_CONTEXT_PATH);
        if (!contextText) {
            throw new Error("Could not read job context: " + VFXR_CONTEXT_PATH);
        }
        ctx = eval("(" + contextText + ")");
        resultPath = ctx.resultPath;

        try {
            app.beginSuppressDialogs();
        } catch (suppressError) {
        }

        // 1. Open the studio template (slate, overlay, show LUT, export template)
        var templateFile = new File(ctx.templateAep);
        if (!templateFile.exists) {
            throw new Error("Template project not found: " + ctx.templateAep);
        }
        app.open(templateFile);

        var templateComp = findComp(ctx.templateComp);
        if (!templateComp) {
            throw new Error("Export template comp \"" + ctx.templateComp + "\" not found in template project");
        }

        // 2. Import the rendered EXR sequence
        var footage = importExrSequence(ctx.outputDirLocal);
        var footageFolder = findFolder("_FOOTAGE_4K");
        if (footageFolder) {
            footage.parentFolder = footageFolder;
        }
        footage.name = ctx.shotCode + "_" + ctx.versionString;

        var width = footage.width;
        var height = footage.height;
        var duration = footage.duration;
        var fps = ctx.fps || 24;

        result.width = width;
        result.height = height;
        result.frameCount = Math.round(duration * fps);

        // 3. Footage precomp (mirrors buildShotFromCode)
        var precompFolder = findFolder("_PRECOMPS");
        var footageComp = app.project.items.addComp(
            ctx.shotCode + "_FOOTAGE", width, height, 1, duration, fps);
        if (precompFolder) {
            footageComp.parentFolder = precompFolder;
        }
        if (ctx.frameStart !== null && ctx.frameStart !== undefined) {
            setCompStartFrame(footageComp, ctx.frameStart);
        }
        addOCIOToLayer(footageComp.layers.add(footage));

        // 4. Shot comp: footage + show LUT + overlay
        var shotComp = app.project.items.addComp(ctx.shotCode, width, height, 1, duration, fps);
        if (ctx.frameStart !== null && ctx.frameStart !== undefined) {
            setCompStartFrame(shotComp, ctx.frameStart);
        }
        shotComp.layers.add(footageComp);

        if (ctx.lutComp) {
            var showLut = findComp(ctx.lutComp);
            if (showLut) {
                var lutLayer = shotComp.layers.add(showLut);
                lutLayer.collapseTransformation = true;
            } else {
                warn("Show LUT comp \"" + ctx.lutComp + "\" not found — rendering without it");
            }
        }

        if (ctx.overlayComp) {
            var overlayComp = findComp(ctx.overlayComp);
            if (overlayComp) {
                var overlayLayer = shotComp.layers.add(overlayComp);
                overlayLayer.moveToBeginning();
                overlayLayer.enabled = true;
                setEssentialProperty(overlayLayer, "DATE", ctx.slate.date);
                setEssentialProperty(overlayLayer, "SHOT NAME", ctx.slate.versionName);
            } else {
                warn("Overlay comp \"" + ctx.overlayComp + "\" not found — rendering without burn-ins");
            }
        }

        // 5. Preview comp from the export template (mirrors buildPreviewForShot)
        var previewsFolder = findFolder("_PREVIEWS");
        var previewComp = templateComp.duplicate();
        previewComp.name = ctx.shotCode + "_PREVIEW";
        if (previewsFolder) {
            previewComp.parentFolder = previewsFolder;
        }

        previewComp.layer("SHOT").replaceSource(shotComp, false);
        try {
            previewComp.layer("THUMBNAIL").replaceSource(shotComp, false);
        } catch (thumbError) {
            warn("THUMBNAIL layer not updated: " + thumbError.toString());
        }

        var slateLayer = previewComp.layer("NETFLIX_SLATE");
        if (slateLayer) {
            // Index-addressed properties match the panel exactly (3 = version
            // name, 10 = shot code); the rest are addressed by name.
            setEssentialPropertyByIndex(slateLayer, 3, ctx.slate.versionName);
            setEssentialProperty(slateLayer, "Date", ctx.slate.date);
            setEssentialProperty(slateLayer, "Desc", ctx.slate.description);
            setEssentialPropertyByIndex(slateLayer, 10, ctx.slate.shotCode);
            setEssentialProperty(slateLayer, "Episode", ctx.slate.episode);
            setEssentialProperty(slateLayer, "Scene", ctx.slate.scene);
            setEssentialProperty(slateLayer, "Frames", result.frameCount);

            setSlateField(slateLayer, ctx.slateScopeProp, ctx.slate.vfxScope, "VFX Scope");
            // The slate's Notes field carries this submission's note; when the
            // artist left it blank, fall back to the shot's own notes so the
            // slate looks the same as an artist-built preview.
            setSlateField(slateLayer, ctx.slateSubmissionProp,
                (ctx.slate.submissionNote !== null &&
                 ctx.slate.submissionNote !== undefined &&
                 ctx.slate.submissionNote !== "")
                    ? ctx.slate.submissionNote
                    : ctx.slate.notes,
                "Submission Note");
        } else {
            warn("NETFLIX_SLATE layer not found in template — slate fields not filled");
        }

        var shotLayer = previewComp.layer("SHOT");
        var targetOutPoint = shotLayer.inPoint + shotComp.duration;
        if (targetOutPoint > previewComp.duration) {
            previewComp.duration = targetOutPoint;
        }
        shotLayer.outPoint = targetOutPoint;
        previewComp.duration = targetOutPoint;

        result.previewComp = previewComp.name;

        // 6. Queue both outputs against the same preview comp. aerender will
        //    render the whole queue in one launch.
        app.project.renderQueue.showWindow(false);
        while (app.project.renderQueue.numItems > 0) {
            app.project.renderQueue.item(1).remove();
        }
        queueOutput(previewComp, ctx.movTemplate, ctx.movOutput, "mov");
        queueOutput(previewComp, ctx.mp4Template, ctx.mp4Output, "mp4");

        if (result.queued.length < 1) {
            throw new Error("Nothing was queued — check output module templates");
        }

        // 7. Save the throwaway project the worker will hand to aerender
        var tempFile = new File(ctx.tempAep);
        var tempParent = tempFile.parent;
        if (tempParent && !tempParent.exists) {
            tempParent.create();
        }
        app.project.save(tempFile);
        result.tempAep = ctx.tempAep;
        result.ok = true;
    } catch (buildError) {
        result.ok = false;
        result.error = buildError && buildError.toString ? buildError.toString() : String(buildError);
        try {
            if (buildError && buildError.line) {
                result.error += " (line " + buildError.line + ")";
            }
        } catch (lineError) {
        }
    }

    try {
        app.endSuppressDialogs(false);
    } catch (endSuppressError) {
    }

    if (!resultPath && ctx && ctx.resultPath) {
        resultPath = ctx.resultPath;
    }
    if (resultPath) {
        writeTextFile(resultPath, toJSON(result));
    }

    // -noui leaves the app running otherwise; the worker waits on exit.
    try {
        app.quit();
    } catch (quitError) {
    }
}());
