///////////////////////////////////////////////////////////////
// METRICS MONITOR — ANALYZER MODULE (MPX Spectrum)          //
// With mouse wheel zoom and drag-pan                        //
///////////////////////////////////////////////////////////////

(() => {
const sampleRate = 192000;    // Do not touch - this value is automatically updated via the config file
const stereoBoost = 3;    // Do not touch - this value is automatically updated via the config file
const eqBoost = 1;    // Do not touch - this value is automatically updated via the config file
const fftSize = 2048;    // Do not touch - this value is automatically updated via the config file
const SpectrumAverageLevel = 15;    // Do not touch - this value is automatically updated via the config file
const minSendIntervalMs = 30;    // Do not touch - this value is automatically updated via the config file
const MPXmode = "auto";    // Do not touch - this value is automatically updated via the config file
const MPXStereoDecoder = "off";    // Do not touch - this value is automatically updated via the config file
const MPXInputCard = "";    // Do not touch - this value is automatically updated via the config file


/////////////////////////////////////////////////////////////////

let mpxCanvas = null;
let mpxCtx = null;

let mpxSpectrum = [];
let mpxSmoothSpectrum = [];

const TOP_MARGIN = 18;
const BOTTOM_MARGIN = 4;
const OFFSET_X = 32;
const Y_STRETCH = 0.8;
const GRID_X_OFFSET = 30;
const BASE_SCALE_DB = [-10, -20, -30, -40, -50, -60, -70, -80];

let MPX_AVERAGE_LEVELS = SpectrumAverageLevel;

// Original dB range (for reset)
const MPX_DB_MIN_DEFAULT = -80;
const MPX_DB_MAX_DEFAULT = 0;

// Current dB range (modifiable by vertical zoom)
let MPX_DB_MIN = -80;
let MPX_DB_MAX = 0;
let MPX_FMAX_HZ = 76000;

let CURVE_GAIN = 0.5;
let CURVE_Y_OFFSET_DB = -40;
let CURVE_VERTICAL_DYNAMICS = 1.9;
let CURVE_X_STRETCH = 1.40;
let CURVE_X_SCALE = 1.0;

let LABEL_CURVE_X_SCALE = 0.9;
let LABEL_X_OFFSET = -64;
let LABEL_Y_OFFSET = -14;

// ============================================================
// ZOOM VARIABLES (Horizontal)
// ============================================================
let zoomLevel = 1.0;
let zoomCenterHz = 38000;
const MIN_ZOOM = 1.0;
const MAX_ZOOM = 20.0;
const ZOOM_STEP = 1.3;

let visibleStartHz = 0;
let visibleEndHz = MPX_FMAX_HZ;

// ============================================================
// ZOOM VARIABLES (Vertical)
// ============================================================
let zoomLevelY = 1.0;
let zoomCenterDB = -40;
const MIN_ZOOM_Y = 1.0;
const MAX_ZOOM_Y = 5.0;
const ZOOM_STEP_Y = 1.2;

let visibleDbMin = MPX_DB_MIN_DEFAULT;
let visibleDbMax = MPX_DB_MAX_DEFAULT;

// Actual maximum frequency of FFT data
let FFT_MAX_HZ = sampleRate / 2;

// Drag variables
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
let dragStartCenterHz = 0;
let dragStartCenterDB = 0;
let hasDragged = false;

// ============================================================
// MAGNIFIER ICON AND TOOLTIP VARIABLES
// ============================================================
let magnifierArea = { x: 0, y: 0, width: 0, height: 0 };
let isHoveringMagnifier = false;
let tooltipElement = null;

if (sampleRate === 48000) {
  CURVE_X_STRETCH = 4.7;
  LABEL_CURVE_X_SCALE = 0.27;
  MPX_FMAX_HZ = 24000;
  zoomCenterHz = 12000;
  FFT_MAX_HZ = 24000;
}
if (sampleRate === 96000) {
  CURVE_X_STRETCH = 2.325;
  LABEL_CURVE_X_SCALE = 0.54;
  MPX_FMAX_HZ = 48000;
  zoomCenterHz = 24000;
  FFT_MAX_HZ = 48000;
}
if (sampleRate === 192000) {
  FFT_MAX_HZ = 96000;
}

// Set initial values
visibleStartHz = 0;
visibleEndHz = MPX_FMAX_HZ;
visibleDbMin = MPX_DB_MIN_DEFAULT;
visibleDbMax = MPX_DB_MAX_DEFAULT;

const currentURL = window.location;
const PORT = currentURL.port || (currentURL.protocol === "https:" ? "443" : "80");
const protocol = currentURL.protocol === "https:" ?  "wss:" : "ws:";
const HOST = currentURL.hostname;
const WS_URL = `${protocol}//${HOST}:${PORT}/data_plugins`;

let mpxSocket = null;

function getDisplayRange() {
  return { min: visibleDbMin, max: visibleDbMax };
}

// ============================================================
// TOOLTIP FUNCTIONS
// ============================================================
function showTooltip() {
  if (tooltipElement) return;

  tooltipElement = document.createElement("div");
  tooltipElement.id = "mpx-zoom-tooltip";
  tooltipElement.innerHTML = `
    <div style="margin-bottom: 10px; font-weight: bold;">Spectrum Zoom Controls</div>
    <div style="margin-bottom: 4px;">• Scroll wheel: Horizontal zoom (frequency)</div>
    <div style="margin-bottom: 4px;">• Ctrl + Scroll: Vertical zoom (dB)</div>
    <div style="margin-bottom: 4px;">• Left-click + Drag: Pan spectrum</div>
    <div style="margin-bottom: 4px;">• Right-click: Reset zoom</div>
  `;

  tooltipElement.style.cssText = `
    position: absolute;
    background: linear-gradient(to bottom, rgba(0, 40, 70, 0.95), rgba(0, 25, 50, 0.95));
    border: 1px solid rgba(143, 234, 255, 0.5);
    border-radius: 8px;
    padding: 12px 16px;
    color: #8feaff;
    font-family: Arial, sans-serif;
    font-size: 12px;
    line-height: 1.4;
    z-index: 10000;
    pointer-events: none;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
    opacity: 0;
    transition: opacity 0.2s ease-in-out;
    max-width: 280px;
    white-space: nowrap;
  `;

  // Add tooltip to parent container
  const parent = mpxCanvas.parentElement;
  parent.style.position = "relative";
  parent.appendChild(tooltipElement);

  // Position: centered above the magnifier
  const tooltipWidth = 280;
  const tooltipLeft = (mpxCanvas.width - tooltipWidth) / 2;
  const tooltipTop = magnifierArea.y - 130;

  tooltipElement.style.left = `${Math.max(5, tooltipLeft)}px`;
  tooltipElement.style.top = `${Math.max(5, tooltipTop)}px`;

  // Fade in
  requestAnimationFrame(() => {
    if (tooltipElement) {
      tooltipElement.style.opacity = "1";
    }
  });
}

function hideTooltip() {
  if (! tooltipElement) return;

  tooltipElement.style.opacity = "0";

  setTimeout(() => {
    if (tooltipElement && tooltipElement.parentElement) {
      tooltipElement.parentElement.removeChild(tooltipElement);
    }
    tooltipElement = null;
  }, 200);
}

// ============================================================
// Frequency to Bin Index Conversion
// ============================================================
function freqToBin(freqHz, totalBins) {
  const normalizedDisplayX = freqHz / (MPX_FMAX_HZ * LABEL_CURVE_X_SCALE);
  const binIndex = normalizedDisplayX * (totalBins - 1) / CURVE_X_STRETCH;
  return Math.round(Math.max(0, Math.min(totalBins - 1, binIndex)));
}

function binToFreq(binIndex, totalBins) {
  const normalizedDisplayX = (binIndex / (totalBins - 1)) * CURVE_X_STRETCH;
  const freqHz = normalizedDisplayX * MPX_FMAX_HZ * LABEL_CURVE_X_SCALE;
  return freqHz;
}

// ============================================================
// ZOOM FUNCTIONS (Horizontal)
// ============================================================
function updateZoomBounds() {
  const visibleRangeHz = MPX_FMAX_HZ / zoomLevel;

  visibleStartHz = zoomCenterHz - visibleRangeHz / 2;
  visibleEndHz = zoomCenterHz + visibleRangeHz / 2;

  if (visibleStartHz < 0) {
    visibleStartHz = 0;
    visibleEndHz = visibleRangeHz;
  }
  if (visibleEndHz > MPX_FMAX_HZ) {
    visibleEndHz = MPX_FMAX_HZ;
    visibleStartHz = MPX_FMAX_HZ - visibleRangeHz;
  }

  zoomCenterHz = (visibleStartHz + visibleEndHz) / 2;
}

function setZoom(newZoomLevel, newCenterHz = null) {
  zoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoomLevel));

  if (newCenterHz !== null) {
    zoomCenterHz = Math.max(0, Math.min(MPX_FMAX_HZ, newCenterHz));
  }

  updateZoomBounds();
  updateCursor();
  drawMpxSpectrum();
}

