/* Mixer screen — 8 channel strips + master strip */

// Convert linear amplitude (0.0–1.0+) to dB, clamped to -60 minimum
function linearToDb(linear) {
  if (linear <= 0.000001) return -60;
  var db = 20 * Math.log10(linear);
  return db < -60 ? -60 : db;
}

/* Pan slider — mouse AND touch drag.
   The old inline handler registered only mousemove/mouseup, which never fire
   during a touch drag on the Pi's WebKit touchscreen (only one synthetic
   mousedown is dispatched), so the dot snapped to the first tap and could not
   be dragged. Mirrors the working touch pattern in EqDragCell.
   Drag spans the full track to -100..100 (matches the pan-dot CSS); values
   within +/-4 snap to exact center C so it's reachable with a fingertip. */
function PanTrack({ value, onChange, dotStyle }){
  const startDrag = (e)=>{
    e.preventDefault();
    const track = e.currentTarget;
    const update = (ev)=>{
      const rect = track.getBoundingClientRect();
      const cx = ev.touches ? ev.touches[0].clientX : ev.clientX;
      const p = Math.max(0, Math.min(1, (cx - rect.left) / rect.width));
      let v = Math.round((p - 0.5) * 200);
      if (Math.abs(v) <= 4) v = 0;
      onChange(v);
    };
    const up = ()=>{
      window.removeEventListener('mousemove',update); window.removeEventListener('mouseup',up);
      window.removeEventListener('touchmove',update); window.removeEventListener('touchend',up);
    };
    window.addEventListener('mousemove',update); window.addEventListener('mouseup',up);
    window.addEventListener('touchmove',update,{passive:false}); window.addEventListener('touchend',up);
    update(e);
  };
  return (
    <div className="strip-pan" onClick={e=>e.stopPropagation()}>
      <div className="pan-track"
           onMouseDown={startDrag} onTouchStart={startDrag}
           onDoubleClick={()=>onChange(0)}>
        <div className="pan-tick"/>
        <div className="pan-dot" style={{left:(50 + value/2) + '%', ...dotStyle}}/>
      </div>
      <div className="pan-val">{value===0?'C':(value<0?'L':'R')+Math.abs(value)}</div>
    </div>
  );
}

function MixerStrip({ ch, idx, state, onSelect, onEdit, onToggle, onFader, onPan, onEqBand, isSelected }){
  const cs = state.channels[ch.id];
  // Meter values AND the MIDI activity LED are written directly to the
  // DOM by MixerScreen's poll effect (data-meter-* / data-led-ch
  // selectors below). No meter or midi-active props here so the strip
  // doesn't re-render on every poll tick.
  const pgOn = cs.pg !== false;  // missing → treat as enabled (matches default)

  return (
    <div className={"strip "+(isSelected?'is-selected':'')} onClick={onSelect}>
      <div className="midi-led" data-led-ch={ch.id}/>
      <div className="strip-head">
        <div className="strip-num" style={{display:'flex',alignItems:'center',gap:6,justifyContent:'center'}}>
          <span>CH {String(ch.id+1).padStart(2,'0')} · MIDI {cs.midiCh+1}</span>
          <label
            onClick={e=>e.stopPropagation()}
            title="Accept MIDI Program Change → swap soundmap"
            style={{display:'inline-flex',alignItems:'center',gap:3,cursor:'pointer',userSelect:'none',fontFamily:'Rajdhani',fontWeight:700,fontSize:9,letterSpacing:'.14em',color: pgOn?'var(--accent-a)':'var(--ink-mute)'}}>
            <input
              type="checkbox"
              checked={pgOn}
              onChange={()=>onToggle(ch.id,'pg')}
              style={{accentColor:'var(--accent-a)',width:11,height:11,margin:0}}/>
            PG
          </label>
        </div>
        <div className="strip-name" title={cs.loopBank || cs.map || ''}>{cs.loopBank || cs.map || '— no map —'}</div>
        <div className="strip-kind" style={{color: ch.color}}>{cs.mapCategory || ch.kind}</div>
      </div>

      <PanTrack value={cs.pan} onChange={v=>onPan(ch.id, v)}/>

      <div className="eq-mini" onClick={e=>e.stopPropagation()}>
        <EqDragCell label="LO" value={cs.eq.low.gain}
          onChange={v=>onEqBand(ch.id,'low',v)}/>
        <EqDragCell label="MID" value={cs.eq.mid.gain}
          onChange={v=>onEqBand(ch.id,'mid',v)}/>
        <EqDragCell label="HI" value={cs.eq.high.gain}
          onChange={v=>onEqBand(ch.id,'high',v)}/>
      </div>

      <div className="fader-area" onClick={e=>e.stopPropagation()}>
        <Meter dataMeterCh={ch.id} dataMeterSide="L"/>
        <Fader db={cs.volume} onChange={v=>onFader(ch.id, v)} selected={isSelected}/>
        <Meter dataMeterCh={ch.id} dataMeterSide="R"/>
      </div>

      <div className="strip-volval">{fmtGain(cs.volume)} dB</div>

      <div className="ms-row" onClick={e=>e.stopPropagation()}>
        <button className={"ms-btn "+(cs.mute?'mute-on':'')} onClick={()=>onToggle(ch.id,'mute')}>MUTE</button>
        <button className={"ms-btn "+(cs.solo?'solo-on':'')} onClick={()=>onToggle(ch.id,'solo')}>SOLO</button>
      </div>

      <button className="strip-edit" onClick={e=>{e.stopPropagation(); onEdit(ch.id);}}>EDIT CHANNEL</button>
    </div>
  );
}

