scripts/dom.jsjavascript
/**
 * @module dom
 * @description
 * DOM glue layer: collects every DOM element the app interacts with into a
 * single `els` object, and provides a handful of small utilities for status
 * indicators, overlay clearing, and effect-button state.
 *
 * Why a dedicated module: separating element lookup from feature code keeps
 * the rest of the app free of `document.getElementById` calls scattered
 * everywhere, makes the set of touched DOM nodes auditable in one place, and
 * lets tests replace `els` with a stub for unit testing.
 *
 * Element resolution happens at module-load time, so this module must be
 * imported after the corresponding DOM nodes exist (i.e. after the HTML
 * `<script type="module">` defer ordering has parsed them).
 */
import { state } from './state.js';
import { t } from './i18n.js';

/**
 * Single source of truth for the DOM nodes touched by the app. Populated once
 * at module load. Some entries may be `null` if the markup omits them (e.g.
 * `mirrorToggle` is kept as a fallback for non-mobile builds).
 */
export const els = {
   video: document.getElementById('video'),
   overlay: document.getElementById('overlay'),
   viewer: document.getElementById('viewer'),
   placeholder: document.getElementById('placeholder'),
   previewImage: document.getElementById('previewImage'),
   logBox: document.getElementById('logBox'),
   statusDot: document.getElementById('statusDot'),
   statusText: document.getElementById('statusText'),
   dbCount: document.getElementById('dbCount'),
   nextId: document.getElementById('nextId'),
   thresholdLabel: document.getElementById('thresholdLabel'),
   effectName: document.getElementById('effectName'),
   effectTracking: document.getElementById('effectTracking'),
   scanBtn: document.getElementById('scanBtn'),
   copyMakeupBtn: document.getElementById('copyMakeupBtn'),
   saveBtn: document.getElementById('saveBtn'),
   analyzeBtn: document.getElementById('analyzeBtn'),
   toggleSettingsBtn: document.getElementById('toggleSettingsBtn'),
   closeSettingsBtn: document.getElementById('closeSettingsBtn'),
   settingsDrawer: document.getElementById('settingsDrawer'),
   closeHistoryBtn: document.getElementById('closeHistoryBtn'),
   historyDrawer: document.getElementById('historyDrawer'),
   historyEntries: document.getElementById('historyEntries'),
   overlayModeBtn: document.getElementById('overlayModeBtn'),
   clearDbBtn: document.getElementById('clearDbBtn'),
   reloadPluginsBtn: document.getElementById('reloadPluginsBtn'),
   ghostylesContainer: document.getElementById('ghostylesContainer'),
   localeSelect: document.getElementById('localeSelect'),
   // Fallback toggle: kept for builds without auto-mirroring. Normally the
   // mirror state is driven by camera facingMode (see camera.js).
   mirrorToggle: document.getElementById('mirrorToggle'),
   switchCameraBtn: document.getElementById('switchCameraBtn'),
   dbCountBadge: document.getElementById('dbCountBadge'),
   fpsSelect: document.getElementById('fpsSelect'),
   recordBtn: document.getElementById('recordBtn')
};

/**
 * Update the status indicator in the header. The `kind` argument selects the
 * dot colour class (`live` = green, `error` = red, anything else = default
 * grey) and `text` populates the adjacent label. Centralising the logic here
 * keeps the indicator visually consistent regardless of which module
 * triggered the change.
 *
 * @param {string} kind  One of 'live', 'init', 'error', or any other string
 *   (defaults to neutral styling). Only 'live' and 'error' apply a CSS class.
 * @param {string} text  Human-readable status text shown to the right of the
 *   dot.
 * @see scripts/camera.js – sets `('live', …)` once the webcam stream starts.
 * @see scripts/main.js – sets `('init', …)` while models load, `('error', …)`
 *   on failure.
 */
export function setStatus(kind, text) {
   els.statusDot.className = 'status-dot';
   if (kind === 'live') els.statusDot.classList.add('live');
   if (kind === 'error') els.statusDot.classList.add('error');
   els.statusText.textContent = text;
}

