// Micara bridge — mixage audio : dominance + noise gate (P1-E)
//
// Module PUR (aucune dépendance DOM / Web Audio / Electron) : chargeable en
// Node (module.exports) pour les tests, et dans le renderer (<script>) via
// window.MicaraMixer. Le renderer branche ces calculs sur la Web Audio API.
//
// Comportement :
//  - noise gate par flux : hystérésis (ouverture à gateOpenDb, fermeture à
//    gateCloseDb) + hold (gateHoldMs) pour ne pas couper les fins de mots.
//  - dominance : parmi les flux ouverts, seul le plus fort (RMS) parle à
//    gain 1.0 ; les autres sont atténués de duckDb. Personne ne parle → 0.
//    Anti-clapot (P1-E2) : bascule de dominant seulement si le challenger
//    dépasse le dominant actuel de dominanceMarginDb ET que dominanceDwellMs
//    s'est écoulé depuis la dernière bascule — sinon deux locuteurs de
//    niveaux proches feraient basculer le dominant à chaque tick et les deux
//    gains resteraient à mi-course (2 micros audibles en même temps).
//  - tick(entries, nowMs) → { gains, dominant, openCount } : gains CIBLES,
//    le renderer les lisse (attack/release) via setTargetAtTime.
(function (root, factory) {
  if (typeof module === 'object' && module.exports) module.exports = factory();
  else if (root) root.MicaraMixer = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';

  const DB_MIN = -120; // plancher dBFS (silence numérique)

  // RMS d'un buffer d'échantillons Float32 → dBFS (clampé à DB_MIN).
  function rmsDb(samples) {
    if (!samples || samples.length === 0) return DB_MIN;
    let sum = 0;
    for (let i = 0; i < samples.length; i++) sum += samples[i] * samples[i];
    const rms = Math.sqrt(sum / samples.length);
    if (rms <= 1e-9) return DB_MIN;
    return Math.max(20 * Math.log10(rms), DB_MIN);
  }

  // Mixeur dominance + noise gate. Sans état DOM : un Map id → {open,
  // lastOpenMs} alimenté par tick(). Les ids sont opaques (phoneId).
  class DominanceMixer {
    constructor(opts = {}) {
      this.gateOpenDb = opts.gateOpenDb ?? -45;   // dBFS : ouverture du gate
      this.gateCloseDb = opts.gateCloseDb ?? -50; // dBFS : fermeture (hystérésis)
      this.gateHoldMs = opts.gateHoldMs ?? 400;   // maintien avant fermeture
      this.duckDb = opts.duckDb ?? -18;           // atténuation des non-dominants
      // Anti-clapot de dominance (P1-E2) : marge + temps de maintien avant
      // toute bascule. Sans ça, deux locuteurs simultanés de niveaux proches
      // font basculer le dominant à chaque tick (50 ms) et les deux gains
      // restent à mi-course → les deux micros restent audibles (18/08).
      this.dominanceMarginDb = opts.dominanceMarginDb ?? 3;   // le challenger doit dépasser le dominant de ≥3 dB
      this.dominanceDwellMs = opts.dominanceDwellMs ?? 300;   // délai minimum entre deux bascules
      this.states = new Map(); // id → { open: boolean, lastOpenMs: number }
      this.dominantId = null;  // dominant actuel (persistant entre les ticks)
      this.lastSwitchMs = 0;   // horodatage de la dernière bascule
    }

    // entries : [{ id, db }] — db en dBFS (rmsDb). nowMs : horloge moteur.
    // Retour : { gains: Map(id → 0..1), dominant: id|null, openCount: number }
    tick(entries, nowMs) {
      // 1) gate par flux : hystérésis + hold
      for (const e of entries) {
        let st = this.states.get(e.id);
        if (!st) { st = { open: false, lastOpenMs: 0 }; this.states.set(e.id, st); }
        if (!st.open) {
          if (e.db >= this.gateOpenDb) { st.open = true; st.lastOpenMs = nowMs; }
        } else if (e.db >= this.gateCloseDb) {
          st.lastOpenMs = nowMs; // au-dessus du seuil bas → on reste ouvert
        } else if (nowMs - st.lastOpenMs >= this.gateHoldMs) {
          st.open = false;
        }
      }

      // 2) dominance : le plus fort parmi les flux ouverts, avec anti-clapot.
      //    Sans hystérésis ni dwell, deux locuteurs de niveaux proches font
      //    basculer le dominant à CHAQUE tick (50 ms) : comme l'attaque
      //    (50 ms) ≈ période de tick et que le release est lent (300 ms),
      //    les deux gains restent à mi-course → les deux micros audibles.
      const open = [];
      for (const e of entries) if (this.states.get(e.id).open) open.push(e);
      const openCount = open.length;
      let dominant = null;
      if (openCount) {
        let loudest = open[0];
        for (const e of open) if (e.db > loudest.db) loudest = e;
        const cur = this.dominantId;
        const curEntry = cur && this.states.has(cur) && this.states.get(cur).open
          ? open.find((e) => e.id === cur)
          : null;
        if (!curEntry) {
          dominant = loudest.id; // pas de dominant actif (ou parti/fermé) → le plus fort
        } else if (loudest.id === cur) {
          dominant = cur; // le dominant actuel est le plus fort → inchangé
        } else if (nowMs - this.lastSwitchMs >= this.dominanceDwellMs
            && loudest.db >= curEntry.db + this.dominanceMarginDb) {
          dominant = loudest.id; // challenger nettement plus fort ET délai écoulé → bascule
        } else {
          dominant = cur; // challenger trop proche ou trop tôt → on garde le dominant
        }
      }
      if (dominant !== this.dominantId) {
        this.dominantId = dominant;
        this.lastSwitchMs = nowMs;
      }

      // 3) gains cibles
      const duckGain = Math.pow(10, this.duckDb / 20);
      const gains = new Map();
      for (const e of entries) {
        const st = this.states.get(e.id);
        gains.set(e.id, st.open ? (e.id === dominant ? 1 : duckGain) : 0);
      }
      return { gains, dominant, openCount };
    }

    // Oublie un flux (téléphone déconnecté) — évite les états orphelins.
    // Si c'était le dominant, on repart du plus fort au prochain tick.
    forget(id) {
      this.states.delete(id);
      if (this.dominantId === id) this.dominantId = null;
    }
  }

  return { rmsDb, DominanceMixer, DB_MIN };
});
