import { useEffect, useRef, useCallback, useState } from "react";
import "@kitware/vtk.js/Rendering/Profiles/Volume";
import "@kitware/vtk.js/Rendering/Profiles/Geometry";
import "@kitware/vtk.js/Rendering/Misc/RenderingAPIs";

import vtkBoundingBox from "@kitware/vtk.js/Common/DataModel/BoundingBox";
import vtkColorTransferFunction from "@kitware/vtk.js/Rendering/Core/ColorTransferFunction";
import vtkImageResliceMapper from "@kitware/vtk.js/Rendering/Core/ImageResliceMapper";
import vtkImageSlice from "@kitware/vtk.js/Rendering/Core/ImageSlice";
import vtkInteractorStyleImage from "@kitware/vtk.js/Interaction/Style/InteractorStyleImage";
import vtkInteractorStyleTrackballCamera from "@kitware/vtk.js/Interaction/Style/InteractorStyleTrackballCamera";
import vtkPiecewiseFunction from "@kitware/vtk.js/Common/DataModel/PiecewiseFunction";
import vtkPlane from "@kitware/vtk.js/Common/DataModel/Plane";
import vtkRenderWindow from "@kitware/vtk.js/Rendering/Core/RenderWindow";
import vtkRenderWindowInteractor from "@kitware/vtk.js/Rendering/Core/RenderWindowInteractor";
import vtkRenderer from "@kitware/vtk.js/Rendering/Core/Renderer";
import vtkVolume from "@kitware/vtk.js/Rendering/Core/Volume";
import vtkVolumeMapper from "@kitware/vtk.js/Rendering/Core/VolumeMapper";
import vtkOpenGLRenderWindow from "@kitware/vtk.js/Rendering/OpenGL/RenderWindow";
// vtk.js ships no .d.ts for this filter (unlike Mapper/Actor) — treat as any.
// @ts-expect-error no type declarations for ImageMarchingCubes
import vtkImageMarchingCubes from "@kitware/vtk.js/Filters/General/ImageMarchingCubes";
import vtkMapper from "@kitware/vtk.js/Rendering/Core/Mapper";
import vtkActor from "@kitware/vtk.js/Rendering/Core/Actor";

// Ensure these paths match your project structure
import type { NiftiData } from "./types";
import type { ViewOrientation, Active2DView } from "./ViewRotationControls";
import type { BoundingBox, SegmentationMask } from "./types";
import type { OrganMaskData } from "./types";
import type { Annotation, AnnotationTool } from "./types";
import { AnnotationOverlay } from "./AnnotationOverlay";

export interface NiftiQuadViewRendererProps {
  niftiData: NiftiData | null;
  windowWidth: number;
  windowLevel: number;
  opacity: number;
  isLoading?: boolean;
  onRotate3D?: (orientation: ViewOrientation) => void;
  active2DView?: Active2DView;
  currentOrientation?: ViewOrientation;
  segmentationEnabled?: boolean;
  boundingBox?: BoundingBox | null;
  onBoundingBoxChange?: (box: BoundingBox | null) => void;
  currentSliceIndex?: number;
  onSliceIndexChange?: (index: number) => void;
  segmentationMask?: SegmentationMask | null;
  // Organ mask overlays
  organMasks?: OrganMaskData[];
  // Client-side annotation tools (bbox / points / pen / brush / polygon)
  annotationTool?: AnnotationTool;
  annotations?: Annotation[];
  onAnnotationsChange?: (annotations: Annotation[]) => void;
}

interface ViewportContainer {
  element: HTMLDivElement;
  id: number;
  label: string;
}

type ExpandedView = null | 0 | 1 | 2 | 3;

