/**
 * Sampling Profiler Visualization
 */
(function () {
  "use strict";

  // ============================================================================
  // Configuration
  // ============================================================================

  const TIMINGS = {
    sampleIntervalMin: 100,
    sampleIntervalMax: 500,
    sampleIntervalDefault: 200,
    sampleToFlame: 600,
    defaultSpeed: 0.05,
  };

  const LAYOUT = { frameSpacing: 6 };

  // Function name to color mapping
  const FUNCTION_COLORS = {
    main: "#306998",
    fibonacci: "#D4A910",
    add: "#E65100",
    multiply: "#7B1FA2",
    calculate: "#D4A910",
  };
  const DEFAULT_FUNCTION_COLOR = "#306998";

  // Easing functions - cubic-bezier approximations
  const EASING_MAP = {
    linear: "linear",
    easeOutQuad: "cubic-bezier(0.25, 0.46, 0.45, 0.94)",
    easeOutCubic: "cubic-bezier(0.215, 0.61, 0.355, 1)",
  };

  function getFunctionColor(funcName) {
    return FUNCTION_COLORS[funcName] || DEFAULT_FUNCTION_COLOR;
  }

  // ============================================================================
  // Animation Manager
  // ============================================================================

  class AnimationManager {
    constructor() {
      this.activeAnimations = new Set();
    }

    to(element, props, duration, easing = "easeOutQuad", onComplete = null) {
      this.killAnimationsOf(element);

      const cssEasing = EASING_MAP[easing] || EASING_MAP.easeOutQuad;

      const transformProps = {};
      const otherProps = {};

      for (const [key, value] of Object.entries(props)) {
        if (key === "position") {
          if (typeof value.x === "number") transformProps.x = value.x;
          if (typeof value.y === "number") transformProps.y = value.y;
        } else if (key === "x" || key === "y") {
          transformProps[key] = value;
        } else if (key === "scale") {
          transformProps.scale = value;
        } else if (key === "alpha" || key === "opacity") {
          otherProps.opacity = value;
        } else {
          otherProps[key] = value;
        }
      }

      const computedStyle = getComputedStyle(element);
      const matrix = new DOMMatrix(computedStyle.transform);
      const currentScale = Math.sqrt(
        matrix.m11 * matrix.m11 + matrix.m21 * matrix.m21,
      );

      transformProps.x ??= matrix.m41;
      transformProps.y ??= matrix.m42;
      transformProps.scale ??= currentScale;

      const initialTransform = this._buildTransformString(
        matrix.m41,
        matrix.m42,
        currentScale,
      );

      const finalTransform = this._buildTransformString(
        transformProps.x,
        transformProps.y,
        transformProps.scale,
      );

      const initialKeyframe = { transform: initialTransform };
      const finalKeyframe = { transform: finalTransform };

      for (const [key, value] of Object.entries(otherProps)) {
        const currentVal =
          key === "opacity"
            ? element.style.opacity || computedStyle.opacity
            : element.style[key];
        initialKeyframe[key] = currentVal;
        finalKeyframe[key] = value;
      }

      const animation = element.animate([initialKeyframe, finalKeyframe], {
        duration,
        easing: cssEasing,
        fill: "forwards",
      });

      this.activeAnimations.add(animation);
      animation.onfinish = () => {
        this.activeAnimations.delete(animation);
        element.style.transform = finalTransform;
        for (const [key, value] of Object.entries(finalKeyframe)) {
          if (key !== "transform") {
            element.style[key] = typeof value === "number" ? `${value}` : value;
          }
        }
        if (onComplete) onComplete();
      };

      return animation;
    }

    killAnimationsOf(element) {
      element.getAnimations().forEach((animation) => animation.cancel());
      this.activeAnimations.forEach((animation) => {
        if (animation.effect && animation.effect.target === element) {
          animation.cancel();
          this.activeAnimations.delete(animation);
        }
      });
    }

    _buildTransformString(x, y, scale = 1) {
      return `translate(${x}px, ${y}px) scale(${scale})`;
    }
  }

  const anim = new AnimationManager();

  // ============================================================================
  // Execution Trace Model
  // ============================================================================

  class ExecutionEvent {
    constructor(
      type,
      functionName,
      lineno,
      timestamp,
      args = null,
      value = null,
    ) {
      this.type = type;
      this.functionName = functionName;
      this.lineno = lineno;
      this.timestamp = timestamp;
      this.args = args;
      this.value = value;
    }
  }

  class ExecutionTrace {
    constructor(source, events) {
      this.source = source;
      this.events = events.map(
        (e) =>
          new ExecutionEvent(e.type, e.func, e.line, e.ts, e.args, e.value),
      );
      this.duration = events.length > 0 ? events[events.length - 1].ts : 0;
    }

    getEventsUntil(timestamp) {
      return this.events.filter((e) => e.timestamp <= timestamp);
    }

    getStackAt(timestamp) {
      const stack = [];
      const events = this.getEventsUntil(timestamp);

      for (const event of events) {
        if (event.type === "call") {
          stack.push({
            func: event.functionName,
            line: event.lineno,
            args: event.args,
          });
        } else if (event.type === "return") {
          stack.pop();
        } else if (event.type === "line") {
          if (stack.length > 0) {
            stack[stack.length - 1].line = event.lineno;
          }
        }
      }
      return stack;
    }

    getNextEvent(timestamp) {
      return this.events.find((e) => e.timestamp > timestamp);
    }

    getSourceLines() {
      return this.source.split("\n");
    }
  }

  // ============================================================================
  // Demo Data
  // ============================================================================

  // This placeholder is replaced by the profiling_trace Sphinx extension
  // during the documentation build with dynamically generated trace data.
  const DEMO_SIMPLE = /* PROFILING_TRACE_DATA */ {
  "source": "def add(a, b):\n    return a + b\n\ndef multiply(x, y):\n    result = 0\n    for i in range(y):\n        result = add(result, x)\n    return result\n\ndef calculate(a, b):\n    sum_val = add(a, b)\n    product = multiply(a, b)\n    return sum_val + product\n\ndef main():\n    result = calculate(3, 4)\n    print(f\"Result: {result}\")\n\nmain()",
  "trace": [
    {
      "type": "line",
      "line": 19,
      "ts": 0
    },
    {
      "type": "call",
      "func": "main",
      "line": 15,
      "ts": 50
    },
    {
      "type": "line",
      "line": 16,
      "ts": 100
    },
    {
      "type": "call",
      "func": "calculate",
      "line": 10,
      "ts": 150
    },
    {
      "type": "line",
      "line": 11,
      "ts": 200
    },
    {
      "type": "call",
      "func": "add",
      "line": 1,
      "ts": 250
    },
    {
      "type": "line",
      "line": 2,
      "ts": 300
    },
    {
      "type": "return",
      "func": "add",
      "ts": 350,
      "value": "7"
    },
    {
      "type": "line",
      "line": 12,
      "ts": 400
    },
    {
      "type": "call",
      "func": "multiply",
      "line": 4,
      "ts": 450
    },
    {
      "type": "line",
      "line": 5,
      "ts": 500
    },
    {
      "type": "line",
      "line": 6,
      "ts": 550
    },
    {
      "type": "line",
      "line": 7,
      "ts": 600
    },
    {
      "type": "call",
      "func": "add",
      "line": 1,
      "ts": 650
    },
    {
      "type": "line",
      "line": 2,
      "ts": 700
    },
    {
      "type": "return",
      "func": "add",
      "ts": 750,
      "value": "3"
    },
    {
      "type": "line",
      "line": 6,
      "ts": 800
    },
    {
      "type": "line",
      "line": 7,
      "ts": 850
    },
    {
      "type": "call",
      "func": "add",
      "line": 1,
      "ts": 900
    },
    {
      "type": "line",
      "line": 2,
      "ts": 950
    },
    {
      "type": "return",
      "func": "add",
      "ts": 1000,
      "value": "6"
    },
    {
      "type": "line",
      "line": 6,
      "ts": 1050
    },
    {
      "type": "line",
      "line": 7,
      "ts": 1100
    },
    {
      "type": "call",
      "func": "add",
      "line": 1,
      "ts": 1150
    },
    {
      "type": "line",
      "line": 2,
      "ts": 1200
    },
    {
      "type": "return",
      "func": "add",
      "ts": 1250,
      "value": "9"
    },
    {
      "type": "line",
      "line": 6,
      "ts": 1300
    },
    {
      "type": "line",
      "line": 7,
      "ts": 1350
    },
    {
      "type": "call",
      "func": "add",
      "line": 1,
      "ts": 1400
    },
    {
      "type": "line",
      "line": 2,
      "ts": 1450
    },
    {
      "type": "return",
      "func": "add",
      "ts": 1500,
      "value": "12"
    },
    {
      "type": "line",
      "line": 6,
      "ts": 1550
    },
    {
      "type": "line",
      "line": 8,
      "ts": 1600
    },
    {
      "type": "return",
      "func": "multiply",
      "ts": 1650,
      "value": "12"
    },
    {
      "type": "line",
      "line": 13,
      "ts": 1700
    },
    {
      "type": "return",
      "func": "calculate",
      "ts": 1750,
      "value": "19"
    },
    {
      "type": "line",
      "line": 17,
      "ts": 1800
    },
    {
      "type": "return",
      "func": "main",
      "ts": 1850,
      "value": null
    }
  ],
  "samples": []
};

  // ============================================================================
  // Code Panel Component
  // ============================================================================

  class CodePanel {
    constructor(source) {
      this.source = source;
      this.currentLine = null;

      this.element = document.createElement("div");
      this.element.id = "code-panel";

      const title = document.createElement("div");
      title.className = "code-panel-title";
      title.textContent = "source code";
      this.element.appendChild(title);

      this.codeContainer = document.createElement("pre");
      this.codeContainer.className = "code-container";
      this.element.appendChild(this.codeContainer);

      this._renderSource();
    }

    updateSource(source) {
      this.source = source;
      this.codeContainer.innerHTML = "";
      this._renderSource();
      this.currentLine = null;
    }

    _renderSource() {
      const lines = this.source.split("\n");

      lines.forEach((line, index) => {
        const lineNumber = index + 1;
        const lineDiv = document.createElement("div");
        lineDiv.className = "line";
        lineDiv.dataset.line = lineNumber;

        const lineNumSpan = document.createElement("span");
        lineNumSpan.className = "line-number";
        lineNumSpan.textContent = lineNumber;
        lineDiv.appendChild(lineNumSpan);

        const codeSpan = document.createElement("span");
        codeSpan.className = "line-content";
        codeSpan.innerHTML = this._highlightSyntax(line);
        lineDiv.appendChild(codeSpan);

        this.codeContainer.appendChild(lineDiv);
      });
    }

    _highlightSyntax(line) {
      return line
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/(f?"[^"]*"|f?'[^']*')/g, '<span class="string">$1</span>')
        .replace(/(#.*$)/g, '<span class="comment">$1</span>')
        .replace(
          /\b(def|if|elif|else|return|for|in|range|print|__name__|__main__)\b/g,
          '<span class="keyword">$1</span>',
        )
        .replace(
          /<span class="keyword">def<\/span>\s+(\w+)/g,
          '<span class="keyword">def</span> <span class="function">$1</span>',
        )
        .replace(/\b(\d+)\b/g, '<span class="number">$1</span>');
    }

    highlightLine(lineNumber) {
      if (this.currentLine === lineNumber) return;

      if (this.currentLine !== null) {
        const prevLine = this.codeContainer.querySelector(
          `[data-line="${this.currentLine}"]`,
        );
        if (prevLine) prevLine.classList.remove("highlighted");
      }

      if (lineNumber === null || lineNumber === undefined) {
        this.currentLine = null;
        return;
      }

      this.currentLine = lineNumber;
      const newLine = this.codeContainer.querySelector(
        `[data-line="${lineNumber}"]`,
      );
      if (newLine) {
        newLine.classList.add("highlighted");
      }
    }

    reset() {
      this.highlightLine(null);
      this.codeContainer.scrollTop = 0;
    }

    destroy() {
      this.element.remove();
    }
  }

  // ============================================================================
  // Stack Frame Component
  // ============================================================================

  class DOMStackFrame {
    constructor(functionName, lineno, args = null) {
      this.functionName = functionName;
      this.lineno = lineno;
      this.args = args;
      this.isActive = false;
      this.color = getFunctionColor(functionName);

      this.element = document.createElement("div");
      this.element.className = "stack-frame";
      this.element.dataset.function = functionName;

      this.bgElement = document.createElement("div");
      this.bgElement.className = "stack-frame-bg";
      this.bgElement.style.backgroundColor = this.color;
      this.element.appendChild(this.bgElement);

      this.textElement = document.createElement("span");
      this.textElement.className = "stack-frame-text";
      this.textElement.textContent = functionName;
      this.element.appendChild(this.textElement);

      this.flashElement = document.createElement("div");
      this.flashElement.className = "stack-frame-flash";
      this.element.appendChild(this.flashElement);

      this.element.addEventListener("pointerover", this._onHover.bind(this));
      this.element.addEventListener("pointerout", this._onHoverOut.bind(this));
    }

    destroy() {
      this.element.parentNode?.removeChild(this.element);
    }

    updateLine(lineno) {
      this.lineno = lineno;
      this.textElement.textContent = this.functionName;
    }

    setActive(isActive) {
      if (this.isActive === isActive) return;
      this.isActive = isActive;
      this.bgElement.style.opacity = isActive ? "1.0" : "0.9";
    }

    _onHover() {
      this.bgElement.style.opacity = "0.8";
    }

    _onHoverOut() {
      this.bgElement.style.opacity = this.isActive ? "1.0" : "0.9";
    }

    flash(duration = 150) {
      this.flashElement.animate([{ opacity: 1 }, { opacity: 0 }], {
        duration,
        easing: "ease-out",
      });
    }

    getPosition() {
      const rect = this.element.getBoundingClientRect();
      return { x: rect.left, y: rect.top };
    }
  }

  // ============================================================================
  // Stack Visualization Component
  // ============================================================================

  class DOMStackVisualization {
    constructor() {
      this.frames = [];
      this.frameSpacing = LAYOUT.frameSpacing;

      this.element = document.createElement("div");
      this.element.className = "stack-visualization";
    }

    processEvent(event) {
      if (event.type === "call") {
        this.pushFrame(event.functionName, event.lineno, event.args);
      } else if (event.type === "return") {
        this.popFrame();
      } else if (event.type === "line") {
        this.updateTopFrameLine(event.lineno);
      }
    }

    updateTopFrameLine(lineno) {
      if (this.frames.length > 0) {
        this.frames[this.frames.length - 1].updateLine(lineno);
      }
    }

    pushFrame(functionName, lineno, args = null) {
      if (this.frames.length > 0) {
        this.frames[this.frames.length - 1].setActive(false);
      }

      const frame = new DOMStackFrame(functionName, lineno, args);
      frame.setActive(true);
      this.element.appendChild(frame.element);
      this.frames.push(frame);

      requestAnimationFrame(() => {
        frame.element.classList.add("visible");
      });
    }

    popFrame() {
      if (this.frames.length === 0) return;

      const frame = this.frames.pop();
      frame.element.classList.remove("visible");
      setTimeout(() => frame.destroy(), 300);

      if (this.frames.length > 0) {
        this.frames[this.frames.length - 1].setActive(true);
      }
    }

    clear() {
      this.frames.forEach((frame) => frame.destroy());
      this.frames = [];
      this.element.innerHTML = "";
    }

    flashAll() {
      this.frames.forEach((frame) => frame.flash());
    }

    createStackClone(container) {
      const clone = this.element.cloneNode(false);
      clone.className = "stack-visualization flying-clone";

      const elementRect = this.element.getBoundingClientRect();
      const containerRect = container.getBoundingClientRect();

      // Position relative to container since contain: strict makes position:fixed relative to container
      clone.style.position = "absolute";
      clone.style.left = elementRect.left - containerRect.left + "px";
      clone.style.top = elementRect.top - containerRect.top + "px";
      clone.style.width = elementRect.width + "px";
      clone.style.pointerEvents = "none";
      clone.style.zIndex = "1000";

      this.frames.forEach((frame) => {
        const frameClone = frame.element.cloneNode(true);
        frameClone.classList.add("visible");
        frameClone.style.opacity = "1";
        frameClone.style.transform = "translateY(0)";
        frameClone.style.transition = "none";
        clone.appendChild(frameClone);
      });

      container.appendChild(clone);
      return clone;
    }

    updateToMatch(targetStack) {
      while (this.frames.length > targetStack.length) {
        this.popFrame();
      }

      targetStack.forEach(({ func, line, args }, index) => {
        if (index < this.frames.length) {
          const frame = this.frames[index];
          if (frame.functionName !== func) {
            frame.updateLine(line);
          }
          frame.setActive(index === targetStack.length - 1);
        } else {
          this.pushFrame(func, line, args);
        }
      });

      if (this.frames.length > 0) {
        this.frames[this.frames.length - 1].setActive(true);
      }
    }
  }

  // ============================================================================
  // Sampling Panel Component
  // ============================================================================

  class DOMSamplingPanel {
    constructor() {
      this.samples = [];
      this.functionCounts = {};
      this.totalSamples = 0;
      this.sampleInterval = TIMINGS.sampleIntervalDefault;
      this.groundTruthFunctions = new Set();
      this.bars = {};

      this.element = document.createElement("div");
      this.element.className = "sampling-panel";

      const header = document.createElement("div");
      header.className = "sampling-header";

      const title = document.createElement("h3");
      title.className = "sampling-title";
      title.textContent = "Sampling Profiler";
      header.appendChild(title);

      const stats = document.createElement("div");
      stats.className = "sampling-stats";

      this.sampleCountEl = document.createElement("span");
      this.sampleCountEl.textContent = "Samples: 0";
      stats.appendChild(this.sampleCountEl);

      this.intervalEl = document.createElement("span");
      this.intervalEl.textContent = `Interval: ${this.sampleInterval}ms`;
      stats.appendChild(this.intervalEl);

      this.missedFunctionsEl = document.createElement("span");
      this.missedFunctionsEl.className = "missed";
      stats.appendChild(this.missedFunctionsEl);

      header.appendChild(stats);
      this.element.appendChild(header);

      this.barsContainer = document.createElement("div");
      this.barsContainer.className = "sampling-bars";
      this.element.appendChild(this.barsContainer);
    }

    setSampleInterval(interval) {
      this.sampleInterval = interval;
      this.intervalEl.textContent = `Interval: ${interval}ms`;
    }

    setGroundTruth(allFunctions) {
      this.groundTruthFunctions = new Set(allFunctions);
      this._updateMissedCount();
    }

    addSample(stack) {
      this.totalSamples++;
      this.sampleCountEl.textContent = `Samples: ${this.totalSamples}`;

      stack.forEach((frame) => {
        const funcName = frame.func;
        this.functionCounts[funcName] =
          (this.functionCounts[funcName] || 0) + 1;
      });

      this._updateBars();
      this._updateMissedCount();
    }

    reset() {
      this.samples = [];
      this.functionCounts = {};
      this.totalSamples = 0;
      this.sampleCountEl.textContent = "Samples: 0";
      this.missedFunctionsEl.textContent = "";
      this.barsContainer.innerHTML = "";
      this.bars = {};
    }

    _updateMissedCount() {
      if (this.groundTruthFunctions.size === 0) return;

      const capturedFunctions = new Set(Object.keys(this.functionCounts));
      const notYetSeen = [...this.groundTruthFunctions].filter(
        (f) => !capturedFunctions.has(f),
      );

      if (notYetSeen.length > 0) {
        this.missedFunctionsEl.textContent = `Not yet seen: ${notYetSeen.length}`;
        this.missedFunctionsEl.classList.add("missed");
        this.missedFunctionsEl.style.color = "";
      } else if (this.totalSamples > 0) {
        this.missedFunctionsEl.textContent = "All captured!";
        this.missedFunctionsEl.classList.remove("missed");
        this.missedFunctionsEl.style.color = "var(--color-green)";
      } else {
        this.missedFunctionsEl.textContent = "";
      }
    }

    _updateBars() {
      const sorted = Object.entries(this.functionCounts).sort(
        (a, b) => b[1] - a[1],
      );

      sorted.forEach(([funcName, count], index) => {
        const percentage =
          this.totalSamples > 0 ? count / this.totalSamples : 0;

        if (!this.bars[funcName]) {
          const row = this._createBarRow(funcName);
          this.barsContainer.appendChild(row);
          this.bars[funcName] = row;
        }

        const row = this.bars[funcName];
        const barFill = row.querySelector(".bar-fill");
        barFill.style.width = `${percentage * 100}%`;

        const percentEl = row.querySelector(".bar-percent");
        percentEl.textContent = `${(percentage * 100).toFixed(0)}%`;

        const currentIndex = Array.from(this.barsContainer.children).indexOf(
          row,
        );
        if (currentIndex !== index) {
          this.barsContainer.insertBefore(
            row,
            this.barsContainer.children[index],
          );
        }
      });
    }

    _createBarRow(funcName) {
      const row = document.createElement("div");
      row.className = "sampling-bar-row";
      row.dataset.function = funcName;

      const label = document.createElement("span");
      label.className = "bar-label";
      label.textContent = funcName;
      row.appendChild(label);

      const barContainer = document.createElement("div");
      barContainer.className = "bar-container";

      const barFill = document.createElement("div");
      barFill.className = "bar-fill";
      barFill.style.backgroundColor = getFunctionColor(funcName);
      barContainer.appendChild(barFill);

      row.appendChild(barContainer);

      const percent = document.createElement("span");
      percent.className = "bar-percent";
      percent.textContent = "0%";
      row.appendChild(percent);

      return row;
    }

    getTargetPosition() {
      const rect = this.barsContainer.getBoundingClientRect();
      return { x: rect.left + rect.width / 2, y: rect.top + 50 };
    }

    showImpactEffect(position) {
      const impact = document.createElement("div");
      impact.className = "impact-circle";
      impact.style.position = "fixed";
      impact.style.left = `${position.x}px`;
      impact.style.top = `${position.y}px`;

      // Append to barsContainer parent to avoid triggering scroll
      this.element.appendChild(impact);

      impact.animate(
        [
          { transform: "translate(-50%, -50%) scale(1)", opacity: 0.6 },
          { transform: "translate(-50%, -50%) scale(4)", opacity: 0 },
        ],
        {
          duration: 300,
          easing: "ease-out",
        },
      ).onfinish = () => impact.remove();
    }
  }

  // ============================================================================
  // Control Panel Component
  // ============================================================================

  class ControlPanel {
    constructor(
      container,
      onPlay,
      onPause,
      onReset,
      onSpeedChange,
      onSeek,
      onStep,
      onSampleIntervalChange = null,
    ) {
      this.container = container;
      this.onPlay = onPlay;
      this.onPause = onPause;
      this.onReset = onReset;
      this.onSpeedChange = onSpeedChange;
      this.onSeek = onSeek;
      this.onStep = onStep;
      this.onSampleIntervalChange = onSampleIntervalChange;

      this.isPlaying = false;
      this.speed = TIMINGS.defaultSpeed;

      this._createControls();
    }

    _createControls() {
      const panel = document.createElement("div");
      panel.id = "control-panel";

      const sampleIntervalHtml = this.onSampleIntervalChange
        ? `
        <div class="control-group">
          <label>Sample interval:</label>
          <input type="range" id="sample-interval"
                 min="${TIMINGS.sampleIntervalMin}"
                 max="${TIMINGS.sampleIntervalMax}"
                 value="${TIMINGS.sampleIntervalDefault}"
                 step="100"
                 aria-label="Sample interval in milliseconds">
          <span id="interval-display">${TIMINGS.sampleIntervalDefault}ms</span>
        </div>
      `
        : "";

      panel.innerHTML = `
        <div class="control-group">
          <button id="play-pause-btn" class="control-btn" aria-label="Play animation">▶ Play</button>
          <button id="reset-btn" class="control-btn" aria-label="Reset visualization to beginning">↻ Reset</button>
          <button id="step-btn" class="control-btn" aria-label="Step to next event">→ Step</button>
        </div>

        ${sampleIntervalHtml}

        <div class="control-group timeline-scrubber">
          <input type="range" id="timeline-scrubber" min="0" max="100" value="0" step="0.1" aria-label="Timeline position">
          <span id="time-display">0ms</span>
        </div>
      `;

      this.container.appendChild(panel);

      this.playPauseBtn = panel.querySelector("#play-pause-btn");
      this.resetBtn = panel.querySelector("#reset-btn");
      this.stepBtn = panel.querySelector("#step-btn");
      this.scrubber = panel.querySelector("#timeline-scrubber");
      this.timeDisplay = panel.querySelector("#time-display");

      this.playPauseBtn.addEventListener("click", () =>
        this._togglePlayPause(),
      );
      this.resetBtn.addEventListener("click", () => this._handleReset());
      this.stepBtn.addEventListener("click", () => this._handleStep());
      this.scrubber.addEventListener("input", (e) => this._handleSeek(e));

      if (this.onSampleIntervalChange) {
        this.sampleIntervalSlider = panel.querySelector("#sample-interval");
        this.intervalDisplay = panel.querySelector("#interval-display");
        this.sampleIntervalSlider.addEventListener("input", (e) =>
          this._handleSampleIntervalChange(e),
        );
      }
    }

    _handleSampleIntervalChange(e) {
      const interval = parseInt(e.target.value);
      this.intervalDisplay.textContent = `${interval}ms`;
      this.onSampleIntervalChange(interval);
    }

    _togglePlayPause() {
      this.isPlaying = !this.isPlaying;

      if (this.isPlaying) {
        this.playPauseBtn.textContent = "⏸ Pause";
        this.playPauseBtn.classList.add("active");
        this.onPlay();
      } else {
        this.playPauseBtn.textContent = "▶ Play";
        this.playPauseBtn.classList.remove("active");
        this.onPause();
      }
    }

    _handleReset() {
      this.isPlaying = false;
      this.playPauseBtn.textContent = "▶ Play";
      this.playPauseBtn.classList.remove("active");
      this.scrubber.value = 0;
      this.timeDisplay.textContent = "0ms";
      this.onReset();
    }

    _handleStep() {
      if (this.onStep) this.onStep();
    }

    _handleSeek(e) {
      const percentage = parseFloat(e.target.value);
      this.onSeek(percentage / 100);
    }

    updateTimeDisplay(currentTime, totalTime) {
      this.timeDisplay.textContent = `${Math.floor(currentTime)}ms / ${Math.floor(totalTime)}ms`;
      const percentage = (currentTime / totalTime) * 100;
      this.scrubber.value = percentage;
    }

    setDuration(duration) {
      this.duration = duration;
    }

    pause() {
      if (this.isPlaying) this._togglePlayPause();
    }

    destroy() {
      const panel = this.container.querySelector("#control-panel");
      if (panel) panel.remove();
    }
  }

  // ============================================================================
  // Visual Effects Manager
  // ============================================================================

  class VisualEffectsManager {
    constructor(container) {
      this.container = container;
      this.flyingAnimationInProgress = false;

      this.flashOverlay = document.createElement("div");
      this.flashOverlay.className = "flash-overlay";
      this.container.appendChild(this.flashOverlay);
    }

    triggerSamplingEffect(stackViz, samplingPanel, currentTime, trace) {
      if (this.flyingAnimationInProgress) return;

      const stack = trace.getStackAt(currentTime);

      if (stack.length === 0) {
        samplingPanel.addSample(stack);
        return;
      }

      this.flyingAnimationInProgress = true;
      stackViz.flashAll();

      const clone = stackViz.createStackClone(this.container);
      const targetPosition = samplingPanel.getTargetPosition();

      this._animateFlash();
      this._animateFlyingStack(clone, targetPosition, () => {
        samplingPanel.showImpactEffect(targetPosition);
        clone.remove();

        const currentStack = trace.getStackAt(currentTime);
        samplingPanel.addSample(currentStack);
        this.flyingAnimationInProgress = false;
      });
    }

    _animateFlash() {
      anim.to(this.flashOverlay, { opacity: 0.1 }, 0).onfinish = () => {
        anim.to(this.flashOverlay, { opacity: 0 }, 150, "easeOutQuad");
      };
    }

    _animateFlyingStack(clone, targetPosition, onComplete) {
      const containerRect = this.container.getBoundingClientRect();
      const cloneRect = clone.getBoundingClientRect();

      // Convert viewport coordinates to container-relative
      const startX = cloneRect.left - containerRect.left + cloneRect.width / 2;
      const startY = cloneRect.top - containerRect.top + cloneRect.height / 2;
      const targetX = targetPosition.x - containerRect.left;
      const targetY = targetPosition.y - containerRect.top;

      const deltaX = targetX - startX;
      const deltaY = targetY - startY;

      anim.to(
        clone,
        {
          x: deltaX,
          y: deltaY,
          scale: 0.3,
          opacity: 0.6,
        },
        TIMINGS.sampleToFlame,
        "easeOutCubic",
        onComplete,
      );
    }
  }

  // ============================================================================
  // Main Visualization Class
  // ============================================================================

  class SamplingVisualization {
    constructor(container) {
      this.container = container;

      this.trace = new ExecutionTrace(DEMO_SIMPLE.source, DEMO_SIMPLE.trace);

      this.currentTime = 0;
      this.isPlaying = false;
      this.playbackSpeed = TIMINGS.defaultSpeed;
      this.eventIndex = 0;

      this.sampleInterval = TIMINGS.sampleIntervalDefault;
      this.lastSampleTime = 0;

      this._createLayout();

      this.effectsManager = new VisualEffectsManager(this.vizColumn);

      this.lastTime = performance.now();
      this._animate();
    }

    _createLayout() {
      this.codePanel = new CodePanel(this.trace.source);
      this.container.appendChild(this.codePanel.element);

      this.vizColumn = document.createElement("div");
      this.vizColumn.className = "viz-column";
      this.container.appendChild(this.vizColumn);

      const stackSection = document.createElement("div");
      stackSection.className = "stack-section";

      const stackTitle = document.createElement("div");
      stackTitle.className = "stack-section-title";
      stackTitle.textContent = "Call Stack";
      stackSection.appendChild(stackTitle);

      this.stackViz = new DOMStackVisualization();
      stackSection.appendChild(this.stackViz.element);
      this.vizColumn.appendChild(stackSection);

      this.samplingPanel = new DOMSamplingPanel();
      this.samplingPanel.setGroundTruth(this._getGroundTruthFunctions());
      this.vizColumn.appendChild(this.samplingPanel.element);

      this.controls = new ControlPanel(
        this.vizColumn,
        () => this.play(),
        () => this.pause(),
        () => this.reset(),
        (speed) => this.setSpeed(speed),
        (progress) => this.seek(progress),
        () => this.step(),
        (interval) => this.setSampleInterval(interval),
      );
      this.controls.setDuration(this.trace.duration);
    }

    _getGroundTruthFunctions() {
      const functions = new Set();
      this.trace.events.forEach((event) => {
        if (event.type === "call") {
          functions.add(event.functionName);
        }
      });
      return [...functions];
    }

    play() {
      this.isPlaying = true;
    }

    pause() {
      this.isPlaying = false;
    }

    reset() {
      this.currentTime = 0;
      this.eventIndex = 0;
      this.isPlaying = false;
      this.lastSampleTime = 0;
      this.stackViz.clear();
      this.codePanel.reset();
      this.samplingPanel.reset();
      this.controls.updateTimeDisplay(0, this.trace.duration);
    }

    setSpeed(speed) {
      this.playbackSpeed = speed;
    }

    setSampleInterval(interval) {
      this.sampleInterval = interval;
      this.samplingPanel.setSampleInterval(interval);
    }

    seek(progress) {
      this.currentTime = progress * this.trace.duration;
      this.eventIndex = 0;
      this.lastSampleTime = 0;
      this._rebuildState();
    }

    step() {
      this.pause();

      const nextEvent = this.trace.getNextEvent(this.currentTime);

      if (nextEvent) {
        // Calculate delta to reach next event + epsilon
        const targetTime = nextEvent.timestamp + 0.1;
        const delta = targetTime - this.currentTime;
        if (delta > 0) {
          this._advanceTime(delta);
        }
      }
    }

    _animate(currentTime = performance.now()) {
      const deltaTime = currentTime - this.lastTime;
      this.lastTime = currentTime;

      this.update(deltaTime);
      requestAnimationFrame((t) => this._animate(t));
    }

    update(deltaTime) {
      if (!this.isPlaying) {
        this.controls.updateTimeDisplay(this.currentTime, this.trace.duration);
        return;
      }

      const virtualDelta = deltaTime * this.playbackSpeed;
      this._advanceTime(virtualDelta);
    }

    _advanceTime(virtualDelta) {
      this.currentTime += virtualDelta;

      if (this.currentTime >= this.trace.duration) {
        this.currentTime = this.trace.duration;
        this.isPlaying = false;
        this.controls.pause();
      }

      while (this.eventIndex < this.trace.events.length) {
        const event = this.trace.events[this.eventIndex];

        if (event.timestamp > this.currentTime) break;

        this._processEvent(event);
        this.eventIndex++;
      }

      this.controls.updateTimeDisplay(this.currentTime, this.trace.duration);

      if (this.currentTime - this.lastSampleTime >= this.sampleInterval) {
        this._takeSample();
        this.lastSampleTime = this.currentTime;
      }
    }

    _processEvent(event) {
      this.stackViz.processEvent(event);

      if (event.type === "call") {
        this.codePanel.highlightLine(event.lineno);
      } else if (event.type === "return") {
        const currentStack = this.trace.getStackAt(this.currentTime);
        if (currentStack.length > 0) {
          this.codePanel.highlightLine(
            currentStack[currentStack.length - 1].line,
          );
        } else {
          this.codePanel.highlightLine(null);
        }
      } else if (event.type === "line") {
        this.codePanel.highlightLine(event.lineno);
      }
    }

    _takeSample() {
      this.effectsManager.triggerSamplingEffect(
        this.stackViz,
        this.samplingPanel,
        this.currentTime,
        this.trace,
      );
    }

    _rebuildState() {
      this.stackViz.clear();
      this.codePanel.reset();
      this.samplingPanel.reset();

      for (let t = 0; t < this.currentTime; t += this.sampleInterval) {
        const stack = this.trace.getStackAt(t);
        this.samplingPanel.addSample(stack);
        this.lastSampleTime = t;
      }

      const stack = this.trace.getStackAt(this.currentTime);
      this.stackViz.updateToMatch(stack);

      if (stack.length > 0) {
        this.codePanel.highlightLine(stack[stack.length - 1].line);
      }

      this.eventIndex = this.trace.getEventsUntil(this.currentTime).length;
    }
  }

  // ============================================================================
  // Initialize
  // ============================================================================

  function init() {
    // If trace data hasn't been injected yet (local dev), don't initialize
    if (!DEMO_SIMPLE) return;

    const appContainer = document.getElementById("sampling-profiler-viz");
    if (appContainer) {
      new SamplingVisualization(appContainer);
    }
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();
