<script lang="ts">
  import { onMount } from "svelte";
  import * as THREE from "three";
  import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
  import { pressureColorPalettes } from "$lib/config/color-map";
  import type { PressureColorMapPreset } from "$lib/types/hud";

  interface ViewerStats {
    total: number;
    max: number;
    avg: number;
  }

  interface MatrixLayout {
    cellSpacing: number;
    boardWidth: number;
    boardDepth: number;
    boardPadding: number;
    gridSpan: number;
    gridDivisions: number;
    labelScale: number;
    labelFloatOffset: number;
  }

  export let pressureMatrix: number[] | null = null;
  export let matrixRows = 12;
  export let matrixCols = 7;
  export let rangeMin = 0;
  export let rangeMax = 16000;
  export let colorMapPreset: PressureColorMapPreset = "emerald";
  export let showStatsPanel = true;

  let viewerEl: HTMLDivElement | undefined;
  let canvasEl: HTMLCanvasElement | undefined;
  let overlayEl: HTMLCanvasElement | undefined;
  let stats: ViewerStats = { total: 0, max: 0, avg: 0 };

  const DEFAULT_RANGE_MAX = 16000;
  const BASE_MATRIX_SPAN = 24;
  const MATRIX_SPAN_GROWTH = 0.6;
  const MIN_MATRIX_SPAN = 24;
  const MAX_MATRIX_SPAN = 58;
  const MIN_CELL_SPACING = 0.52;
  const MAX_CELL_SPACING = 3.8;
  const MIN_BOARD_PADDING = 2.6;
  const MAX_BOARD_PADDING = 6.8;
  const MIN_GRID_DIVISIONS = 12;
  const MAX_GRID_DIVISIONS = 48;
  const MIN_LABEL_SCALE = 0.72;
  const MAX_LABEL_SCALE = 2.45;
  const MATRIX_OFFSET_Y = -2.4;
  const MATRIX_OFFSET_Z = 12;
  const HEIGHT_SCALE = 10.6;
  const BASE_HEIGHT = 0.12;
  const GLOW_START = 0.3;
  const SMOOTHING_SPEED = 8.2;
  const CAMERA_FOV = 36;
  const CAMERA_DISTANCE_MIN = 30;
  const CAMERA_DISTANCE_MAX = 122;
  const CAMERA_FIT_PADDING = 1.04;
  const CAMERA_ELEVATION_DEG = 64;
  const CAMERA_TARGET_X = 0;
  const CAMERA_TARGET_Y = MATRIX_OFFSET_Y + 0.2;
  const CAMERA_TARGET_Z = MATRIX_OFFSET_Z - 0.4;
  const MATRIX_ROTATION_Y = 0;

  const labelVector = new THREE.Vector3();
  $: resolvedColorPalette = pressureColorPalettes[colorMapPreset] ?? pressureColorPalettes.emerald;
  $: surfaceBaseColor = new THREE.Color(resolvedColorPalette.surfaceBase);
  $: surfaceLowColor = new THREE.Color(resolvedColorPalette.surfaceLow);
  $: surfaceMidColor = new THREE.Color(resolvedColorPalette.surfaceMid);
  $: surfaceHighColor = new THREE.Color(resolvedColorPalette.surfaceHigh);
  $: surfaceHotColor = new THREE.Color(resolvedColorPalette.surfaceHot);
  $: labelZeroColor = new THREE.Color(resolvedColorPalette.labelZero);
  $: labelLowColor = new THREE.Color(resolvedColorPalette.labelLow);
  $: labelMidColor = new THREE.Color(resolvedColorPalette.labelMid);
  $: labelHighColor = new THREE.Color(resolvedColorPalette.labelHigh);
  $: sceneClearColor = new THREE.Color(resolvedColorPalette.uiTheme.bg30);
  $: sceneBoardColor = new THREE.Color(resolvedColorPalette.uiTheme.bg20);
  $: sceneGridCenterColor = new THREE.Color(resolvedColorPalette.surfaceMid);
  $: sceneGridLineColor = new THREE.Color(resolvedColorPalette.uiTheme.bg20);
  $: sceneAmbientLightColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.textMainRgb);
  $: sceneKeyLightColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowAltRgb);
  $: sceneAccentLightColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowRgb);
  $: surfaceThemeAccentColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowRgb);
  $: labelThemeAccentColor = rgbTripletToThreeColor(resolvedColorPalette.uiTheme.glowRgb);
  $: labelHighlightCss = colorToCss(surfaceHotColor);
  $: viewerThemeStyle = [
    `--matrix-bg-10: ${resolvedColorPalette.uiTheme.bg10}`,
    `--matrix-bg-20: ${resolvedColorPalette.uiTheme.bg20}`,
    `--matrix-bg-30: ${resolvedColorPalette.uiTheme.bg30}`,
    `--matrix-text-main-rgb: ${resolvedColorPalette.uiTheme.textMainRgb}`,
    `--matrix-text-dim-rgb: ${resolvedColorPalette.uiTheme.textDimRgb}`,
    `--matrix-border-rgb: ${resolvedColorPalette.uiTheme.borderRgb}`,
    `--matrix-border-strong-rgb: ${resolvedColorPalette.uiTheme.borderStrongRgb}`,
    `--matrix-surface-rgb: ${resolvedColorPalette.uiTheme.surfaceRgb}`,
    `--matrix-surface-alt-rgb: ${resolvedColorPalette.uiTheme.surfaceAltRgb}`,
    `--matrix-surface-deep-rgb: ${resolvedColorPalette.uiTheme.surfaceDeepRgb}`,
    `--matrix-glow-rgb: ${resolvedColorPalette.uiTheme.glowRgb}`,
    `--matrix-glow-alt-rgb: ${resolvedColorPalette.uiTheme.glowAltRgb}`
  ].join("; ");

  let rendererRef: THREE.WebGLRenderer | null = null;
  let boardMaterialRef: THREE.MeshBasicMaterial | null = null;
  let gridRef: THREE.GridHelper | null = null;
  let gridMaterialRef: THREE.Material | THREE.Material[] | null = null;
  let ambientLightRef: THREE.AmbientLight | null = null;
  let dirLightRef: THREE.DirectionalLight | null = null;
  let sideLightRef: THREE.DirectionalLight | null = null;

  function sanitizeGridValue(value: number): number {
    return clamp(Math.round(Number.isFinite(value) ? value : 12), 1, 128);
  }

  function sanitizeRangePair(minValue: number, maxValue: number): { min: number; max: number } {
    const resolvedMin = Math.round(Number.isFinite(minValue) ? minValue : 0);
    const resolvedMax = Math.max(Math.round(Number.isFinite(maxValue) ? maxValue : DEFAULT_RANGE_MAX), resolvedMin + 1);
    return { min: resolvedMin, max: resolvedMax };
  }

  $: resolvedMatrixRows = sanitizeGridValue(matrixRows);
  $: resolvedMatrixCols = sanitizeGridValue(matrixCols);
  $: resolvedRange = sanitizeRangePair(rangeMin, rangeMax);
  $: resolvedRangeMin = resolvedRange.min;
  $: resolvedRangeMax = resolvedRange.max;
  $: matrixLayout = buildMatrixLayout(resolvedMatrixRows, resolvedMatrixCols);
  $: statsNote = `${resolvedMatrixRows}x${resolvedMatrixCols} / color range ${resolvedRangeMin}-${resolvedRangeMax} / label raw`;

  function clamp(value: number, min: number, max: number): number {
    return Math.min(max, Math.max(min, value));
  }

  function smoothstep(edge0: number, edge1: number, x: number): number {
    const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
    return t * t * (3 - 2 * t);
  }

  function normalizeRawValue(value: number, minValue: number, maxValue: number): number {
    return clamp((value - minValue) / Math.max(1, maxValue - minValue), 0, 1);
  }

  function rgbTripletToThreeColor(rgbTriplet: string): THREE.Color {
    return new THREE.Color(`rgb(${rgbTriplet.replace(/\s+/g, ", ")})`);
  }

  function surfaceColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
    const value = clamp(valueNormalized, 0, 1);
    let mapped: THREE.Color;

    if (value <= 0.45) {
      const t = smoothstep(0, 0.45, value);
      mapped = target.copy(surfaceBaseColor).lerp(surfaceLowColor, t);
    } else if (value <= 0.78) {
      const t = smoothstep(0.45, 0.78, value);
      mapped = target.copy(surfaceLowColor).lerp(surfaceMidColor, t);
    } else {
      const t = smoothstep(0.78, 1, value);
      mapped = target.copy(surfaceMidColor).lerp(surfaceHighColor, t);
    }

    const baseAccentStrength = (1 - smoothstep(0.12, 0.52, value)) * 0.34;
    const highlightStrength = smoothstep(0.82, 1, value) * 0.3;
    return mapped.lerp(surfaceThemeAccentColor, baseAccentStrength).lerp(surfaceHotColor, highlightStrength);
  }

  function glowColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
    const value = clamp(valueNormalized, 0, 1);
    const glowStrength = smoothstep(0.55, 1, value);
    return surfaceColorMap(value, target).lerp(surfaceHotColor, glowStrength * 0.42);
  }

  function labelColorMap(valueNormalized: number, target: THREE.Color = new THREE.Color()): THREE.Color {
    const value = clamp(valueNormalized, 0, 1);
    let mapped: THREE.Color;

    if (value <= 0.34) {
      const t = smoothstep(0, 0.34, value);
      mapped = target.copy(labelZeroColor).lerp(labelLowColor, t);
    } else if (value <= 0.76) {
      const t = smoothstep(0.34, 0.76, value);
      mapped = target.copy(labelLowColor).lerp(labelMidColor, t);
    } else {
      const t = smoothstep(0.76, 1, value);
      mapped = target.copy(labelMidColor).lerp(labelHighColor, t);
    }

    const baseAccentStrength = (1 - smoothstep(0.16, 0.58, value)) * 0.46;
    const highlightStrength = smoothstep(0.8, 1, value) * 0.36;
    return mapped.lerp(labelThemeAccentColor, baseAccentStrength).lerp(surfaceHotColor, highlightStrength);
  }

  function shapeHeightValue(valueNormalized: number): number {
    return Math.pow(clamp(valueNormalized, 0, 1), 0.9);
  }

  function shapeGlowStrength(valueNormalized: number): number {
    return smoothstep(GLOW_START, 1, Math.pow(clamp(valueNormalized, 0, 1), 0.82));
  }

  function buildMatrixLayout(rows: number, cols: number): MatrixLayout {
    const longestEdge = Math.max(rows, cols, 1);
    const edgeSpan = Math.max(longestEdge - 1, 1);
    const targetSpan = clamp(BASE_MATRIX_SPAN + edgeSpan * MATRIX_SPAN_GROWTH, MIN_MATRIX_SPAN, MAX_MATRIX_SPAN);
    const cellSpacing = clamp(targetSpan / edgeSpan, MIN_CELL_SPACING, MAX_CELL_SPACING);
    const boardWidth = Math.max(cols, 1) * cellSpacing;
    const boardDepth = Math.max(rows, 1) * cellSpacing;
    const boardPadding = clamp(cellSpacing * 1.62, MIN_BOARD_PADDING, MAX_BOARD_PADDING);
    const gridSpan = Math.max(boardWidth, boardDepth) + boardPadding * 2;
    const gridDivisions = Math.round(clamp(longestEdge * 1.6, MIN_GRID_DIVISIONS, MAX_GRID_DIVISIONS));
    const labelScale = clamp(24 / (longestEdge + 2), MIN_LABEL_SCALE, MAX_LABEL_SCALE);
    const labelFloatOffset = clamp(cellSpacing * 0.42, 0.36, 1.12);

    return {
      cellSpacing,
      boardWidth,
      boardDepth,
      boardPadding,
      gridSpan,
      gridDivisions,
      labelScale,
      labelFloatOffset
    };
  }

  function fitCameraDistance(boardWidth: number, boardDepth: number, boardPadding: number, viewportAspect: number): number {
    const paddedWidth = boardWidth + boardPadding * 2;
    const paddedDepth = boardDepth + boardPadding * 2;
    const safeAspect = Math.max(viewportAspect, 0.5);
    const effectiveHalfSpan = Math.max(paddedDepth * 0.5, (paddedWidth * 0.5) / safeAspect);
    const fovRadians = THREE.MathUtils.degToRad(CAMERA_FOV * 0.5);
    const fitDistance = (effectiveHalfSpan / Math.tan(fovRadians)) * CAMERA_FIT_PADDING;
    return clamp(fitDistance, CAMERA_DISTANCE_MIN, CAMERA_DISTANCE_MAX);
  }

  function normalizeField(source: Float32Array, target: Float32Array, minValue: number, maxValue: number): number {
    let max = 0;

    for (let index = 0; index < source.length; index += 1) {
      const value = source[index];
      target[index] = normalizeRawValue(value, minValue, maxValue);
      max = Math.max(max, value);
    }

    return max;
  }

  function copyExternalField(target: Float32Array, values: number[]): void {
    for (let index = 0; index < target.length; index += 1) {
      const value = Number(values[index] ?? 0);
      target[index] = Number.isFinite(value) ? value : 0;
    }
  }

  function compactDisplayValue(rawValue: number, minValue: number, maxValue: number): number {
    if (rawValue <= minValue + 4) {
      return 0;
    }

    return Math.round(rawValue);
  }

  function colorToCss(color: THREE.Color): string {
    return `rgb(${Math.round(color.r * 255)} ${Math.round(color.g * 255)} ${Math.round(color.b * 255)})`;
  }

  $: labelPalette = Array.from({ length: 33 }, (_, index) => {
    const t = index / 32;
    return colorToCss(labelColorMap(t, new THREE.Color()));
  });
  $: labelGlowPalette = Array.from({ length: 33 }, (_, index) => {
    const t = index / 32;
    return colorToCss(glowColorMap(t, new THREE.Color()));
  });

  function applyGridTheme(grid: THREE.GridHelper, divisions: number): void {
    const colorAttribute = grid.geometry.getAttribute("color");
    if (!(colorAttribute instanceof THREE.BufferAttribute)) {
      return;
    }

    for (let division = 0; division <= divisions; division += 1) {
      const lineColor = division === divisions / 2 ? sceneGridCenterColor : sceneGridLineColor;
      const vertexBase = division * 4;

      for (let vertexOffset = 0; vertexOffset < 4; vertexOffset += 1) {
        colorAttribute.setXYZ(vertexBase + vertexOffset, lineColor.r, lineColor.g, lineColor.b);
      }
    }

    colorAttribute.needsUpdate = true;
  }

  function applySceneTheme(): void {
    if (!rendererRef || !boardMaterialRef || !gridRef || !gridMaterialRef) {
      return;
    }

    rendererRef.setClearColor(sceneClearColor, 1);
    boardMaterialRef.color.copy(sceneBoardColor);
    boardMaterialRef.needsUpdate = true;
    applyGridTheme(gridRef, matrixLayout.gridDivisions);

    if (Array.isArray(gridMaterialRef)) {
      for (const material of gridMaterialRef) {
        material.transparent = true;
        material.opacity = 0.034;
        material.needsUpdate = true;
      }
    } else {
      gridMaterialRef.transparent = true;
      gridMaterialRef.opacity = 0.034;
      gridMaterialRef.needsUpdate = true;
    }

    ambientLightRef?.color.copy(sceneAmbientLightColor);
    dirLightRef?.color.copy(sceneKeyLightColor);
    sideLightRef?.color.copy(sceneAccentLightColor);
  }

  $: applySceneTheme();

  onMount(() => {
    if (!viewerEl || !canvasEl || !overlayEl) {
      return;
    }

    const gridRows = resolvedMatrixRows;
    const gridCols = resolvedMatrixCols;
    const { cellSpacing, boardWidth, boardDepth, boardPadding, gridSpan, gridDivisions, labelScale, labelFloatOffset } =
      matrixLayout;
    const instanceCount = gridRows * gridCols;

    const overlayContext = overlayEl.getContext("2d");
    if (!overlayContext) {
      return;
    }

    const renderer = new THREE.WebGLRenderer({
      canvas: canvasEl,
      antialias: true,
      alpha: true,
      powerPreference: "high-performance"
    });
    rendererRef = renderer;
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.setClearColor(sceneClearColor, 1);
    renderer.outputColorSpace = THREE.SRGBColorSpace;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(CAMERA_FOV, 1, 0.1, 500);
    const cameraElevation = THREE.MathUtils.degToRad(CAMERA_ELEVATION_DEG);
    const updateCameraPlacement = (viewportWidth: number, viewportHeight: number) => {
      const aspect = viewportWidth / Math.max(viewportHeight, 1);
      const cameraDistance = fitCameraDistance(boardWidth, boardDepth, boardPadding, aspect);
      const heightOffset = Math.sin(cameraElevation) * cameraDistance;
      const depthOffset = Math.cos(cameraElevation) * cameraDistance;
      camera.position.set(CAMERA_TARGET_X, CAMERA_TARGET_Y + heightOffset, CAMERA_TARGET_Z + depthOffset);
      camera.lookAt(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);
    };
    updateCameraPlacement(1, 1);
    camera.lookAt(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);

    const controls = new OrbitControls(camera, canvasEl);
    controls.enableRotate = false;
    controls.enableZoom = false;
    controls.enablePan = false;
    controls.enableDamping = false;
    controls.target.set(CAMERA_TARGET_X, CAMERA_TARGET_Y, CAMERA_TARGET_Z);
    controls.enabled = false;

    const ambientLight = new THREE.AmbientLight(sceneAmbientLightColor, 0.26);
    const dirLight = new THREE.DirectionalLight(sceneKeyLightColor, 0.34);
    dirLight.position.set(50, 100, 50);
    const sideLight = new THREE.DirectionalLight(sceneAccentLightColor, 0.16);
    sideLight.position.set(-50, 50, -50);
    ambientLightRef = ambientLight;
    dirLightRef = dirLight;
    sideLightRef = sideLight;
    scene.add(ambientLight, dirLight, sideLight);

    const matrixGroup = new THREE.Group();
    matrixGroup.position.set(0, MATRIX_OFFSET_Y, MATRIX_OFFSET_Z);
    matrixGroup.rotation.y = MATRIX_ROTATION_Y;
    scene.add(matrixGroup);

    const board = new THREE.Mesh(
      new THREE.PlaneGeometry(boardWidth + boardPadding * 2, boardDepth + boardPadding * 2),
      new THREE.MeshBasicMaterial({
        color: sceneBoardColor,
        transparent: true,
        opacity: 0.12,
        toneMapped: false
      })
    );
    boardMaterialRef = board.material;
    board.rotation.x = -Math.PI / 2;
    board.position.y = -0.04;
    matrixGroup.add(board);

    const grid = new THREE.GridHelper(gridSpan, gridDivisions, sceneGridCenterColor, sceneGridLineColor);
    gridRef = grid;
    grid.position.y = 0;
    const gridMaterial = grid.material;
    gridMaterialRef = gridMaterial;
    if (Array.isArray(gridMaterial)) {
      for (const material of gridMaterial) {
        material.transparent = true;
        material.opacity = 0.034;
      }
    } else {
      gridMaterial.transparent = true;
      gridMaterial.opacity = 0.034;
    }
    matrixGroup.add(grid);
    applySceneTheme();

    const cellX = new Float32Array(instanceCount);
    const cellZ = new Float32Array(instanceCount);
    for (let row = 0; row < gridRows; row += 1) {
      for (let col = 0; col < gridCols; col += 1) {
        const index = row * gridCols + col;
        cellX[index] = (col - gridCols / 2 + 0.5) * cellSpacing;
        cellZ[index] = (row - gridRows / 2 + 0.5) * cellSpacing;
      }
    }

    const targetField = new Float32Array(instanceCount);
    const smoothedField = new Float32Array(instanceCount);
    const normalizedField = new Float32Array(instanceCount);
    const heightField = new Float32Array(instanceCount);
    const compactField = new Uint16Array(instanceCount);
    let lastFrameAt = performance.now();

    const drawNumberOverlay = () => {
      if (!viewerEl || !overlayEl) {
        return;
      }

      const width = viewerEl.clientWidth;
      const height = viewerEl.clientHeight;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const fontSize = clamp((Math.min(width, height) / 66) * labelScale + cellSpacing * 1.1, 6.4, 26);

      overlayContext.setTransform(dpr, 0, 0, dpr, 0, 0);
      overlayContext.clearRect(0, 0, width, height);
      overlayContext.textAlign = "center";
      overlayContext.textBaseline = "middle";

      for (let index = 0; index < instanceCount; index += 1) {
        labelVector.set(cellX[index], heightField[index] + labelFloatOffset, cellZ[index]);
        labelVector.applyMatrix4(matrixGroup.matrixWorld);
        labelVector.project(camera);

        if (labelVector.z < -1 || labelVector.z > 1) {
          continue;
        }

        const screenX = (labelVector.x * 0.5 + 0.5) * width;
        const screenY = (-labelVector.y * 0.5 + 0.5) * height;

        if (screenX < -12 || screenX > width + 12 || screenY < -12 || screenY > height + 12) {
          continue;
        }

        const normalized = normalizedField[index];
        const displayValue = compactField[index];
        const displayText = String(displayValue);
        const digitCount = displayText.length;
        const digitScale = digitCount <= 2 ? 1 : digitCount === 3 ? 0.86 : digitCount === 4 ? 0.74 : 0.64;
        const bucket = Math.min(32, Math.round(normalized * 32));
        const baseGlyphSize = fontSize + smoothstep(0, 1, normalized) * (2.3 * labelScale + cellSpacing * 0.26);
        const glyphSize = clamp(baseGlyphSize * digitScale, 5.2, 26);
        const glowSizeFactor = digitCount >= 4 ? 0.76 : digitCount === 3 ? 0.88 : 1;
        const glowBlur = (4.8 + smoothstep(0.08, 1, normalized) * (10.4 * Math.max(0.72, labelScale))) * glowSizeFactor;

        overlayContext.font = `600 ${glyphSize.toFixed(1)}px "JetBrains Mono", "IBM Plex Mono", "Consolas", monospace`;
        overlayContext.shadowBlur = glowBlur;
        overlayContext.shadowColor = labelGlowPalette[bucket];

        overlayContext.fillStyle = labelPalette[bucket];
        overlayContext.globalAlpha = displayValue === 0 ? 0.86 : 1;
        overlayContext.fillText(displayText, screenX, screenY);

        if (normalized >= 0.8) {
          overlayContext.fillStyle = labelHighlightCss;
          overlayContext.globalAlpha = smoothstep(0.8, 1, normalized) * 0.34;
          overlayContext.fillText(displayText, screenX, screenY);
        }
      }

      overlayContext.globalAlpha = 1;
      overlayContext.shadowBlur = 0;
    };

    const resize = () => {
      if (!viewerEl || !overlayEl) {
        return;
      }

      const width = viewerEl.clientWidth;
      const height = viewerEl.clientHeight;
      const dpr = Math.min(window.devicePixelRatio || 1, 2);

      if (width <= 0 || height <= 0) {
        return;
      }

      renderer.setSize(width, height, false);
      updateCameraPlacement(width, height);
      camera.aspect = width / height;
      camera.updateProjectionMatrix();

      overlayEl.width = Math.round(width * dpr);
      overlayEl.height = Math.round(height * dpr);
      overlayEl.style.width = `${width}px`;
      overlayEl.style.height = `${height}px`;
    };

    resize();

    const resizeObserver = new ResizeObserver(() => {
      resize();
    });
    resizeObserver.observe(viewerEl);

    renderer.setAnimationLoop((timestamp: number) => {
      const deltaSeconds = Math.min((timestamp - lastFrameAt) / 1000, 0.06);
      lastFrameAt = timestamp;

      let shouldHardResetToZero = true;
      if (pressureMatrix && pressureMatrix.length > 0) {
        copyExternalField(targetField, pressureMatrix);
        for (let index = 0; index < instanceCount; index += 1) {
          if (targetField[index] > 0) {
            shouldHardResetToZero = false;
            break;
          }
        }
      } else {
        targetField.fill(0);
      }

      if (shouldHardResetToZero) {
        smoothedField.fill(0);
      }

      const smoothing = 1 - Math.exp(-deltaSeconds * SMOOTHING_SPEED);
      for (let index = 0; index < instanceCount; index += 1) {
        smoothedField[index] += (targetField[index] - smoothedField[index]) * smoothing;
      }

      const maxValue = normalizeField(smoothedField, normalizedField, resolvedRangeMin, resolvedRangeMax);
      let total = 0;
      let activeCount = 0;

      for (let index = 0; index < instanceCount; index += 1) {
        const normalized = normalizedField[index];
        const heightValue = shapeHeightValue(normalized);
        const height = BASE_HEIGHT + heightValue * HEIGHT_SCALE;

        heightField[index] = height;
        compactField[index] = compactDisplayValue(smoothedField[index], resolvedRangeMin, resolvedRangeMax);

        total += smoothedField[index];
        if (smoothedField[index] > 30) {
          activeCount += 1;
        }
      }

      renderer.render(scene, camera);
      drawNumberOverlay();

      stats = {
        total,
        max: maxValue,
        avg: activeCount > 0 ? total / activeCount : 0
      };
    });

    return () => {
      resizeObserver.disconnect();
      renderer.setAnimationLoop(null);
      controls.dispose();
      board.geometry.dispose();
      board.material.dispose();
      if (Array.isArray(gridMaterial)) {
        for (const item of gridMaterial) {
          item.dispose();
        }
      } else {
        gridMaterial.dispose();
      }
      renderer.dispose();
      rendererRef = null;
      boardMaterialRef = null;
      gridRef = null;
      gridMaterialRef = null;
      ambientLightRef = null;
      dirLightRef = null;
      sideLightRef = null;
    };
  });