function MasterStrip({ state, setState, onFader, midiActive, onOpenMaster }){
  const m = state.master;
  // Meter values are written directly to the DOM by MixerScreen's poll
  // effect (data-meter-master selectors). No meter prop here.

  const limFx = m.fx && m.fx.find(function(f){ return f.id === 'mlim'; });
  const cmpFx = m.fx && m.fx.find(function(f){ return f.id === 'mcomp'; });
  const limOn = limFx ? limFx.enabled : false;
  const cmpOn = cmpFx ? cmpFx.enabled : false;

  const toggleLim = (e)=>{
    e.stopPropagation();
    const next = !limOn;
    setState(s=>{
      const fx = s.master.fx.map(function(f){ return f.id === 'mlim' ? {...f, enabled: next} : f; });
      return {...s, master:{...s.master, fx: fx}};
    });
    TauriAPI.toggleMasterEffect('mlim', next);
  };
  const toggleCmp = (e)=>{
    e.stopPropagation();
    const next = !cmpOn;
    setState(s=>{
      const fx = s.master.fx.map(function(f){ return f.id === 'mcomp' ? {...f, enabled: next} : f; });
      return {...s, master:{...s.master, fx: fx}};
    });
    TauriAPI.toggleMasterEffect('mcomp', next);
  };

  return (
    <div className="strip is-master" onClick={onOpenMaster}>
      <div className="strip-head">
        <div className="strip-num" style={{color:'#22d3ee'}}>MASTER OUT · STEREO</div>
        <div className="strip-name" style={{background:'linear-gradient(90deg,#9b5cff,#22d3ee)',WebkitBackgroundClip:'text',backgroundClip:'text',color:'transparent'}}>MAIN MIX</div>
        <div className="strip-kind">+{m.headroom}dB HEADROOM</div>
      </div>

      <PanTrack value={m.pan||0}
        onChange={v=>{setState(s=>({...s, master:{...s.master, pan:v}})); TauriAPI.setMasterPan(v);}}
        dotStyle={{background:'#9b5cff', boxShadow:'0 0 8px rgba(155,92,255,0.6)'}}/>

      <div className="eq-mini" onClick={e=>e.stopPropagation()}>
        <EqDragCell label="LO" value={m.eq.low.gain}
          onChange={v=>{setState(s=>({...s, master:{...s.master, eq:{...s.master.eq, low:{...s.master.eq.low, gain:v}}}})); TauriAPI.setMasterEq('low', v, m.eq.low.freq);}}/>
        <EqDragCell label="MID" value={m.eq.mid.gain}
          onChange={v=>{setState(s=>({...s, master:{...s.master, eq:{...s.master.eq, mid:{...s.master.eq.mid, gain:v}}}})); TauriAPI.setMasterEq('mid', v, m.eq.mid.freq);}}/>
        <EqDragCell label="HI" value={m.eq.high.gain}
          onChange={v=>{setState(s=>({...s, master:{...s.master, eq:{...s.master.eq, high:{...s.master.eq.high, gain:v}}}})); TauriAPI.setMasterEq('high', v, m.eq.high.freq);}}/>
      </div>

      <div className="fader-area" onClick={e=>e.stopPropagation()}>
        <Meter dataMeterMaster="L"/>
        <Fader db={m.volume} onChange={v=>onFader(v)}/>
        <Meter dataMeterMaster="R"/>
      </div>

      <div className="strip-volval">{fmtGain(m.volume)} dB</div>

      <div className="ms-row" onClick={e=>e.stopPropagation()}>
        <button className={"ms-btn "+(limOn?'lim-on':'')}
          style={limOn ? {background:'rgba(34,211,238,0.2)',borderColor:'#22d3ee',color:'#22d3ee'} : {background:'rgba(34,211,238,0.08)',borderColor:'#1a3d48',color:'#a7e8f5'}}
          onClick={toggleLim}>LIM ON</button>
        <button className={"ms-btn "+(cmpOn?'cmp-on':'')}
          style={cmpOn ? {background:'rgba(155,92,255,0.2)',borderColor:'#9b5cff',color:'#9b5cff'} : {background:'rgba(155,92,255,0.08)',borderColor:'#2a2040',color:'#d5bfff'}}
          onClick={toggleCmp}>BUS CMP</button>
      </div>

      <button className="strip-edit" onClick={onOpenMaster} style={{borderColor:'#1a3d48', background:'linear-gradient(180deg,rgba(34,211,238,0.08),rgba(155,92,255,0.04))'}}>MASTER FX</button>
    </div>
  );
}

