/*
 * ai-runner.js — executes Adobe Illustrator ExtendScript (.jsx) against ai-dom.js.
 *
 * Responsibilities:
 *  - strip ExtendScript preprocessor directives (#target, #include, #strict, …)
 *  - provide the ExtendScript runtime shims scripts expect ($, File, Folder,
 *    alert/confirm/prompt, Window/ScriptUI) so they don't throw
 *  - run the script inside a single undo batch and capture its console output
 *  - expose main.js_run_jsx(...) as the Swift entry point
 */
(function (global) {
  'use strict';

  var OUTPUT = [];
  // ExtendScript localizable strings are objects like {en:'…', ru:'…'} — render en.
  function loc(s) {
    if (s && typeof s === 'object' && !(s instanceof Array)) {
      if (typeof s.en === 'string') return s.en;
      for (var k in s) if (typeof s[k] === 'string') return s[k];
    }
    return String(s);
  }
  function out(s) { s = loc(s); OUTPUT.push(s); try { ace_wk_logger.log('[jsx] ' + s); } catch (e) {} }

  // ---- SVGE-1340: pre-seeded prompt() answers --------------------------------
  //
  // The Scripts palette can now ask the user for a script's parameters BEFORE the
  // run (double-click on a script → parameters dialog). The dialog is built by a
  // STATIC scan of the .jsx source for its prompt() calls — ExtendScript has no
  // parameter convention of its own, so the prompt() calls ARE the declaration
  // (see demo/08-random-delete.jsx, which states that contract explicitly).
  // SK-1623 added an EXPLICIT one on top of this (the `@param` header, below);
  // the prompt() scan stays as the fallback for every script without it.
  //
  // TRANSPORT: the answers ride on the SOURCE STRING itself, as one extra LAST
  // line `//__SVGE_JSX_PARAMS__:<JSON array of strings>`. This is the only channel
  // that reaches BOTH engines: under master=skia the .jsx is run by scripting.mm
  // on JavaScriptCore, which calls AI_runJsx(source) with the source as its ONLY
  // argument, so a new function parameter would need the C++ core signature
  // changed too. Appending (never prepending) keeps every line number of the
  // script itself intact, so error "(line N)" reporting is unaffected, and the
  // payload is a plain `//` comment: if anything about it is off, the line is
  // simply left in the source and ignored by the JS parser.
  //
  // The payload is RAW JSON, not base64: JSON.stringify escapes every control
  // character, so the array always fits on one line, and — unlike atob() — it
  // needs no browser-only global (the native JavaScriptCore context has none).
  //
  // The queue is POSITIONAL: the Nth prompt() executed at runtime gets the Nth
  // answer. When it runs dry (a prompt inside a loop, or a branch the scanner
  // could not see) prompt() falls back to its own default exactly as before, so a
  // mis-scanned script degrades to today's headless behaviour and never hangs.
  var PROMPT_ANSWERS = [];
  var PROMPT_AT = 0;

  /// Next pre-seeded answer for a prompt(), or the script's own default.
  /// Returns {value, supplied} so the [prompt] log line can show the value that
  /// was actually used only when it came from the dialog.
  function nextPromptAnswer(d) {
    if (PROMPT_AT < PROMPT_ANSWERS.length) {
      return { value: String(PROMPT_ANSWERS[PROMPT_AT++]), supplied: true };
    }
    return { value: d != null ? String(d) : '', supplied: false };
  }

  // ---- SK-1623: DECLARED script parameters (`@param` header) -----------------
  //
  // A .jsx can DECLARE its parameters in its header comment, one line each:
  //
  //     // @param <key> | <Label> | <type> | <default> | <extra>
  //
  //   key      the name the script reads the value back with
  //   Label    English text shown in the dialog (default: the key)
  //   type     string | number | integer | boolean | choice (default: string)
  //   default  pre-filled value
  //   extra    number/integer → "min..max"; choice → comma-separated options
  //
  // The Scripts palette (Run, double-click) and File ▸ Scripts show a modal built
  // from those lines BEFORE the run and transport the collected values here as
  // `//__SVGE_JSX_ARGS__:<JSON object>` on the source tail — same trailing-comment
  // trick as the positional prompt answers above, for the same reason (one source
  // string is the only payload that reaches both the WebView and the native
  // JavaScriptCore engine). The object is handed to the script as the global
  // `SVGE_PARAMS`, typed (numbers as numbers, booleans as booleans). A script
  // stays Illustrator-compatible by guarding with typeof:
  //
  //     var n = (typeof SVGE_PARAMS !== 'undefined' && SVGE_PARAMS.fonts != null)
  //           ? SVGE_PARAMS.fonts : DEFAULT;
  //
  // A script that declares nothing gets `{}` and behaves exactly as before.
  var SCRIPT_ARGS = {};

  // `//__SVGE_JSX_PARAMS__:<json>` / `//__SVGE_JSX_ARGS__:<json>` on the LAST line.
  var PARAMS_RE = /\n[ \t]*\/\/__SVGE_JSX_PARAMS__:([^\n\r]*)[\r\n \t]*$/;
  var ARGS_RE = /\n[ \t]*\/\/__SVGE_JSX_ARGS__:([^\n\r]*)[\r\n \t]*$/;

  /// Splits the transported answers/arguments off the source (both markers may be
  /// present, in any order — they are peeled off the tail until none is left).
  /// Never throws: a malformed payload is dropped and the script still runs with
  /// its own defaults.
  function takeParams(src) {
    PROMPT_ANSWERS = [];
    PROMPT_AT = 0;
    SCRIPT_ARGS = {};
    for (var strip = 0; strip < 4; strip++) {
      var m = PARAMS_RE.exec(src);
      if (m) {
        try {
          var arr = JSON.parse(String(m[1]).replace(/[\s]+$/, ''));
          if (arr && arr.length) {
            for (var i = 0; i < arr.length; i++) {
              PROMPT_ANSWERS.push(arr[i] == null ? '' : String(arr[i]));
            }
          }
        } catch (e) { PROMPT_ANSWERS = []; }
        src = src.slice(0, m.index);
        continue;
      }
      m = ARGS_RE.exec(src);
      if (m) {
        try {
          var obj = JSON.parse(String(m[1]).replace(/[\s]+$/, ''));
          if (obj && typeof obj === 'object') SCRIPT_ARGS = obj;
        } catch (e2) { SCRIPT_ARGS = {}; }
        src = src.slice(0, m.index);
        continue;
      }
      break;
    }
    return src;
  }

  // ---- ExtendScript runtime shims -------------------------------------------
  function ExtFile(path) { this.fsName = path || ''; this.name = (path || '').split('/').pop(); this.path = path || ''; this.exists = false; }
  ExtFile.prototype.open = function () { return true; };
  ExtFile.prototype.write = function () { return true; };
  ExtFile.prototype.writeln = function () { return true; };
  ExtFile.prototype.read = function () { return ''; };
  ExtFile.prototype.close = function () { return true; };
  ExtFile.prototype.toString = function () { return this.fsName; };

  function ExtFolder(path) { this.fsName = path || ''; this.name = (path || '').split('/').pop(); this.path = path || ''; }
  ExtFolder.prototype.getFiles = function () { return []; };
  ExtFolder.prototype.toString = function () { return this.fsName; };
  ExtFolder.selectDialog = function () { return null; };
  ExtFolder.myDocuments = new ExtFolder('~/Documents');
  ExtFolder.desktop = new ExtFolder('~/Desktop');

  var Dollar = {
    writeln: function () { out(Array.prototype.join.call(arguments, ' ')); },
    write: function () { OUTPUT.push(Array.prototype.join.call(arguments, ' ')); },
    sleep: function () {},
    getenv: function () { return ''; },
    setenv: function () {},
    evalFile: function () { out('$.evalFile ignored (sandbox)'); },
    gc: function () {},
    global: global,
    os: 'Macintosh',
    version: '28.0',
    locale: 'en_US',
    screens: [{ left: 0, top: 0, right: 1920, bottom: 1080 }],
    hiresTimer: 0
  };

  // ScriptUI stub — scripts that build dialogs run headless with defaults.
  function SUIWindow() { this.children = []; }
  SUIWindow.prototype.add = function () { var c = new SUIControl(); this.children.push(c); return c; };
  SUIWindow.prototype.show = function () { return 1; };   // as if user clicked OK
  SUIWindow.prototype.close = function () {};
  SUIWindow.prototype.center = function () {};
  function SUIControl() { this.text = ''; this.value = 0; this.selection = null; this.children = []; }
  SUIControl.prototype.add = function () { var c = new SUIControl(); this.children.push(c); return c; };
  SUIControl.prototype.onClick = function () {};
  var SUIFactory = function () { return new SUIWindow(); };
  SUIFactory.alert = function (m) { hostAlert(loc(m)); };
  SUIFactory.confirm = function (m) { return hostConfirm(loc(m)); };
  SUIFactory.prompt = function (m, d) {
    var a = nextPromptAnswer(d);
    if (a.supplied) { out('[prompt] ' + loc(m) + ' → ' + a.value); return a.value; }
    return hostPrompt(loc(m), d);
  };

  // ---- SVGE-1707: native interactive dialogs --------------------------------
  //
  // "quando gira lo script e chiede qualcosa dovrebbe creare una finestra." A script
  // that calls prompt()/confirm()/alert() at runtime must pop a REAL window and block
  // until the user answers, the value flowing straight back into the script.
  //
  // The bridge is `__SVGE_host`, injected into the native JavaScriptCore context by
  // scripting.mm ONLY when the app installed a modal handler (SVGESkiaScripting). Its
  // prompt/confirm/alert are SYNCHRONOUS host calls that run the run's thread on a native
  // NSAlert. When the bridge is absent — the CLI gate / scripting_test (headless), or the
  // legacy WKWebView path — these fall back to exactly the old headless behaviour
  // (prompt→its own default, confirm→true, alert→console only), so nothing that ran before
  // changes and no run can ever hang waiting on a window that will not appear.
  function hostBridge() {
    try { return (typeof __SVGE_host !== 'undefined' && __SVGE_host) ? __SVGE_host : null; }
    catch (e) { return null; }
  }
  function hostAlert(m) {
    out('[alert] ' + m);
    var h = hostBridge();
    if (h && typeof h.alert === 'function') { try { h.alert(String(m)); } catch (e) {} }
  }
  function hostConfirm(m) {
    var h = hostBridge();
    if (h && typeof h.confirm === 'function') {
      var r = true;
      try { r = !!h.confirm(String(m)); } catch (e) { r = true; }
      out('[confirm] ' + m + ' → ' + (r ? 'OK' : 'Cancel'));
      return r;
    }
    out('[confirm] ' + m);
    return true;
  }
  function hostPrompt(m, d) {
    var h = hostBridge();
    if (h && typeof h.prompt === 'function') {
      var r;
      try { r = h.prompt(String(m), d != null ? String(d) : ''); }
      catch (e) { r = null; }
      out('[prompt] ' + m + (r == null ? ' (cancelled)' : ' → ' + r));
      return r;   // null when the user cancelled — matches the browser prompt() contract
    }
    // Headless fallback: the script's own default, exactly as before.
    out('[prompt] ' + m);
    return d != null ? String(d) : '';
  }

  function localize(s) { return typeof s === 'string' ? s : (s && s.toString ? s.toString() : ''); }

  // ---- preprocessor ----------------------------------------------------------
  function preprocess(code) {
    // Drop ExtendScript directives that JS can't parse.
    code = code.replace(/^\s*#(target|targetengine|engine|script|strict|includepath|include)\b.*$/gim, function (m) {
      return '// [stripped] ' + m.trim();
    });
    // Strip a UTF-8 BOM if present.
    if (code.charCodeAt(0) === 0xFEFF) code = code.slice(1);
    return code;
  }

  // ---- base64 (scripts arrive base64-encoded from Swift) ----------------------
  function b64decode(str) {
    try { return decodeURIComponent(escape(atob(str))); }
    catch (e) { try { return atob(str); } catch (e2) { return str; } }
  }

  // ---- runner ----------------------------------------------------------------
  function runJsx(source) {
    OUTPUT = [];
    // SVGE-1340 — peel the pre-seeded prompt() answers off the tail FIRST (this
    // also resets the queue, so answers can never leak into the next run).
    source = takeParams(String(source == null ? '' : source));
    var code = preprocess(source);
    try { if (window.svge_debug) svge_debug('SCRIPT ▸ run (' + (source ? source.length : 0) + ' chars)'); } catch (_) {}

    // #648 STRUCTURED RETURN: `value` carries the script's final-expression /
    // return VALUE (JSON-serializable), so a bare expression like
    // `SVGETest.getSelectionInfo()` surfaces its result WITHOUT `$.writeln`. It
    // stays null when the script produced no value (writeln-only scripts), so the
    // console `output` path (existing callers, #635) is untouched.
    var result = { ok: true, value: null, output: '', error: null };

    // The ExtendScript runtime shims are handed to the script as FUNCTION
    // PARAMETERS, NOT by clobbering the page globals. This is critical: the
    // editor's own code (svgCanvas.getSvgString, the test-hooks oracle, etc.)
    // uses jQuery via the global `$` — if we overwrote `window.$` with the
    // ExtendScript `Dollar` object for the duration of the run, every canvas
    // method the script calls (getSvgString, setResolution, bbox …) would throw
    // "$ is not a function". Passing them as params gives the SCRIPT its `$`
    // (Dollar) while the page keeps jQuery, so both worlds work together.
    var rt = {
      $: Dollar, File: ExtFile, Folder: ExtFolder, Window: SUIFactory,
      ScriptUI: { newFont: function () { return {}; }, newImage: function () { return {}; } },
      localize: localize,
      alert: function (m) { hostAlert(loc(m)); },
      confirm: function (m) { return hostConfirm(loc(m)); },
      prompt: function (m, d) {
        // SVGE-1340: answered by the palette's parameters dialog when the run was
        // started from a double-click (pre-seeded answer wins, no re-ask). Otherwise
        // SVGE-1707: a real native input window when the app installed the host bridge,
        // falling back to the headless default when it did not (CLI gate / WebView).
        var a = nextPromptAnswer(d);
        if (a.supplied) { out('[prompt] ' + loc(m) + ' → ' + a.value); return a.value; }
        return hostPrompt(loc(m), d);
      },
      // SK-1623 — the values collected by the parameters modal from the script's
      // `@param` header. Always present (an empty object when the script declares
      // nothing or was run headlessly), so a guarded read never throws here while
      // the same .jsx still falls back to its own default under real Illustrator.
      SVGE_PARAMS: SCRIPT_ARGS
    };
    global.__SVGE_rt = rt;
    global.__SVGE_src = code;   // the preprocessed source, direct-eval'd for its value

    try {
      if (global.AIDOM && global.AIDOM.AI) global.AIDOM.AI.begin();

      // Run inside a function scope — NOT the raw global scope. ExtendScript
      // engines give scripts their own global where identifiers like `top`,
      // `name`, `parent`, `length`, `location` are ordinary variables. In a
      // browser window those are read-only Window properties, so a top-level
      // `var top = 40` would silently fail and leave `top === window.top`.
      // A function wrapper makes such `var`s true locals that shadow Window,
      // while `app`, colors and enums still resolve up the scope chain. The
      // ExtendScript shims arrive as params (see above) so they shadow the page
      // globals only inside this scope, never clobbering window.$ etc.
      //
      // #648: the wrapper `return eval(window.__SVGE_src)` — a DIRECT eval in the
      // function's scope — both (a) preserves the params/var-shadowing semantics
      // above (the code still sees $/File/… and its `var`s stay function-local)
      // AND (b) yields the completion value of the script's LAST expression, which
      // the IIFE returns. That value is what we serialize into result.value.
      var indirectEval = eval;
      var value = indirectEval('(function($,File,Folder,Window,ScriptUI,localize,alert,confirm,prompt,SVGE_PARAMS){\n' +
        'return eval(window.__SVGE_src);\n' +
        '})(window.__SVGE_rt.$,window.__SVGE_rt.File,window.__SVGE_rt.Folder,window.__SVGE_rt.Window,' +
        'window.__SVGE_rt.ScriptUI,window.__SVGE_rt.localize,window.__SVGE_rt.alert,' +
        'window.__SVGE_rt.confirm,window.__SVGE_rt.prompt,window.__SVGE_rt.SVGE_PARAMS);');

      // Serialize the captured value defensively: keep it raw when it round-trips
      // through JSON (so the whole result serializes cleanly); otherwise fall back
      // to a string; `undefined` (no value produced) stays null.
      if (value === undefined) {
        result.value = null;
      } else {
        try { JSON.stringify(value); result.value = value; }
        catch (eVal) { try { result.value = String(value); } catch (eVal2) { result.value = null; } }
      }

      if (global.AIDOM && global.AIDOM.AI) global.AIDOM.AI.commit();
    } catch (e) {
      result.ok = false;
      result.error = (e && e.message ? e.message : String(e)) + (e && e.line ? ' (line ' + e.line + ')' : '');
      out('ERROR: ' + result.error);
      try { if (global.AIDOM && global.AIDOM.AI) global.AIDOM.AI.commit(); } catch (e2) {}
    } finally {
      try { delete global.__SVGE_rt; } catch (e) { global.__SVGE_rt = undefined; }
      try { delete global.__SVGE_src; } catch (e) { global.__SVGE_src = undefined; }
    }

    result.output = OUTPUT.join('\n');
    try {
      if (window.svge_debug) {
        svge_debug('SCRIPT ' + (result.ok ? '◂ ok' : '✗ ' + result.error) +
                   (result.output ? ' | ' + result.output.replace(/\n/g, ' ⏎ ').slice(0, 200) : ''));
      }
    } catch (_) {}
    return result;
  }

  // ---- Swift entry points ----------------------------------------------------
  // SVGEAddon / main are lexical top-level bindings from add.js (class/const),
  // NOT properties of window — reference them by bare name.
  if (typeof SVGEAddon !== 'undefined' && SVGEAddon.prototype) {
    // main.js_run_jsx(base64) — decode + run, returns a JSON string {ok,output,error}
    SVGEAddon.prototype.js_run_jsx = function (b64) {
      var res;
      try { res = runJsx(b64decode(b64)); }
      catch (e) { res = { ok: false, output: '', error: String(e) }; }
      try { window.webkit.messageHandlers.msg.postMessage({ ai_script_result: res }); } catch (e) {}
      return JSON.stringify(res);
    };
    // main.js_run_jsx_raw(source) — run an already-decoded source string
    SVGEAddon.prototype.js_run_jsx_raw = function (src) {
      var res = runJsx(String(src || ''));
      try { window.webkit.messageHandlers.msg.postMessage({ ai_script_result: res }); } catch (e) {}
      return JSON.stringify(res);
    };
  }

  // Also expose directly for console use.
  global.AI_runJsx = runJsx;

})(this);
