/* ═══════════ NTPPA · playable Web-Audio piano (home "Play" band) ═══════════
   Authentic tone from additive harmonic synthesis + ADSR + low-pass +
   hammer-noise transient (no samples). Mouse / touch / computer-keyboard.
   Fires resonance ripples on a reactive canvas. Sound is opt-out. */
(function () {
  "use strict";
  var kb = document.getElementById('playKb');
  if (!kb) return;
  var reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  var actx = null, master = null, muted = false;

  function audio() {
    if (actx) return actx;
    var AC = window.AudioContext || window.webkitAudioContext; if (!AC) return null;
    actx = new AC();
    master = actx.createGain(); master.gain.value = 0.5;
    master.connect(actx.destination);
    return actx;
  }
  function voice(freq) {
    if (!actx || muted) return;
    var t = actx.currentTime;
    var bus = actx.createGain(); bus.gain.value = 0.9;
    var lp = actx.createBiquadFilter(); lp.type = 'lowpass';
    lp.frequency.value = Math.min(3800, freq * 6 + 1200); lp.Q.value = 0.5;
    bus.connect(lp); lp.connect(master);
    var parts = [[1, 1], [2, 0.46], [3, 0.26], [4, 0.14], [5, 0.07], [6, 0.035]], dur = 1.9;
    parts.forEach(function (p, i) {
      var o = actx.createOscillator(); o.type = 'sine';
      o.frequency.value = freq * p[0] * (1 + 0.0007 * p[0] * p[0]);
      var g = actx.createGain();
      g.gain.setValueAtTime(0, t);
      g.gain.linearRampToValueAtTime(p[1] * 0.9, t + 0.004);
      g.gain.exponentialRampToValueAtTime(0.0001, t + dur * (1 - i * 0.1));
      o.connect(g); g.connect(bus); o.start(t); o.stop(t + dur);
    });
    var len = Math.floor(actx.sampleRate * 0.05), buf = actx.createBuffer(1, len, actx.sampleRate), d = buf.getChannelData(0);
    for (var i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, 3);
    var n = actx.createBufferSource(); n.buffer = buf;
    var nf = actx.createBiquadFilter(); nf.type = 'bandpass'; nf.frequency.value = freq * 3;
    var ng = actx.createGain(); ng.gain.value = 0.05;
    n.connect(nf); nf.connect(ng); ng.connect(bus); n.start(t);
  }

  /* build 2 octaves C4..B5 */
  var WHITE = [['C', 0], ['D', 2], ['E', 4], ['F', 5], ['G', 7], ['A', 9], ['B', 11]];
  var BLACK = { 0: 'C#', 2: 'D#', 5: 'F#', 7: 'G#', 9: 'A#' };
  var COMP = { a:0, w:1, s:2, e:3, d:4, f:5, t:6, g:7, y:8, h:9, u:10, j:11, k:12, o:13, l:14 };
  var octaves = 2, whiteCount = WHITE.length * octaves, keyBySemi = {};
  function freqOf(semi) { return 261.63 * Math.pow(2, semi / 12); }
  for (var oc = 0; oc < octaves; oc++) {
    for (var w = 0; w < WHITE.length; w++) {
      var semi = WHITE[w][1] + oc * 12;
      var el = document.createElement('div'); el.className = 'pk-w'; el.dataset.semi = semi;
      el.setAttribute('role', 'button'); el.setAttribute('aria-label', WHITE[w][0] + (oc + 4));
      el.innerHTML = '<span class="lbl">' + WHITE[w][0] + '</span>';
      kb.appendChild(el); keyBySemi[semi] = el;
      el.addEventListener('pointerdown', mk(semi, el));
    }
  }
  var perWhite = 100 / whiteCount, wIndex = 0;
  for (var o2 = 0; o2 < octaves; o2++) {
    for (var w2 = 0; w2 < WHITE.length; w2++) {
      var off = WHITE[w2][1];
      if (BLACK[off] !== undefined) {
        var sB = off + 1 + o2 * 12;
        var b = document.createElement('div'); b.className = 'pk-b'; b.dataset.semi = sB;
        b.setAttribute('role', 'button'); b.setAttribute('aria-label', BLACK[off] + (o2 + 4));
        b.style.left = ((wIndex + 1) * perWhite) + '%';
        kb.appendChild(b); keyBySemi[sB] = b;
        b.addEventListener('pointerdown', mk(sB, b));
      }
      wIndex++;
    }
  }
  function mk(semi, el) { return function (e) { e.preventDefault(); strike(semi, el); }; }

  var hint = document.getElementById('playHint'), hintHidden = false;
  function strike(semi, el) {
    audio(); if (actx && actx.state === 'suspended') actx.resume();
    voice(freqOf(semi));
    if (el) { el.classList.add('on'); setTimeout(function () { el.classList.remove('on'); }, 140); }
    ripple(el);
    if (!hintHidden && hint) { hintHidden = true; hint.style.opacity = '.55'; }
  }
  var down = {};
  addEventListener('keydown', function (e) {
    if (e.metaKey || e.ctrlKey || e.altKey) return;
    var k = e.key.toLowerCase(); if (down[k] || COMP[k] === undefined) return;
    down[k] = 1; strike(COMP[k], keyBySemi[COMP[k]]);
  });
  addEventListener('keyup', function (e) { down[e.key.toLowerCase()] = 0; });

  var mute = document.getElementById('playMute'), mlbl = document.getElementById('playMuteLbl');
  if (mute) mute.addEventListener('click', function () {
    muted = !muted; mute.classList.toggle('muted', muted); mute.setAttribute('aria-pressed', String(!muted));
    if (mlbl) mlbl.textContent = muted ? 'Sound off' : 'Sound on';
  });

  /* reactive canvas — resonance rings from struck keys */
  var cv = document.getElementById('playCanvas'); if (!cv || reduce) return;
  var c = cv.getContext('2d'), rings = [], W, H, DPR;
  function size() { DPR = Math.min(devicePixelRatio || 1, 2); W = cv.clientWidth; H = cv.clientHeight; cv.width = W * DPR; cv.height = H * DPR; c.setTransform(DPR, 0, 0, DPR, 0, 0); }
  size(); addEventListener('resize', size);
  function ripple(el) {
    var x = W / 2, y = H * 0.78;
    if (el) { var r = el.getBoundingClientRect(), cr = cv.getBoundingClientRect(); x = r.left - cr.left + r.width / 2; y = r.top - cr.top; }
    rings.push({ x: x, y: y, r: 6, a: 0.5, hue: Math.random() < 0.5 ? '168,85,247' : '232,207,147' });
  }
  (function frame() {
    c.clearRect(0, 0, W, H);
    for (var k = rings.length - 1; k >= 0; k--) {
      var g = rings[k]; g.r += 3; g.a *= 0.965;
      c.beginPath(); c.arc(g.x, g.y, g.r, 0, 6.2832);
      c.strokeStyle = 'rgba(' + g.hue + ',' + g.a + ')'; c.lineWidth = 1.4; c.stroke();
      if (g.a < 0.02) rings.splice(k, 1);
    }
    requestAnimationFrame(frame);
  })();
})();