function fmtGain(v){
  const n = Math.round(v*10)/10;
  if(n===0) return '0.0';
  return (n>0?'+':'')+n.toFixed(1);
}

function EqDragCell({ label, value, onChange }){
  const startY = useRef(0), startV = useRef(0);
  const onDown = (e)=>{
    e.preventDefault();
    startY.current = e.touches?e.touches[0].clientY:e.clientY;
    startV.current = value;
    const move = (ev)=>{
      const y = ev.touches?ev.touches[0].clientY:ev.clientY;
      const dy = startY.current - y;
      const nv = Math.max(-18, Math.min(18, startV.current + (dy/80)*36));
      onChange && onChange(Math.round(nv*10)/10);
    };
    const up = ()=>{
      window.removeEventListener('mousemove',move);window.removeEventListener('mouseup',up);
      window.removeEventListener('touchmove',move);window.removeEventListener('touchend',up);
    };
    window.addEventListener('mousemove',move);window.addEventListener('mouseup',up);
    window.addEventListener('touchmove',move,{passive:false});window.addEventListener('touchend',up);
  };
  const onDbl = ()=> onChange && onChange(0);
  // mini gain bar (visual feedback)
  const pct = (value+18)/36;
  return (
    <div className="eq-cell eq-drag" onMouseDown={onDown} onTouchStart={onDown} onDoubleClick={onDbl}>
      <div className="eq-bar">
        <div className="eq-bar-center"/>
        <div className={"eq-bar-fill "+(value>=0?'pos':'neg')}
             style={{
               bottom: value>=0?'50%':((0.5 - (0.5-pct))*100)+'%',
               top: value>=0?((0.5 - pct)*100)+'%':'50%',
             }}/>
      </div>
      <div className="lbl">{label}</div>
      <div className="val">{fmtGain(value)}</div>
    </div>
  );
}