// ============================================================
// ZOOM FUNCTIONS (Vertical)
// ============================================================
function updateZoomBoundsY() {
  const totalDbRange = MPX_DB_MAX_DEFAULT - MPX_DB_MIN_DEFAULT;
  const visibleRangeDb = totalDbRange / zoomLevelY;

  visibleDbMin = zoomCenterDB - visibleRangeDb / 2;
  visibleDbMax = zoomCenterDB + visibleRangeDb / 2;

  if (visibleDbMin < MPX_DB_MIN_DEFAULT) {
    visibleDbMin = MPX_DB_MIN_DEFAULT;
    visibleDbMax = MPX_DB_MIN_DEFAULT + visibleRangeDb;
  }
  if (visibleDbMax > MPX_DB_MAX_DEFAULT) {
    visibleDbMax = MPX_DB_MAX_DEFAULT;
    visibleDbMin = MPX_DB_MAX_DEFAULT - visibleRangeDb;
  }

  zoomCenterDB = (visibleDbMin + visibleDbMax) / 2;
}

function setZoomY(newZoomLevel, newCenterDB = null) {
  zoomLevelY = Math.max(MIN_ZOOM_Y, Math.min(MAX_ZOOM_Y, newZoomLevel));

  if (newCenterDB !== null) {
    zoomCenterDB = Math.max(MPX_DB_MIN_DEFAULT, Math.min(MPX_DB_MAX_DEFAULT, newCenterDB));
  }

  updateZoomBoundsY();
  updateCursor();
  drawMpxSpectrum();
}

