/**
 * Vue App — main entry point.
 * Core state, computed properties, canvas rendering, preview, menu.
 *
 * Method bundles are loaded from separate files and spread in:
 *   ElementMethods (elements.js), SyncMethods (sync.js), PropMethods (props.js)
 */

const PREVIEW_TYPES = [
  { key: 'oil_temp', label: 'Huile', unit: '\u00b0C', min: 40, max: 150, value: 95, decimals: 0, accent: '#FF7043', warnHigh: 80, dangerHigh: 90 },
  { key: 'boost', label: 'Turbo', unit: 'kPa', min: -20, max: 160, value: 85, decimals: 0, accent: '#42A5F5', warnHigh: 85, dangerHigh: 95 },
  { key: 'rpm', label: 'RPM', unit: 'rpm', min: 0, max: 7000, value: 3500, decimals: 0, accent: '#FFA726', warnHigh: 86, dangerHigh: 93 },
  { key: 'speed', label: 'Vitesse', unit: 'km/h', min: 0, max: 260, value: 85, decimals: 0, accent: '#66BB6A', warnHigh: null, dangerHigh: null },
  { key: 'coolant', label: 'Eau', unit: '\u00b0C', min: 40, max: 130, value: 88, decimals: 0, accent: '#4FC3F7', warnHigh: 78, dangerHigh: 89 },
  { key: 'voltage', label: 'Batterie', unit: 'V', min: 10.5, max: 15.5, value: 14.2, decimals: 1, accent: '#AB47BC', warnHigh: null, dangerHigh: null },
  { key: 'fuel_level', label: 'Essence', unit: '%', min: 0, max: 100, value: 72, decimals: 0, accent: '#66BB6A', warnHigh: null, dangerHigh: null, warnLow: 15, dangerLow: 7 },
  { key: 'fuel_rate', label: 'Conso.', unit: 'L/h', min: 0, max: 20, value: 3.5, decimals: 1, accent: '#FFA726', warnHigh: 60, dangerHigh: 80 },
  { key: 'fuel_l100', label: 'L/100km', unit: 'L/100', min: 0, max: 30, value: 7.2, decimals: 1, accent: '#FFA726', warnHigh: 40, dangerHigh: 67 },
];

let _nextId = 1;

function assignIds(elements, depth) {
  for (const el of elements) {
    el._id = _nextId++;
    el._depth = depth || 0;
    if (el.isConditional || el.isGroup || el.isForLoop) {
      el.children = el.children || [];
      assignIds(el.children, (depth||0)+1);
    }
  }
  return elements;
}

const EFFECT_KEYS = new Set(['alpha', 'glow', 'glow_color']);