/**
 * Clear the main overlay canvas and reset its visual state. Also cancels any
 * pending fade-out timer set by `engine.triggerOverlayFadeout` so a stale
 * timer doesn't fade out a freshly drawn frame. Called between detection
 * passes and on effect changes.
 *
 * @see scripts/engine.js – `detectFaceInCam()` clears the overlay before
 *   drawing the new detection result.
 * @see scripts/ghostyles-manager.js – `toggleEffect()` uses this indirectly
 *   via `clearActiveEffect()` when switching off an effect.
 */
export function clearOverlay() {
   const ctx = els.overlay.getContext('2d');
   ctx.clearRect(0, 0, els.overlay.width, els.overlay.height);
   els.overlay.style.transition = 'none';
   els.overlay.style.opacity = '1';
   if (state.overlayFadeTimeout) clearTimeout(state.overlayFadeTimeout);
}

/**
 * Reset UI and runtime state after an effect is deactivated: drop the active
 * class from every preview button, undo the scan-button styling, blank the
 * "effect name" / "tracking" labels, null out the cached detection result
 * and last composited canvas, disable the copy-makeup button, and clear the
 * overlay canvas. Side-effect only — no event is dispatched.
 *
 * @see scripts/ghostyles-manager.js – `toggleEffect()` calls this when the
 *   user deactivates the currently active effect.
 */
export function clearActiveEffect() {
   const previewBtns = els.ghostylesContainer ? els.ghostylesContainer.querySelectorAll('.preview-btn') : [];

   previewBtns.forEach(btn => btn.classList.remove('active'));
   if (els.scanBtn) {
      els.scanBtn.style.background = '';
      els.scanBtn.style.borderColor = '';
      els.scanBtn.style.color = '';
   }

   if (els.effectName) els.effectName.textContent = t('not_available_label');
   if (els.effectTracking) els.effectTracking.textContent = t('off_status');

   state.activeEffect = null;
   state.lastKnownEffectResult = null;
   state.lastCompositedCanvas = null;
   if (els.copyMakeupBtn) els.copyMakeupBtn.disabled = true;
   clearOverlay();
}

/**
 * Apply UI styling after a ghostyle button is selected: mark the chosen
 * button active and clear the others, hide any preview image still on
 * screen, restyle the scan button to the "armed" appearance, cancel any
 * pending overlay fade, and update the "effect name" / "tracking" labels
 * from the active ghostyle record. Reads `state.activeEffect` directly, so
 * callers must set it before invoking this.
 *
 * @param {HTMLButtonElement} button  The button that was just clicked.
 * @see scripts/ghostyles-manager.js – `toggleEffect()` calls this after
 *   setting `state.activeEffect`.
 */
export function effectSelected(button) {
   const previewBtns = els.ghostylesContainer ? els.ghostylesContainer.querySelectorAll('.preview-btn') : [];

   previewBtns.forEach(btn => btn.classList.toggle('active', btn === button));
   if (els.previewImage) {
      els.previewImage.style.display = 'none';
      els.previewImage.removeAttribute('src');
   }

   if (els.scanBtn) {
      els.scanBtn.style.background = 'linear-gradient(180deg, rgba(159, 122, 234, 0.35), rgba(159, 122, 234, 0.15))';
      els.scanBtn.style.borderColor = 'rgba(159, 122, 234, 0.5)';
      els.scanBtn.style.color = '#fff';
   }

   if (state.overlayFadeTimeout)
      clearTimeout(state.overlayFadeTimeout);
   if (els.overlay) {
      els.overlay.style.transition = 'none';
      els.overlay.style.opacity = '1';
   }

   const style = state.loadedGhostyles.get(state.activeEffect);
   if(style) {
      if (els.effectName) els.effectName.textContent = style.name;
      if (els.effectTracking) els.effectTracking.textContent = state.activeEffect;
   } else {
      console.warn(t('active_style_missing_console', { effect: state.activeEffect }));
      if (els.effectName) els.effectName.textContent = t('not_available_label');
      if (els.effectTracking) els.effectTracking.textContent = t('off_status');
   }
}