// ============================================================
// RESET FUNCTIONS
// ============================================================
function zoomReset() {
  // Horizontal reset
  zoomCenterHz = MPX_FMAX_HZ / 2;
  zoomLevel = 1.0;
  updateZoomBounds();

  // Vertical reset
  zoomCenterDB = (MPX_DB_MIN_DEFAULT + MPX_DB_MAX_DEFAULT) / 2;
  zoomLevelY = 1.0;
  updateZoomBoundsY();

  updateCursor();
  drawMpxSpectrum();
}

function updateCursor() {
  if (! mpxCanvas) return;

  if (isHoveringMagnifier) {
    mpxCanvas.style.cursor = "help";
  } else if (isDragging) {
    mpxCanvas.style.cursor = "grabbing";
  } else if (zoomLevel > MIN_ZOOM || zoomLevelY > MIN_ZOOM_Y) {
    mpxCanvas.style.cursor = "grab";
  } else {
    mpxCanvas.style.cursor = "pointer";
  }
}

/////////////////////////////////////////////////////////////////
// Resize
/////////////////////////////////////////////////////////////////
function resizeMpxCanvas() {
  if (! mpxCanvas || !mpxCanvas.parentElement) return;

  const rect = mpxCanvas.parentElement.getBoundingClientRect();
  mpxCanvas.width = rect.width > 0 ? rect.width : 400;
  mpxCanvas.height = rect.height > 0 ? rect.height : 240;

  drawMpxSpectrum();
}

window.addEventListener("resize", resizeMpxCanvas);

/////////////////////////////////////////////////////////////////
// Handle MPX array
/////////////////////////////////////////////////////////////////
function handleMpxArray(data) {
  if (!Array.isArray(data) || data.length === 0) return;

  const arr = [];

  for (let i = 0; i < data.length; i++) {
    const mag = data[i].m || 0;
    let db = 20 * Math.log10(mag + 1e-15);

    if (db < MPX_DB_MIN_DEFAULT) db = MPX_DB_MIN_DEFAULT;
    if (db > MPX_DB_MAX_DEFAULT) db = MPX_DB_MAX_DEFAULT;

    arr.push(db);
  }

  if (mpxSmoothSpectrum.length === 0) {
    mpxSmoothSpectrum = new Array(arr.length).fill(MPX_DB_MIN_DEFAULT);
  }

  const len = Math.min(arr.length, mpxSmoothSpectrum.length);

  for (let i = 0; i < len; i++) {
    mpxSmoothSpectrum[i] =
      (mpxSmoothSpectrum[i] * (MPX_AVERAGE_LEVELS - 1) + arr[i]) /
      MPX_AVERAGE_LEVELS;
  }

  if (arr.length > len) {
    for (let i = len; i < arr.length; i++) {
      mpxSmoothSpectrum[i] = arr[i];
    }
  }

  mpxSpectrum = mpxSmoothSpectrum.slice();
  drawMpxSpectrum();
}

