const EMBEDDED_DATA = {{FLAMEGRAPH_DATA}};

// Global string table for resolving string indices
let stringTable = [];
let normalData = null;
let invertedData = null;
let currentThreadFilter = 'all';
let isInverted = false;
let useModuleNames = true;

// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
// and automatically switch with theme changes - no JS color arrays needed!

// Opcode mappings - loaded from embedded data (generated by Python)
let OPCODE_NAMES = {};
let DEOPT_MAP = {};

// Initialize opcode mappings from embedded data
function initOpcodeMapping(data) {
    if (data && data.opcode_mapping) {
        OPCODE_NAMES = data.opcode_mapping.names || {};
        DEOPT_MAP = data.opcode_mapping.deopt || {};
    }
}

// Get opcode info from opcode number
function getOpcodeInfo(opcode) {
    const opname = OPCODE_NAMES[opcode] || `<${opcode}>`;
    const baseOpcode = DEOPT_MAP[opcode];
    const isSpecialized = baseOpcode !== undefined;
    const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname;

    return {
        opname: opname,
        baseOpname: baseOpname,
        isSpecialized: isSpecialized
    };
}

// ============================================================================
// String Resolution
// ============================================================================

function resolveString(index, table = stringTable) {
  if (index === null || index === undefined) {
    return null;
  }
  if (typeof index === 'number' && index >= 0 && index < table.length) {
    return table[index];
  }
  return String(index);
}

function resolveStringIndices(node, table) {
  if (!node) return node;

  const resolved = { ...node };

  if (typeof resolved.name === 'number') {
    resolved.name = resolveString(resolved.name, table);
  }
  if (typeof resolved.filename === 'number') {
    resolved.filename = resolveString(resolved.filename, table);
  }
  if (typeof resolved.funcname === 'number') {
    resolved.funcname = resolveString(resolved.funcname, table);
  }
  if (typeof resolved.module === 'number') {
    resolved.module = resolveString(resolved.module, table);
  }
  if (typeof resolved.label === 'number') {
    resolved.label = resolveString(resolved.label, table);
  }

  if (Array.isArray(resolved.source)) {
    resolved.source = resolved.source.map(index =>
      typeof index === 'number' ? resolveString(index, table) : index
    );
  }

  if (Array.isArray(resolved.children)) {
    resolved.children = resolved.children.map(child => resolveStringIndices(child, table));
  }

  return resolved;
}

// Escape HTML special characters
function escapeHtml(str) {
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}

// Get display path based on user preference (module or full path)
function getDisplayName(moduleName, filename) {
  if (useModuleNames) {
    return moduleName || filename;
  }
  return filename;
}

function selectFlamegraphData() {
  const baseData = isShowingElided ? elidedFlamegraphData : normalData;

  if (!isInverted) {
    return baseData;
  }

  if (isShowingElided) {
    if (!invertedElidedData) {
      invertedElidedData = generateInvertedFlamegraph(baseData);
    }
    return invertedElidedData;
  }

  if (!invertedData) {
    invertedData = generateInvertedFlamegraph(baseData);
  }
  return invertedData;
}

function updateFlamegraphView() {
  const selectedData = selectFlamegraphData();
  const selectedThreadId = currentThreadFilter !== 'all' ? parseInt(currentThreadFilter, 10) : null;
  const filteredData = selectedThreadId !== null ? filterDataByThread(selectedData, selectedThreadId) : selectedData;
  const tooltip = createPythonTooltip(filteredData);
  const chart = createFlamegraph(tooltip, filteredData.value, filteredData);
  renderFlamegraph(chart, filteredData);
  populateThreadStats(selectedData, selectedThreadId);
}

// ============================================================================
// Theme & UI Controls
// ============================================================================

function toggleTheme() {
  toggleAndSaveTheme();

  // Re-render flamegraph with new theme colors
  if (window.flamegraphData && normalData) {
    updateFlamegraphView();
  }
}

function toggleSidebar() {
  const sidebar = document.getElementById('sidebar');
  if (sidebar) {
    const isCollapsing = !sidebar.classList.contains('collapsed');

    if (isCollapsing) {
      // Save current width before collapsing
      const currentWidth = sidebar.offsetWidth;
      sidebar.dataset.expandedWidth = currentWidth;
      localStorage.setItem('flamegraph-sidebar-width', currentWidth);
    } else {
      // Restore width when expanding
      const savedWidth = sidebar.dataset.expandedWidth || localStorage.getItem('flamegraph-sidebar-width');
      if (savedWidth) {
        sidebar.style.width = savedWidth + 'px';
      }
    }

    sidebar.classList.toggle('collapsed');
    localStorage.setItem('flamegraph-sidebar', sidebar.classList.contains('collapsed') ? 'collapsed' : 'expanded');

    // Resize chart after sidebar animation
    setTimeout(() => {
      resizeChart();
    }, 300);
  }
}

function resizeChart() {
  if (window.flamegraphChart && window.flamegraphData) {
    const chartArea = document.querySelector('.chart-area');
    if (chartArea) {
      window.flamegraphChart.width(chartArea.clientWidth - 32);
      d3.select("#chart").datum(window.flamegraphData).call(window.flamegraphChart);
    }
  }
}

function toggleSection(sectionId) {
  const section = document.getElementById(sectionId);
  if (section) {
    section.classList.toggle('collapsed');
    // Save state
    const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}');
    collapsedSections[sectionId] = section.classList.contains('collapsed');
    localStorage.setItem('flamegraph-collapsed-sections', JSON.stringify(collapsedSections));
  }
}

// Restore theme from localStorage, or use browser preference
function restoreUIState() {
  applyTheme(getPreferredTheme());

  // Restore sidebar state
  const savedSidebar = localStorage.getItem('flamegraph-sidebar');
  if (savedSidebar === 'collapsed') {
    const sidebar = document.getElementById('sidebar');
    if (sidebar) sidebar.classList.add('collapsed');
  }

  // Restore sidebar width
  const savedWidth = localStorage.getItem('flamegraph-sidebar-width');
  if (savedWidth) {
    const sidebar = document.getElementById('sidebar');
    if (sidebar) {
      sidebar.style.width = savedWidth + 'px';
    }
  }

  // Restore collapsed sections
  const collapsedSections = JSON.parse(localStorage.getItem('flamegraph-collapsed-sections') || '{}');
  for (const [sectionId, isCollapsed] of Object.entries(collapsedSections)) {
    if (isCollapsed) {
      const section = document.getElementById(sectionId);
      if (section) section.classList.add('collapsed');
    }
  }
}

