// bind.jsx — FlatRender data-driven binder (v1 convention).
//
// Reads a JSON "bind-spec" and writes the user's values INTO a template, then
// saves a "bound" .aep that aerender renders. This is the data-driven successor
// to the legacy JSXGenerator string-concat approach: render-svc emits the JSON,
// this one generic script interprets it.
//
// Environment:
//   FR_BIND_AEP   — path to the template .aep to open
//   FR_BIND_SPEC  — path to the bind-spec JSON file
//   FR_BIND_SAVE  — path to write the bound .aep (aerender input)
//   FR_BIND_QUIT  — "0" to keep AE open (debug); default quits
//
// Bind-spec shape (v1):
//   {
//     "comp": "frfinal", "duration": 15, "fps": 30,
//     "colors": [ { "key": "frd_primary", "value": "#3366ff" } ],   // frshare text layers
//     "data":   [ { "key": "frd_toggle",  "value": "1" } ],         // frd_ data layers
//     "layers": [
//        { "key":"frl_title","type":"text","value":"Hello",
//          "font":"Vazirmatn","size":72,"justify":"CENTER_JUSTIFY" },
//        { "key":"frl_logo","type":"media","value":"C:/work/cdn/logo.png" },
//        { "key":"frl_music","type":"audio","value":"C:/work/cdn/track.mp3" }
//     ]
//   }
//
// Conventions (see docs/aep-template-convention.md):
//   - colours live as TEXT layers inside the `frshare` comp; their Source Text IS
//     the colour value, and template expressions read them → set once, propagate.
//   - frl_ = editable visible layers (text / media / audio).
//   - frd_ = data/direction layers (hidden values, RTL companions).

(function () {
  function getenv(n) { try { return $.getenv(n); } catch (e) { return null; } }
  function readFile(p) { var f = new File(p); f.open("r"); var s = f.read(); f.close(); return s; }
  function marker(p, txt) { try { var f = new File(p); f.open("w"); f.write(txt); f.close(); } catch (e) {} }

  var aep = getenv("FR_BIND_AEP");
  var specPath = getenv("FR_BIND_SPEC");
  var savePath = getenv("FR_BIND_SAVE");
  var donePath = (savePath || "bind") + ".done";
  var errPath = (savePath || "bind") + ".error";

  try {
    if (aep) app.open(new File(aep));
    var proj = app.project;

    var spec = {};
    if (specPath) spec = eval("(" + readFile(specPath) + ")"); // bind-spec JSON

    // index the spec by layer key for O(1) lookups while walking the project
    var colorMap = {}, dataMap = {}, layerMap = {}, i;
    if (spec.colors) for (i = 0; i < spec.colors.length; i++) colorMap[spec.colors[i].key] = spec.colors[i];
    if (spec.data) for (i = 0; i < spec.data.length; i++) dataMap[spec.data[i].key] = spec.data[i];
    if (spec.layers) for (i = 0; i < spec.layers.length; i++) layerMap[spec.layers[i].key] = spec.layers[i];

    // ── helpers ───────────────────────────────────────────────────────────
    function setText(layer, value) {
      var p = layer.property("Source Text");
      if (!p) return;
      var d = p.value;
      d.text = (value == null) ? "" : ("" + value);
      p.setValue(d);
    }
    function justifyOf(name) {
      switch (name) {
        case "LEFT_JUSTIFY": return ParagraphJustification.LEFT_JUSTIFY;
        case "RIGHT_JUSTIFY": return ParagraphJustification.RIGHT_JUSTIFY;
        case "FULL_JUSTIFY": return ParagraphJustification.FULL_JUSTIFY;
        default: return ParagraphJustification.CENTER_JUSTIFY;
      }
    }
    function textBind(layer, item) {
      var p = layer.property("Source Text");
      if (!p) return;
      var d = p.value;
      d.text = (item.value == null) ? "" : ("" + item.value);
      if (item.font) d.font = item.font;
      if (item.size) d.fontSize = item.size;
      if (item.justify) d.justification = justifyOf(item.justify);
      p.setValue(d);
      // NOTE (v1 TODO): box auto-fit + positionMode 0-8 anchoring go here later.
    }
    function mediaBind(layer, item) {
      if (!item.value) return;
      try {
        var footage = proj.importFile(new ImportOptions(new File(item.value)));
        layer.replaceSource(footage, false);
        // NOTE (v1 TODO): scale-to-fit the box (legacy mediaBind) goes here later.
      } catch (e) {}
    }

    function bindLayer(layer) {
      var name = layer.name;
      if (layerMap[name]) {
        var it = layerMap[name];
        if (it.type === "media" || it.type === "audio") mediaBind(layer, it);
        else textBind(layer, it);
      } else if (dataMap[name]) {
        setText(layer, dataMap[name].value); // frd_ data / direction layers
      }
    }

    // ── walk every comp; bind layers by name (frl_/frd_ live anywhere in the tree) ──
    for (i = 1; i <= proj.numItems; i++) {
      var item = proj.item(i);
      if (!(item instanceof CompItem)) continue;
      if (item.name === "frshare") {
        for (var c = 1; c <= item.numLayers; c++) {
          var L = item.layer(c);
          if (colorMap[L.name]) setText(L, colorMap[L.name].value);
        }
      }
      for (var l = 1; l <= item.numLayers; l++) bindLayer(item.layer(l));
    }

    // ── comp timing ─────────────────────────────────────────────────────────
    var comp = null, target = spec.comp || "frfinal";
    for (i = 1; i <= proj.numItems; i++) {
      var x = proj.item(i);
      if (x instanceof CompItem && x.name === target) { comp = x; break; }
    }
    if (comp) {
      if (spec.duration) comp.duration = spec.duration;
      if (spec.fps) comp.frameRate = spec.fps;
    }

    // ── save the bound project for aerender ───────────────────────────────────
    if (savePath) proj.save(new File(savePath));
    marker(donePath, "ok comp=" + (comp ? comp.name : "?"));
  } catch (e) {
    marker(errPath, "bind error: " + e.toString());
  }
  try { if (getenv("FR_BIND_QUIT") !== "0") app.quit(); } catch (e) {}
})();