/////////////////////////////////////////////////////////////////
// Drawing
/////////////////////////////////////////////////////////////////
function drawMpxBackground() {
  const grd = mpxCtx.createLinearGradient(0, 0, 0, mpxCanvas.height);
  grd.addColorStop(0, "#001225");
  grd.addColorStop(1, "#002044");
  mpxCtx.fillStyle = grd;
  mpxCtx.fillRect(0, 0, mpxCanvas.width, mpxCanvas.height);
}

/////////////////////////////////////////////////////////////////
// DRAW MAGNIFIER ICON (CENTERED)
/////////////////////////////////////////////////////////////////
function drawMagnifierIcon() {
  const x = mpxCanvas.width / 2;
  const y = mpxCanvas.height - 13;

  // Store area for hover detection
  magnifierArea = {
    x: x - 10,
    y: y - 10,
    width: 20,
    height: 16
  };

  // Fixed Color
  const color = "rgba(143, 234, 255, 0.8)";

  mpxCtx.save();
  mpxCtx.font = "11px Arial";
  mpxCtx.fillStyle = color;
  mpxCtx.textAlign = "center";
  mpxCtx.textBaseline = "middle";
  mpxCtx. fillText("1.0x", x, y);
  mpxCtx.restore();
}

/////////////////////////////////////////////////////////////////
// GRID WITH ZOOM SUPPORT
/////////////////////////////////////////////////////////////////
function drawMpxGrid() {
  mpxCtx.lineWidth = 0.5;
  mpxCtx.strokeStyle = "rgba(255,255,255,0.12)";
  mpxCtx.font = "10px Arial";
  mpxCtx.fillStyle = "rgba(255,255,255,0.75)";

  const headerY = TOP_MARGIN - 6;

  mpxCtx.textAlign = "left";
  mpxCtx.fillText("", 15, headerY);

  const baseMarkers = [
    { f: 19000, label: "19k" },
    { f: 38000, label: "38k" },
    { f: 57000, label: "57k" },
    { f: 76000, label: "76k" },
    { f: 95000, label: "95k" },
  ];

  const markers = (zoomLevel > 1) ?  generateFrequencyMarkers() : baseMarkers;

  mpxCtx.font = "11px Arial";
  mpxCtx.fillStyle = "rgba(255,255,255,0.65)";

  const gridTopY = TOP_MARGIN;
  const gridBottomY = mpxCanvas.height - BOTTOM_MARGIN;

  markers.forEach(m => {
    if (zoomLevel > 1) {
      if (m.f < visibleStartHz || m.f > visibleEndHz) return;

      const normalizedPos = (m.f - visibleStartHz) / (visibleEndHz - visibleStartHz);
      const x = GRID_X_OFFSET + normalizedPos * (mpxCanvas.width - GRID_X_OFFSET);

      mpxCtx.strokeStyle = "rgba(255,255,255,0.10)";
      mpxCtx.beginPath();
      mpxCtx.moveTo(x, gridTopY);
      mpxCtx.lineTo(x, gridBottomY);
      mpxCtx.stroke();

      mpxCtx.textAlign = "center";
      mpxCtx.fillText(m.label, x, headerY);
    } else {
      const x =
        GRID_X_OFFSET +
        (m.f / (MPX_FMAX_HZ * LABEL_CURVE_X_SCALE)) *
        (mpxCanvas.width - GRID_X_OFFSET);

      mpxCtx.strokeStyle = "rgba(255,255,255,0.10)";
      mpxCtx.beginPath();
      mpxCtx.moveTo(x, gridTopY);
      mpxCtx.lineTo(x, gridBottomY);
      mpxCtx.stroke();

      mpxCtx.fillText(m.label, x + 60 + LABEL_X_OFFSET, headerY);
    }
  });

  const range = getDisplayRange();
  const usableHeight = mpxCanvas.height - TOP_MARGIN - BOTTOM_MARGIN;

  const dbMarkers = generateDbMarkers();

  dbMarkers.forEach(v => {
    if (v < visibleDbMin || v > visibleDbMax) return;

    const norm = (v - range.min) / (range.max - range.min);
    const y = TOP_MARGIN + (1 - norm) * usableHeight * Y_STRETCH;

    if (y >= TOP_MARGIN && y <= mpxCanvas.height - BOTTOM_MARGIN) {
      mpxCtx.strokeStyle = "rgba(255,255,255,0.12)";
      mpxCtx.beginPath();
      mpxCtx.moveTo(0, y);
      mpxCtx.lineTo(mpxCanvas.width, y);
      mpxCtx.stroke();

      mpxCtx.textAlign = "right";
      mpxCtx.fillText(`${v}`, OFFSET_X - 6, y + 10 + LABEL_Y_OFFSET);
    }
  });
}