export const NiftiQuadViewRenderer = ({
  niftiData,
  windowWidth,
  windowLevel,
  opacity,
  isLoading = false,
  onRotate3D,
  segmentationEnabled = false,
  boundingBox,
  onBoundingBoxChange,
  currentSliceIndex = 0,
  onSliceIndexChange,
  segmentationMask,
  organMasks = [],
  annotationTool = "none",
  annotations = [],
  onAnnotationsChange,
}: NiftiQuadViewRendererProps) => {
  const containerRef = useRef<HTMLDivElement>(null);
  const [expandedView, setExpandedView] = useState<ExpandedView>(null);
  
  // Slice index state for each 2D view: [axial, coronal, sagittal]
  const [sliceIndices, setSliceIndices] = useState<[number, number, number]>([0, 0, 0]);
  const [totalSlices, setTotalSlices] = useState<[number, number, number]>([1, 1, 1]);
  
  // Segmentation drawing state
  const [activeDrawingView, setActiveDrawingView] = useState<0 | 1 | 2 | null>(null);
  const [isDrawing, setIsDrawing] = useState(false);
  const [startPoint, setStartPoint] = useState<{ x: number; y: number } | null>(null);
  const [tempBox, setTempBox] = useState<{ x: number; y: number; width: number; height: number } | null>(null);
  
  // Track mouse button state to prevent interactor rebinding during drag
  // (From Snippet 1)
  const isMouseDownRef = useRef(false);
  const activeViewportRef = useRef<HTMLElement | null>(null);

  // Context ref stores all VTK instances
  const contextRef = useRef<{
    renderWindow: any;
    renderWindowView: any;
    interactor: any;
    renderers: any[];
    containers: ViewportContainer[];
    imageSliceActors: any[];
    slicePlanes: any[];
    volumeActor: any;
    ctf: any;
    pf: any;
    iStyle: any;
    tStyle: any;
    // Organ mask overlays: a 3D surface (actor/mapper/ctf=MC-filter) + the 2D slice
    // reslice actors (one per axial/coronal/sagittal pane) in sliceActors.
    organMaskActors: Map<string, { actor: any; mapper: any; ctf: any; pf: any; sliceActors?: any[] }>;
  } | null>(null);
  
  const loadedDataRef = useRef<any>(null);

  // --------------------------------------------------------------------------
  // Helper: Resize Viewports
  // --------------------------------------------------------------------------
  const resizeViewportContainer = useCallback((
    _view: HTMLElement,
    ren: any,
    element: HTMLElement
  ) => {
    const vp = ren.getViewport();
    const border = 2;
    // Position the frame as PERCENTAGES of the container. The WebGL canvas fills the
    // container (style width:100%), so a normalized VTK viewport [vp0,vp1,vp2,vp3] maps
    // to the same %-rect and lines up with the rendered content. Computing px from
    // getBoundingClientRect() was fragile: the viewer opens in a dialog that animates
    // with transform: scale(.95→1), and ResizeObserver does NOT fire on transform
    // changes — so a rect captured mid-zoom froze the frame ~5% small/offset while the
    // canvas stretched to fill, leaving the slice spilling past the top/right borders.
    // Percentages track the canvas under any transform or resize. (box-sizing:border-box
    // is set on the element, so the 2px frame is drawn inside the viewport rect.)
    element.style.position = "absolute";
    element.style.left = `${vp[0] * 100}%`;
    element.style.bottom = `${vp[1] * 100}%`;
    element.style.width = `${(vp[2] - vp[0]) * 100}%`;
    element.style.height = `${(vp[3] - vp[1]) * 100}%`;
    element.style.border = `solid ${border}px hsl(var(--border))`;
    element.style.borderRadius = "6px";
  }, []);

  // --------------------------------------------------------------------------
  // Helper: Bind Interactor Styles
  // --------------------------------------------------------------------------
  const bindInteractor = useCallback((interactor: any, el: HTMLElement | null) => {
    if (!contextRef.current || !containerRef.current) return;
    const { iStyle, tStyle } = contextRef.current;
    const full = containerRef.current;

    // Switch the interaction STYLE based on the active pane (3D → trackball camera,
    // 2D → image), but ALWAYS bind events to the full canvas container — never a
    // per-pane overlay. VTK's findPokedRenderer resolves the cursor to a viewport
    // in full-canvas coordinates; binding to a sub-region fed it offset coordinates,
    // so dragging inside the 3D pane manipulated the adjacent 2D renderers. With
    // full-canvas binding each drag targets only the renderer under the cursor.
    if (el) {
      interactor.setInteractorStyle(el.dataset.viewId === "3" ? tStyle : iStyle);
    }
    if (interactor.getContainer() !== full) {
      if (interactor.getContainer()) {
        interactor.unbindEvents();
      }
      interactor.bindEvents(full);
    }
  }, []);

  // While a drawing tool is active, disable VTK mouse interaction entirely so a
  // draw drag never also pans / window-levels / rotates a view (2D panes must not
  // rotate). Re-bind to the full canvas when back to "Chọn" (none).
  useEffect(() => {
    const interactor = contextRef.current?.interactor;
    if (!interactor) return;
    const active = annotationTool !== "none";
    try {
      if (active) {
        interactor.disable?.();
        if (interactor.getContainer?.()) interactor.unbindEvents();
      } else {
        interactor.enable?.();
        if (containerRef.current && interactor.getContainer?.() !== containerRef.current) {
          interactor.bindEvents(containerRef.current);
        }
      }
    } catch {
      /* noop */
    }
  }, [annotationTool]);

  // --------------------------------------------------------------------------
  // Helper: 3D Rotation Logic
  // --------------------------------------------------------------------------
  const rotate3DView = useCallback((orientation: ViewOrientation) => {
    if (!contextRef.current || !niftiData?.imageData) return;
    
    const { renderers, renderWindow } = contextRef.current;
    const bounds = niftiData.imageData.getBounds();
    const center = vtkBoundingBox.getCenter(bounds);
    const diagonal = vtkBoundingBox.getDiagonalLength(bounds);
    const distance = (diagonal ?? 0) * 1.5;
    
    // Renderer index 3 is the 3D view
    const cam3d = renderers[3].getActiveCamera();
    
    // Set camera position and orientation based on medical standard views
    switch (orientation) {
      case "anterior":
        cam3d.setPosition(center[0], center[1] - distance, center[2]);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, 0, 1);
        break;
      case "posterior":
        cam3d.setPosition(center[0], center[1] + distance, center[2]);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, 0, 1);
        break;
      case "left-lateral":
        cam3d.setPosition(center[0] - distance, center[1], center[2]);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, 0, 1);
        break;
      case "right-lateral":
        cam3d.setPosition(center[0] + distance, center[1], center[2]);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, 0, 1);
        break;
      case "superior":
        cam3d.setPosition(center[0], center[1], center[2] + distance);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, 1, 0);
        break;
      case "inferior":
        cam3d.setPosition(center[0], center[1], center[2] - distance);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, -1, 0);
        break;
      default:
        // Default view (anterior)
        cam3d.setPosition(center[0], center[1] - distance, center[2]);
        cam3d.setFocalPoint(...center);
        cam3d.setViewUp(0, 0, 1);
    }
    
    renderWindow.render();
    onRotate3D?.(orientation);
  }, [niftiData, onRotate3D]);

  // Expose rotation function via ref for parent components
  useEffect(() => {
    if (contextRef.current) {
      (contextRef.current as any).rotate3D = rotate3DView;
    }
  }, [rotate3DView]);

  // Handle external rotation requests
  useEffect(() => {
    if (!onRotate3D || !contextRef.current) return;
    
    const handleRotate = (orientation: ViewOrientation) => {
      // If purely 2D strings are passed (optional handling), we can ignore or map them.
      if (orientation === "axial" || orientation === "coronal" || orientation === "sagittal") {
        return;
      }
      // Handle 3D rotations
      rotate3DView(orientation);
    };

    (contextRef.current as any).handleRotate = handleRotate;
  }, [onRotate3D, rotate3DView]);

  // --------------------------------------------------------------------------
  // Layout Logic
  // --------------------------------------------------------------------------
  const getViewports = useCallback((expanded: ExpandedView): [number, number, number, number][] => {
    if (expanded === null) {
      // 2x2 grid
      return [
        [0.01, 0.51, 0.49, 0.99],  // top-left (Axial)
        [0.51, 0.51, 0.99, 0.99], // top-right (Coronal)
        [0.01, 0.01, 0.49, 0.49],  // bottom-left (Sagittal)
        [0.51, 0.01, 0.99, 0.49], // bottom-right (3D)
      ];
    } else {
      // Expanded layout: main view large on left, others stacked on right
      const mainViewport: [number, number, number, number] = [0.01, 0.01, 0.74, 0.99];
      const sideViewports: [number, number, number, number][] = [
        [0.75, 0.67, 0.99, 0.99],  // top
        [0.75, 0.34, 0.99, 0.66],  // middle
        [0.75, 0.01, 0.99, 0.33],  // bottom
      ];
      
      const result: [number, number, number, number][] = [];
      let sideIndex = 0;
      for (let i = 0; i < 4; i++) {
        if (i === expanded) {
          result.push(mainViewport);
        } else {
          result.push(sideViewports[sideIndex]);
          sideIndex++;
        }
      }
      return result;
    }
  }, []);

  const resize = useCallback(() => {
    if (!containerRef.current || !contextRef.current) return;
    
    const rect = containerRef.current.getBoundingClientRect();
    const { renderWindowView, renderers, containers, renderWindow } = contextRef.current;
    
    renderWindowView.setSize(rect.width, rect.height);
    
    containers.forEach((c) => {
      resizeViewportContainer(containerRef.current!, renderers[c.id], c.element);
    });
    
    renderWindow.render();
  }, [resizeViewportContainer]);

  // Update viewports when expanded state changes
  useEffect(() => {
    if (!contextRef.current) return;
    
    const { renderers, containers, renderWindow, renderWindowView } = contextRef.current;
    const viewports = getViewports(expandedView);
    
    renderers.forEach((ren, i) => {
      ren.setViewport(viewports[i][0], viewports[i][1], viewports[i][2], viewports[i][3]);
    });
    
    if (containerRef.current) {
      const rect = containerRef.current.getBoundingClientRect();
      renderWindowView.setSize(rect.width, rect.height);
      
      containers.forEach((c) => {
        resizeViewportContainer(containerRef.current!, renderers[c.id], c.element);
      });
    }

    // Re-fit every view to its new viewport so the slice / volume FILLS the resized pane.
    // Without this an expanded pane keeps the small framing it had as a 2×2 quadrant
    // (content stuck tiny in a corner). resetCamera preserves each camera's direction +
    // view-up — so orientation and any 3D rotation are kept — and only re-centers + rescales.
    renderers.forEach((ren) => ren.resetCamera());

    renderWindow.render();
  }, [expandedView, getViewports, resizeViewportContainer]);

  // --------------------------------------------------------------------------
  // Initialization (VTK Setup)
  // --------------------------------------------------------------------------
  useEffect(() => {
    if (!containerRef.current) return;

    const renderWindow = vtkRenderWindow.newInstance();
    const renderWindowView = vtkOpenGLRenderWindow.newInstance();
    
    const rect = containerRef.current.getBoundingClientRect();
    renderWindowView.setSize(rect.width, rect.height);
    renderWindow.addView(renderWindowView);
    renderWindowView.setContainer(containerRef.current);

    const iStyle = vtkInteractorStyleImage.newInstance();
    const tStyle = vtkInteractorStyleTrackballCamera.newInstance();

    const interactor = vtkRenderWindowInteractor.newInstance();
    interactor.setView(renderWindowView);
    interactor.initialize();
    interactor.setInteractorStyle(tStyle);

    const renderers: any[] = [];
    const containers: ViewportContainer[] = [];
    const labels = ["AXIAL", "CORONAL", "SAGITTAL", "3D"];

    // Create 4 viewports
    const viewports = getViewports(null);

    // Pin / unpin all VTK interaction to a single pane for the lifetime of a drag.
    // VTK re-resolves the "poked" renderer on EVERY mouse-move (findPokedRenderer),
    // and it skips renderers whose getInteractive() is false — so marking the other
    // panes non-interactive while a drag is in flight keeps the gesture on the pane
    // it started in (e.g. a 3D-pane rotate never leaks into axial/coronal/sagittal).
    const confineInteractionTo = (activeId: number) => {
      renderers.forEach((ren, idx) => ren.setInteractive(idx === activeId));
    };
    const releaseInteraction = () => {
      renderers.forEach((ren) => ren.setInteractive(true));
    };

    for (let i = 0; i < 4; i++) {
      // Black background for 2D views, dark gray/blue tint for 3D
      const bgColor: [number, number, number] = i === 3 ? [0.1, 0.1, 0.15] : [0, 0, 0];
      const ren = vtkRenderer.newInstance({ background: bgColor });
      ren.setViewport(viewports[i][0], viewports[i][1], viewports[i][2], viewports[i][3]);

      const container = document.createElement("div");
      container.dataset.viewId = String(i);
      container.style.position = "absolute";
      container.style.boxSizing = "border-box";
      container.style.overflow = "hidden";
      container.style.borderRadius = "6px";
      
      containerRef.current.appendChild(container);

      // Add label
      const label = document.createElement("div");
      label.textContent = labels[i];
      label.style.position = "absolute";
      label.style.top = "8px";
      label.style.left = "8px";
      label.style.color = "rgba(255, 255, 255, 0.7)";
      label.style.fontSize = "11px";
      label.style.fontWeight = "600";
      label.style.letterSpacing = "0.05em";
      label.style.textTransform = "uppercase";
      label.style.zIndex = "10";
      label.style.pointerEvents = "none";
      label.style.textShadow = "0 1px 2px rgba(0,0,0,0.8)";
      container.appendChild(label);
      
      // Add slice info label for 2D views (bottom-left)
      if (i < 3) {
        const sliceLabel = document.createElement("div");
        sliceLabel.id = `slice-info-${i}`;
        sliceLabel.style.position = "absolute";
        sliceLabel.style.bottom = "8px";
        sliceLabel.style.left = "8px";
        sliceLabel.style.color = "rgba(255, 255, 255, 0.8)";
        sliceLabel.style.fontSize = "11px";
        sliceLabel.style.fontWeight = "500";
        sliceLabel.style.zIndex = "10";
        sliceLabel.style.pointerEvents = "none";
        sliceLabel.style.textShadow = "0 1px 2px rgba(0,0,0,0.8)";
        sliceLabel.style.fontFamily = "monospace";
        sliceLabel.textContent = "Slice: -/-";
        container.appendChild(sliceLabel);
      }

      // --- INTEGRATED EVENT HANDLING LOGIC START ---
      // Handle mouse down - lock interactor to this viewport
      container.addEventListener("mousedown", (e) => {
        isMouseDownRef.current = true;
        activeViewportRef.current = container;
        confineInteractionTo(Number(container.dataset.viewId));
        bindInteractor(interactor, container);
        // Toggle expand on double-click — detected HERE via e.detail===2 (the 2nd press of
        // a double-click) rather than a "dblclick" listener, because VTK's interactor takes
        // pointer capture on press, so the native click/dblclick land on the parent canvas
        // container, NOT this per-pane div. mousedown still fires on the div, so this is the
        // only place a per-pane double-click is observable.
        if (e.detail === 2) {
          const viewId = Number(container.dataset.viewId);
          setExpandedView((prev) => (prev === viewId ? null : (viewId as ExpandedView)));
        }
      });

      // Handle mouse up - unlock interactor
      container.addEventListener("mouseup", () => {
        isMouseDownRef.current = false;
        activeViewportRef.current = null;
        releaseInteraction();
      });

      // Handle mouse leave - only unbind if not dragging
      container.addEventListener("mouseleave", () => {
        if (!isMouseDownRef.current) {
          bindInteractor(interactor, null);
        }
      });

      // Handle pointer enter - only bind if not dragging and no active viewport
      container.addEventListener("pointerenter", () => {
        if (!isMouseDownRef.current && !activeViewportRef.current) {
          bindInteractor(interactor, container);
        }
      });

      // Handle pointer leave - only unbind if not dragging
      container.addEventListener("pointerleave", () => {
        if (!isMouseDownRef.current) {
          bindInteractor(interactor, null);
        }
      });

      // --- INTEGRATED EVENT HANDLING LOGIC END ---

      renderWindow.addRenderer(ren);
      renderers.push(ren);
      containers.push({ element: container, id: i, label: labels[i] });
      
      resizeViewportContainer(containerRef.current, ren, container);
    }

    // Handle global mouse up to catch releases outside viewports (From Snippet 1)
    const handleGlobalMouseUp = () => {
      isMouseDownRef.current = false;
      activeViewportRef.current = null;
      releaseInteraction();
    };
    window.addEventListener("mouseup", handleGlobalMouseUp);

    // Create slice mappers and planes
    const slicePlanes: any[] = [];
    const imageSliceActors: any[] = [];

    // Axial plane (Z normal)
    const axialPlane = vtkPlane.newInstance();
    axialPlane.setNormal(0, 0, 1);
    slicePlanes.push(axialPlane);

    // Coronal plane (Y normal)
    const coronalPlane = vtkPlane.newInstance();
    coronalPlane.setNormal(0, 1, 0);
    slicePlanes.push(coronalPlane);

    // Sagittal plane (X normal)
    const sagittalPlane = vtkPlane.newInstance();
    sagittalPlane.setNormal(1, 0, 0);
    slicePlanes.push(sagittalPlane);

    // Create 2D slice actors
    for (let i = 0; i < 3; i++) {
      const mapper = vtkImageResliceMapper.newInstance();
      mapper.setSlicePlane(slicePlanes[i]);

      const actor = vtkImageSlice.newInstance();
      actor.setMapper(mapper);

      const sliceCtf = vtkColorTransferFunction.newInstance();
      sliceCtf.addRGBPoint(0, 0, 0, 0);
      sliceCtf.addRGBPoint(1, 1, 1, 1);

      actor.getProperty().setRGBTransferFunction(0, sliceCtf);
      actor.getProperty().setColorWindow(windowWidth);
      actor.getProperty().setColorLevel(windowLevel);

      renderers[i].addActor(actor);

      const cam = renderers[i].getActiveCamera();
      cam.setParallelProjection(true);
      // Camera positioning is handled in the data loading effect

      imageSliceActors.push({ actor, mapper, ctf: sliceCtf });
    }

    // Volume actor for 3D view
    const ctf = vtkColorTransferFunction.newInstance();
    // Default transfer function points (will be updated)
    ctf.addRGBPoint(0, 0, 0, 0);
    ctf.addRGBPoint(255, 1, 1, 1);

    const pf = vtkPiecewiseFunction.newInstance();
    pf.addPoint(0, 0.0);
    pf.addPoint(255, 1.0);

    const volumeActor = vtkVolume.newInstance();
    const volumeMapper = vtkVolumeMapper.newInstance({ sampleDistance: 1.0 });
    
    volumeActor.setMapper(volumeMapper);
    volumeActor.getProperty().setRGBTransferFunction(0, ctf);
    volumeActor.getProperty().setScalarOpacity(0, pf);
    volumeActor.getProperty().setInterpolationTypeToLinear();
    volumeActor.getProperty().setShade(true);
    volumeActor.getProperty().setAmbient(0.2);
    volumeActor.getProperty().setDiffuse(0.7);
    volumeActor.getProperty().setSpecular(0.3);
    volumeActor.getProperty().setSpecularPower(8.0);

    contextRef.current = {
      renderWindow,
      renderWindowView,
      interactor,
      renderers,
      containers,
      imageSliceActors,
      slicePlanes,
      volumeActor: { actor: volumeActor, mapper: volumeMapper },
      ctf,
      pf,
      iStyle,
      tStyle,
      organMaskActors: new Map(),
    };

    const resizeObserver = new ResizeObserver(resize);
    resizeObserver.observe(containerRef.current);

    return () => {
      resizeObserver.disconnect();
      // Clean up global mouse up listener
      window.removeEventListener("mouseup", handleGlobalMouseUp);
      
      if (contextRef.current) {
        containers.forEach((c) => c.element.remove());
        
        imageSliceActors.forEach(({ actor, mapper, ctf: sliceCtf }) => {
          actor.delete();
          mapper.delete();
          sliceCtf?.delete();
        });

        // Free any organ-mask overlays still in the scene, else their marching-cubes
        // filter + mapper + actor leak when the viewer closes with organs selected.
        contextRef.current.organMaskActors.forEach((m) => {
          m.actor?.delete?.();
          m.mapper?.delete?.();
          m.ctf?.delete?.();
          (m.sliceActors ?? []).forEach((s: any) => {
            s.actor?.delete?.();
            s.mapper?.delete?.();
            s.ctf?.delete?.();
            s.pf?.delete?.();
          });
        });
        contextRef.current.organMaskActors.clear();

        slicePlanes.forEach((p) => p.delete());
        volumeActor.delete();
        volumeMapper.delete();
        ctf.delete();
        pf.delete();
        renderers.forEach((r) => r.delete());
        interactor.delete();
        renderWindowView.delete();
        renderWindow.delete();
        contextRef.current = null;
      }
    };
  }, [bindInteractor, resize, resizeViewportContainer, getViewports]);

  // --------------------------------------------------------------------------
  // Update Window/Level & Opacity
  // --------------------------------------------------------------------------
  useEffect(() => {
    if (!contextRef.current) return;

    const { imageSliceActors, volumeActor, ctf, pf, renderWindow } = contextRef.current;

    const low = windowLevel - windowWidth / 2;
    const high = windowLevel + windowWidth / 2;

    // 2D: strict grayscale
    imageSliceActors.forEach(({ actor, ctf: sliceCtf }) => {
      actor.getProperty().setColorWindow(windowWidth);
      actor.getProperty().setColorLevel(windowLevel);

      sliceCtf.removeAllPoints();
      sliceCtf.addRGBPoint(low - 1, 0, 0, 0);
      sliceCtf.addRGBPoint(low, 0, 0, 0);
      sliceCtf.addRGBPoint(high, 1, 1, 1);
      sliceCtf.addRGBPoint(high + 1, 1, 1, 1);

      actor.getProperty().setRGBTransferFunction(0, sliceCtf);
    });

    // 3D: window/level driven
    ctf.removeAllPoints();
    ctf.addRGBPoint(low - 200, 0, 0, 0);
    ctf.addRGBPoint(low, 0.4, 0.2, 0.1);
    ctf.addRGBPoint(low + (high - low) * 0.3, 0.8, 0.6, 0.5);
    ctf.addRGBPoint(low + (high - low) * 0.5, 0.9, 0.8, 0.7);
    ctf.addRGBPoint(high, 1, 1, 0.9);
    ctf.addRGBPoint(high + 200, 1, 1, 1);

    pf.removeAllPoints();
    pf.addPoint(low - 200, 0);
    pf.addPoint(low, 0);
    pf.addPoint(low + (high - low) * 0.2, opacity * 0.2);
    pf.addPoint(low + (high - low) * 0.5, opacity * 0.5);
    pf.addPoint(high, opacity);

    volumeActor.actor.getProperty().setRGBTransferFunction(0, ctf);
    volumeActor.actor.getProperty().setScalarOpacity(0, pf);

    renderWindow.render();
  }, [windowWidth, windowLevel, opacity]);

  // --------------------------------------------------------------------------
  // Load Data
  // --------------------------------------------------------------------------
  useEffect(() => {
    if (!niftiData?.imageData || !contextRef.current) return;
    
    // Prevent reloading if data hasn't changed
    if (loadedDataRef.current === niftiData.imageData) return;
    loadedDataRef.current = niftiData.imageData;

    const { imageSliceActors, slicePlanes, volumeActor, renderers, renderWindow, ctf, pf } = contextRef.current;

    const im = niftiData.imageData;
    const bounds = im.getBounds();
    const center = vtkBoundingBox.getCenter(bounds);
    const dims = im.getDimensions();
    const spacing = im.getSpacing();

    // Set total slices for each view: [axial(Z), coronal(Y), sagittal(X)]
    setTotalSlices([dims[2], dims[1], dims[0]]);
    
    // Calculate initial center slice indices
    const centerSlices: [number, number, number] = [
      Math.floor(dims[2] / 2),
      Math.floor(dims[1] / 2),
      Math.floor(dims[0] / 2),
    ];
    setSliceIndices(centerSlices);

    // Set slice plane origins at center of volume
    const centerX = (bounds[0] + bounds[1]) / 2;
    const centerY = (bounds[2] + bounds[3]) / 2;
    const centerZ = (bounds[4] + bounds[5]) / 2;

    slicePlanes[0].setOrigin(centerX, centerY, centerZ); // Axial
    slicePlanes[1].setOrigin(centerX, centerY, centerZ); // Coronal
    slicePlanes[2].setOrigin(centerX, centerY, centerZ); // Sagittal

    // Set input data for 2D slices
    imageSliceActors.forEach(({ mapper }) => {
      mapper.setInputData(im);
    });

    // Set input data for 3D volume
    volumeActor.mapper.setInputData(im);

    // Add volume to 3D renderer
    renderers[3].removeAllActors();
    renderers[3].removeAllVolumes();
    renderers[3].addVolume(volumeActor.actor);

    // Configure volume properties
    const dataArray = im.getPointData().getScalars();
    const dataRange = dataArray.getRange();

    volumeActor.actor.getProperty().setScalarOpacityUnitDistance(
      0,
      (vtkBoundingBox.getDiagonalLength(bounds) ?? 0) / Math.max(...im.getDimensions())
    );
    volumeActor.actor.getProperty().setGradientOpacityMinimumValue(0, 0);
    volumeActor.actor.getProperty().setGradientOpacityMaximumValue(0, (dataRange[1] - dataRange[0]) * 0.05);
    volumeActor.actor.getProperty().setRGBTransferFunction(0, ctf);
    volumeActor.actor.getProperty().setScalarOpacity(0, pf);

    // ------------------------------------------------------------------------
    // Set Standard Orientations (Medical Standards)
    // ------------------------------------------------------------------------
    
    // 1. Axial: Looking from head to toe, patient's right on left (Radiological)
    const axialCam = renderers[0].getActiveCamera();
    axialCam.setPosition(center[0], center[1], center[2] - 1);
    axialCam.setFocalPoint(center[0], center[1], center[2]);
    axialCam.setViewUp(0, -1, 0);
    renderers[0].resetCamera(); // Fit to screen, but keeps direction
    
    // 2. Coronal: Looking from front to back, patient's right on left
    const coronalCam = renderers[1].getActiveCamera();
    coronalCam.setPosition(center[0], center[1] - 1, center[2]);
    coronalCam.setFocalPoint(center[0], center[1], center[2]);
    coronalCam.setViewUp(0, 0, 1);
    renderers[1].resetCamera();

    // 3. Sagittal: Looking from left to right, anterior at top
    const sagittalCam = renderers[2].getActiveCamera();
    sagittalCam.setPosition(center[0] - 1, center[1], center[2]);
    sagittalCam.setFocalPoint(center[0], center[1], center[2]);
    sagittalCam.setViewUp(0, 0, 1);
    renderers[2].resetCamera();
    
    // 4. 3D View: Default Anterior View
    // We use the rotation function logic here to ensure consistency
    rotate3DView("anterior");

    renderWindow.render();
  }, [niftiData, rotate3DView]);

  // --------------------------------------------------------------------------
  // Wheel scroll handlers for 2D slice navigation
  // --------------------------------------------------------------------------
  useEffect(() => {
    if (!contextRef.current || !niftiData?.imageData) return;
    
    const { containers, slicePlanes, renderWindow, renderers } = contextRef.current;
    const im = niftiData.imageData;
    const bounds = im.getBounds();
    const spacing = im.getSpacing();
    const dims = im.getDimensions();
    
    const handleWheel = (viewIndex: number) => (e: WheelEvent) => {
      e.preventDefault();

      // Ctrl/⌘ + wheel = zoom this 2D view (parallel camera); plain wheel = slice nav.
      if (e.ctrlKey || e.metaKey) {
        const cam = renderers[viewIndex].getActiveCamera();
        cam.zoom(e.deltaY < 0 ? 1.1 : 0.9);
        renderWindow.render();
        return;
      }

      const delta = e.deltaY > 0 ? 1 : -1;
      const plane = slicePlanes[viewIndex];
      const origin = plane.getOrigin();
      
      // Determine which axis to scroll based on view
      // Axial (0): scroll Z, Coronal (1): scroll Y, Sagittal (2): scroll X
      const axisIndex = viewIndex === 0 ? 2 : viewIndex === 1 ? 1 : 0;
      const step = spacing[axisIndex] * delta;
      
      const newOrigin = [...origin];
      newOrigin[axisIndex] += step;
      
      // Clamp to bounds
      const minBound = bounds[axisIndex * 2];
      const maxBound = bounds[axisIndex * 2 + 1];
      newOrigin[axisIndex] = Math.max(minBound, Math.min(maxBound, newOrigin[axisIndex]));
      
      plane.setOrigin(newOrigin[0], newOrigin[1], newOrigin[2]);
      
      // Calculate new slice index
      const totalSlicesForView = dims[axisIndex];
      const relativePos = (newOrigin[axisIndex] - minBound) / (maxBound - minBound);
      const newSliceIndex = Math.round(relativePos * (totalSlicesForView - 1));
      
      setSliceIndices(prev => {
        const updated = [...prev] as [number, number, number];
        updated[viewIndex] = Math.max(0, Math.min(totalSlicesForView - 1, newSliceIndex));
        return updated;
      });
      
      renderWindow.render();
    };
    
    // Attach wheel handlers to 2D views only (indices 0, 1, 2)
    const wheelHandlers: ((e: WheelEvent) => void)[] = [];
    for (let i = 0; i < 3; i++) {
      const handler = handleWheel(i);
      wheelHandlers.push(handler);
      containers[i].element.addEventListener("wheel", handler, { passive: false });
    }
    
    return () => {
      for (let i = 0; i < 3; i++) {
        containers[i].element.removeEventListener("wheel", wheelHandlers[i]);
      }
    };
  }, [niftiData]);

  // --------------------------------------------------------------------------
  // Render Organ Mask Overlays in 3D View
  // --------------------------------------------------------------------------
  useEffect(() => {
    if (!contextRef.current) return;

    const { renderers, renderWindow, organMaskActors, slicePlanes } = contextRef.current;
    const renderer3D = renderers[3];

    // Track selected overlays keyed by a STABLE id (the mask file id), not the display
    // label, so two organs that happen to share a label don't collapse into one.
    const currentKeys = new Set(organMasks.map((m) => m.id ?? m.organName));

    // Remove overlays no longer selected
    organMaskActors.forEach((maskData, key) => {
      if (!currentKeys.has(key as any)) {
        renderer3D.removeActor(maskData.actor);
        maskData.actor.delete();
        maskData.mapper.delete();
        maskData.ctf?.delete?.();
        maskData.pf?.delete?.();
        (maskData.sliceActors ?? []).forEach((s: any, vi: number) => {
          renderers[vi].removeActor(s.actor);
          s.actor.delete();
          s.mapper.delete();
          s.ctf?.delete?.();
          s.pf?.delete?.();
        });
        organMaskActors.delete(key);
      }
    });
    
    // Add organs not yet in the scene. Render each mask as a coloured ISO-SURFACE
    // (marching cubes at 0.5) rather than a second volume: vtk.js does NOT composite
    // two overlapping volumes in one renderer (the overlay silently fails to show),
    // whereas a surface mixes correctly with the main volume — and a binary organ mask
    // reads more clearly as a solid shell. Semi-transparent so the anatomy shows through.
    organMasks.forEach((maskData) => {
      const { organName, imageData, color } = maskData;
      const key = maskData.id ?? organName;
      if (organMaskActors.has(key)) return;

      const mc = vtkImageMarchingCubes.newInstance({
        contourValue: 0.5,
        computeNormals: true,
        mergePoints: true,
      });
      mc.setInputData(imageData);

      const maskMapper = vtkMapper.newInstance();
      maskMapper.setInputConnection(mc.getOutputPort());
      maskMapper.setScalarVisibility(false);

      const maskActor = vtkActor.newInstance();
      maskActor.setMapper(maskMapper);
      maskActor.getProperty().setColor(color[0] / 255, color[1] / 255, color[2] / 255);
      maskActor.getProperty().setOpacity(0.7);

      renderer3D.addActor(maskActor);

      // 2D overlay: reslice the mask onto each 2D pane (axial/coronal/sagittal) using the
      // SAME slice plane as the main image, so it tracks slice scrolling. Coloured where
      // mask>0, transparent elsewhere (piecewise opacity), nearest interp for crisp edges.
      const sliceActors = [0, 1, 2].map((vi) => {
        const sm = vtkImageResliceMapper.newInstance();
        sm.setSlicePlane(slicePlanes[vi]);
        sm.setInputData(imageData);

        const sCtf = vtkColorTransferFunction.newInstance();
        sCtf.addRGBPoint(0, 0, 0, 0);
        sCtf.addRGBPoint(1, color[0] / 255, color[1] / 255, color[2] / 255);

        const sPwf = vtkPiecewiseFunction.newInstance();
        sPwf.addPoint(0, 0); // background → transparent
        sPwf.addPoint(0.5, 0);
        sPwf.addPoint(1, 0.6); // mask → opaque colour fill

        const sa = vtkImageSlice.newInstance();
        sa.setMapper(sm);
        sa.getProperty().setRGBTransferFunction(0, sCtf);
        sa.getProperty().setPiecewiseFunction(0, sPwf);
        // Map the binary data range [0,1] onto the transfer functions. Without this the
        // default colour window (255) squashes value 1 to ~0 → near-black instead of the
        // organ colour (window 1 / level 0.5 makes 0→0 and 1→1).
        sa.getProperty().setColorWindow(1);
        sa.getProperty().setColorLevel(0.5);
        sa.getProperty().setInterpolationTypeToNearest();
        renderers[vi].addActor(sa);
        return { actor: sa, mapper: sm, ctf: sCtf, pf: sPwf };
      });

      // ctf slot reused to hold the marching-cubes filter so the remove path frees it.
      organMaskActors.set(key, { actor: maskActor, mapper: maskMapper, ctf: mc, pf: null, sliceActors });
    });

    // The organ surfaces can sit beyond the camera's stale far-clip plane (set for the
    // main volume, e.g. far=1000 while a surface is ~1018-1283 away) and get culled —
    // recompute near/far so overlays render. resetCameraClippingRange keeps the framing.
    renderer3D.resetCameraClippingRange();
    renderWindow.render();
  }, [organMasks]);

  // --------------------------------------------------------------------------
  // Update Slice Info Labels
  // --------------------------------------------------------------------------
  useEffect(() => {
    if (!contextRef.current) return;
    
    const { containers } = contextRef.current;
    
    // Update slice info labels for 2D views
    for (let i = 0; i < 3; i++) {
      const sliceLabel = containers[i].element.querySelector(`#slice-info-${i}`) as HTMLDivElement;
      if (sliceLabel) {
        sliceLabel.textContent = `Slice: ${sliceIndices[i] + 1}/${totalSlices[i]}`;
      }
    }
  }, [sliceIndices, totalSlices]);

  // Segmentation drawing handlers
  const viewTypes: Array<"axial" | "coronal" | "sagittal"> = ["axial", "coronal", "sagittal"];
  
  const handleSegmentationMouseDown = useCallback((viewIndex: 0 | 1 | 2, e: React.MouseEvent<HTMLDivElement>) => {
    if (!segmentationEnabled) return;
    
    const rect = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width;
    const y = (e.clientY - rect.top) / rect.height;
    
    setActiveDrawingView(viewIndex);
    setIsDrawing(true);
    setStartPoint({ x, y });
    setTempBox(null);
  }, [segmentationEnabled]);

  const handleSegmentationMouseMove = useCallback((viewIndex: 0 | 1 | 2, e: React.MouseEvent<HTMLDivElement>) => {
    if (!isDrawing || !startPoint || activeDrawingView !== viewIndex) return;
    
    const rect = e.currentTarget.getBoundingClientRect();
    const currentX = (e.clientX - rect.left) / rect.width;
    const currentY = (e.clientY - rect.top) / rect.height;
    
    const x = Math.min(startPoint.x, currentX);
    const y = Math.min(startPoint.y, currentY);
    const width = Math.abs(currentX - startPoint.x);
    const height = Math.abs(currentY - startPoint.y);
    
    setTempBox({ x, y, width, height });
  }, [isDrawing, startPoint, activeDrawingView]);

  const handleSegmentationMouseUp = useCallback((viewIndex: 0 | 1 | 2) => {
    if (!isDrawing || activeDrawingView !== viewIndex || !tempBox) {
      setIsDrawing(false);
      setStartPoint(null);
      setActiveDrawingView(null);
      return;
    }
    
    const dims = niftiData?.dimensions || [512, 512, 512];
    const imageWidth = viewIndex === 2 ? dims[1] : dims[0];
    const imageHeight = viewIndex === 0 ? dims[1] : dims[2];
    
    const box: BoundingBox = {
      x: tempBox.x * imageWidth,
      y: tempBox.y * imageHeight,
      width: tempBox.width * imageWidth,
      height: tempBox.height * imageHeight,
      sliceIndex: currentSliceIndex,
      viewType: viewTypes[viewIndex],
    };
    
    onBoundingBoxChange?.(box);
    setIsDrawing(false);
    setStartPoint(null);
    setActiveDrawingView(null);
    setTempBox(null);
  }, [isDrawing, activeDrawingView, tempBox, niftiData, currentSliceIndex, viewTypes, onBoundingBoxChange]);

  // Render segmentation overlay for a viewport
  const renderSegmentationOverlay = useCallback((viewIndex: 0 | 1 | 2, container: ViewportContainer) => {
    if (!segmentationEnabled && !segmentationMask) return null;
    
    const showBox = (activeDrawingView === viewIndex && tempBox) || 
                    (boundingBox && boundingBox.viewType === viewTypes[viewIndex] && !segmentationMask);
    
    const showMask = segmentationMask && segmentationMask.viewType === viewTypes[viewIndex];
    
    const boxToRender = activeDrawingView === viewIndex && tempBox 
      ? tempBox 
      : boundingBox && boundingBox.viewType === viewTypes[viewIndex]
        ? (() => {
            const dims = niftiData?.dimensions || [512, 512, 512];
            const imageWidth = viewIndex === 2 ? dims[1] : dims[0];
            const imageHeight = viewIndex === 0 ? dims[1] : dims[2];
            return {
              x: boundingBox.x / imageWidth,
              y: boundingBox.y / imageHeight,
              width: boundingBox.width / imageWidth,
              height: boundingBox.height / imageHeight,
            };
          })()
        : null;
    
    // Calculate mask bounds (normalized)
    const maskBounds = showMask ? (() => {
      const dims = niftiData?.dimensions || [512, 512, 512];
      const imageWidth = viewIndex === 2 ? dims[1] : dims[0];
      const imageHeight = viewIndex === 0 ? dims[1] : dims[2];
      return {
        x: segmentationMask.bounds.x / imageWidth,
        y: segmentationMask.bounds.y / imageHeight,
        width: segmentationMask.bounds.width / imageWidth,
        height: segmentationMask.bounds.height / imageHeight,
      };
    })() : null;
    
    return (
      <div
        key={`seg-overlay-${viewIndex}`}
        className="absolute inset-0 z-30"
        style={{ 
          pointerEvents: segmentationEnabled ? "auto" : "none",
          cursor: segmentationEnabled ? "crosshair" : "default",
        }}
        onMouseDown={(e) => segmentationEnabled && handleSegmentationMouseDown(viewIndex, e)}
        onMouseMove={(e) => segmentationEnabled && handleSegmentationMouseMove(viewIndex, e)}
        onMouseUp={() => segmentationEnabled && handleSegmentationMouseUp(viewIndex)}
        onMouseLeave={() => segmentationEnabled && handleSegmentationMouseUp(viewIndex)}
      >
        {/* Bounding box (shown while drawing or before prediction) */}
        {showBox && boxToRender && (
          <div
            className={`absolute border-2 ${isDrawing ? "border-primary/70" : "border-primary"} bg-primary/10 pointer-events-none`}
            style={{
              left: `${boxToRender.x * 100}%`,
              top: `${boxToRender.y * 100}%`,
              width: `${boxToRender.width * 100}%`,
              height: `${boxToRender.height * 100}%`,
            }}
          >
            <div className="absolute -top-1 -left-1 w-2 h-2 bg-primary rounded-full" />
            <div className="absolute -top-1 -right-1 w-2 h-2 bg-primary rounded-full" />
            <div className="absolute -bottom-1 -left-1 w-2 h-2 bg-primary rounded-full" />
            <div className="absolute -bottom-1 -right-1 w-2 h-2 bg-primary rounded-full" />
          </div>
        )}
        
        {/* Segmentation mask overlay */}
        {showMask && maskBounds && (
          <div
            className="absolute pointer-events-none"
            style={{
              left: `${maskBounds.x * 100}%`,
              top: `${maskBounds.y * 100}%`,
              width: `${maskBounds.width * 100}%`,
              height: `${maskBounds.height * 100}%`,
            }}
          >
            {/* Mask visualization - gradient fill with animated border */}
            <div 
              className="absolute inset-0 rounded-sm animate-pulse"
              style={{
                background: "linear-gradient(135deg, rgba(34, 197, 94, 0.4) 0%, rgba(34, 197, 94, 0.2) 100%)",
                border: "2px solid rgba(34, 197, 94, 0.8)",
                boxShadow: "0 0 12px rgba(34, 197, 94, 0.5), inset 0 0 20px rgba(34, 197, 94, 0.1)",
              }}
            />
            
            {/* Mask image if provided */}
            {segmentationMask.maskImageData && (
              <img 
                src={`data:image/png;base64,${segmentationMask.maskImageData}`}
                alt="Segmentation mask"
                className="absolute inset-0 w-full h-full object-cover opacity-60"
                style={{ mixBlendMode: "screen" }}
              />
            )}
            
            {/* Confidence badge */}
            {segmentationMask.confidence && (
              <div 
                className="absolute -top-6 left-0 px-2 py-0.5 rounded text-xs font-medium"
                style={{
                  background: "rgba(34, 197, 94, 0.9)",
                  color: "white",
                }}
              >
                {Math.round(segmentationMask.confidence * 100)}% confidence
              </div>
            )}
            
            {/* Corner markers for mask */}
            <div className="absolute -top-1 -left-1 w-3 h-3 border-t-2 border-l-2 border-green-500 rounded-tl" />
            <div className="absolute -top-1 -right-1 w-3 h-3 border-t-2 border-r-2 border-green-500 rounded-tr" />
            <div className="absolute -bottom-1 -left-1 w-3 h-3 border-b-2 border-l-2 border-green-500 rounded-bl" />
            <div className="absolute -bottom-1 -right-1 w-3 h-3 border-b-2 border-r-2 border-green-500 rounded-br" />
          </div>
        )}
      </div>
    );
  }, [segmentationEnabled, segmentationMask, activeDrawingView, tempBox, boundingBox, isDrawing, niftiData, viewTypes, handleSegmentationMouseDown, handleSegmentationMouseMove, handleSegmentationMouseUp]);

  // Inject overlays into viewport containers
  useEffect(() => {
    if (!contextRef.current || !segmentationEnabled) return;
    
    const { containers } = contextRef.current;
    
    // Add visual indicator that segmentation is enabled
    containers.forEach((c, i) => {
      if (i < 3) {
        c.element.style.cursor = "crosshair";
      }
    });
    
    return () => {
      if (contextRef.current) {
        contextRef.current.containers.forEach((c) => {
          c.element.style.cursor = "";
        });
      }
    };
  }, [segmentationEnabled]);

  return (
    <div className="relative w-full h-full" ref={containerRef}>
      {/* Per-pane 2D overlays: segmentation (bbox/MedSAM) + annotation tools.
          The wrapper itself is pointer-transparent; each inner overlay re-enables
          pointer events only when its tool is active, so an idle overlay never
          steals VTK's slice-scroll / zoom. */}
      {(segmentationEnabled || annotationTool !== "none" || annotations.length > 0) &&
        contextRef.current?.containers.slice(0, 3).map((container, i) => {
          const viewIndex = i as 0 | 1 | 2;
          const rect = container.element.getBoundingClientRect();
          const containerRect = containerRef.current?.getBoundingClientRect();

          if (!containerRect) return null;

          return (
            <div
              key={`overlay-wrapper-${viewIndex}`}
              className="absolute z-30"
              style={{
                left: rect.left - containerRect.left,
                top: rect.top - containerRect.top,
                width: rect.width,
                height: rect.height,
                pointerEvents: "none",
              }}
            >
              {renderSegmentationOverlay(viewIndex, container)}
              {(annotationTool !== "none" || annotations.length > 0) && (
                <AnnotationOverlay
                  view={viewTypes[viewIndex]}
                  sliceIndex={sliceIndices[viewIndex]}
                  tool={annotationTool}
                  annotations={annotations}
                  onCommit={(a) => onAnnotationsChange?.([...annotations, a])}
                  onRequestExpand={() =>
                    setExpandedView((prev) => (prev === viewIndex ? null : (viewIndex as ExpandedView)))
                  }
                  onForwardWheel={(e) =>
                    container.element.dispatchEvent(
                      new WheelEvent("wheel", {
                        deltaY: e.deltaY,
                        ctrlKey: e.ctrlKey,
                        metaKey: e.metaKey,
                        bubbles: true,
                        cancelable: true,
                      }),
                    )
                  }
                />
              )}
            </div>
          );
        })}
      
      {isLoading && (
        <div className="absolute inset-0 flex items-center justify-center bg-background/80 backdrop-blur-sm z-20">
          <div className="flex flex-col items-center gap-3">
            <div className="w-10 h-10 border-4 border-primary border-t-transparent rounded-full animate-spin" />
            <span className="text-sm text-muted-foreground">Loading NIfTI data...</span>
          </div>
        </div>
      )}

      {!niftiData && !isLoading && (
        <div className="absolute inset-0 flex items-center justify-center z-20">
          <div className="text-center p-8">
            <div className="text-6xl mb-4 opacity-30">🧠</div>
            <p className="text-muted-foreground">Upload NIfTI files to view</p>
          </div>
        </div>
      )}
    </div>
  );
};