const app = Vue.createApp({
  data() {
    return {
      face: { name: 'Classic Arc', author: 'OBD Dash', bg: '#00000000', vars: {}, elements: [] },
      renderCtx: {
        progress: 0.65, percent: 65, rawValue: 65, formattedValue: '65',
        unit: '\u00b0C', label: 'Oil Temp', minValue: 0, maxValue: 100,
        accentColor: '#4FC3F7', zone: 'normal',
        warnLowProgress: null, warnHighProgress: 0.8,
        dangerLowProgress: null, dangerHighProgress: 0.9
      },
      previewType: 'oil_temp',
      showThresholds: true,
      thresholdWarnHigh: 80,
      thresholdDangerHigh: 90,
      tab: 'elements',
      selectedIds: [],
      _selectionAnchor: null,
      collapsedIds: [],
      showPalette: false,
      codeText: '',
      qrModal: false,
      qrFullscreen: false,
      qrScanModal: false,
      autocomplete: { show: false, x: 0, y: 0, items: [], matchStart: -1, target: null, targetType: null, targetKey: null },
      savedFaces: [],
      toast: null,
      _toastTimer: null,
      _codeInputTimer: null,
      _thumbRAF: null,
      _scanner: null,
      _syncing: false,
      showPreviewSettings: false,
      activeMenu: null,
      animatePreview: false,
      dragSource: null,
      dragTarget: null,
      dragPos: null,
      previewLayout: '1x1',
      cellMenu: { show: false, x: 0, y: 0, idx: 0 },
      cellPickerModal: { show: false, idx: 0 },
      cellClipboard: null,
      activeCell: 0,
      cells: [
        { code: '', previewType: 'oil_temp' },
        { code: '', previewType: 'boost' },
        { code: '', previewType: 'rpm' },
        { code: '', previewType: 'speed' }
      ],
      _cellFaces: [null, null, null, null],
      previewBg: '#000000',
      previewBgImage: null,
      _previewBgImg: null,
      /** Extra data bindings for multi-value gauges. Each: { type: 'oil_temp', value: 50 } */
      dataBindings: [],
      _undoStack: [],
      _redoStack: [],
      _undoTimer: null,
      _blinkLoop: null
    };
  },

  computed: {
    allTypes() { return Object.keys(TYPE_DEFS); },
    presets() { return PRESETS; },
    PREVIEW_TYPES() { return PREVIEW_TYPES; },
    basicPresets() { return PRESETS.filter(p => !p.tags); },
    advancedPresets() { return PRESETS.filter(p => p.tags && p.tags.length > 0); },
    categories() { return TYPE_CATEGORIES; },
    selectedId: {
      get() { return this.selectedIds.length === 1 ? this.selectedIds[0] : null; },
      set(val) { this.selectedIds = val != null ? [val] : []; this._selectionAnchor = val; }
    },
    multiSelected() { return this.selectedIds.length > 1; },
    canUndo() { return this._undoStack.length > 0; },
    canRedo() { return this._redoStack.length > 0; },
    noCell() { return this.activeCell < 0 && this.previewLayout !== '1x1'; },
    layoutOptions() {
      return [
        { key: '1x1', cols: 1, rows: 1, rects: [[1,1,18,18]] },
        { key: '2x1', cols: 2, rows: 1, rects: [[1,1,8.5,18],[10.5,1,8.5,18]] },
        { key: '1x2', cols: 1, rows: 2, rects: [[1,1,18,8.5],[1,10.5,18,8.5]] },
        { key: '2x2', cols: 2, rows: 2, rects: [[1,1,8.5,8.5],[10.5,1,8.5,8.5],[1,10.5,8.5,8.5],[10.5,10.5,8.5,8.5]] }
      ];
    },

    varEntries() {
      return Object.entries(this.face.vars || {}).map(([name, expr]) => ({ name, expr }));
    },

    flatElements() {
      const collapsed = this.collapsedIds;
      const list = [];
      const flatten = (els, depth) => {
        for (const el of els) {
          const id = el._id;
          const isCollapsed = collapsed.includes(id);
          if (el.isConditional) {
            list.push({ id, depth, isConditional: true, display: `if ${el.condition}`, node: el, hidden: !!el._hidden, collapsed: isCollapsed });
            if (!isCollapsed) flatten(el.children || [], depth + 1);
          } else if (el.isGroup) {
            const childCount = (el.children || []).length;
            const suffix = isCollapsed ? ` (${childCount})` : '';
            list.push({ id, depth, isGroup: true, display: (el.name || 'Groupe') + suffix, node: el, hidden: !!el._hidden, collapsed: isCollapsed });
            if (!isCollapsed) flatten(el.children || [], depth + 1);
          } else if (el.isForLoop) {
            const stepStr = el.step !== '1' ? ' step ' + el.step : '';
            const display = `for ${el.varName} ${el.from}..${el.to}${stepStr}`;
            const isCollapsed2 = collapsed.includes(id);
            list.push({ id, depth, isForLoop: true, display, node: el, hidden: !!el._hidden, collapsed: isCollapsed2 });
            if (!isCollapsed2) flatten(el.children || [], depth + 1);
          } else {
            let display = el.type;
            if (el.args.length) display += ' ' + el.args.slice(0, 3).join(' ');
            list.push({ id, depth, isConditional: false, display, node: el, hidden: !!el._hidden });
          }
        }
      };
      flatten(this.face.elements, 0);
      return list;
    },

    selectedElement() {
      if (!this.selectedId) return null;
      const find = (els) => {
        for (const el of els) {
          if (el._id === this.selectedId) return el;
          if ((el.isConditional || el.isGroup || el.isForLoop) && el.children) {
            const found = find(el.children);
            if (found) return found;
          }
        }
        return null;
      };
      return find(this.face.elements);
    },

    selectedName() {
      if (this.multiSelected) return this.selectedIds.length + ' elements';
      const el = this.selectedElement;
      if (!el) return '';
      if (el.isGroup) return el.name || 'Groupe';
      if (el.isForLoop) return `for ${el.varName}`;
      return el.isConditional ? `if ${el.condition}` : el.type;
    },

    argDefs() {
      const el = this.selectedElement;
      if (!el || el.isConditional || el.isGroup || el.isForLoop) return [];
      const def = TYPE_DEFS[el.type];
      if (!def) return [];
      const base = def.args.map((a, i) => ({ label: a.l, type: a.t || 'number', step: a.s || 1, index: i }));
      // For 'shape': generate extra vertex labels beyond the base 3 points
      if (el.type === 'shape' && el.args.length > 6) {
        for (let i = 6; i < el.args.length; i += 2) {
          const pt = (i / 2) + 1;
          base.push({ label: 'x' + pt, type: 'number', step: 1, index: i });
          if (i + 1 < el.args.length) base.push({ label: 'y' + pt, type: 'number', step: 1, index: i + 1 });
        }
      }
      return base;
    },

    isShape() {
      const el = this.selectedElement;
      return el && el.type === 'shape';
    },

    shapeVertexCount() {
      const el = this.selectedElement;
      return el && el.type === 'shape' ? Math.floor(el.args.length / 2) : 0;
    },

    propDefs() {
      const el = this.selectedElement;
      if (!el || el.isConditional || el.isGroup || el.isForLoop) return [];
      const def = TYPE_DEFS[el.type];
      if (!def) return [];
      return def.props.map(p => ({ key: p.k, label: p.l, type: p.t, step: p.s, options: p.o }));
    },

    stylePropDefs() { return this.propDefs.filter(p => !EFFECT_KEYS.has(p.key)); },
    effectPropDefs() { return this.propDefs.filter(p => EFFECT_KEYS.has(p.key)); },

    resolvedRenderCtx() {
      void this.renderCtx.progress;
      void this.renderCtx.zone;
      return resolveCustomVars(this.face, this.renderCtx);
    }
  },

  methods: {
    // Spread method bundles from separate files
    ...ElementMethods,
    ...SyncMethods,
    ...PropMethods,

    // ── Canvas rendering ──

    // ── Multi-cell management ──

    switchCell(idx) {
      const layout = this.layoutOptions.find(l => l.key === this.previewLayout) || this.layoutOptions[0];
      const cellCount = layout.cols * layout.rows;
      if (idx < 0 || idx >= cellCount || idx === this.activeCell) return;

      // Save current cell (if one is active)
      if (this.activeCell >= 0) {
        this.cells[this.activeCell].code = serializeFace(this.face);
        this.cells[this.activeCell].previewType = this.previewType;
        this._cellFaces[this.activeCell] = JSON.parse(JSON.stringify(this.face));
      }

      // Switch
      this.activeCell = idx;

      // Load new cell
      const cell = this.cells[idx];
      if (cell.code) {
        this.codeText = cell.code;
        this._syncing = true;
        const parsed = parseDSL(cell.code);
        this.face = { ...parsed, elements: assignIds(parsed.elements, 0) };
        this._syncing = false;
        this.codeText = cell.code;
      } else {
        // Empty cell — load default preset for this type
        const defaultPreset = PRESETS.find(p => !p.tags) || PRESETS[0];
        if (defaultPreset) {
          this.codeText = defaultPreset.code;
          this._syncing = true;
          const parsed = parseDSL(defaultPreset.code);
          this.face = { ...parsed, elements: assignIds(parsed.elements, 0) };
          this._syncing = false;
        }
        this.cells[idx].code = serializeFace(this.face);
      }
      this.previewType = cell.previewType;
      this.onPreviewTypeChange();
      this.selectedIds = [];
      this._undoStack = [];
      this._redoStack = [];
      this.renderCanvas();
    },

    /** Build a render context for a non-active cell, using the shared slider progress. */
    _buildCellCtx(previewType) {
      const t = PREVIEW_TYPES.find(p => p.key === previewType) || PREVIEW_TYPES[0];
      // Use the shared slider progress (same for all cells)
      const progress = this.renderCtx.progress;
      const raw = t.min + (t.max - t.min) * progress;
      const whp = t.warnHigh != null ? t.warnHigh / 100 : null;
      const dhp = t.dangerHigh != null ? t.dangerHigh / 100 : null;
      const zone = dhp != null && progress >= dhp ? 'danger'
        : whp != null && progress >= whp ? 'warning' : 'normal';
      return {
        progress, percent: progress * 100, rawValue: raw,
        formattedValue: t.decimals > 0 ? raw.toFixed(t.decimals) : String(Math.round(raw)),
        unit: t.unit, label: t.label,
        minValue: t.min, maxValue: t.max,
        accentColor: t.accent, zone,
        warnLowProgress: null, warnHighProgress: whp,
        dangerLowProgress: null, dangerHighProgress: dhp
      };
    },

    _getCellFace(idx) {
      if (idx === this.activeCell) return this.face;
      if (this._cellFaces[idx]) return this._cellFaces[idx];
      const code = this.cells[idx].code;
      if (code) {
        try {
          const parsed = parseDSL(code);
          this._cellFaces[idx] = parsed;
          return parsed;
        } catch (_) {}
      }
      return null;
    },

    cellOverlayStyle(idx) {
      const layout = this.layoutOptions.find(l => l.key === this.previewLayout) || this.layoutOptions[0];
      const { cols, rows } = layout;
      const gapPct = 0;
      const cellWPct = (100 - gapPct * (cols - 1)) / cols;
      const cellHPct = (100 - gapPct * (rows - 1)) / rows;
      const col = idx % cols;
      const row = Math.floor(idx / cols);
      return {
        position: 'absolute',
        left: (col * (cellWPct + gapPct)) + '%',
        top: (row * (cellHPct + gapPct)) + '%',
        width: cellWPct + '%',
        height: cellHPct + '%',
        cursor: 'pointer',
        zIndex: 10
      };
    },

    // ── Cell overlay actions ──

    onCellOverlayClick(idx) {
      this.cellMenu.show = false;
      if (!this.cells[idx].code) {
        this.cellPickerModal = { show: true, idx };
        this.$nextTick(() => this._renderCellPickerThumbs());
        return;
      }
      this.switchCell(idx);
    },

    onCellContextMenu(e, idx) {
      const wrapper = e.target.closest('.canvas-wrapper');
      const rect = wrapper ? wrapper.getBoundingClientRect() : { left: 0, top: 0 };
      this.cellMenu = { show: true, x: e.clientX - rect.left, y: e.clientY - rect.top, idx };
    },

    copyCellToClipboard(idx) {
      this.cellClipboard = { code: this.cells[idx].code, previewType: this.cells[idx].previewType };
      this.showToast('Cadran copie');
    },

    pasteCellFromClipboard(idx) {
      if (!this.cellClipboard) return;
      this.cells[idx].code = this.cellClipboard.code;
      this.cells[idx].previewType = this.cellClipboard.previewType;
      this._cellFaces[idx] = null;
      this.autoSaveSession();
      this.renderCanvas();
      this.showToast('Cadran colle');
    },

    clearCell(idx) {
      this.cells[idx].code = '';
      this._cellFaces[idx] = null;
      if (this.activeCell === idx) this.activeCell = -1;
      this.autoSaveSession();
      this.renderCanvas();
      this.showToast('Cadran supprime');
    },

    cellPickerNewBlank() {
      const idx = this.cellPickerModal.idx;
      this.cells[idx].code = '@name "Nouveau"\n@author ""\nbg #00000000\n';
      this._cellFaces[idx] = null;
      this.cellPickerModal.show = false;
      this.switchCell(idx);
    },

    cellPickerLoadPreset(presetIdx) {
      const idx = this.cellPickerModal.idx;
      const preset = PRESETS[presetIdx];
      if (!preset) return;
      this.cells[idx].code = preset.code;
      this._cellFaces[idx] = null;
      this.cellPickerModal.show = false;
      this.switchCell(idx);
    },

    cellPickerLoadSaved(savedIdx) {
      const idx = this.cellPickerModal.idx;
      const entry = this.savedFaces[savedIdx];
      if (!entry) return;
      this.cells[idx].code = entry.code;
      this._cellFaces[idx] = null;
      this.cellPickerModal.show = false;
      this.switchCell(idx);
    },

    cellPickerImportFile() {
      this.$refs.cellFileInput.click();
    },

    onCellFileImport(e) {
      const file = e.target.files[0];
      if (!file) return;
      const idx = this.cellPickerModal.idx;
      const reader = new FileReader();
      reader.onload = (ev) => {
        this.cells[idx].code = ev.target.result;
        this._cellFaces[idx] = null;
        this.cellPickerModal.show = false;
        this.switchCell(idx);
      };
      reader.readAsText(file);
      e.target.value = '';
    },

    _renderCellPickerThumbs() {
      const ctx = { progress: 0.65, percent: 65, rawValue: 65, formattedValue: '65',
        unit: '\u00b0C', label: 'Huile', minValue: 0, maxValue: 100,
        accentColor: '#4FC3F7', zone: 'normal',
        warnLowProgress: null, warnHighProgress: 0.8,
        dangerLowProgress: null, dangerHighProgress: 0.9 };
      for (let i = 0; i < PRESETS.length; i++) {
        const el = document.getElementById('cellpick-preset-' + i);
        if (!el) continue;
        const c = el.getContext('2d');
        c.fillStyle = '#000'; c.fillRect(0, 0, 64, 64);
        try { renderFace(c, parseDSL(PRESETS[i].code), ctx, 64); } catch(_) {}
      }
      for (let i = 0; i < this.savedFaces.length; i++) {
        const el = document.getElementById('cellpick-saved-' + i);
        if (!el) continue;
        const c = el.getContext('2d');
        c.fillStyle = '#000'; c.fillRect(0, 0, 64, 64);
        try { renderFace(c, parseDSL(this.savedFaces[i].code), ctx, 64); } catch(_) {}
      }
    },

    // ── Canvas rendering ──

    renderCanvas() {
      const canvas = document.getElementById('gaugeCanvas');
      if (!canvas) return;
      const c = canvas.getContext('2d');
      const size = 400;
      c.clearRect(0, 0, size, size);

      const layout = this.layoutOptions.find(l => l.key === this.previewLayout) || this.layoutOptions[0];
      const { cols, rows } = layout;
      const cellCount = cols * rows;
      const gap = 0;
      const cellW = (size - gap * (cols - 1)) / cols;
      const cellH = (size - gap * (rows - 1)) / rows;

      // Black background for grid gaps
      if (cellCount > 1) {
        c.fillStyle = '#000';
        c.fillRect(0, 0, size, size);
      }

      for (let row = 0; row < rows; row++) {
        for (let col = 0; col < cols; col++) {
          const idx = row * cols + col;
          const cx = col * (cellW + gap);
          const cy = row * (cellH + gap);
          const cellSize = Math.min(cellW, cellH);
          const isActive = idx === this.activeCell;

          // Get face for this cell
          const cellFace = this._getCellFace(idx);
          if (!cellFace) {
            // Empty cell — draw placeholder
            c.fillStyle = '#0a0a0f';
            c.fillRect(cx, cy, cellW, cellH);
            c.fillStyle = '#333';
            c.font = '24px sans-serif';
            c.textAlign = 'center';
            c.textBaseline = 'middle';
            c.fillText('+', cx + cellW / 2, cy + cellH / 2);
            continue;
          }

          // All cells share the same slider progress
          const cellCtx = isActive ? this.renderCtx : this._buildCellCtx(this.cells[idx].previewType);

          // Inject extra data binding variables into context
          // Clean previous binding vars first to avoid stale data
          if (cellCtx._customVars) {
            for (const k of Object.keys(cellCtx._customVars)) {
              if (k.match(/^\$(value|progress|raw|zone|unit|label|accent|min|max)-/)) {
                delete cellCtx._customVars[k];
              }
            }
          }
          if (this.dataBindings.length > 0) {
            if (!cellCtx._customVars) cellCtx._customVars = {};
            for (let bi = 0; bi < this.dataBindings.length; bi++) {
              const b = this.dataBindings[bi];
              const bt = PREVIEW_TYPES.find(p => p.key === b.type);
              if (!bt) continue;
              const bProgress = (b.value - bt.min) / (bt.max - bt.min);
              const bZone = bt.dangerHigh != null && bProgress >= bt.dangerHigh / 100 ? 'danger'
                : bt.warnHigh != null && bProgress >= bt.warnHigh / 100 ? 'warning' : 'normal';
              const fmtValue = bt.decimals > 0 ? b.value.toFixed(bt.decimals) : String(Math.round(b.value));
              // Named by type key
              cellCtx._customVars['$value-' + b.type] = fmtValue;
              cellCtx._customVars['$progress-' + b.type] = String(bProgress);
              cellCtx._customVars['$raw-' + b.type] = String(b.value);
              cellCtx._customVars['$zone-' + b.type] = bZone;
              cellCtx._customVars['$unit-' + b.type] = bt.unit;
              cellCtx._customVars['$label-' + b.type] = bt.label;
              cellCtx._customVars['$accent-' + b.type] = bt.accent;
              cellCtx._customVars['$min-' + b.type] = String(bt.min);
              cellCtx._customVars['$max-' + b.type] = String(bt.max);
              // Indexed (1-based)
              const idx = bi + 1;
              cellCtx._customVars['$value-' + idx] = fmtValue;
              cellCtx._customVars['$progress-' + idx] = String(bProgress);
              cellCtx._customVars['$raw-' + idx] = String(b.value);
              cellCtx._customVars['$zone-' + idx] = bZone;
              cellCtx._customVars['$unit-' + idx] = bt.unit;
              cellCtx._customVars['$label-' + idx] = bt.label;
              cellCtx._customVars['$accent-' + idx] = bt.accent;
              cellCtx._customVars['$min-' + idx] = String(bt.min);
              cellCtx._customVars['$max-' + idx] = String(bt.max);
            }
          }

          c.save();
          c.beginPath();
          c.rect(cx, cy, cellW, cellH);
          c.clip();

          // Cell background
          if (this._previewBgImg) {
            c.drawImage(this._previewBgImg, cx, cy, cellW, cellH);
          } else {
            c.fillStyle = this.previewBg;
            c.fillRect(cx, cy, cellW, cellH);
          }

          c.translate(cx + (cellW - cellSize) / 2, cy + (cellH - cellSize) / 2);
          const showSelection = isActive && this.activeCell >= 0;
          renderFace(c, cellFace, cellCtx, cellSize, showSelection ? this.selectedElement : null, true);
          c.restore();

          // Active cell highlight (only when a cell is selected, not in preview mode)
          if (cellCount > 1 && isActive && this.activeCell >= 0) {
            c.save();
            c.strokeStyle = '#4FC3F7';
            c.lineWidth = 2;
            c.strokeRect(cx + 1, cy + 1, cellW - 2, cellH - 2);
            c.restore();
          }
        }
      }

      if (!this._thumbRAF) {
        this._thumbRAF = requestAnimationFrame(() => {
          this._thumbRAF = null;
          this.renderThumbnails();
        });
      }
      // Check if ANY visible cell uses $time or blink
      let needsLoop = this.hasBlinkElements(this.face.elements) || this._faceUsesTime();
      if (!needsLoop && cellCount > 1) {
        for (let i = 0; i < cellCount; i++) {
          if (i === this.activeCell) continue;
          const f = this._getCellFace(i);
          if (f && JSON.stringify(f).includes('$time')) { needsLoop = true; break; }
        }
      }
      if (needsLoop && !this._animLoop) {
        this._animLoop = requestAnimationFrame(() => {
          this._animLoop = null;
          this.renderCanvas();
        });
      }
    },

    hasBlinkElements(els) {
      for (const el of els) {
        if (el.props && el.props.blink) return true;
        if ((el.isConditional || el.isGroup || el.isForLoop) && el.children && this.hasBlinkElements(el.children)) return true;
      }
      return false;
    },

    _faceUsesTime() {
      try { return JSON.stringify(this.face).includes('$time'); }
      catch { return false; }
    },

    renderThumbnails() {
      for (const el of this.flatElements) {
        if (el.isConditional || el.isGroup || el.isForLoop) continue;
        const canvas = document.getElementById('thumb-' + el.id);
        if (!canvas) continue;
        const c = canvas.getContext('2d');
        const s = 32;
        c.clearRect(0, 0, s, s);
        c.fillStyle = this.face.bg ? resolveColor(this.face.bg, this.renderCtx) : '#111';
        c.fillRect(0, 0, s, s);
        try { drawElement(c, el.node, this.renderCtx, s); } catch(_) {}
      }
    },

    // ── Preview ──

    onPreviewTypeChange() {
      const t = PREVIEW_TYPES.find(p => p.key === this.previewType);
      if (!t) return;
      this.renderCtx.unit = t.unit;
      this.renderCtx.label = t.label;
      this.renderCtx.minValue = t.min;
      this.renderCtx.maxValue = t.max;
      this.renderCtx.accentColor = t.accent;
      if (t.warnHigh != null) {
        this.thresholdWarnHigh = t.warnHigh;
        this.thresholdDangerHigh = t.dangerHigh;
        this.renderCtx.warnHighProgress = t.warnHigh / 100;
        this.renderCtx.dangerHighProgress = t.dangerHigh / 100;
      } else {
        this.renderCtx.warnHighProgress = null;
        this.renderCtx.dangerHighProgress = null;
      }
      if (t.warnLow != null) {
        this.renderCtx.warnLowProgress = t.warnLow / 100;
        this.renderCtx.dangerLowProgress = t.dangerLow != null ? t.dangerLow / 100 : null;
      } else {
        this.renderCtx.warnLowProgress = null;
        this.renderCtx.dangerLowProgress = null;
      }
      this.onPercentChange();
    },

    onPercentChange() {
      const p = this.renderCtx.percent;
      const t = PREVIEW_TYPES.find(pt => pt.key === this.previewType);
      this.renderCtx.progress = p / 100;
      if (t) {
        const raw = t.min + (t.max - t.min) * p / 100;
        this.renderCtx.rawValue = raw;
        this.renderCtx.formattedValue = t.decimals > 0 ? raw.toFixed(t.decimals) : String(Math.round(raw));
      } else {
        this.renderCtx.rawValue = p;
        this.renderCtx.formattedValue = String(p);
      }
      const progress = this.renderCtx.progress;
      const dhp = this.renderCtx.dangerHighProgress;
      const whp = this.renderCtx.warnHighProgress;
      const dlp = this.renderCtx.dangerLowProgress;
      const wlp = this.renderCtx.warnLowProgress;
      if (dhp != null && progress >= dhp) this.renderCtx.zone = 'danger';
      else if (dlp != null && progress <= dlp) this.renderCtx.zone = 'danger';
      else if (whp != null && progress >= whp) this.renderCtx.zone = 'warning';
      else if (wlp != null && progress <= wlp) this.renderCtx.zone = 'warning';
      else this.renderCtx.zone = 'normal';
      this.renderCanvas();
    },

    toggleAnimation() {
      this.animatePreview = !this.animatePreview;
      if (this.animatePreview) {
        this._animDir = 1;
        this._animLast = performance.now();
        this._animFrame = requestAnimationFrame((t) => this.animateStep(t));
      } else if (this._animFrame) {
        cancelAnimationFrame(this._animFrame);
        this._animFrame = null;
      }
    },

    animateStep(ts) {
      if (!this.animatePreview) return;
      const dt = ts - (this._animLast || ts);
      this._animLast = ts;
      const speed = 30;
      this.renderCtx.percent += this._animDir * speed * dt / 1000;
      if (this.renderCtx.percent >= 100) { this.renderCtx.percent = 100; this._animDir = -1; }
      else if (this.renderCtx.percent <= 0) { this.renderCtx.percent = 0; this._animDir = 1; }
      this.onPercentChange();
      this._animFrame = requestAnimationFrame((t) => this.animateStep(t));
    },

    updateThresholds() {
      this.renderCtx.warnHighProgress = this.thresholdWarnHigh / 100;
      this.renderCtx.dangerHighProgress = this.thresholdDangerHigh / 100;
      this.onPercentChange();
    },

    // ── Data bindings (multi-value) ──

    addDataBinding(typeKey) {
      if (this.dataBindings.find(b => b.type === typeKey)) return;
      const t = PREVIEW_TYPES.find(p => p.key === typeKey);
      if (!t) return;
      this.dataBindings.push({ type: typeKey, value: t.min + (t.max - t.min) * 0.5 });
      this.renderCanvas();
    },

    removeDataBinding(idx) {
      this.dataBindings.splice(idx, 1);
      this.renderCanvas();
    },

    onBindingValueChange() {
      this.renderCanvas();
    },

    /** Available types not already bound. */
    availableBindingTypes() {
      const used = new Set(this.dataBindings.map(b => b.type));
      used.add(this.previewType);
      return PREVIEW_TYPES.filter(t => !used.has(t.key));
    },

    // ── Menu bar ──

    toggleMenu(name) {
      this.activeMenu = this.activeMenu === name ? null : name;
    },

    hoverMenu(name) {
      if (this.activeMenu && this.activeMenu !== 'mobile') this.activeMenu = name;
    },

    menuStyle(name) {
      const ref = 'menu' + name.charAt(0).toUpperCase() + name.slice(1);
      const btn = this.$refs[ref];
      if (!btn) return { position: 'fixed', top: '42px', left: '0px' };
      const rect = btn.getBoundingClientRect();
      return { position: 'fixed', top: (rect.bottom + 2) + 'px', left: rect.left + 'px' };
    },

    // ── Toast ──

    showToast(msg) {
      this.toast = msg;
      clearTimeout(this._toastTimer);
      this._toastTimer = setTimeout(() => { this.toast = null; }, 2000);
    }
  },

  watch: {
    tab(val) {
      if (val === 'io') this.$nextTick(() => this.renderPresetThumbnails());
    }
  },

  mounted() {
    this.loadLibrary();
    this.loadPreviewBg();
    if (!this.loadSession()) {
      this.codeText = PRESETS[0].code;
      this.syncFaceFromCode();
    }
    this.onPreviewTypeChange();
    CanvasInteractions.init(this);
    const ro = new ResizeObserver(() => this.renderCanvas());
    ro.observe(document.getElementById('gaugeCanvas'));
  }
});

app.mount('#app');