// ============================================================================
// Logo/Favicon Setup
// ============================================================================

function setupLogos() {
    const logo = document.querySelector('.sidebar-logo-img img');
    if (!logo) return;

    const navbarLogoContainer = document.getElementById('navbar-logo');
    if (navbarLogoContainer) {
        const navbarLogo = logo.cloneNode(true);
        navbarLogoContainer.appendChild(navbarLogo);
    }

    const favicon = document.createElement('link');
    favicon.rel = 'icon';
    favicon.type = 'image/png';
    favicon.href = logo.src;
    document.head.appendChild(favicon);
}

// ============================================================================
// Status Bar
// ============================================================================

function updateStatusBar(nodeData, rootValue) {
  const funcname = resolveString(nodeData.funcname) || resolveString(nodeData.name) || "--";
  const filename = resolveString(nodeData.filename) || "";
  const moduleName = resolveString(nodeData.module) || "";
  const lineno = nodeData.lineno;
  const timeMs = (nodeData.value / 1000).toFixed(2);
  const percent = rootValue > 0 ? ((nodeData.value / rootValue) * 100).toFixed(1) : "0.0";

  const brandEl = document.getElementById('status-brand');
  const taglineEl = document.getElementById('status-tagline');
  if (brandEl) brandEl.style.display = 'none';
  if (taglineEl) taglineEl.style.display = 'none';

  const locationEl = document.getElementById('status-location');
  const funcItem = document.getElementById('status-func-item');
  const timeItem = document.getElementById('status-time-item');
  const percentItem = document.getElementById('status-percent-item');

  if (locationEl) locationEl.style.display = filename && filename !== "~" ? 'flex' : 'none';
  if (funcItem) funcItem.style.display = 'flex';
  if (timeItem) timeItem.style.display = 'flex';
  if (percentItem) percentItem.style.display = 'flex';

  const fileEl = document.getElementById('status-file');
  if (fileEl && filename && filename !== "~") {
    const displayName = getDisplayName(moduleName, filename);
    fileEl.textContent = lineno ? `${displayName}:${lineno}` : displayName;
  }

  const funcEl = document.getElementById('status-func');
  if (funcEl) funcEl.textContent = funcname.length > 40 ? funcname.substring(0, 37) + '...' : funcname;

  const timeEl = document.getElementById('status-time');
  if (timeEl) timeEl.textContent = `${timeMs} ms`;

  const percentEl = document.getElementById('status-percent');
  if (percentEl) percentEl.textContent = `${percent}%`;
}

function clearStatusBar() {
  const ids = ['status-location', 'status-func-item', 'status-time-item', 'status-percent-item'];
  ids.forEach(id => {
    const el = document.getElementById(id);
    if (el) el.style.display = 'none';
  });

  const brandEl = document.getElementById('status-brand');
  const taglineEl = document.getElementById('status-tagline');
  if (brandEl) brandEl.style.display = 'flex';
  if (taglineEl) taglineEl.style.display = 'flex';
}

// ============================================================================
// Tooltip
// ============================================================================

