"""DWT anisotropy — quantify surface anisotropy using wavelet decomposition."""

from __future__ import annotations

import numpy as np

from backend.data_types import DataField, RecordTable
from backend.node_registry import register_node


def _next_power_of_2(n: int) -> int:
    """Return the smallest power of 2 >= n."""
    p = 1
    while p < n:
        p <<= 1
    return p


def _pad_to_pow2(data: np.ndarray) -> np.ndarray:
    """Pad *data* to the next power-of-2 dimensions using edge values."""
    rows, cols = data.shape
    new_rows = _next_power_of_2(rows)
    new_cols = _next_power_of_2(cols)
    if new_rows == rows and new_cols == cols:
        return data.copy()
    out = np.zeros((new_rows, new_cols), dtype=np.float64)
    out[:rows, :cols] = data
    # edge-pad
    if new_cols > cols:
        out[:rows, cols:] = data[:, -1:]
    if new_rows > rows:
        out[rows:, :cols] = data[-1:, :]
    if new_rows > rows and new_cols > cols:
        out[rows:, cols:] = data[-1, -1]
    return out


def _haar_decompose_2d(data: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """
    One level of 2-D Haar wavelet decomposition.

    Returns (LL, LH, HL, HH) each of shape (rows//2, cols//2).
    LL = (a+b+c+d)/2   approximation
    LH = (a+b-c-d)/2   horizontal detail  (captures vertical features)
    HL = (a-b+c-d)/2   vertical detail    (captures horizontal features)
    HH = (a-b-c+d)/2   diagonal detail
    """
    rows, cols = data.shape
    a = data[0::2, 0::2]  # top-left
    b = data[0::2, 1::2]  # top-right
    c = data[1::2, 0::2]  # bottom-left
    d = data[1::2, 1::2]  # bottom-right

    ll = (a + b + c + d) / 2.0
    lh = (a + b - c - d) / 2.0
    hl = (a - b + c - d) / 2.0
    hh = (a - b - c + d) / 2.0
    return ll, lh, hl, hh


def _compute_dwt_anisotropy(
    data: np.ndarray,
    n_levels: int,
) -> tuple[list[float], list[float], list[float], list[np.ndarray]]:
    """
    Multi-level 2-D Haar decomposition with per-level energy ratios.

    Returns
    -------
    x_energies : per-level sum(HL**2)
    y_energies : per-level sum(LH**2)
    ratios     : per-level x_energy / y_energy
    ratio_maps : per-level ratio arrays (at decomposition resolution)
    """
    padded = _pad_to_pow2(np.asarray(data, dtype=np.float64))
    current = padded

    x_energies: list[float] = []
    y_energies: list[float] = []
    ratios: list[float] = []
    ratio_maps: list[np.ndarray] = []

    for _ in range(n_levels):
        if current.shape[0] < 2 or current.shape[1] < 2:
            break
        ll, lh, hl, hh = _haar_decompose_2d(current)

        x_energy = float(np.sum(hl ** 2))
        y_energy = float(np.sum(lh ** 2))
        ratio = x_energy / (y_energy + 1e-30)

        x_energies.append(x_energy)
        y_energies.append(y_energy)
        ratios.append(ratio)

        # Build a per-pixel ratio map at this level's resolution
        hl_sq = hl ** 2
        lh_sq = lh ** 2
        level_map = hl_sq / (lh_sq + 1e-30)
        ratio_maps.append(level_map)

        current = ll

    return x_energies, y_energies, ratios, ratio_maps


def _build_anisotropy_map(
    ratio_maps: list[np.ndarray],
    orig_rows: int,
    orig_cols: int,
) -> np.ndarray:
    """
    Combine per-level ratio maps into a single anisotropy map at
    the original field resolution by upsampling and averaging.
    """
    if not ratio_maps:
        return np.ones((orig_rows, orig_cols), dtype=np.float64)

    from scipy.ndimage import zoom

    target = np.zeros((orig_rows, orig_cols), dtype=np.float64)
    for level_map in ratio_maps:
        zy = orig_rows / level_map.shape[0]
        zx = orig_cols / level_map.shape[1]
        upsampled = zoom(level_map, (zy, zx), order=1)
        # zoom may produce shape off by one — trim or pad
        upsampled = upsampled[:orig_rows, :orig_cols]
        if upsampled.shape[0] < orig_rows or upsampled.shape[1] < orig_cols:
            tmp = np.ones((orig_rows, orig_cols), dtype=np.float64)
            tmp[: upsampled.shape[0], : upsampled.shape[1]] = upsampled
            upsampled = tmp
        target += upsampled

    target /= len(ratio_maps)
    return target


@register_node(display_name="DWT Anisotropy")
class DWTAnisotropy:
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "field": ("DATA_FIELD",),
                "n_levels": (
                    "INT",
                    {"default": 4, "min": 1, "max": 10},
                ),
                "ratio_threshold": (
                    "FLOAT",
                    {"default": 0.2, "min": 0.001, "max": 10.0, "step": 0.01},
                ),
            }
        }

    OUTPUTS = (
        ('DATA_FIELD', 'anisotropy_map'),
        ('RECORD_TABLE', 'statistics'),
    )
    FUNCTION = "process"

    DESCRIPTION = (
        "Quantify surface anisotropy using a multi-level 2-D Haar wavelet decomposition. "
        "At each level, horizontal (HL) and vertical (LH) detail energies are compared to "
        "produce an X/Y energy ratio. Ratio > 1 indicates more horizontal features; "
        "ratio < 1 indicates more vertical features. Equivalent to Gwyddion's dwtanisotropy.c."
    )

    def process(
        self,
        field: DataField,
        n_levels: int,
        ratio_threshold: float,
    ) -> tuple:
        data = np.asarray(field.data, dtype=np.float64)
        orig_rows, orig_cols = data.shape

        x_energies, y_energies, ratios, ratio_maps = _compute_dwt_anisotropy(
            data, int(n_levels),
        )

        # Build per-pixel anisotropy map
        aniso_map = _build_anisotropy_map(ratio_maps, orig_rows, orig_cols)
        aniso_field = field.replace(data=aniso_map)

        # Build statistics table
        rows = []
        for i, (xe, ye, r) in enumerate(zip(x_energies, y_energies, ratios)):
            rows.append({
                "level": i + 1,
                "x_energy": float(xe),
                "y_energy": float(ye),
                "ratio": float(r),
                "anisotropic": abs(r - 1.0) > float(ratio_threshold),
            })
        stats = RecordTable(rows)

        return (aniso_field, stats)
