"""Multiplanar-reformation geometry helpers for the viewer.

Pure functions (no VTK/Qt) that the GUI relies on: mapping each orthogonal view
to its data axes, deriving anatomical orientation labels (R/L/A/P/S/I) from the
image affine, and converting a world point to the slice index / in-plane
position for linked-crosshair navigation. Keeping this logic here makes it
unit-testable without a display.
"""
from __future__ import annotations
import numpy as np

# vtkImageViewer2 slice-orientation constants used by the GUI.
VIEW_ORIENTATION = {"Axial": 2, "Coronal": 1, "Sagittal": 0}
VIEWS = ("Axial", "Coronal", "Sagittal")

# For each view: (slice_axis, horizontal_axis, vertical_axis) in DATA (i,j,k)
# index space, matching vtkImageViewer2's SliceOrientationXY/XZ/YZ layout.
VIEW_AXES = {
    "Axial":    (2, 0, 1),   # slice along k; i horizontal, j vertical
    "Coronal":  (1, 0, 2),   # slice along j; i horizontal, k vertical
    "Sagittal": (0, 1, 2),   # slice along i; j horizontal, k vertical
}

# RAS+ world-axis -> (negative-end letter, positive-end letter).
# +x = Right, +y = Anterior, +z = Superior.
_WORLD_LETTERS = {0: ("L", "R"), 1: ("P", "A"), 2: ("I", "S")}


def world_axis_letter(affine, data_axis: int, sign: int = 1) -> str:
    """Anatomical letter for moving along a data axis (sign=+1 increasing).

    Derived from the affine, so a flipped axis (e.g. MNI's negative x column)
    yields the correct side rather than a hard-coded guess.
    """
    v = np.asarray(affine, dtype=float)[:3, data_axis] * sign
    a = int(np.argmax(np.abs(v)))
    neg, pos = _WORLD_LETTERS[a]
    return pos if v[a] >= 0 else neg


def orientation_labels(affine, view: str) -> dict:
    """Screen-edge anatomical labels for a view: {left,right,top,bottom}.

    Assumes vtkImageViewer2's default camera (increasing horizontal axis to the
    right, increasing vertical axis up). Flips from that convention are a display
    concern to confirm live, not a change to this mapping.
    """
    _s, h_ax, v_ax = VIEW_AXES[view]
    return {
        "right": world_axis_letter(affine, h_ax, +1),
        "left": world_axis_letter(affine, h_ax, -1),
        "top": world_axis_letter(affine, v_ax, +1),
        "bottom": world_axis_letter(affine, v_ax, -1),
    }


def slice_index_for(volume, world_point, view: str) -> int:
    """Data-grid slice index (clamped) showing `world_point` in `view`."""
    s_ax = VIEW_AXES[view][0]
    v = volume.world_to_voxel(world_point)
    n = volume.shape[s_ax]
    return int(np.clip(round(float(v[s_ax])), 0, n - 1))


def slice_indices(volume, world_point) -> dict:
    """Slice index per view for a focus point (linked-crosshair navigation)."""
    return {view: slice_index_for(volume, world_point, view) for view in VIEWS}


def crosshair_extents(volume) -> dict:
    """World-space min/max corner of the volume, for spanning crosshair lines."""
    ni, nj, nk = volume.shape
    corners = [volume.voxel_to_world((i, j, k))
               for i in (0, ni - 1) for j in (0, nj - 1) for k in (0, nk - 1)]
    corners = np.array(corners)
    return {"min": corners.min(axis=0), "max": corners.max(axis=0)}