function generateFrequencyMarkers() {
  const visibleRange = visibleEndHz - visibleStartHz;
  let step;

  if (visibleRange > 50000) {
    step = 19000;
  } else if (visibleRange > 20000) {
    step = 10000;
  } else if (visibleRange > 10000) {
    step = 5000;
  } else if (visibleRange > 5000) {
    step = 2000;
  } else if (visibleRange > 2000) {
    step = 1000;
  } else {
    step = 500;
  }

  const markers = [];
  const startMarker = Math.ceil(visibleStartHz / step) * step;

  for (let f = startMarker; f <= visibleEndHz; f += step) {
    let label;
    if (f >= 1000) {
      label = (f / 1000).toFixed(f % 1000 === 0 ? 0 : 1) + "k";
    } else {
      label = f.toString();
    }
    markers.push({ f: f, label: label });
  }

  return markers;
}

function generateDbMarkers() {
  const visibleRange = visibleDbMax - visibleDbMin;
  let step;

  if (visibleRange > 60) {
    step = 10;
  } else if (visibleRange > 30) {
    step = 10;
  } else if (visibleRange > 15) {
    step = 5;
  } else if (visibleRange > 8) {
    step = 2;
  } else {
    step = 1;
  }

  const markers = [];
  const startMarker = Math.ceil(visibleDbMin / step) * step;

  for (let db = startMarker; db <= visibleDbMax; db += step) {
    markers.push(db);
  }

  return markers;
}

/////////////////////////////////////////////////////////////////
// SPECTRUM TRACE WITH ZOOM SUPPORT
/////////////////////////////////////////////////////////////////
function drawMpxSpectrumTrace() {
  if (! mpxSpectrum.length) return;

  const range = getDisplayRange();
  const usableHeight = mpxCanvas.height - TOP_MARGIN - BOTTOM_MARGIN;

  mpxCtx.beginPath();
  mpxCtx.strokeStyle = "#8feaff";
  mpxCtx.lineWidth = 1.0;

  if (zoomLevel > 1) {
    // ZOOMED: Frequency-based display
    const usableWidth = mpxCanvas.width - OFFSET_X;
    const totalBins = mpxSpectrum.length;

    const startBin = freqToBin(visibleStartHz, totalBins);
    const endBin = freqToBin(visibleEndHz, totalBins);

    let firstPoint = true;

    for (let i = startBin; i <= endBin; i++) {
      const binFreq = binToFreq(i, totalBins);
      const normalizedX = (binFreq - visibleStartHz) / (visibleEndHz - visibleStartHz);
      const x = OFFSET_X + normalizedX * usableWidth;

      let rawVal = mpxSpectrum[i];
      let val = (rawVal * CURVE_GAIN) + CURVE_Y_OFFSET_DB;
      val = MPX_DB_MIN_DEFAULT + (val - MPX_DB_MIN_DEFAULT) * CURVE_VERTICAL_DYNAMICS;

      if (val < MPX_DB_MIN_DEFAULT) val = MPX_DB_MIN_DEFAULT;
      if (val > MPX_DB_MAX_DEFAULT) val = MPX_DB_MAX_DEFAULT;

      const norm = (val - range.min) / (range.max - range.min);
      const y = TOP_MARGIN + (1 - norm) * usableHeight * Y_STRETCH;

      if (firstPoint) {
        mpxCtx.moveTo(x, y);
        firstPoint = false;
      } else {
        mpxCtx.lineTo(x, y);
      }
    }
  } else {
    // NOT ZOOMED: Original display with CURVE_X_STRETCH
    const usableWidth = (mpxCanvas.width - OFFSET_X) * CURVE_X_SCALE;
    const leftStart = OFFSET_X + (mpxCanvas.width - OFFSET_X - usableWidth);

    for (let i = 0; i < mpxSpectrum.length; i++) {
      let rawVal = mpxSpectrum[i];
      let val = (rawVal * CURVE_GAIN) + CURVE_Y_OFFSET_DB;
      val = MPX_DB_MIN_DEFAULT + (val - MPX_DB_MIN_DEFAULT) * CURVE_VERTICAL_DYNAMICS;

      if (val < MPX_DB_MIN_DEFAULT) val = MPX_DB_MIN_DEFAULT;
      if (val > MPX_DB_MAX_DEFAULT) val = MPX_DB_MAX_DEFAULT;

      const norm = (val - range.min) / (range.max - range.min);
      const y = TOP_MARGIN + (1 - norm) * usableHeight * Y_STRETCH;

      const x = leftStart + (i / (mpxSpectrum.length - 1)) * usableWidth * CURVE_X_STRETCH;

      if (i === 0) mpxCtx.moveTo(x, y);
      else mpxCtx.lineTo(x, y);
    }
  }

  mpxCtx.stroke();
}

