/*
 * CT Subs — панель After Effects: будує субтитри з файлу AE Subs (.aesubs),
 * який віддає транскрибатор на сайті. Формат описано в README.md поруч.
 *
 * ExtendScript = ES3: без JSON, let/const, стрілок і Array.prototype.map.
 *
 * Режими:
 *   lines      — один текстовий шар на картку (1–3 рядки), без анімації;
 *   karaokeW   — картки, слово підсвічується на своєму старті;
 *   karaokeC   — картки, підсвічування йде по літерах;
 *   typewriter — картки, літери з'являються по черзі;
 *   words      — один шар, на екрані одне слово за раз.
 */
(function (thisObj) {
  var SCRIPT_NAME = 'CT Subs';
  var FORMAT = 'ae-subs';
  var MAX_VERSION = 1;

  // Налаштування тексту: "Based On" і "Units" у Range Selector.
  var BASED_ON_CHARS_NO_SPACES = 2;
  var UNITS_INDEX = 2;

  var L = {
    load: { en: 'Load .aesubs…', uk: 'Відкрити .aesubs…' },
    noFile: { en: 'No file loaded', uk: 'Файл не вибрано' },
    loaded: { en: '%1 lines, %2 words', uk: 'Рядків: %1, слів: %2' },
    mode: { en: 'Mode', uk: 'Режим' },
    modeLines: { en: 'Lines', uk: 'Рядки' },
    modeKaraokeW: { en: 'Karaoke: by word', uk: 'Караоке: по словах' },
    modeKaraokeC: { en: 'Karaoke: by letter', uk: 'Караоке: по літерах' },
    modeTypewriter: { en: 'Typewriter', uk: 'Друкарська машинка' },
    modeWords: { en: 'One word at a time', uk: 'По одному слову' },
    perCard: { en: 'Lines per card', uk: 'Рядків на картці' },
    fontSize: { en: 'Font size', uk: 'Розмір шрифту' },
    posY: { en: 'Vertical position, %', uk: 'Позиція по вертикалі, %' },
    maxWidth: { en: 'Max width, %', uk: 'Макс. ширина, %' },
    safeZone: { en: 'Safe zone', uk: 'Безпечна зона' },
    safeNone: { en: 'None', uk: 'Без обмежень' },
    stroke: { en: 'Stroke', uk: 'Обведення' },
    strokeColor: { en: 'Stroke color', uk: 'Колір обведення' },
    offset: { en: 'Start at current time', uk: 'Починати з поточного часу' },
    template: {
      en: 'Selected text layer is used as a style template',
      uk: 'Виділений текстовий шар слугує шаблоном стилю',
    },
    baseColor: { en: 'Text color', uk: 'Колір тексту' },
    hiColor: { en: 'Highlight', uk: 'Підсвічування' },
    create: { en: 'Create subtitles', uk: 'Створити субтитри' },
    needComp: { en: 'Open a composition first.', uk: 'Спершу відкрий композицію.' },
    needFile: { en: 'Load an .aesubs file first.', uk: 'Спершу відкрий файл .aesubs.' },
    needAudio: {
      en: 'Select a layer with audio from a file (song or video).',
      uk: 'Виділи шар зі звуком із файлу (пісню чи відео).',
    },
    badFile: { en: 'This is not an AE Subs file: %1', uk: 'Це не файл AE Subs: %1' },
    newer: {
      en: 'The file was made by a newer version (v%1). Update the plugin.',
      uk: 'Файл створено новішою версією (v%1). Онови плагін.',
    },
    done: { en: 'Created %1 layer(s).', uk: 'Створено шарів: %1.' },
  };

  var LANG = String($.locale || 'en').substr(0, 2);

  /** t('loaded', 3, 10): рядок мовою AE з підставленими %1, %2. */
  function t(key) {
    var out = L[key][LANG] || L[key].en;
    for (var i = 1; i < arguments.length; i++) out = out.split('%' + i).join(String(arguments[i]));
    return out;
  }

  // ---------- JSON (ES3 не має JSON.parse) ----------

  function parseJson(src) {
    var i = 0;
    function fail(msg) {
      throw new Error(msg + ' @' + i);
    }
    function ws() {
      while (i < src.length && ' \t\r\n﻿'.indexOf(src.charAt(i)) !== -1) i++;
    }
    function value() {
      ws();
      var c = src.charAt(i);
      if (c === '{') return object();
      if (c === '[') return array();
      if (c === '"') return string();
      if (c === 't' && src.substr(i, 4) === 'true') return (i += 4), true;
      if (c === 'f' && src.substr(i, 5) === 'false') return (i += 5), false;
      if (c === 'n' && src.substr(i, 4) === 'null') return (i += 4), null;
      return number();
    }
    function object() {
      var out = {};
      i++;
      ws();
      if (src.charAt(i) === '}') return i++, out;
      for (;;) {
        ws();
        var key = string();
        ws();
        if (src.charAt(i++) !== ':') fail('":" expected');
        out[key] = value();
        ws();
        var c = src.charAt(i++);
        if (c === '}') return out;
        if (c !== ',') fail('"," expected');
      }
    }
    function array() {
      var out = [];
      i++;
      ws();
      if (src.charAt(i) === ']') return i++, out;
      for (;;) {
        out.push(value());
        ws();
        var c = src.charAt(i++);
        if (c === ']') return out;
        if (c !== ',') fail('"," expected');
      }
    }
    function string() {
      if (src.charAt(i) !== '"') fail('string expected');
      i++;
      var out = '';
      for (;;) {
        if (i >= src.length) fail('unterminated string');
        var c = src.charAt(i++);
        if (c === '"') return out;
        if (c !== '\\') {
          out += c;
          continue;
        }
        var e = src.charAt(i++);
        if (e === 'u') {
          out += String.fromCharCode(parseInt(src.substr(i, 4), 16));
          i += 4;
        } else {
          out += { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' }[e] || e;
        }
      }
    }
    function number() {
      var m = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/.exec(src.substr(i, 32));
      if (!m) fail('unexpected token');
      i += m[0].length;
      return parseFloat(m[0]);
    }
    var result = value();
    ws();
    if (i < src.length) fail('trailing data');
    return result;
  }

  function readSubs(file) {
    file.encoding = 'UTF-8';
    if (!file.open('r')) throw new Error(file.error);
    var text = file.read();
    file.close();
    var doc = parseJson(text);
    if (!doc || doc.format !== FORMAT || !(doc.lines instanceof Array)) throw new Error(t('badFile', file.name));
    if (doc.version > MAX_VERSION) throw new Error(t('newer', doc.version));
    return doc;
  }

  // ---------- Побудова ----------

  /** Групує рядки по n у картки; кінець картки не заходить на старт наступної. */
  function makeCards(lines, perCard) {
    var cards = [];
    for (var i = 0; i < lines.length; i += perCard) {
      var group = lines.slice(i, i + perCard);
      var texts = [];
      var words = [];
      for (var j = 0; j < group.length; j++) {
        texts.push(group[j].text);
        words = words.concat(group[j].words);
      }
      cards.push({ start: group[0].start, end: group[group.length - 1].end, text: texts.join('\r'), words: words });
    }
    for (var k = 0; k < cards.length - 1; k++) {
      if (cards[k].end > cards[k + 1].start) cards[k].end = cards[k + 1].start;
    }
    return cards;
  }

  function templateLayer(comp) {
    var sel = comp.selectedLayers;
    for (var i = 0; i < sel.length; i++) if (sel[i] instanceof TextLayer) return sel[i];
    return null;
  }

  function sourceText(layer) {
    return layer.property('ADBE Text Properties').property('ADBE Text Document');
  }

  function textWidth(layer, doc, text) {
    doc.text = text;
    sourceText(layer).setValue(doc);
    return layer.sourceRectAtTime(layer.inPoint, false).width;
  }

  /**
   * Безпечні зони вертикального відео: стеля для позиції й ширини та зсув центру від кнопок справа.
   * Ті самі значення, що в ae-panel/src/styles.ts (safeZones); ExtendScript не імпортує TS, тож копія.
   */
  var SAFE_ZONES = {
    tiktok: { maxPosY: 72, maxWidth: 76, shiftX: -4 },
    reels: { maxPosY: 74, maxWidth: 78, shiftX: -3 },
    shorts: { maxPosY: 72, maxWidth: 78, shiftX: -3 },
  };

  function safePlacement(zone, posY, maxWidth, vertical) {
    var z = SAFE_ZONES[zone];
    if (!z) return { posY: posY, maxWidth: maxWidth, shiftX: 0 };
    return {
      posY: Math.min(posY, z.maxPosY),
      maxWidth: Math.min(maxWidth, z.maxWidth),
      shiftX: vertical ? z.shiftX : 0,
    };
  }

  /**
   * Переносить слова, що не влазять у maxWidth (px шару). Кожен рядок картки переносимо окремо.
   * Пробіли стають \r, тож кількість літер без пробілів (для караоке) не змінюється.
   */
  function wrapText(layer, doc, text, maxWidth) {
    var src = text.split('\r');
    var out = [];
    for (var i = 0; i < src.length; i++) {
      var words = src[i].split(' ');
      var line = words[0];
      for (var w = 1; w < words.length; w++) {
        var trial = line + ' ' + words[w];
        if (textWidth(layer, doc, trial) > maxWidth) {
          out.push(line);
          line = words[w];
        } else {
          line = trial;
        }
      }
      out.push(line);
    }
    return out.join('\r');
  }

  /**
   * Рядки, що не влазять у ширину, ділимо за словами на кілька рядків (кожен — зі своїм часом),
   * щоб «1 рядок на картці» справді був одним рядком, а не переносився на два.
   * Без таймінгів слів рядок лишаємо як є (його перенесе wrapText).
   */
  function fitLines(comp, lines, opts) {
    var probe = addTextLayer(comp, ' ', opts, false);
    var doc = sourceText(probe).value;
    var maxWidth = (comp.width * opts.maxWidth) / 100;
    var out = [];
    try {
      for (var i = 0; i < lines.length; i++) {
        var line = lines[i];
        var words = line.words || [];
        if (doc.boxText || words.length < 2 || textWidth(probe, doc, line.text) <= maxWidth) {
          out.push(line);
          continue;
        }
        var parts = [];
        var cur = [words[0]];
        for (var w = 1; w < words.length; w++) {
          var trial = cur.concat([words[w]]);
          if (textWidth(probe, doc, joinWords(trial)) > maxWidth) {
            parts.push(cur);
            cur = [words[w]];
          } else {
            cur = trial;
          }
        }
        parts.push(cur);
        for (var p = 0; p < parts.length; p++) {
          var ws = parts[p];
          var next = parts[p + 1];
          out.push({
            text: joinWords(ws),
            start: p === 0 ? line.start : ws[0].start,
            // До початку наступного шматка того ж рядка — без миготіння між ними.
            end: next ? next[0].start : line.end,
            words: ws,
          });
        }
      }
    } finally {
      probe.remove();
    }
    return out;
  }

  function joinWords(words) {
    var texts = [];
    for (var i = 0; i < words.length; i++) texts.push(words[i].text);
    return texts.join(' ');
  }

  /**
   * Текстовий шар зі стилем шаблону або з налаштувань панелі. Картка переноситься за шириною
   * і тримається за нижній центр, тож зайві рядки ростуть угору, а не вилазять за кадр.
   */
  function addTextLayer(comp, text, opts, wrap) {
    var layer = comp.layers.addText(text);
    var doc = sourceText(layer).value;
    if (opts.template) {
      doc = sourceText(opts.template).value;
    } else {
      doc.fontSize = opts.fontSize;
      doc.applyFill = true;
      doc.fillColor = opts.baseColor;
      doc.applyStroke = opts.stroke > 0;
      if (opts.stroke > 0) {
        doc.strokeColor = opts.strokeColor;
        doc.strokeWidth = opts.stroke;
        doc.strokeOverFill = false;
      }
      doc.justification = ParagraphJustification.CENTER_JUSTIFY;
    }
    // Абзацний текст шаблону AE переносить сам.
    if (wrap && !doc.boxText) text = wrapText(layer, doc, text, (comp.width * opts.maxWidth) / 100);
    doc.text = text;
    sourceText(layer).setValue(doc);

    var transform = layer.property('ADBE Transform Group');
    if (wrap && !doc.boxText) {
      var rect = layer.sourceRectAtTime(layer.inPoint, false);
      transform.property('ADBE Anchor Point').setValue([rect.left + rect.width / 2, rect.top + rect.height]);
    }
    var pos = opts.template
      ? opts.template.property('ADBE Transform Group').property('ADBE Position').value
      : [comp.width / 2 + (comp.width * (opts.shiftX || 0)) / 100, (comp.height * opts.posY) / 100];
    transform.property('ADBE Position').setValue(pos);
    return layer;
  }

  /**
   * Присвоєння inPoint у скрипті зсуває шар, а не обрізає його, тому спершу inPoint, потім outPoint.
   * Ключі ставимо вже після цього, інакше вони поїдуть разом із шаром.
   */
  function setSpan(layer, start, end, offset) {
    layer.inPoint = start + offset;
    layer.outPoint = end + offset;
  }

  /**
   * Додає аніматор із Range Selector в індексах літер (без пробілів).
   * Після addProperty старі посилання в AE стають недійсними, тому все беремо заново за індексом.
   */
  function addIndexAnimator(layer, name, propMatchName, propValue) {
    var animators = function () {
      return layer.property('ADBE Text Properties').property('ADBE Text Animators');
    };
    var idx = animators().addProperty('ADBE Text Animator').propertyIndex;
    var animator = function () {
      return animators().property(idx);
    };
    animator().name = name;
    animator().property('ADBE Text Animator Properties').addProperty(propMatchName);
    animator().property('ADBE Text Animator Properties').property(propMatchName).setValue(propValue);
    animator().property('ADBE Text Selectors').addProperty('ADBE Text Selector');
    var selector = animator().property('ADBE Text Selectors').property(1);
    var advanced = selector.property('ADBE Text Range Advanced');
    advanced.property('ADBE Text Range Units').setValue(UNITS_INDEX);
    advanced.property('ADBE Text Range Type2').setValue(BASED_ON_CHARS_NO_SPACES);
    return animator().property('ADBE Text Selectors').property(1);
  }

  function holdKeys(prop, times, values) {
    prop.setValuesAtTimes(times, values);
    for (var k = 1; k <= prop.numKeys; k++) {
      prop.setInterpolationTypeAtKey(k, KeyframeInterpolationType.HOLD, KeyframeInterpolationType.HOLD);
    }
  }

  function charCount(words) {
    var n = 0;
    for (var i = 0; i < words.length; i++) n += words[i].chars.length;
    return n;
  }

  /** Підсвічування: кінець виділення рухається по словах або по літерах. */
  function animateKaraoke(layer, card, byLetter, offset, opts) {
    // Колір властивості аніматора — RGBA, на відміну від TextDocument.fillColor (RGB).
    var hi = opts.hiColor;
    var selector = addIndexAnimator(layer, 'Karaoke', 'ADBE Text Fill Color', [hi[0], hi[1], hi[2], 1]);
    selector.property('ADBE Text Index Start').setValue(0);
    var times = [card.start + offset];
    var values = [0];
    var done = 0;
    for (var i = 0; i < card.words.length; i++) {
      var word = card.words[i];
      if (byLetter) {
        for (var c = 0; c < word.chars.length; c++) {
          times.push(word.chars[c].start + offset);
          values.push(done + c + 1);
        }
      } else {
        times.push(word.start + offset);
        values.push(done + word.chars.length);
      }
      done += word.chars.length;
    }
    holdKeys(selector.property('ADBE Text Index End'), times, values);
  }

  /** Друкарська машинка: приховані літери — від Start до кінця, Start іде вперед. */
  function animateTypewriter(layer, card, offset) {
    var selector = addIndexAnimator(layer, 'Typewriter', 'ADBE Text Opacity', 0);
    var total = charCount(card.words);
    selector.property('ADBE Text Index End').setValue(total);
    var times = [card.start + offset];
    var values = [0];
    var done = 0;
    for (var i = 0; i < card.words.length; i++) {
      var chars = card.words[i].chars;
      for (var c = 0; c < chars.length; c++) {
        times.push(chars[c].start + offset);
        values.push(done + c + 1);
      }
      done += chars.length;
    }
    holdKeys(selector.property('ADBE Text Index Start'), times, values);
  }

  /** Один шар, Source Text змінюється на кожному слові; довгі паузи — порожньо. */
  function buildWordByWord(comp, doc, offset, opts) {
    var words = [];
    for (var i = 0; i < doc.lines.length; i++) words = words.concat(doc.lines[i].words);
    if (words.length === 0) return 0;
    var layer = addTextLayer(comp, words[0].text, opts, false);
    layer.name = 'Subs · words';
    setSpan(layer, words[0].start, words[words.length - 1].end, offset);
    var source = sourceText(layer);
    var times = [];
    var values = [];
    for (var w = 0; w < words.length; w++) {
      var docW = source.value;
      docW.text = words[w].text;
      times.push(words[w].start + offset);
      values.push(docW);
      var next = words[w + 1];
      if (!next || next.start - words[w].end > opts.gap) {
        var blank = source.value;
        blank.text = ' ';
        times.push(words[w].end + offset);
        values.push(blank);
      }
    }
    source.setValuesAtTimes(times, values);
    return 1;
  }

  function build(doc, opts) {
    var comp = app.project.activeItem;
    if (!(comp instanceof CompItem)) throw new Error(t('needComp'));
    opts.template = templateLayer(comp);
    // Зсув безпечної зони — лише для вертикального відео (і від панелі, і з вікна скрипта).
    if (comp.width >= comp.height) opts.shiftX = 0;
    // offset від панелі CEP: час початку аудіошару в композиції.
    var offset = typeof opts.offset === 'number' ? opts.offset : opts.fromCti ? comp.time : 0;
    if (opts.mode === 'words') return buildWordByWord(comp, doc, offset, opts);

    var cards = makeCards(fitLines(comp, doc.lines, opts), opts.perCard);
    for (var i = 0; i < cards.length; i++) {
      var card = cards[i];
      var layer = addTextLayer(comp, card.text, opts, true);
      layer.name = 'Subs · ' + (i + 1);
      setSpan(layer, card.start, card.end, offset);
      if (opts.mode === 'karaokeW') animateKaraoke(layer, card, false, offset, opts);
      if (opts.mode === 'karaokeC') animateKaraoke(layer, card, true, offset, opts);
      if (opts.mode === 'typewriter') animateTypewriter(layer, card, offset);
    }
    return cards.length;
  }

  // ---------- Інтерфейс ----------

  function hexToRgb(hex) {
    return [((hex >> 16) & 255) / 255, ((hex >> 8) & 255) / 255, (hex & 255) / 255];
  }

  function colorButton(parent, label, initial) {
    var group = parent.add('group');
    group.add('statictext', undefined, label);
    var swatch = group.add('button', undefined, '');
    swatch.preferredSize = [48, 20];
    swatch.hex = initial;
    swatch.onDraw = function () {
      var g = this.graphics;
      g.rectPath(0, 0, this.size[0], this.size[1]);
      g.fillPath(g.newBrush(g.BrushType.SOLID_COLOR, hexToRgb(this.hex)));
    };
    swatch.onClick = function () {
      var picked = $.colorPicker(swatch.hex);
      if (picked < 0) return;
      swatch.hex = picked;
      // Перемальовуємо: інакше ScriptUI не викличе onDraw.
      swatch.hide();
      swatch.show();
    };
    return swatch;
  }

  function buildUi(root) {
    var win = root instanceof Panel ? root : new Window('palette', SCRIPT_NAME, undefined, { resizeable: true });
    win.orientation = 'column';
    win.alignChildren = ['fill', 'top'];
    win.spacing = 8;
    win.margins = 12;

    var doc = null;

    var fileRow = win.add('group');
    fileRow.alignChildren = ['left', 'center'];
    var loadBtn = fileRow.add('button', undefined, t('load'));
    var status = win.add('statictext', undefined, t('noFile'), { truncate: 'middle' });
    status.preferredSize.width = 240;

    var modeRow = win.add('group');
    modeRow.add('statictext', undefined, t('mode'));
    var modes = ['lines', 'karaokeW', 'karaokeC', 'typewriter', 'words'];
    var modeList = modeRow.add('dropdownlist', undefined, [
      t('modeLines'),
      t('modeKaraokeW'),
      t('modeKaraokeC'),
      t('modeTypewriter'),
      t('modeWords'),
    ]);
    modeList.selection = 0;

    var cardRow = win.add('group');
    cardRow.add('statictext', undefined, t('perCard'));
    var perCard = cardRow.add('dropdownlist', undefined, ['1', '2', '3']);
    perCard.selection = 0;

    var sizeRow = win.add('group');
    sizeRow.add('statictext', undefined, t('fontSize'));
    var fontSize = sizeRow.add('edittext', undefined, '72');
    fontSize.characters = 5;

    var zoneRow = win.add('group');
    zoneRow.add('statictext', undefined, t('safeZone'));
    var zones = ['none', 'tiktok', 'reels', 'shorts'];
    var zoneList = zoneRow.add('dropdownlist', undefined, [t('safeNone'), 'TikTok', 'Instagram Reels', 'YouTube Shorts']);
    zoneList.selection = 1;

    var posRow = win.add('group');
    posRow.add('statictext', undefined, t('posY'));
    var posY = posRow.add('edittext', undefined, '72');
    posY.characters = 5;

    var widthRow = win.add('group');
    widthRow.add('statictext', undefined, t('maxWidth'));
    var maxWidth = widthRow.add('edittext', undefined, '85');
    maxWidth.characters = 5;

    var strokeRow = win.add('group');
    strokeRow.add('statictext', undefined, t('stroke'));
    var stroke = strokeRow.add('edittext', undefined, '6');
    stroke.characters = 5;

    var baseColor = colorButton(win, t('baseColor'), 0xffffff);
    var strokeColor = colorButton(win, t('strokeColor'), 0x000000);
    var hiColor = colorButton(win, t('hiColor'), 0xf2b84b);

    var fromCti = win.add('checkbox', undefined, t('offset'));
    var hint = win.add('statictext', undefined, t('template'), { multiline: true });
    hint.preferredSize.width = 240;

    var createBtn = win.add('button', undefined, t('create'));

    modeList.onChange = function () {
      var mode = modes[modeList.selection.index];
      perCard.enabled = mode !== 'words';
      hiColor.enabled = mode === 'karaokeW' || mode === 'karaokeC';
    };
    modeList.onChange();

    loadBtn.onClick = function () {
      // Windows приймає тільки рядок-фільтр, macOS — тільки функцію.
      var filter =
        $.os.indexOf('Windows') !== -1
          ? 'AE Subs:*.aesubs;*.json,All files:*.*'
          : function (f) {
              return f instanceof Folder || /\.(aesubs|json)$/i.test(f.name);
            };
      var file = File.openDialog(t('load'), filter);
      if (!file) return;
      try {
        doc = readSubs(file);
        var words = 0;
        for (var i = 0; i < doc.lines.length; i++) words += doc.lines[i].words.length;
        status.text = decodeURI(file.name) + ' — ' + t('loaded', doc.lines.length, words);
      } catch (err) {
        doc = null;
        status.text = t('noFile');
        alert(err.message, SCRIPT_NAME, true);
      }
    };

    createBtn.onClick = function () {
      if (!doc) return alert(t('needFile'), SCRIPT_NAME);
      var comp = app.project.activeItem;
      var place = safePlacement(
        zones[zoneList.selection.index],
        parseFloat(posY.text) || 72,
        Math.min(100, Math.max(10, parseFloat(maxWidth.text) || 85)),
        comp instanceof CompItem && comp.height > comp.width,
      );
      var opts = {
        mode: modes[modeList.selection.index],
        perCard: perCard.selection.index + 1,
        fontSize: Math.max(1, parseFloat(fontSize.text) || 72),
        posY: place.posY,
        maxWidth: place.maxWidth,
        shiftX: place.shiftX,
        stroke: Math.max(0, parseFloat(stroke.text) || 0),
        strokeColor: hexToRgb(strokeColor.hex),
        baseColor: hexToRgb(baseColor.hex),
        hiColor: hexToRgb(hiColor.hex),
        fromCti: fromCti.value,
        gap: 0.6,
      };
      app.beginUndoGroup(SCRIPT_NAME);
      try {
        var count = build(doc, opts);
        status.text = t('done', count);
      } catch (err) {
        alert(err.message, SCRIPT_NAME, true);
      } finally {
        app.endUndoGroup();
      }
    };

    win.onResizing = win.onResize = function () {
      this.layout.resize();
    };
    if (win instanceof Window) {
      win.center();
      win.show();
    } else {
      win.layout.layout(true);
    }
  }

  // ---------- API для CEP-панелі (ae-panel/) ----------

  function stringify(value) {
    if (value === null || value === undefined) return 'null';
    if (typeof value === 'number') return isFinite(value) ? String(value) : 'null';
    if (typeof value === 'boolean') return value ? 'true' : 'false';
    if (typeof value === 'string') {
      return (
        '"' +
        value.replace(/[\\"\u0000-\u001f]/g, function (c) {
          var code = c.charCodeAt(0).toString(16);
          return c === '"' || c === '\\' ? '\\' + c : '\\u' + '0000'.substr(code.length) + code;
        }) +
        '"'
      );
    }
    var parts = [];
    if (value instanceof Array) {
      for (var i = 0; i < value.length; i++) parts.push(stringify(value[i]));
      return '[' + parts.join(',') + ']';
    }
    for (var key in value) {
      if (value.hasOwnProperty(key)) parts.push(stringify(key) + ':' + stringify(value[key]));
    }
    return '{' + parts.join(',') + '}';
  }

  /** Перший виділений шар зі звуком і файлом-джерелом. */
  function selectedAudio() {
    var comp = app.project.activeItem;
    if (!(comp instanceof CompItem)) return { error: t('needComp') };
    var sel = comp.selectedLayers;
    for (var i = 0; i < sel.length; i++) {
      var layer = sel[i];
      if (layer instanceof AVLayer && layer.hasAudio && layer.source && layer.source.file) {
        return {
          path: layer.source.file.fsName,
          layerName: layer.name,
          startTime: layer.startTime,
          inPoint: layer.inPoint,
          outPoint: layer.outPoint,
          stretch: layer.stretch,
          timeRemap: layer.timeRemapEnabled,
        };
      }
    }
    return { error: t('needAudio') };
  }

  function safely(fn) {
    try {
      return stringify(fn());
    } catch (err) {
      return stringify({ error: err.message });
    }
  }

  $.global.CTSubsHost = {
    selectedAudio: function () {
      return safely(selectedAudio);
    },
    build: function (docJson, optsJson) {
      return safely(function () {
        app.beginUndoGroup(SCRIPT_NAME);
        try {
          return { count: build(parseJson(docJson), parseJson(optsJson)) };
        } finally {
          app.endUndoGroup();
        }
      });
    },
    pickColor: function (hex) {
      return String($.colorPicker(hex));
    },
    /** Для Pro-рушія (ae-panel/host/CTSubsPro.jsx), щоб не дублювати базові функції. */
    util: {
      parseJson: parseJson,
      stringify: stringify,
      safely: safely,
      setSpan: setSpan,
      templateLayer: templateLayer,
      t: t,
    },
  };

  // Панель CEP вантажить цей файл заради API і ставить прапорець; інакше це звичайний ScriptUI-скрипт.
  // Прапорець одноразовий: панель і File → Scripts ділять один рушій, тож без скидання
  // скрипт із меню не відкривав би вікно, поки панель завантажена.
  var fromPanel = $.global.__CT_SUBS_CEP__ === true;
  $.global.__CT_SUBS_CEP__ = false;
  if (!fromPanel) buildUi(thisObj);
})(this);