</script>

<div class="viewer-root" bind:this={viewerEl} style={viewerThemeStyle}>
  <canvas class="viewer-canvas" bind:this={canvasEl} aria-label="Pressure Matrix Viewer"></canvas>
  <canvas class="viewer-overlay" bind:this={overlayEl} aria-hidden="true"></canvas>

  <div class="viewer-vignette" aria-hidden="true"></div>
  <div class="viewer-noise" aria-hidden="true"></div>

  {#if showStatsPanel}
    <div class="viewer-controls">
      <section class="stats-panel" aria-label="Pressure Summary">
        <p class="stats-label">Pressure Matrix</p>
        <div class="stats-grid">
          <article class="stats-card stats-card-wide">
            <span class="stats-key">Total Pressure</span>
            <strong class="stats-value">{stats.total.toFixed(0)}</strong>
          </article>
          <article class="stats-card">
            <span class="stats-key">Max</span>
            <strong class="stats-value">{stats.max.toFixed(0)}</strong>
          </article>
          <article class="stats-card">
            <span class="stats-key">Avg</span>
            <strong class="stats-value">{stats.avg.toFixed(0)}</strong>
          </article>
        </div>
        <p class="stats-note">{statsNote}</p>
      </section>
    </div>
  {/if}
</div>

<style>
  .viewer-root {
    position: absolute;
    inset: 0;
    overflow: hidden;
    background:
      radial-gradient(circle at 50% 58%, rgb(var(--matrix-glow-rgb) / 0.11), transparent 32%),
      radial-gradient(circle at 50% 12%, rgb(var(--matrix-glow-alt-rgb) / 0.09), transparent 26%),
      linear-gradient(180deg, color-mix(in srgb, var(--matrix-bg-10) 84%, transparent), color-mix(in srgb, var(--matrix-bg-30) 96%, black 4%));
  }

  .viewer-canvas,
  .viewer-overlay {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    display: block;
  }

  .viewer-overlay {
    pointer-events: none;
    z-index: 1;
  }

  .viewer-vignette,
  .viewer-noise {
    position: absolute;
    inset: 0;
    pointer-events: none;
  }

  .viewer-vignette {
    background: radial-gradient(circle at center, transparent 54%, rgb(0 0 0 / 0.18) 100%);
  }

  .viewer-noise {
    background:
      repeating-linear-gradient(
        180deg,
        rgb(var(--matrix-glow-alt-rgb) / 0.025) 0,
        rgb(var(--matrix-glow-alt-rgb) / 0.025) 1px,
        transparent 1px,
        transparent 3px
      );
  }

  .viewer-controls {
    position: absolute;
    top: clamp(4.8rem, 10vh, 6.2rem);
    left: clamp(2.6rem, 4vw, 3.4rem);
    display: grid;
    gap: 0.75rem;
    z-index: 2;
    max-inline-size: min(18rem, 32vw);
  }

  .stats-panel {
    display: grid;
    gap: 0.58rem;
    padding: 0.74rem 0.84rem 0.82rem;
    border: 1px solid rgb(var(--matrix-border-rgb) / 0.32);
    border-radius: 0.76rem;
    background: linear-gradient(180deg, rgb(var(--matrix-surface-alt-rgb) / 0.92), rgb(var(--matrix-surface-deep-rgb) / 0.86));
    box-shadow:
      inset 0 1px 0 rgb(var(--matrix-border-strong-rgb) / 0.08),
      0 0 24px rgb(var(--matrix-glow-rgb) / 0.08);
  }

  .stats-label,
  .stats-key,
  .stats-note {
    margin: 0;
    color: rgb(var(--matrix-text-dim-rgb) / 0.84);
    font-size: 0.58rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
  }

  .stats-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.46rem;
  }

  .stats-card {
    display: grid;
    gap: 0.24rem;
    min-height: 4.2rem;
    padding: 0.58rem 0.64rem;
    border: 1px solid rgb(var(--matrix-border-rgb) / 0.24);
    border-radius: 0.56rem;
    background: linear-gradient(180deg, rgb(var(--matrix-surface-rgb) / 0.9), rgb(var(--matrix-surface-deep-rgb) / 0.86));
  }

  .stats-card-wide {
    grid-column: 1 / -1;
  }

  .stats-value {
    color: rgb(var(--matrix-text-main-rgb) / 0.98);
    font-size: clamp(1.16rem, 1vw + 0.82rem, 1.56rem);
    line-height: 1;
    font-weight: 600;
  }

  @media (max-width: 960px) {
    .viewer-controls {
      left: clamp(1rem, 2.4vw, 1.4rem);
      max-inline-size: min(13.5rem, 42vw);
    }

    .stats-grid {
      grid-template-columns: 1fr;
    }

    .stats-card-wide {
      grid-column: auto;
    }
  }

  @media (max-height: 760px) {
    .viewer-controls {
      top: clamp(4rem, 8vh, 4.8rem);
    }

    .stats-panel {
      padding: 0.62rem 0.7rem;
    }

    .stats-card {
      min-height: 3.6rem;
    }
  }
</style>