/////////////////////////////////////////////////////////////////
// MAIN DRAWING FUNCTION
/////////////////////////////////////////////////////////////////
function drawMpxSpectrum() {
  if (!mpxCtx || !mpxCanvas) return;

  updateZoomBounds();
  updateZoomBoundsY();

  drawMpxBackground();
  drawMpxGrid();
  drawMpxSpectrumTrace();

  // Spectrum name based on sample rate
  let spectrumName = "Spectrum Analyzer";

  if (sampleRate === 48000) {
    spectrumName = "FM Audio Spectrum";
  } else if (sampleRate === 96000) {
    spectrumName = "FM Baseband Spectrum";
  } else if (sampleRate === 192000) {
    spectrumName = "MPX Spectrum";
  }

  mpxCtx.font = "12px Arial";
  mpxCtx.fillStyle = "rgba(255,255,255,0.85)";
  mpxCtx.textAlign = "left";
  mpxCtx.textBaseline = "alphabetic";

  mpxCtx.fillText(spectrumName, 8, mpxCanvas.height - 10);

  mpxCtx.textAlign = "right";
  mpxCtx.fillText(sampleRate + " Hz", mpxCanvas.width - 8, mpxCanvas.height - 10);

  // Zoom info or magnifier icon in the center
  if (zoomLevel > 1.0 || zoomLevelY > 1.0) {
    // Zoom active: show info
    let infoText = "";

    if (zoomLevel > 1.0 && zoomLevelY > 1.0) {
      const startKHz = (visibleStartHz / 1000).toFixed(1);
      const endKHz = (visibleEndHz / 1000).toFixed(1);
      infoText = `X:${zoomLevel.toFixed(1)}x Y:${zoomLevelY.toFixed(1)}x [${startKHz}k-${endKHz}k]`;
    } else if (zoomLevel > 1.0) {
      const startKHz = (visibleStartHz / 1000).toFixed(1);
      const endKHz = (visibleEndHz / 1000).toFixed(1);
      infoText = `${zoomLevel.toFixed(1)}x [${startKHz}k - ${endKHz}k]`;
    } else {
      infoText = `Y:${zoomLevelY.toFixed(1)}x [${visibleDbMin.toFixed(0)} to ${visibleDbMax.toFixed(0)} dB]`;
    }

    mpxCtx.fillStyle = "rgba(143, 234, 255, 0.8)";
    mpxCtx.font = "11px Arial";
    mpxCtx.textAlign = "center";
    mpxCtx.fillText(infoText, mpxCanvas.width / 2, mpxCanvas.height - 10);
  } else {
    // No zoom: show magnifier icon
    drawMagnifierIcon();
  }
}