function MixerScreen({ state, setState, onEdit, onOpenMaster }){
  const sel = state.selectedChannel;
  const mixerRef = useRef(null);

  useEffect(()=>{
    if(!TauriAPI.available) return;
    let active = true;
    let t;
    // Cached meter element refs. Built lazily on first successful poll
    // (after MixerStrip / MasterStrip have rendered). This avoids
    // querySelector cost on every poll — we resolve each .fill div once,
    // then mutate `style.height` directly without React involvement.
    let cache = null;

    // Linear amplitude → dB → 0..100 % height. Inlined so the hot poll
    // path doesn't pay a function-call cost on Pi WebKit.
    const dbHeight = (linear)=>{
      if (linear <= 0.000001) return 0;
      const db = 20 * Math.log10(linear);
      if (db < -60) return 0;
      // -60..+3 dB → 0..100 %
      const pct = ((db + 60) / 63) * 100;
      return pct < 0 ? 0 : (pct > 100 ? 100 : pct);
    };

    const ensureCache = ()=>{
      if (cache) return cache;
      const root = mixerRef.current;
      if (!root) return null;
      const ch = [];
      let ok = true;
      for (let i = 0; i < 8; i++) {
        const l = root.querySelector('[data-meter-ch="'+i+'"][data-meter-side="L"]');
        const r = root.querySelector('[data-meter-ch="'+i+'"][data-meter-side="R"]');
        const led = root.querySelector('[data-led-ch="'+i+'"]');
        // Cache last rendered bucket per element so we can skip clip-path
        // writes when the value hasn't changed visibly. WebKit on Pi 5
        // re-paints on every clip-path mutation even with `will-change`,
        // so suppressing same-bucket writes is the dominant cost win.
        ch.push({ l: l, r: r, led: led, lit: false, lLast: -1, rLast: -1 });
        if (!l || !r || !led) ok = false;
      }
      const ml = root.querySelector('[data-meter-master="L"]');
      const mr = root.querySelector('[data-meter-master="R"]');
      if (!ml || !mr) ok = false;
      if (!ok) return null;       // Mount race — try again next tick.
      cache = { ch: ch, master: { l: ml, r: mr, lLast: -1, rLast: -1 } };
      return cache;
    };

    // Linear amplitude above which the per-channel MIDI activity LED lights
    // up. ~-50 dB linear → 0.003.
    const LED_THRESHOLD = 0.003;

    // Quantize the visible level to 2 % buckets. A meter that hovers
    // around 47.3 % vs 47.6 % looks identical to the eye; gating on the
    // bucket means we hit the WebKit paint pipeline only when the value
    // really moved, not on every poll.
    const QUANT = 2;

    // Set the clip-path on a fill, but only when the quantized bucket
    // changed since the last write. `bucketKey` is "(el, lastField)" —
    // we read/write the cached integer next to the element.
    const setLevelIfChanged = (slot, sideKey, linear)=>{
      const pct = dbHeight(linear);
      const bucket = (pct / QUANT) | 0;       // floor to 2 % step
      if (slot[sideKey] === bucket) return;   // no visible change
      slot[sideKey] = bucket;
      slot[sideKey === 'lLast' ? 'l' : 'r'].style.clipPath =
        'inset(' + (100 - bucket * QUANT) + '% 0 0 0)';
    };

    const poll = ()=>{
      TauriAPI.getMeterLevels().then(function(data){
        if(!active || !data) return;
        const c = ensureCache();
        if (!c) return;
        const peaks = data.channel_peaks || [];
        for (let i = 0; i < c.ch.length; i++) {
          const p = peaks[i];
          if (!p) continue;
          // peaks are [left, right] arrays from the Rust DTO.
          const left = Array.isArray(p) ? p[0] : (p.left != null ? p.left : 0);
          const right = Array.isArray(p) ? p[1] : (p.right != null ? p.right : 0);
          setLevelIfChanged(c.ch[i], 'lLast', left);
          setLevelIfChanged(c.ch[i], 'rLast', right);
          // Per-channel activity LED — classList toggle only when the
          // boolean actually flips (cached `lit` flag).
          const lit = (left > LED_THRESHOLD) || (right > LED_THRESHOLD);
          if (lit !== c.ch[i].lit) {
            c.ch[i].led.classList.toggle('lit', lit);
            c.ch[i].lit = lit;
          }
        }
        const mp = data.master_peak;
        if (mp) {
          const ml = Array.isArray(mp) ? mp[0] : (mp.left != null ? mp.left : 0);
          const mr = Array.isArray(mp) ? mp[1] : (mp.right != null ? mp.right : 0);
          setLevelIfChanged(c.master, 'lLast', ml);
          setLevelIfChanged(c.master, 'rLast', mr);
        }
      });
      // 200 ms (5 Hz) — the audio engine still does per-buffer (5.8 ms)
      // peak hold inside Rust, so we never miss a transient; we just
      // sample the held peak less often. With the bucket-quantized
      // skip-write above, the steady-state cost on Pi WebKit is nearly
      // zero when the strip is idle.
      if(active) t = setTimeout(poll, 200);
    };
    poll();
    return ()=>{ active = false; clearTimeout(t); cache = null; };
  }, []);

  return (
    <div className="mixer" ref={mixerRef}>
      {CHANNELS.map((ch, idx)=>(
        <MixerStrip key={ch.id} ch={ch} idx={idx} state={state}
          isSelected={sel===ch.id}
          onSelect={()=>setState(s=>({...s, selectedChannel: ch.id}))}
          onEdit={id=>onEdit(id)}
          onToggle={(id, which)=>{
            setState(s=>{
              const c = {...s.channels[id]};
              if (which === 'pg') {
                // pg defaults to true (missing → on). Explicit toggle so the
                // first click on a session that lacks the field still turns it off.
                c.pg = (c.pg !== false) ? false : true;
              } else {
                c[which] = !c[which];
              }
              if(which==='mute') TauriAPI.setMute(id, c.mute);
              if(which==='solo') TauriAPI.setSolo(id, c.solo);
              return {...s, channels:{...s.channels, [id]: c}};
            });
          }}
          onFader={(id, v)=>{
            setState(s=>({...s, channels:{...s.channels, [id]: {...s.channels[id], volume:v}}}));
            TauriAPI.setVolume(id, v);
          }}
          onPan={(id, v)=>{
            setState(s=>({...s, channels:{...s.channels, [id]: {...s.channels[id], pan:v}}}));
            TauriAPI.setPan(id, v);
          }}
          onEqBand={(id, band, v)=>{
            setState(s=>{
              const ch = s.channels[id];
              const newEq = {...ch.eq, [band]:{...ch.eq[band], gain:v}};
              TauriAPI.setEqBand(id, band, v, newEq[band].freq);
              return {...s, channels:{...s.channels, [id]: {...ch, eq:newEq}}};
            });
          }}
        />
      ))}
      <MasterStrip state={state} setState={setState}
        onFader={v=>{setState(s=>({...s, master:{...s.master, volume:v}})); TauriAPI.setMasterVolume(v);}}
        onOpenMaster={onOpenMaster}
      />
    </div>
  );
}

Object.assign(window, { MixerScreen, fmtGain, EqDragCell });
