"""CIE 色度图底图渲染与缓存。

将"重型图像渲染"（colour-science 的谱迹颜色填充）与"轻量框架数据层"
（参考/实测三角形、标签、覆盖率）解耦。

底图：
- 仅在首次调用或缓存失效时通过 colour-science 渲染一次；
- 渲染结果保存为 numpy RGBA 数组，同时落盘到 settings/cache/，
  下次启动直接 imread 加载，避免重新跑色彩科学计算。

调用方在每次绘图时只需 `ax.imshow(bg, extent=bbox)`，再叠加自己的矢量层。
"""

from __future__ import annotations

import hashlib
import os
import threading
from typing import Tuple

import numpy as np

# 谱迹底图分辨率（边长，单位像素）。1024 对于 14 inch 画布足够细腻，
# 文件大小 ~1-2MB，单次渲染 ~0.5-1 s，缓存后毫秒级加载。
_DIAGRAM_RES = 1024

# 缓存版本号：当渲染参数或风格调整时递增，强制重新生成。
_CACHE_VERSION = "v1"

_BBox = Tuple[float, float, float, float]  # (xmin, xmax, ymin, ymax)

_CIE1931_BBOX: _BBox = (0.0, 0.8, 0.0, 0.9)
_CIE1976_BBOX: _BBox = (0.0, 0.65, 0.0, 0.6)


_memory_cache: dict[str, np.ndarray] = {}
_lock = threading.Lock()


def _cache_dir() -> str:
    # 项目根目录通过本文件位置反推：app/plots/ -> 项目根
    here = os.path.dirname(os.path.abspath(__file__))
    root = os.path.abspath(os.path.join(here, "..", ".."))
    d = os.path.join(root, "settings", "cache")
    os.makedirs(d, exist_ok=True)
    return d


def _cache_key(kind: str, bbox: _BBox) -> str:
    sig = f"{kind}|{bbox}|{_DIAGRAM_RES}|{_CACHE_VERSION}"
    h = hashlib.md5(sig.encode("utf-8")).hexdigest()[:10]
    return f"chromaticity_{kind}_{h}.npy"


def _cache_path(kind: str, bbox: _BBox) -> str:
    return os.path.join(_cache_dir(), _cache_key(kind, bbox))


def _render_chromaticity(kind: str, bbox: _BBox) -> np.ndarray:
    """通过 colour-science 离屏渲染谱迹底图，返回 RGBA float 数组。"""
    # 延迟导入：仅在缓存未命中时支付 colour.plotting 的加载开销。
    import matplotlib
    prev_backend = matplotlib.get_backend()
    try:
        matplotlib.use("Agg", force=True)
    except Exception:
        pass

    import matplotlib.pyplot as plt
    from colour.plotting import (
        plot_chromaticity_diagram_CIE1931,
        plot_chromaticity_diagram_CIE1976UCS,
    )

    xmin, xmax, ymin, ymax = bbox
    aspect = (xmax - xmin) / (ymax - ymin)
    height = _DIAGRAM_RES
    width = int(round(height * aspect))

    fig = plt.figure(figsize=(width / 100.0, height / 100.0), dpi=100)
    ax = fig.add_axes([0.0, 0.0, 1.0, 1.0])

    if kind == "cie1931":
        plot_chromaticity_diagram_CIE1931(
            axes=ax, show=False, title=False,
            tight_layout=False, transparent_background=True,
            bounding_box=bbox,
        )
    elif kind == "cie1976":
        plot_chromaticity_diagram_CIE1976UCS(
            axes=ax, show=False, title=False,
            tight_layout=False, transparent_background=True,
            bounding_box=bbox,
        )
    else:
        plt.close(fig)
        raise ValueError(f"unknown diagram kind: {kind!r}")

    ax.set_xlim(xmin, xmax)
    ax.set_ylim(ymin, ymax)
    ax.set_axis_off()
    ax.set_position([0.0, 0.0, 1.0, 1.0])

    fig.canvas.draw()
    # 从 canvas 抓取 RGBA 数组
    buf = np.asarray(fig.canvas.buffer_rgba()).copy()
    buf = np.flipud(buf)
    plt.close(fig)

    try:
        matplotlib.use(prev_backend, force=True)
    except Exception:
        pass

    return buf


def _load_or_render(kind: str, bbox: _BBox) -> np.ndarray:
    key = _cache_key(kind, bbox)
    with _lock:
        if key in _memory_cache:
            return _memory_cache[key]

        disk = _cache_path(kind, bbox)
        if os.path.isfile(disk):
            try:
                arr = np.load(disk)
                _memory_cache[key] = arr
                return arr
            except Exception:
                # 缓存损坏则重新渲染
                try:
                    os.remove(disk)
                except OSError:
                    pass

        arr = _render_chromaticity(kind, bbox)
        _memory_cache[key] = arr
        try:
            np.save(disk, arr)
        except Exception:
            pass
        return arr


def get_cie1931_background() -> Tuple[np.ndarray, _BBox]:
    """返回 (RGBA 数组, bbox)，可直接 ax.imshow(arr, extent=[*bbox])。"""
    return _load_or_render("cie1931", _CIE1931_BBOX), _CIE1931_BBOX


def get_cie1976_background() -> Tuple[np.ndarray, _BBox]:
    return _load_or_render("cie1976", _CIE1976_BBOX), _CIE1976_BBOX


def clear_cache(*, disk: bool = False) -> None:
    """清空内存缓存（可选连同磁盘）。供调试/样式调整时使用。"""
    with _lock:
        _memory_cache.clear()
        if disk:
            d = _cache_dir()
            for name in os.listdir(d):
                if name.startswith("chromaticity_") and name.endswith(".npy"):
                    try:
                        os.remove(os.path.join(d, name))
                    except OSError:
                        pass