/////////////////////////////////////////////////////////////////
// MOUSE EVENTS
/////////////////////////////////////////////////////////////////
function setupMouseEvents(canvas) {

  // ============================================================
  // MOUSE MOVE - MAGNIFIER HOVER DETECTION
  // ============================================================
  canvas.addEventListener("mousemove", (e) => {
    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;

    // Check if mouse is over magnifier icon (only when no zoom is active)
    const wasHovering = isHoveringMagnifier;

    if (zoomLevel <= MIN_ZOOM && zoomLevelY <= MIN_ZOOM_Y) {
      isHoveringMagnifier =
        mouseX >= magnifierArea.x &&
        mouseX <= magnifierArea.x + magnifierArea.width &&
        mouseY >= magnifierArea.y &&
        mouseY <= magnifierArea.y + magnifierArea.height;
    } else {
      isHoveringMagnifier = false;
    }

    // Show/hide tooltip
    if (isHoveringMagnifier && !wasHovering) {
      showTooltip();
      drawMpxSpectrum();
    } else if (!isHoveringMagnifier && wasHovering) {
      hideTooltip();
      drawMpxSpectrum();
    }

    updateCursor();

    // Drag logic
    if (! isDragging) return;

    const deltaX = Math.abs(e.clientX - dragStartX);
    const deltaY = Math.abs(e.clientY - dragStartY);

    if (deltaX > 5 || deltaY > 5) {
      hasDragged = true;
    }

    if (! hasDragged) return;

    e.preventDefault();
    e.stopPropagation();

    // Horizontal panning
    if (zoomLevel > MIN_ZOOM) {
      const fullDeltaX = e.clientX - dragStartX;
      const visibleRangeHz = visibleEndHz - visibleStartHz;
      const pxPerHz = (canvas.width - OFFSET_X) / visibleRangeHz;
      const deltaHz = -fullDeltaX / pxPerHz;
      zoomCenterHz = dragStartCenterHz + deltaHz;
      zoomCenterHz = Math.max(0, Math.min(MPX_FMAX_HZ, zoomCenterHz));
    }

    // Vertical panning
    if (zoomLevelY > MIN_ZOOM_Y) {
      const fullDeltaY = e.clientY - dragStartY;
      const usableHeight = canvas.height - TOP_MARGIN - BOTTOM_MARGIN;
      const visibleRangeDb = visibleDbMax - visibleDbMin;
      const pxPerDb = (usableHeight * Y_STRETCH) / visibleRangeDb;
      const deltaDb = fullDeltaY / pxPerDb;
      zoomCenterDB = dragStartCenterDB + deltaDb;
      zoomCenterDB = Math.max(MPX_DB_MIN_DEFAULT, Math.min(MPX_DB_MAX_DEFAULT, zoomCenterDB));
    }

    updateZoomBounds();
    updateZoomBoundsY();
    drawMpxSpectrum();
  });

  // ============================================================
  // MOUSE LEAVE CANVAS
  // ============================================================
  canvas.addEventListener("mouseleave", () => {
    if (isHoveringMagnifier) {
      isHoveringMagnifier = false;
      hideTooltip();
      drawMpxSpectrum();
    }

    if (isDragging) {
      isDragging = false;
      hasDragged = false;
      updateCursor();
    }
  });

  // ============================================================
  // MOUSE WHEEL ZOOM
  // ============================================================
  canvas.addEventListener("wheel", (e) => {
    e.preventDefault();
    e.stopPropagation();

    // Hide tooltip when zooming
    if (isHoveringMagnifier) {
      isHoveringMagnifier = false;
      hideTooltip();
    }

    const rect = canvas.getBoundingClientRect();
    const mouseX = e.clientX - rect.left;
    const mouseY = e.clientY - rect.top;

    if (e.ctrlKey) {
      // CTRL pressed: Vertical zoom
      const usableHeight = canvas.height - TOP_MARGIN - BOTTOM_MARGIN;
      const normalizedY = (mouseY - TOP_MARGIN) / (usableHeight * Y_STRETCH);
      const range = getDisplayRange();

      let dbAtMouse = range.max - normalizedY * (range.max - range.min);
      dbAtMouse = Math.max(MPX_DB_MIN_DEFAULT, Math.min(MPX_DB_MAX_DEFAULT, dbAtMouse));

      const zoomDelta = e.deltaY > 0 ? (1 / ZOOM_STEP_Y) : ZOOM_STEP_Y;
      setZoomY(zoomLevelY * zoomDelta, dbAtMouse);
    } else {
      // No CTRL: Horizontal zoom
      let freqAtMouse;
      if (zoomLevel > 1) {
        const normalizedX = (mouseX - OFFSET_X) / (canvas.width - OFFSET_X);
        freqAtMouse = visibleStartHz + normalizedX * (visibleEndHz - visibleStartHz);
      } else {
        const usableWidth = (canvas.width - OFFSET_X) * CURVE_X_SCALE;
        const leftStart = OFFSET_X + (canvas.width - OFFSET_X - usableWidth);
        const curveWidth = usableWidth * CURVE_X_STRETCH;
        const normalizedX = (mouseX - leftStart) / curveWidth;
        freqAtMouse = normalizedX * MPX_FMAX_HZ * LABEL_CURVE_X_SCALE;
      }

      freqAtMouse = Math.max(0, Math.min(MPX_FMAX_HZ, freqAtMouse));

      const zoomDelta = e.deltaY > 0 ? (1 / ZOOM_STEP) : ZOOM_STEP;
      setZoom(zoomLevel * zoomDelta, freqAtMouse);
    }
  }, { passive: false });

  // ============================================================
  // LEFT MOUSE BUTTON PRESSED - DRAG START
  // ============================================================
  canvas.addEventListener("mousedown", (e) => {
    if (e.button !== 0) return;
    if (zoomLevel <= MIN_ZOOM && zoomLevelY <= MIN_ZOOM_Y) return;

    // Don't drag when hovering over magnifier
    if (isHoveringMagnifier) return;

    e.preventDefault();
    e.stopPropagation();

    isDragging = true;
    hasDragged = false;
    dragStartX = e.clientX;
    dragStartY = e.clientY;
    dragStartCenterHz = zoomCenterHz;
    dragStartCenterDB = zoomCenterDB;

    updateCursor();
  });

  // ============================================================
  // MOUSE BUTTON RELEASED
  // ============================================================
  canvas.addEventListener("mouseup", (e) => {
    if (e.button === 0 && isDragging) {
      isDragging = false;
      updateCursor();

      if (hasDragged) {
        e.stopPropagation();
      }
    }
  });

  // ============================================================
  // RIGHT MOUSE BUTTON - ZOOM RESET
  // ============================================================
  canvas.addEventListener("contextmenu", (e) => {
    e.preventDefault();
    e.stopPropagation();

    zoomReset();
  });

  // ============================================================
  // CLICK - Pass through for mode switch
  // ============================================================
  canvas.addEventListener("click", (e) => {
    // Ignore click on magnifier (no mode switch)
    if (isHoveringMagnifier) {
      e.stopPropagation();
      return;
    }

    if (hasDragged) {
      e.stopPropagation();
      hasDragged = false;
      return;
    }
  });
}

