import * as THREE from "three";
import { formatBytes } from "./viewerMath";
import type {
  NetworkStats,
  PerformanceStats,
} from "./viewerTypes";

export function updateNetworkPanel(networkPanelEl: HTMLDivElement, networkStats: NetworkStats) {
  networkPanelEl.textContent = describeNetworkPanel(networkStats);
}

export function describeNetworkPanel(networkStats: NetworkStats) {
  const now = performance.now();
  const uptimeSeconds = networkStats.streamOpenedAtMs
    ? (now - networkStats.streamOpenedAtMs) / 1000
    : 0;
  const recentBytes = networkStats.throughputSamples.reduce((sum, sample) => sum + sample.bytes, 0);
  const recentWindowSeconds = networkStats.throughputSamples.length > 1
    ? Math.max((now - networkStats.throughputSamples[0].atMs) / 1000, 1)
    : 1;
  const kbPerSecond = recentBytes / 1024 / recentWindowSeconds;
  const averageDeltaBytes = networkStats.deltasReceived > 0
    ? networkStats.deltaBytes / networkStats.deltasReceived
    : 0;
  const secondsSinceLastDelta = networkStats.lastDeltaAtMs
    ? ((now - networkStats.lastDeltaAtMs) / 1000).toFixed(1)
    : "n/a";

  return [
    `snapshot:    ${formatBytes(networkStats.snapshotBytes)}`,
    `stream:      ${networkStats.streamConnected ? "live" : "offline"}`,
    `deltas:      ${networkStats.deltasReceived}`,
    `events:      ${networkStats.eventsReceived}`,
    `avg delta:   ${formatBytes(averageDeltaBytes)}`,
    `last delta:  ${formatBytes(networkStats.lastDeltaBytes)}`,
    `recent rate: ${kbPerSecond.toFixed(1)} KB/s`,
    `changed:     ${networkStats.lastEntityChanges}`,
    `uptime:      ${uptimeSeconds.toFixed(1)}s`,
    `last packet: ${secondsSinceLastDelta}s`,
  ].join("\n");
}

export function summarizeNetworkStats(networkStats: NetworkStats): string {
  const kbPerSecond = estimateDownlinkKbPerSecond(networkStats);
  const direction = networkStats.streamConnected ? "live" : "offline";
  return `${direction} | down ${kbPerSecond.toFixed(1)} KB/s | ${networkStats.deltasReceived} d`;
}

export function estimateDownlinkKbPerSecond(networkStats: NetworkStats): number {
  const now = performance.now();
  const recentBytes = networkStats.throughputSamples.reduce((sum, sample) => sum + sample.bytes, 0);
  const recentWindowSeconds = networkStats.throughputSamples.length > 1
    ? Math.max((now - networkStats.throughputSamples[0].atMs) / 1000, 1)
    : 1;
  return recentBytes / 1024 / recentWindowSeconds;
}

export function recordPerformanceStats(performanceStats: PerformanceStats, frameMs: number) {
  const now = performance.now();
  performanceStats.lastFrameMs = frameMs;
  performanceStats.frameSamples.push({ atMs: now, frameMs });
  const cutoff = now - 4000;
  performanceStats.frameSamples = performanceStats.frameSamples.filter((sample) => sample.atMs >= cutoff);
}

export function updatePerformancePanel(
  performancePanelEl: HTMLDivElement,
  performanceStats: PerformanceStats,
  renderer: THREE.WebGLRenderer,
) {
  const text = describePerformancePanel(performanceStats, renderer);
  if (text) {
    performancePanelEl.textContent = text;
  }
}

export function describePerformancePanel(
  performanceStats: PerformanceStats,
  renderer: THREE.WebGLRenderer,
) {
  const now = performance.now();
  if (
    performanceStats.lastPanelUpdateAtMs > 0 &&
    now - performanceStats.lastPanelUpdateAtMs < 250
  ) {
    return undefined;
  }

  const samples = performanceStats.frameSamples;
  const elapsedWindowSeconds = samples.length > 1
    ? Math.max((samples[samples.length - 1].atMs - samples[0].atMs) / 1000, 0.25)
    : 1;
  const averageFrameMs = samples.length > 0
    ? samples.reduce((sum, sample) => sum + sample.frameMs, 0) / samples.length
    : 0;
  const worstFrameMs = samples.length > 0
    ? samples.reduce((max, sample) => Math.max(max, sample.frameMs), 0)
    : 0;
  const fps = samples.length > 1
    ? (samples.length - 1) / elapsedWindowSeconds
    : 0;
  const recentLowFps = averageFrameMs > 0 ? 1000 / Math.max(worstFrameMs, averageFrameMs) : 0;
  const renderInfo = renderer.info;

  performanceStats.lastPanelUpdateAtMs = now;
  return [
    `fps:         ${fps.toFixed(1)}`,
    `frame avg:   ${averageFrameMs.toFixed(2)} ms`,
    `frame last:  ${performanceStats.lastFrameMs.toFixed(2)} ms`,
    `frame worst: ${worstFrameMs.toFixed(2)} ms`,
    `recent low:  ${recentLowFps.toFixed(1)}`,
    `draw calls:  ${renderInfo.render.calls}`,
    `triangles:   ${renderInfo.render.triangles}`,
    `points:      ${renderInfo.render.points}`,
    `lines:       ${renderInfo.render.lines}`,
    `geometries:  ${renderInfo.memory.geometries}`,
    `textures:    ${renderInfo.memory.textures}`,
    `pixel ratio: ${renderer.getPixelRatio().toFixed(2)}`,
  ].join("\n");
}

export function summarizePerformanceStats(performanceStats: PerformanceStats): string {
  const fps = estimateFps(performanceStats);
  return `FPS ${fps.toFixed(1)} | ${performanceStats.lastFrameMs.toFixed(1)} ms`;
}

export function estimateFps(performanceStats: PerformanceStats): number {
  const samples = performanceStats.frameSamples;
  const elapsedWindowSeconds = samples.length > 1
    ? Math.max((samples[samples.length - 1].atMs - samples[0].atMs) / 1000, 0.25)
    : 1;
  return samples.length > 1
    ? (samples.length - 1) / elapsedWindowSeconds
    : 0;
}

export function describeCompactStatsLine(networkStats: NetworkStats, performanceStats: PerformanceStats): string {
  const onlineLabel = networkStats.streamConnected ? "Online" : "Offline";
  const fps = estimateFps(performanceStats);
  const down = estimateDownlinkKbPerSecond(networkStats);
  return `${onlineLabel} | FPS ${fps.toFixed(1)} | Down ${down.toFixed(1)} KB/s`;
}