function createPythonTooltip(data) {
  const pythonTooltip = flamegraph.tooltip.defaultFlamegraphTooltip();

  pythonTooltip.show = function (d, element) {
    if (!this._tooltip) {
      this._tooltip = d3.select("body")
        .append("div")
        .attr("class", "python-tooltip")
        .style("opacity", 0);
    }

    const timeMs = (d.data.value / 1000).toFixed(2);
    const selfSamples = d.data.self || 0;
    const selfMs = (selfSamples / 1000).toFixed(2);
    const percentage = ((d.data.value / data.value) * 100).toFixed(2);
    const calls = d.data.calls || 0;
    const childCount = d.children ? d.children.length : 0;
    const source = d.data.source;

    const funcname = resolveString(d.data.funcname) || resolveString(d.data.name);
    const filename = resolveString(d.data.filename) || "";
    const moduleName = resolveString(d.data.module) || "";
    const displayName = escapeHtml(useModuleNames ? (moduleName || filename) : filename);
    const isSpecialFrame = filename === "~";

    // Build source section
    let sourceSection = "";
    if (source && Array.isArray(source) && source.length > 0) {
      const sourceLines = source
        .map((line) => {
          const isCurrent = line.startsWith("→");
          const escaped = escapeHtml(line);
          return `<div class="tooltip-source-line${isCurrent ? ' current' : ''}">${escaped}</div>`;
        })
        .join("");

      sourceSection = `
        <div class="tooltip-source">
          <div class="tooltip-source-title">Source Code:</div>
          <div class="tooltip-source-code">${sourceLines}</div>
        </div>`;
    }

    // Create bytecode/opcode section if available
    let opcodeSection = "";
    const opcodes = d.data.opcodes;
    if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) {
      // Sort opcodes by sample count (descending)
      const sortedOpcodes = Object.entries(opcodes)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 8); // Limit to top 8

      const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0);
      const maxCount = sortedOpcodes[0][1] || 1;

      const opcodeLines = sortedOpcodes.map(([opcode, count]) => {
        const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10));
        const pct = ((count / totalOpcodeSamples) * 100).toFixed(1);
        const barWidth = (count / maxCount) * 100;
        const specializedBadge = opcodeInfo.isSpecialized
          ? '<span class="tooltip-opcode-badge">SPECIALIZED</span>'
          : '';
        const baseOpHint = opcodeInfo.isSpecialized
          ? `<span class="tooltip-opcode-base-hint">(${opcodeInfo.baseOpname})</span>`
          : '';
        const nameClass = opcodeInfo.isSpecialized
          ? 'tooltip-opcode-name specialized'
          : 'tooltip-opcode-name';

        return `
          <div class="tooltip-opcode-row">
            <div class="${nameClass}">
              ${opcodeInfo.opname}${baseOpHint}${specializedBadge}
            </div>
            <div class="tooltip-opcode-count">${count.toLocaleString()} (${pct}%)</div>
            <div class="tooltip-opcode-bar">
              <div class="tooltip-opcode-bar-fill" style="width: ${barWidth}%;"></div>
            </div>
          </div>`;
      }).join('');

      opcodeSection = `
        <div class="tooltip-opcodes">
          <div class="tooltip-opcodes-title">Bytecode Instructions:</div>
          <div class="tooltip-opcodes-list">
            ${opcodeLines}
          </div>
        </div>`;
    }

    const fileLocationHTML = isSpecialFrame ? "" : `
      <div class="tooltip-location">${displayName}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;

    // Differential stats section
    let diffSection = "";
    if (d.data.diff !== undefined && d.data.baseline !== undefined) {
      const baselineSelf = (d.data.baseline / 1000).toFixed(2);
      const currentSelf = ((d.data.self_time || 0) / 1000).toFixed(2);
      const diffMs = (d.data.diff / 1000).toFixed(2);
      const diffPct = d.data.diff_pct;
      const sign = d.data.diff >= 0 ? "+" : "";
      const diffClass = d.data.diff > 0 ? "regression" : (d.data.diff < 0 ? "improvement" : "neutral");

      diffSection = `
        <div class="tooltip-diff">
          <div class="tooltip-diff-title">Self-Time Comparison:</div>
          <div class="tooltip-diff-row">
            <span class="tooltip-stat-label">Baseline Self:</span>
            <span class="tooltip-stat-value">${baselineSelf} ms</span>
          </div>
          <div class="tooltip-diff-row">
            <span class="tooltip-stat-label">Current Self:</span>
            <span class="tooltip-stat-value">${currentSelf} ms</span>
          </div>
          <div class="tooltip-diff-row ${diffClass}">
            <span class="tooltip-stat-label">Difference:</span>
            <span class="tooltip-stat-value">${sign}${diffMs} ms (${sign}${diffPct.toFixed(1)}%)</span>
          </div>
        </div>`;
    }

    const tooltipHTML = `
      <div class="tooltip-header">
        <div class="tooltip-title">${funcname}</div>
        ${fileLocationHTML}
      </div>
      <div class="tooltip-stats">
        <span class="tooltip-stat-label">Total Time:</span>
        <span class="tooltip-stat-value">${timeMs} ms</span>

        ${selfSamples > 0 ? `
          <span class="tooltip-stat-label">Self Time:</span>
          <span class="tooltip-stat-value">${selfMs} ms</span>
        ` : ''}

        <span class="tooltip-stat-label">Percentage:</span>
        <span class="tooltip-stat-value accent">${percentage}%</span>

        ${calls > 0 ? `
          <span class="tooltip-stat-label">Function Calls:</span>
          <span class="tooltip-stat-value">${calls.toLocaleString()}</span>
        ` : ''}

        ${childCount > 0 ? `
          <span class="tooltip-stat-label">Child Functions:</span>
          <span class="tooltip-stat-value">${childCount}</span>
        ` : ''}
      </div>
      ${diffSection}
      ${sourceSection}
      ${opcodeSection}
      <div class="tooltip-hint">
        ${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
      </div>
    `;

    // Position tooltip
    const event = d3.event || window.event;
    const mouseX = event.pageX || event.clientX;
    const mouseY = event.pageY || event.clientY;
    const padding = 12;

    this._tooltip.html(tooltipHTML);

    // Measure tooltip
    const node = this._tooltip.style("display", "block").style("opacity", 0).node();
    const tooltipWidth = node.offsetWidth || 320;
    const tooltipHeight = node.offsetHeight || 200;

    // Calculate position
    let left = mouseX + padding;
    let top = mouseY + padding;

    if (left + tooltipWidth > window.innerWidth) {
      left = mouseX - tooltipWidth - padding;
      if (left < 0) left = padding;
    }

    if (top + tooltipHeight > window.innerHeight) {
      top = mouseY - tooltipHeight - padding;
      if (top < 0) top = padding;
    }

    this._tooltip
      .style("left", left + "px")
      .style("top", top + "px")
      .transition()
      .duration(150)
      .style("opacity", 1);

    // Update status bar
    updateStatusBar(d.data, data.value);
  };

  pythonTooltip.hide = function () {
    if (this._tooltip) {
      this._tooltip.transition().duration(150).style("opacity", 0);
    }
    clearStatusBar();
  };

  return pythonTooltip;
}

// ============================================================================
// Flamegraph Creation
// ============================================================================

function ensureLibraryLoaded() {
  if (typeof flamegraph === "undefined") {
    console.error("d3-flame-graph library not loaded");
    document.getElementById("chart").innerHTML =
      '<div style="padding: 40px; text-align: center; color: var(--text-muted);">Error: d3-flame-graph library failed to load</div>';
    throw new Error("d3-flame-graph library failed to load");
  }
}

const HEAT_THRESHOLDS = [
  [0.6, 8],
  [0.35, 7],
  [0.18, 6],
  [0.12, 5],
  [0.06, 4],
  [0.03, 3],
  [0.01, 2],
];

function getHeatLevel(percentage) {
  for (const [threshold, level] of HEAT_THRESHOLDS) {
    if (percentage >= threshold) return level;
  }
  return 1;
}

function getHeatColors() {
  const style = getComputedStyle(document.documentElement);
  const colors = {};
  for (let i = 1; i <= 8; i++) {
    colors[i] = style.getPropertyValue(`--heat-${i}`).trim();
  }
  return colors;
}

function getDiffColors() {
  const style = getComputedStyle(document.documentElement);
  return {
    elided: style.getPropertyValue('--diff-elided').trim(),
    new: style.getPropertyValue('--diff-new').trim(),
    neutral: style.getPropertyValue('--diff-neutral').trim(),
    regressionDeep: style.getPropertyValue('--diff-regression-deep').trim(),
    regressionMedium: style.getPropertyValue('--diff-regression-medium').trim(),
    regressionLight: style.getPropertyValue('--diff-regression-light').trim(),
    regressionVerylight: style.getPropertyValue('--diff-regression-verylight').trim(),
    improvementDeep: style.getPropertyValue('--diff-improvement-deep').trim(),
    improvementMedium: style.getPropertyValue('--diff-improvement-medium').trim(),
    improvementLight: style.getPropertyValue('--diff-improvement-light').trim(),
    improvementVerylight: style.getPropertyValue('--diff-improvement-verylight').trim(),
  };
}

function getDiffColorForNode(node, diffColors) {
  if (isShowingElided) {
    return diffColors.elided;
  }

  const diff_pct = node.data.diff_pct || 0;
  const diff_samples = node.data.diff || 0;
  const self_time = node.data.self_time || 0;

  if (diff_pct === 100 && self_time > 0 && Math.abs(diff_samples - self_time) < 0.1) {
    return diffColors.new;
  }

  // Neutral zone: small percentage change
  if (Math.abs(diff_pct) < 15) {
    return diffColors.neutral;
  }

  // Regression (red scale)
  if (diff_pct > 0) {
    if (diff_pct >= 100) return diffColors.regressionDeep;
    if (diff_pct > 50) return diffColors.regressionMedium;
    if (diff_pct > 30) return diffColors.regressionLight;
    return diffColors.regressionVerylight;
  }

  // Improvement (blue scale)
  if (diff_pct <= -100) return diffColors.improvementDeep;
  if (diff_pct < -50) return diffColors.improvementMedium;
  if (diff_pct < -30) return diffColors.improvementLight;
  return diffColors.improvementVerylight;
}

function createFlamegraph(tooltip, rootValue, data) {
  const chartArea = document.querySelector('.chart-area');
  const width = chartArea ? chartArea.clientWidth - 32 : window.innerWidth - 320;
  const heatColors = getHeatColors();

  const isDifferential = data && data.stats && data.stats.is_differential;
  const diffColors = isDifferential ? getDiffColors() : null;

  let chart = flamegraph()
    .width(width)
    .cellHeight(20)
    .transitionDuration(300)
    .minFrameSize(1)
    .tooltip(tooltip)
    .inverted(true)
    .getName(d => resolveString(useModuleNames ? d.data.label : d.data.name) || resolveString(d.data.name) || '')
    .setColorMapper(function (d) {
      if (d.depth === 0) return 'transparent';

      if (isDifferential) {
        return getDiffColorForNode(d, diffColors);
      }

      const percentage = d.data.value / rootValue;
      const level = getHeatLevel(percentage);
      return heatColors[level];
    });

  return chart;
}

function renderFlamegraph(chart, data) {
  d3.select("#chart").datum(data).call(chart);
  window.flamegraphChart = chart;
  window.flamegraphData = data;
  populateStats(data);
}

// ============================================================================
// Search
// ============================================================================

function updateSearchHighlight(searchTerm, searchInput) {
  d3.selectAll("#chart rect")
    .classed("search-match", false)
    .classed("search-dim", false);

  // Clear active state from all hotspots
  document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));

  if (searchTerm && searchTerm.length > 0) {
    let matchCount = 0;

    d3.selectAll("#chart rect").each(function (d) {
      if (d && d.data) {
        const name = resolveString(d.data.name) || "";
        const funcname = resolveString(d.data.funcname) || "";
        const filename = resolveString(d.data.filename) || "";
        const moduleName = resolveString(d.data.module) || "";
        const displayName = getDisplayName(moduleName, filename);
        const lineno = d.data.lineno;
        const term = searchTerm.toLowerCase();

        // Check if search term looks like path:line pattern
        const fileLineMatch = term.match(/^(.+):(\d+)$/);
        let matches = false;

        if (fileLineMatch) {
          const searchFile = fileLineMatch[1];
          const searchLine = parseInt(fileLineMatch[2], 10);
          matches = displayName.toLowerCase().includes(searchFile) && lineno === searchLine;
        } else {
          // Regular substring search
          matches =
            name.toLowerCase().includes(term) ||
            funcname.toLowerCase().includes(term) ||
            displayName.toLowerCase().includes(term);
        }

        if (matches) {
          matchCount++;
          d3.select(this).classed("search-match", true);
        } else {
          d3.select(this).classed("search-dim", true);
        }
      }
    });

    if (searchInput) {
      searchInput.classList.remove("has-matches", "no-matches");
      searchInput.classList.add(matchCount > 0 ? "has-matches" : "no-matches");
    }

    // Mark matching hotspot as active
    document.querySelectorAll('.hotspot').forEach(h => {
      if (h.dataset.searchterm && h.dataset.searchterm.toLowerCase() === searchTerm.toLowerCase()) {
        h.classList.add('active');
      }
    });
  } else if (searchInput) {
    searchInput.classList.remove("has-matches", "no-matches");
  }
}

function searchForHotspot(funcname) {
  const searchInput = document.getElementById('search-input');
  const searchWrapper = document.querySelector('.search-wrapper');
  if (searchInput) {
    // Toggle: if already searching for this term, clear it
    if (searchInput.value.trim() === funcname) {
      clearSearch();
    } else {
      searchInput.value = funcname;
      if (searchWrapper) {
        searchWrapper.classList.add('has-value');
      }
      performSearch();
    }
  }
}

function initSearchHandlers() {
  const searchInput = document.getElementById("search-input");
  const searchWrapper = document.querySelector(".search-wrapper");
  if (!searchInput) return;

  let searchTimeout;
  function performSearch() {
    const term = searchInput.value.trim();
    updateSearchHighlight(term, searchInput);
    // Toggle has-value class for clear button visibility
    if (searchWrapper) {
      searchWrapper.classList.toggle("has-value", term.length > 0);
    }
  }

  searchInput.addEventListener("input", function () {
    clearTimeout(searchTimeout);
    searchTimeout = setTimeout(performSearch, 150);
  });

  window.performSearch = performSearch;
}

function clearSearch() {
  const searchInput = document.getElementById("search-input");
  const searchWrapper = document.querySelector(".search-wrapper");
  if (searchInput) {
    searchInput.value = "";
    searchInput.classList.remove("has-matches", "no-matches");
    if (searchWrapper) {
      searchWrapper.classList.remove("has-value");
    }
    // Clear highlights
    d3.selectAll("#chart rect")
      .classed("search-match", false)
      .classed("search-dim", false);
    // Clear active hotspot
    document.querySelectorAll('.hotspot').forEach(h => h.classList.remove('active'));
  }
}

// ============================================================================
// Resize Handler
// ============================================================================

function handleResize() {
  let resizeTimeout;
  window.addEventListener("resize", function () {
    clearTimeout(resizeTimeout);
    resizeTimeout = setTimeout(resizeChart, 100);
  });
}

function initSidebarResize() {
  const sidebar = document.getElementById('sidebar');
  const resizeHandle = document.getElementById('sidebar-resize-handle');
  if (!sidebar || !resizeHandle) return;

  let isResizing = false;
  let startX = 0;
  let startWidth = 0;
  const minWidth = 200;
  const maxWidth = 600;

  resizeHandle.addEventListener('mousedown', function(e) {
    isResizing = true;
    startX = e.clientX;
    startWidth = sidebar.offsetWidth;
    resizeHandle.classList.add('resizing');
    document.body.classList.add('resizing-sidebar');
    e.preventDefault();
  });

  document.addEventListener('mousemove', function(e) {
    if (!isResizing) return;

    const deltaX = e.clientX - startX;
    const newWidth = Math.min(Math.max(startWidth + deltaX, minWidth), maxWidth);
    sidebar.style.width = newWidth + 'px';
    e.preventDefault();
  });

  document.addEventListener('mouseup', function() {
    if (isResizing) {
      isResizing = false;
      resizeHandle.classList.remove('resizing');
      document.body.classList.remove('resizing-sidebar');

      // Save the new width
      const width = sidebar.offsetWidth;
      localStorage.setItem('flamegraph-sidebar-width', width);

      // Resize chart after sidebar resize
      setTimeout(() => {
        resizeChart();
      }, 10);
    }
  });
}

// ============================================================================
// Thread Stats
// ============================================================================

// Mode constants (must match constants.py)
const PROFILING_MODE_WALL = 0;
const PROFILING_MODE_CPU = 1;
const PROFILING_MODE_GIL = 2;
const PROFILING_MODE_ALL = 3;

function populateThreadStats(data, selectedThreadId = null) {
  const stats = data?.stats;
  if (!stats || !stats.thread_stats) {
    return;
  }

  const mode = stats.mode !== undefined ? stats.mode : PROFILING_MODE_WALL;
  let threadStats;

  if (selectedThreadId !== null && stats.per_thread_stats && stats.per_thread_stats[selectedThreadId]) {
    threadStats = stats.per_thread_stats[selectedThreadId];
  } else {
    threadStats = stats.thread_stats;
  }

  if (!threadStats || typeof threadStats.total !== 'number' || threadStats.total <= 0) {
    return;
  }

  const section = document.getElementById('thread-stats-bar');
  if (!section) {
    return;
  }

  section.style.display = 'block';

  const gilHeldStat = document.getElementById('gil-held-stat');
  const gilReleasedStat = document.getElementById('gil-released-stat');
  const gilWaitingStat = document.getElementById('gil-waiting-stat');

  if (mode === PROFILING_MODE_GIL) {
    // In GIL mode, hide GIL-related stats
    if (gilHeldStat) gilHeldStat.style.display = 'none';
    if (gilReleasedStat) gilReleasedStat.style.display = 'none';
    if (gilWaitingStat) gilWaitingStat.style.display = 'none';
  } else {
    // Show all stats
    if (gilHeldStat) gilHeldStat.style.display = 'block';
    if (gilReleasedStat) gilReleasedStat.style.display = 'block';
    if (gilWaitingStat) gilWaitingStat.style.display = 'block';

    const gilHeldPct = threadStats.has_gil_pct || 0;
    const gilHeldPctElem = document.getElementById('gil-held-pct');
    if (gilHeldPctElem) gilHeldPctElem.textContent = `${gilHeldPct.toFixed(1)}%`;
    const gilHeldFill = document.getElementById('gil-held-fill');
    if (gilHeldFill) gilHeldFill.style.width = `${gilHeldPct}%`;

    // GIL Released = not holding GIL and not waiting for it
    const gilReleasedPct = Math.max(0, 100 - (threadStats.has_gil_pct || 0) - (threadStats.gil_requested_pct || 0));
    const gilReleasedPctElem = document.getElementById('gil-released-pct');
    if (gilReleasedPctElem) gilReleasedPctElem.textContent = `${gilReleasedPct.toFixed(1)}%`;
    const gilReleasedFill = document.getElementById('gil-released-fill');
    if (gilReleasedFill) gilReleasedFill.style.width = `${gilReleasedPct}%`;

    const gilWaitingPct = threadStats.gil_requested_pct || 0;
    const gilWaitingPctElem = document.getElementById('gil-waiting-pct');
    if (gilWaitingPctElem) gilWaitingPctElem.textContent = `${gilWaitingPct.toFixed(1)}%`;
    const gilWaitingFill = document.getElementById('gil-waiting-fill');
    if (gilWaitingFill) gilWaitingFill.style.width = `${gilWaitingPct}%`;
  }

  const gcPct = threadStats.gc_pct || 0;
  const gcPctElem = document.getElementById('gc-pct');
  if (gcPctElem) gcPctElem.textContent = `${gcPct.toFixed(1)}%`;
  const gcFill = document.getElementById('gc-fill');
  if (gcFill) gcFill.style.width = `${gcPct}%`;

  // Exception stats
  const excPct = threadStats.has_exception_pct || 0;
  const excPctElem = document.getElementById('exc-pct');
  if (excPctElem) excPctElem.textContent = `${excPct.toFixed(1)}%`;
  const excFill = document.getElementById('exc-fill');
  if (excFill) excFill.style.width = `${excPct}%`;
}

// ============================================================================
// Profile Summary Stats
// ============================================================================

function formatNumber(num) {
  if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
  if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
  return num.toLocaleString();
}

function formatDuration(seconds) {
  if (seconds >= 3600) {
    const h = Math.floor(seconds / 3600);
    const m = Math.floor((seconds % 3600) / 60);
    return `${h}h ${m}m`;
  }
  if (seconds >= 60) {
    const m = Math.floor(seconds / 60);
    const s = Math.floor(seconds % 60);
    return `${m}m ${s}s`;
  }
  return seconds.toFixed(2) + 's';
}

function populateProfileSummary(data) {
  const stats = data.stats || {};
  const totalSamples = stats.total_samples || data.value || 0;
  const duration = stats.duration_sec || 0;
  const sampleRate = stats.sample_rate || (duration > 0 ? totalSamples / duration : 0);
  const errorRate = stats.error_rate || 0;
  const missedSamples= stats.missed_samples || 0;

  const samplesEl = document.getElementById('stat-total-samples');
  if (samplesEl) samplesEl.textContent = formatNumber(totalSamples);

  const durationEl = document.getElementById('stat-duration');
  if (durationEl) durationEl.textContent = duration > 0 ? formatDuration(duration) : '--';

  const rateEl = document.getElementById('stat-sample-rate');
  if (rateEl) rateEl.textContent = sampleRate > 0 ? formatNumber(Math.round(sampleRate)) : '--';

  // Count unique functions
  // Use normal (non-inverted) tree structure, but respect thread filtering
  const uniqueFunctions = new Set();
  function collectUniqueFunctions(node) {
    if (!node) return;
    const filename = resolveString(node.filename) || 'unknown';
    const funcname = resolveString(node.funcname) || resolveString(node.name) || 'unknown';
    const lineno = node.lineno || 0;
    const key = `${filename}|${lineno}|${funcname}`;
    uniqueFunctions.add(key);
    if (node.children) node.children.forEach(collectUniqueFunctions);
  }
  // In inverted mode, use normalData (with thread filter if active)
  // In normal mode, use the passed data (already has thread filter applied if any)
  let functionCountSource;
  if (!normalData) {
    functionCountSource = data;
  } else if (isInverted) {
    if (currentThreadFilter !== 'all') {
      functionCountSource = filterDataByThread(normalData, parseInt(currentThreadFilter));
    } else {
      functionCountSource = normalData;
    }
  } else {
    functionCountSource = data;
  }
  collectUniqueFunctions(functionCountSource);

  const functionsEl = document.getElementById('stat-functions');
  if (functionsEl) functionsEl.textContent = formatNumber(uniqueFunctions.size);

  // Efficiency bar
  if (errorRate !== undefined && errorRate !== null) {
    const efficiency = Math.max(0, Math.min(100, (100 - errorRate)));

    const efficiencySection = document.getElementById('efficiency-section');
    if (efficiencySection) efficiencySection.style.display = 'block';

    const efficiencyValue = document.getElementById('stat-efficiency');
    if (efficiencyValue) efficiencyValue.textContent = efficiency.toFixed(1) + '%';

    const efficiencyFill = document.getElementById('efficiency-fill');
    if (efficiencyFill) efficiencyFill.style.width = efficiency + '%';
  }
  // MissedSamples bar
  if (missedSamples !== undefined && missedSamples !== null) {
    const sampleEfficiency = Math.max(0, missedSamples);

    const efficiencySection = document.getElementById('efficiency-section');
    if (efficiencySection) efficiencySection.style.display = 'block';

    const sampleEfficiencyValue = document.getElementById('stat-missed-samples');
    if (sampleEfficiencyValue) sampleEfficiencyValue.textContent = sampleEfficiency.toFixed(1) + '%';

    const sampleEfficiencyFill = document.getElementById('missed-samples-fill');
    if (sampleEfficiencyFill) sampleEfficiencyFill.style.width = sampleEfficiency + '%';
  }
}

// ============================================================================
// Elided Stacks (Differential)
// ============================================================================

let elidedFlamegraphData = null;
let invertedElidedData = null;
let isShowingElided = false;

function setupElidedToggle(data) {
  const stats = data.stats || {};
  const elidedCount = stats.elided_count || 0;
  const elidedFlamegraph = stats.elided_flamegraph;

  if (!elidedCount || !elidedFlamegraph) {
    return;
  }

  elidedFlamegraphData = resolveStringIndices(elidedFlamegraph, elidedFlamegraph.strings);

  const toggleElided = document.getElementById('toggle-elided');
  if (toggleElided) {
    toggleElided.style.display = 'flex';

    toggleElided.onclick = function() {
      isShowingElided = !isShowingElided;
      updateToggleUI('toggle-elided', isShowingElided);
      updateFlamegraphView();
    };
  }
}

// ============================================================================
// Hotspot Stats
// ============================================================================

function populateStats(data) {
  // Populate profile summary
  populateProfileSummary(data);

  // Populate thread statistics if available
  populateThreadStats(data);

  // Setup elided stacks toggle if this is a differential flamegraph
  setupElidedToggle(data);

  // For hotspots: use normal (non-inverted) tree structure, but respect thread filtering.
  // In inverted view, the tree structure changes but the hottest functions remain the same.
  // However, if a thread filter is active, we need to show that thread's hotspots.
  let hotspotSource;
  if (!normalData) {
    hotspotSource = data;
  } else if (isInverted) {
    // In inverted mode, use normalData (with thread filter if active)
    if (currentThreadFilter !== 'all') {
      hotspotSource = filterDataByThread(normalData, parseInt(currentThreadFilter));
    } else {
      hotspotSource = normalData;
    }
  } else {
    // In normal mode, use the passed data (already has thread filter applied if any)
    hotspotSource = data;
  }
  const totalSamples = hotspotSource.value || 0;

  const functionMap = new Map();

  function collectFunctions(node) {
    if (!node) return;

    let filename = resolveString(node.filename);
    let funcname = resolveString(node.funcname);
    let moduleName = resolveString(node.module);

    if (!filename || !funcname) {
      const nameStr = resolveString(node.name);
      if (nameStr?.includes('(')) {
        const match = nameStr.match(/^(.+?)\s*\((.+?):(\d+)\)$/);
        if (match) {
          funcname = funcname || match[1];
          filename = filename || match[2];
        }
      }
    }

    filename = filename || 'unknown';
    funcname = funcname || 'unknown';
    moduleName = moduleName || 'unknown';

    if (filename !== 'unknown' && funcname !== 'unknown' && node.value > 0) {
      const directSamples = node.self || 0;

      const funcKey = `${filename}:${node.lineno || '?'}:${funcname}`;

      if (functionMap.has(funcKey)) {
        const existing = functionMap.get(funcKey);
        existing.directSamples += directSamples;
        existing.directPercent = (existing.directSamples / totalSamples) * 100;
        if (directSamples > existing.maxSingleSamples) {
          existing.filename = filename;
          existing.module = moduleName;
          existing.lineno = node.lineno || '?';
          existing.maxSingleSamples = directSamples;
        }
      } else {
        functionMap.set(funcKey, {
          filename: filename,
          module: moduleName,
          lineno: node.lineno || '?',
          funcname: funcname,
          directSamples,
          directPercent: (directSamples / totalSamples) * 100,
          maxSingleSamples: directSamples
        });
      }
    }

    if (node.children) {
      node.children.forEach(child => collectFunctions(child));
    }
  }

  collectFunctions(hotspotSource);

  const hotSpots = Array.from(functionMap.values())
    .filter(f => f.directPercent > 0.5)
    .sort((a, b) => b.directPercent - a.directPercent)
    .slice(0, 3);

  // Populate and animate hotspot cards
  for (let i = 0; i < 3; i++) {
    const num = i + 1;
    const card = document.getElementById(`hotspot-${num}`);
    const funcEl = document.getElementById(`hotspot-func-${num}`);
    const fileEl = document.getElementById(`hotspot-file-${num}`);
    const percentEl = document.getElementById(`hotspot-percent-${num}`);
    const samplesEl = document.getElementById(`hotspot-samples-${num}`);

    if (i < hotSpots.length && hotSpots[i]) {
      const h = hotSpots[i];
      const filename = h.filename || 'unknown';
      const lineno = h.lineno ?? '?';
      const moduleName = h.module || 'unknown';
      const isSpecialFrame = filename === '~' && (lineno === 0 || lineno === '?');

      let funcDisplay = h.funcname || 'unknown';
      if (funcDisplay.length > 28) funcDisplay = funcDisplay.substring(0, 25) + '...';

      if (funcEl) funcEl.textContent = funcDisplay;
      if (fileEl) {
        if (isSpecialFrame) {
          fileEl.textContent = '--';
        } else {
          const displayName = getDisplayName(moduleName, filename);
          fileEl.textContent = `${displayName}:${lineno}`;
        }
      }
      if (percentEl) percentEl.textContent = `${h.directPercent.toFixed(1)}%`;
      if (samplesEl) samplesEl.textContent = ` (${h.directSamples.toLocaleString()})`;
    } else {
      if (funcEl) funcEl.textContent = '--';
      if (fileEl) fileEl.textContent = '--';
      if (percentEl) percentEl.textContent = '--';
      if (samplesEl) samplesEl.textContent = '';
    }

    // Add click handler and animate entrance
    if (card) {
      if (i < hotSpots.length && hotSpots[i]) {
        const h = hotSpots[i];
        const moduleName = h.module || 'unknown';
        const filename = h.filename || 'unknown';
        const displayName = getDisplayName(moduleName, filename);
        const hasValidLocation = displayName !== 'unknown' && h.lineno !== '?';
        const searchTerm = hasValidLocation ? `${displayName}:${h.lineno}` : h.funcname;
        card.dataset.searchterm = searchTerm;
        card.onclick = () => searchForHotspot(searchTerm);
        card.style.cursor = 'pointer';
      } else {
        card.onclick = null;
        delete card.dataset.searchterm;
        card.style.cursor = 'default';
      }

      setTimeout(() => {
        card.classList.add('visible');
      }, 100 + i * 80);
    }
  }
}

// ============================================================================
// Thread Filter
// ============================================================================

function initThreadFilter(data) {
  const threadFilter = document.getElementById('thread-filter');
  const threadSection = document.getElementById('thread-section');

  if (!threadFilter || !data.threads) return;

  threadFilter.innerHTML = '<option value="all">All Threads</option>';

  const threads = data.threads || [];
  threads.forEach(threadId => {
    const option = document.createElement('option');
    option.value = threadId;
    option.textContent = `Thread ${threadId}`;
    threadFilter.appendChild(option);
  });

  if (threads.length > 1 && threadSection) {
    threadSection.style.display = 'block';
  }
}

function filterByThread() {
  const threadFilter = document.getElementById('thread-filter');
  if (!threadFilter || !normalData) return;

  const selectedThread = threadFilter.value;
  currentThreadFilter = selectedThread;

  updateFlamegraphView();
}

function filterDataByThread(data, threadId) {
  function filterNode(node) {
    if (!node.threads || !node.threads.includes(threadId)) {
      return null;
    }

    const filteredNode = { ...node, children: [] };

    if (node.children && Array.isArray(node.children)) {
      filteredNode.children = node.children
        .map(child => filterNode(child))
        .filter(child => child !== null);
    }

    return filteredNode;
  }

  function recalculateValue(node) {
    if (!node.children || node.children.length === 0) {
      return node.value || 0;
    }
    const childrenValue = node.children.reduce((sum, child) => sum + recalculateValue(child), 0);
    node.value = Math.max(node.value || 0, childrenValue);
    return node.value;
  }

  const filteredRoot = { ...data, children: [] };

  if (data.children && Array.isArray(data.children)) {
    filteredRoot.children = data.children
      .map(child => filterNode(child))
      .filter(child => child !== null);
  }

  recalculateValue(filteredRoot);
  return filteredRoot;
}

// ============================================================================
// Control Functions
// ============================================================================

function resetZoom() {
  if (window.flamegraphChart) {
    window.flamegraphChart.resetZoom();
  }
}

function exportSVG() {
  const svgElement = document.querySelector("#chart svg");
  if (!svgElement) {
    console.warn("Cannot export: No flamegraph SVG found");
    return;
  }
  const serializer = new XMLSerializer();
  const svgString = serializer.serializeToString(svgElement);
  const blob = new Blob([svgString], { type: "image/svg+xml" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "python-performance-flamegraph.svg";
  a.click();
  URL.revokeObjectURL(url);
}

// ============================================================================
// Inverted Flamegraph
// ============================================================================

// Example: "file.py|10|foo" or "~|0|<GC>" for special frames
function getInvertNodeKey(node) {
  return `${node.filename || '~'}|${node.lineno || 0}|${node.funcname || node.name}`;
}

function accumulateInvertedNode(parent, stackFrame, leaf, isDifferential) {
  const key = getInvertNodeKey(stackFrame);

  if (!parent.children[key]) {
    const newNode = {
      name: stackFrame.name,
      label: stackFrame.label,
      value: 0,
      self: 0,
      children: {},
      filename: stackFrame.filename,
      module: stackFrame.module,
      lineno: stackFrame.lineno,
      funcname: stackFrame.funcname,
      source: stackFrame.source,
      opcodes: null,
      threads: new Set()
    };

    if (isDifferential) {
      newNode.baseline = 0;
      newNode.baseline_total = 0;
      newNode.self_time = 0;
      newNode.diff = 0;
      newNode.diff_pct = 0;
    }

    parent.children[key] = newNode;
  }

  const node = parent.children[key];
  node.value += leaf.value;
  node.self += stackFrame.self || 0;
  if (leaf.threads) {
    leaf.threads.forEach(t => node.threads.add(t));
  }
  if (stackFrame.opcodes) {
    if (!node.opcodes) {
      node.opcodes = { ...stackFrame.opcodes };
    } else {
      for (const [op, count] of Object.entries(stackFrame.opcodes)) {
        node.opcodes[op] = (node.opcodes[op] || 0) + count;
      }
    }
  }

  if (isDifferential) {
    node.baseline += stackFrame.baseline || 0;
    node.baseline_total += stackFrame.baseline_total || 0;
    node.self_time += stackFrame.self_time || 0;
    node.diff += stackFrame.diff || 0;

    if (node.baseline > 0) {
      node.diff_pct = (node.diff / node.baseline) * 100.0;
    } else if (node.self_time > 0) {
      node.diff_pct = 100.0;
    }
  }

  return node;
}

function processLeaf(invertedRoot, path, leafNode, isDifferential) {
  if (!path || path.length === 0) {
    return;
  }

  let invertedParent = accumulateInvertedNode(invertedRoot, leafNode, leafNode, isDifferential);

  // Walk backwards through the call stack
  for (let i = path.length - 2; i >= 0; i--) {
    invertedParent = accumulateInvertedNode(invertedParent, path[i], leafNode, isDifferential);
  }
}

function traverseInvert(path, currentNode, invertedRoot, isDifferential) {
  const selfValue = currentNode.self || 0;

  if (selfValue > 0) {
    processLeaf(invertedRoot, path, { ...currentNode, value: selfValue }, isDifferential);
  }

  const children = currentNode.children || [];
  children.forEach(child => traverseInvert(path.concat([child]), child, invertedRoot, isDifferential));
}

function convertInvertDictToArray(node) {
  if (node.threads instanceof Set) {
    node.threads = Array.from(node.threads).sort((a, b) => a - b);
  }

  const children = node.children;
  if (children && typeof children === 'object' && !Array.isArray(children)) {
    node.children = Object.values(children);
    node.children.sort((a, b) => b.value - a.value || a.name.localeCompare(b.name));
    node.children.forEach(convertInvertDictToArray);
  }
  return node;
}

function generateInvertedFlamegraph(data) {
  const isDifferential = data && data.stats && data.stats.is_differential;

  const invertedRoot = {
    name: data.name,
    label: data.label,
    value: data.value,
    children: {},
    stats: data.stats,
    threads: data.threads
  };

  const children = data.children || [];
  if (children.length === 0) {
    // Single-frame tree: the root is its own leaf
    processLeaf(invertedRoot, [data], data, isDifferential);
  } else {
    children.forEach(child => traverseInvert([child], child, invertedRoot, isDifferential));
  }

  convertInvertDictToArray(invertedRoot);
  return invertedRoot;
}

function toggleInvert() {
  isInverted = !isInverted;
  updateToggleUI('toggle-invert', isInverted);
  updateFlamegraphView();
}

function togglePathDisplay() {
  useModuleNames = !useModuleNames;
  updateToggleUI('toggle-path-display', useModuleNames);
  updateFlamegraphView();
}

// ============================================================================
// Initialization
// ============================================================================

function initFlamegraph() {
  ensureLibraryLoaded();
  restoreUIState();
  setupLogos();

  if (EMBEDDED_DATA.strings) {
    stringTable = EMBEDDED_DATA.strings;
    normalData = resolveStringIndices(EMBEDDED_DATA, EMBEDDED_DATA.strings);
  } else {
    normalData = EMBEDDED_DATA;
  }

  // Initialize opcode mapping from embedded data
  initOpcodeMapping(EMBEDDED_DATA);

  // Inverted data will be built on first toggle
  invertedData = null;

  initThreadFilter(normalData);

  // Toggle legend based on differential mode
  const isDifferential = normalData && normalData.stats && normalData.stats.is_differential;
  const heatmapLegend = document.getElementById('heatmap-legend-section');
  const diffLegend = document.getElementById('diff-legend-section');
  if (isDifferential) {
    if (heatmapLegend) heatmapLegend.style.display = 'none';
    if (diffLegend) diffLegend.style.display = 'block';
  } else {
    if (heatmapLegend) heatmapLegend.style.display = 'block';
    if (diffLegend) diffLegend.style.display = 'none';
  }

  const tooltip = createPythonTooltip(normalData);
  const chart = createFlamegraph(tooltip, normalData.value, normalData);
  renderFlamegraph(chart, normalData);
  initSearchHandlers();
  initSidebarResize();
  handleResize();

  const toggleInvertBtn = document.getElementById('toggle-invert');
  if (toggleInvertBtn) {
    toggleInvertBtn.addEventListener('click', toggleInvert);
  }

  const togglePathDisplayBtn = document.getElementById('toggle-path-display');
  if (togglePathDisplayBtn) {
    togglePathDisplayBtn.addEventListener('click', togglePathDisplay);
  }
}

// Keyboard shortcut: Enter/Space activates toggle switches
document.addEventListener('keydown', function(e) {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
        return;
    }
    if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('toggle-switch')) {
        e.preventDefault();
        e.target.click();
    }
});

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