/////////////////////////////////////////////////////////////////
// WebSocket
/////////////////////////////////////////////////////////////////
function setupMpxSocket() {
  if (
    mpxSocket &&
    (mpxSocket.readyState === WebSocket.OPEN ||
      mpxSocket.readyState === WebSocket.CONNECTING)
  ) return;

  try {
    mpxSocket = new WebSocket(WS_URL);

    mpxSocket.onmessage = evt => {
      let msg;
      try {
        msg = JSON.parse(evt.data);
      } catch {
        return;
      }

      if (! msg || typeof msg !== "object") return;
      if (msg.type !== "MPX") return;

      if (Array.isArray(msg.value)) handleMpxArray(msg.value);
    };
  } catch {
    setTimeout(setupMpxSocket, 5000);
  }
}

/////////////////////////////////////////////////////////////////
// Public API
/////////////////////////////////////////////////////////////////
function init(containerId = "level-meter-container") {
  const parent = document.getElementById(containerId);
  parent.innerHTML = "";

  // Reset tooltip
  tooltipElement = null;
  isHoveringMagnifier = false;

  const block = document.createElement("div");
  block.style.display = "block";
  block.style.margin = "0 auto";
  block.style.padding = "0";

  const wrap = document.createElement("div");
  wrap.id = "mpxCanvasContainer";

  const canvas = document.createElement("canvas");
  canvas.id = "mpxCanvas";

  wrap.appendChild(canvas);
  block.appendChild(wrap);
  parent.appendChild(block);

  mpxCanvas = canvas;
  mpxCtx = canvas.getContext("2d");

  setupMouseEvents(canvas);

  resizeMpxCanvas();
  block.style.width = mpxCanvas.width + "px";

  updateZoomBounds();
  updateZoomBoundsY();
  updateCursor();

  setupMpxSocket();
}

window.MetricsAnalyzer = {
  init,
  zoomReset,
  setZoom,
  setZoomY,
  getZoomLevel: () => zoomLevel,
  getZoomLevelY: () => zoomLevelY,
  getVisibleRange: () => ({ start: visibleStartHz, end: visibleEndHz }),
  getVisibleDbRange: () => ({ min: visibleDbMin, max: visibleDbMax })
};

})();