/* ===========================================================================
 *  OnMic — face + mouth detection (YuNet via ONNX Runtime Web, no Python).
 *  Used by "Detect cameras": find each camera's mouth region so we can measure
 *  LIP motion (not whole-frame motion) and correlate it with the mics.
 *  YuNet 2023mar: input 1x3x640x640 (BGR, 0-255), multi-stride heads
 *  (cls/obj/bbox/kps at strides 8/16/32). Landmarks order:
 *    0 right-eye, 1 left-eye, 2 nose, 3 right-mouth-corner, 4 left-mouth-corner.
 * ======================================================================== */
(function (root) {
  "use strict";

  var INSZ = 640, STRIDES = [8, 16, 32];
  var session = null, ready = false, lastError = "";

  function haveOrt(){ return typeof ort !== "undefined" && ort && ort.InferenceSession; }
  function isReady(){ return ready; }
  function getError(){ return lastError; }

  function init(panelDir, fsMod, logFn){
    return new Promise(function(resolve){
      if (ready) { resolve(true); return; }
      if (!haveOrt()) { lastError = "onnxruntime-web not loaded"; resolve(false); return; }
      try {
        ort.env.wasm.numThreads = 1; ort.env.wasm.simd = false;
        ort.env.wasm.wasmPaths = panelDir + "/vendor/";
      } catch(e){}
      var modelPath = panelDir + "/models/face_detection_yunet.onnx";
      var bytes;
      try { bytes = fsMod.readFileSync(modelPath); }
      catch(e){ lastError = "face model not found: " + modelPath; resolve(false); return; }
      var u8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
      ort.InferenceSession.create(u8).then(function(s){
        session = s; ready = true;
        if (logFn) logFn("[face] model ready (yunet)");
        resolve(true);
      }).catch(function(err){ lastError = String(err); resolve(false); });
    });
  }

  // RGBA ImageData at 640x640 -> Float32 [1,3,640,640] planar BGR, 0-255.
  function preprocess(imgData){
    var W = INSZ, H = INSZ, HW = W * H, d = imgData.data, out = new Float32Array(3 * HW);
    for (var p = 0, i = 0; p < HW; p++, i += 4){
      out[p]        = d[i + 2];   // B
      out[HW + p]   = d[i + 1];   // G
      out[2*HW + p] = d[i];       // R
    }
    return out;
  }

  // Decode the YuNet heads (in 640x640 space). outMap[name].data are Float32Arrays.
  // Returns faces: { score, x, y, w, h, cx, cy, lm:[{x,y}*5] }, NMS-filtered.
  function decode(outMap, scoreThresh){
    scoreThresh = (scoreThresh == null) ? 0.6 : scoreThresh;
    var faces = [];
    for (var si = 0; si < STRIDES.length; si++){
      var stride = STRIDES[si], fw = INSZ / stride;
      var cls = outMap["cls_"+stride].data, obj = outMap["obj_"+stride].data,
          bb  = outMap["bbox_"+stride].data, kp = outMap["kps_"+stride].data;
      var N = cls.length;
      for (var i = 0; i < N; i++){
        var c = cls[i] < 0 ? 0 : (cls[i] > 1 ? 1 : cls[i]);
        var o = obj[i] < 0 ? 0 : (obj[i] > 1 ? 1 : obj[i]);
        var sc = Math.sqrt(c * o);
        if (sc < scoreThresh) continue;
        var col = i % fw, row = (i / fw) | 0;
        var cx = (col + bb[i*4])   * stride;
        var cy = (row + bb[i*4+1]) * stride;
        var w  = Math.exp(bb[i*4+2]) * stride;
        var h  = Math.exp(bb[i*4+3]) * stride;
        var lm = [];
        for (var k = 0; k < 5; k++) lm.push({ x:(col + kp[i*10+2*k])*stride, y:(row + kp[i*10+2*k+1])*stride });
        faces.push({ score:sc, x:cx-w/2, y:cy-h/2, w:w, h:h, cx:cx, cy:cy, lm:lm });
      }
    }
    return nms(faces, 0.3);
  }

  function iou(a, b){
    var x1 = Math.max(a.x, b.x), y1 = Math.max(a.y, b.y),
        x2 = Math.min(a.x+a.w, b.x+b.w), y2 = Math.min(a.y+a.h, b.y+b.h);
    var iw = Math.max(0, x2-x1), ih = Math.max(0, y2-y1), inter = iw*ih;
    var uni = a.w*a.h + b.w*b.h - inter;
    return uni > 0 ? inter/uni : 0;
  }
  function nms(faces, iouT){
    faces.sort(function(p,q){ return q.score - p.score; });
    var keep = [];
    for (var i = 0; i < faces.length; i++){
      var ok = true;
      for (var j = 0; j < keep.length; j++){ if (iou(faces[i], keep[j]) > iouT){ ok = false; break; } }
      if (ok) keep.push(faces[i]);
      if (keep.length >= 8) break;
    }
    return keep;
  }

  // Mouth region (in the same space as the faces) from the two mouth-corner
  // landmarks (3,4), padded a bit. Returns {x,y,w,h} or null.
  function mouthRect(face){
    if (!face || !face.lm || face.lm.length < 5) return null;
    var r = face.lm[3], l = face.lm[4];
    var mx = (r.x + l.x)/2, my = (r.y + l.y)/2;
    var dx = r.x - l.x, dy = r.y - l.y, span = Math.sqrt(dx*dx + dy*dy);
    if (!(span > 0)) span = face.w * 0.35;
    var w = span * 1.6, h = span * 1.1;            // a bit wider than the lips, enough vertical for jaw
    return { x: mx - w/2, y: my - h/2, w: w, h: h };
  }

  // Async: run YuNet on a 640x640 ImageData. Returns decoded faces (640 space).
  function detect(imgData640, scoreThresh){
    if (!ready || !session) return Promise.resolve([]);
    var input = preprocess(imgData640);
    var feeds = {}; feeds[session.inputNames[0]] = new ort.Tensor("float32", input, [1,3,INSZ,INSZ]);
    return session.run(feeds).then(function(out){ return decode(out, scoreThresh); })
      .catch(function(){ return []; });
  }

  var API = { init:init, detect:detect, decode:decode, preprocess:preprocess,
              mouthRect:mouthRect, nms:nms, isReady:isReady, getError:getError, INSZ:INSZ };
  if (typeof module === "object" && module.exports) module.exports = API;
  root.OnMicFace = API;
})(typeof window!=="undefined"?window:this);
