"""灰阶 Pattern 配置面板（Gamma / CCT / 对比度 / EOTF 共用）。

特性：
- 多预设管理：内置 + 用户保存，支持 新建 / 另存为 / 复制 / 重命名 / 删除 / 导入 / 导出
- 内置预设锁定，不可覆盖、删除、改名
- 行内色块预览，前景色根据亮度自适应
- 生成器：N 点等分 / PQ 编码 / Gamma 2.2/2.4 曲线
- 剪贴板批量粘贴（每行 "R,G,B" / "R G B" / "灰度%"）
- 自动校验：重复 / 越界 / 非灰阶 / 非单调

为保持向后兼容，仍导出 ``create_gamma_pattern_panel`` / ``toggle_gamma_pattern_panel``。
"""

from __future__ import annotations

import json
import os
import sys
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, simpledialog
from typing import TYPE_CHECKING

import ttkbootstrap as ttk

from app.pq import pq_config

if TYPE_CHECKING:
    from pqAutomationApp import PQAutomationApp


# ============================================================
# 列定义
# ============================================================

_COLUMNS = ("idx", "pct", "r", "g", "b", "hex")
_HEADINGS = {
    "idx": "#",
    "pct": "灰度 %",
    "r": "R",
    "g": "G",
    "b": "B",
    "hex": "HEX",
}
_WIDTHS = {"idx": 40, "pct": 70, "r": 60, "g": 60, "b": 60, "hex": 80}

_TEST_KIND = "gray"  # 当前仅管理灰阶系测试共用的 pattern


# ============================================================
# 工具
# ============================================================

def _gray_pct_of(rgb) -> str:
    try:
        r = int(rgb[0])
    except Exception:
        return ""
    return str(int(round(r / 255 * 100)))


def _hex_of(rgb) -> str:
    try:
        r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
    except Exception:
        return ""
    return f"#{r:02X}{g:02X}{b:02X}"


def _luminance(rgb) -> float:
    r, g, b = rgb
    return 0.2126 * r + 0.7152 * g + 0.0722 * b


def _fg_for_bg(rgb) -> str:
    return "#000000" if _luminance(rgb) >= 140 else "#ffffff"


def _get_settings_dir(self: "PQAutomationApp") -> str:
    if getattr(self, "config_file", None):
        return os.path.dirname(self.config_file)
    if getattr(sys, "frozen", False):
        base_dir = os.path.dirname(sys.executable)
    else:
        base_dir = os.path.dirname(
            os.path.dirname(
                os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
            )
        )
    return os.path.join(base_dir, "settings")


# ============================================================
# 内置生成器
# ============================================================

def _gen_even(n: int) -> list[list[int]]:
    n = max(2, n)
    out = []
    for i in range(n):
        pct = 100.0 - (100.0 / (n - 1)) * i
        v = int(round(pct / 100.0 * 255))
        out.append([v, v, v])
    return out


def _gen_pq(n: int) -> list[list[int]]:
    """N 点：线性 nits（0..10000）等分后 PQ 编码到 8-bit（低端更密集）。"""
    n = max(2, n)
    c1, c2, c3 = 0.8359375, 18.8515625, 18.6875
    m1, m2 = 0.1593017578125, 78.84375
    out = []
    for i in range(n):
        nits = 10000.0 * (1.0 - i / (n - 1))
        L = max(0.0, nits / 10000.0)
        if L <= 0:
            v_pq = 0.0
        else:
            Lm1 = L ** m1
            v_pq = ((c1 + c2 * Lm1) / (1.0 + c3 * Lm1)) ** m2
        v = int(round(max(0.0, min(1.0, v_pq)) * 255))
        out.append([v, v, v])
    return out


def _gen_gamma(n: int, gamma: float = 2.2) -> list[list[int]]:
    """N 点：线性光 0..1 等分后 ^(1/gamma) 编码到 8-bit（暗端更密集）。"""
    n = max(2, n)
    out = []
    for i in range(n):
        lin = 1.0 - i / (n - 1)
        v = int(round((lin ** (1.0 / gamma)) * 255))
        out.append([v, v, v])
    return out


_GENERATORS = {
    "等分 (linear code)": _gen_even,
    "PQ 编码 (HDR)": _gen_pq,
    "Gamma 2.2 (SDR)": lambda n: _gen_gamma(n, 2.2),
    "Gamma 2.4 (SDR)": lambda n: _gen_gamma(n, 2.4),
}


# ============================================================
# 面板入口
# ============================================================

def create_gamma_pattern_panel(self: "PQAutomationApp"):
    """创建灰阶 Pattern 配置面板。"""
    frame = ttk.Frame(self.view_stack)
    self.gamma_pattern_frame = frame
    self.gamma_pattern_visible = False

    # 内部状态
    self._gamma_pattern_params: list[list[int]] = []
    self._gamma_current_preset: str | None = None
    self._gamma_preset_locked: bool = False
    self._gamma_dirty: bool = False
    self._gamma_preset_list: list[dict] = []

    root = ttk.Frame(frame, padding=10)
    root.pack(fill=tk.BOTH, expand=True)

    # ===== 标题 =====
    title_row = ttk.Frame(root)
    title_row.pack(fill=tk.X, pady=(0, 6))
    ttk.Label(
        title_row, text="灰阶 Pattern 配置", font=("微软雅黑", 14, "bold")
    ).pack(side=tk.LEFT)
    ttk.Label(
        title_row,
        text="（Gamma / CCT / 对比度 / EOTF 共用此列表）",
        style="Muted.TLabel",
    ).pack(side=tk.LEFT, padx=(8, 0))

    # ===== 预设管理行 =====
    preset_box = ttk.LabelFrame(root, text="预设", padding=8)
    preset_box.pack(fill=tk.X, pady=(0, 8))

    preset_row1 = ttk.Frame(preset_box)
    preset_row1.pack(fill=tk.X)

    ttk.Label(preset_row1, text="选择：").pack(side=tk.LEFT)
    self._gamma_preset_var = tk.StringVar()
    self._gamma_preset_combo = ttk.Combobox(
        preset_row1,
        textvariable=self._gamma_preset_var,
        state="readonly",
        width=32,
    )
    self._gamma_preset_combo.pack(side=tk.LEFT, padx=(4, 6))
    self._gamma_preset_combo.bind(
        "<<ComboboxSelected>>", lambda e: _on_preset_selected(self)
    )

    ttk.Button(
        preset_row1, text="应用为当前",
        bootstyle="success", width=12,
        command=lambda: _activate_selected_preset(self),
    ).pack(side=tk.LEFT, padx=2)

    self._gamma_active_label = ttk.Label(
        preset_row1, text="", style="SuccessState.TLabel", font=("微软雅黑", 9, "bold")
    )
    self._gamma_active_label.pack(side=tk.LEFT, padx=(10, 0))

    # 第二行：CRUD
    preset_row2 = ttk.Frame(preset_box)
    preset_row2.pack(fill=tk.X, pady=(6, 0))

    for txt, style, cmd in [
        ("新建空预设", "secondary-outline", lambda: _new_preset(self)),
        ("另存为...", "primary-outline", lambda: _save_as_preset(self)),
        ("复制...", "secondary-outline", lambda: _duplicate_preset(self)),
        ("重命名...", "secondary-outline", lambda: _rename_preset(self)),
        ("删除", "danger-outline", lambda: _delete_preset(self)),
        ("导入...", "secondary-outline", lambda: _import_preset(self)),
        ("导出...", "secondary-outline", lambda: _export_preset(self)),
    ]:
        ttk.Button(
            preset_row2, text=txt, bootstyle=style, width=11, command=cmd
        ).pack(side=tk.LEFT, padx=2)

    # 描述行
    self._gamma_meta_label = ttk.Label(
        preset_box, text="", style="Muted.TLabel", font=("微软雅黑", 9)
    )
    self._gamma_meta_label.pack(anchor=tk.W, pady=(6, 0))

    # ===== 中部：左表格 + 右编辑区 =====
    mid = ttk.Frame(root)
    mid.pack(fill=tk.BOTH, expand=True)
    mid.columnconfigure(0, weight=1)
    mid.columnconfigure(1, weight=0)
    mid.rowconfigure(0, weight=1)

    # ---- 左：表格 ----
    table_frame = ttk.LabelFrame(mid, text="灰阶点列表", padding=6)
    table_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=(0, 8))

    tree_container = ttk.Frame(table_frame)
    tree_container.pack(fill=tk.BOTH, expand=True)

    self.gamma_pattern_tree = ttk.Treeview(
        tree_container,
        columns=_COLUMNS,
        show="headings",
        height=18,
        selectmode="browse",
    )
    for col in _COLUMNS:
        self.gamma_pattern_tree.heading(col, text=_HEADINGS[col])
        self.gamma_pattern_tree.column(col, width=_WIDTHS[col], anchor=tk.CENTER)
    self.gamma_pattern_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

    scrollbar = ttk.Scrollbar(
        tree_container, orient=tk.VERTICAL, command=self.gamma_pattern_tree.yview
    )
    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
    self.gamma_pattern_tree.configure(yscrollcommand=scrollbar.set)

    self.gamma_pattern_tree.bind(
        "<<TreeviewSelect>>", lambda e: _on_select(self)
    )

    # ---- 右：编辑区 ----
    right = ttk.Frame(mid)
    right.grid(row=0, column=1, sticky=tk.NS)

    edit_frame = ttk.LabelFrame(right, text="编辑选中点灯", padding=8)
    edit_frame.pack(fill=tk.X)

    self._gamma_edit_r_var = tk.StringVar()
    self._gamma_edit_g_var = tk.StringVar()
    self._gamma_edit_b_var = tk.StringVar()
    self._gamma_edit_pct_var = tk.StringVar()

    ttk.Label(edit_frame, text="灰度 %").grid(row=0, column=0, sticky=tk.W, pady=3)
    ttk.Entry(edit_frame, textvariable=self._gamma_edit_pct_var, width=10).grid(
        row=0, column=1, sticky=tk.W, pady=3, padx=(4, 0)
    )
    ttk.Button(
        edit_frame, text="按 % 填充",
        bootstyle="secondary-outline", width=10,
        command=lambda: _fill_rgb_from_pct(self),
    ).grid(row=0, column=2, padx=(6, 0), pady=3)

    for i, ch in enumerate(("R", "G", "B"), start=1):
        ttk.Label(edit_frame, text=ch).grid(row=i, column=0, sticky=tk.W, pady=3)
        var = getattr(self, f"_gamma_edit_{ch.lower()}_var")
        ttk.Entry(edit_frame, textvariable=var, width=10).grid(
            row=i, column=1, sticky=tk.W, pady=3, padx=(4, 0)
        )

    btn_row1 = ttk.Frame(edit_frame)
    btn_row1.grid(row=4, column=0, columnspan=3, sticky=tk.EW, pady=(8, 0))
    for txt, style, cmd in [
        ("更新选中", "primary", lambda: _update_selected(self)),
        ("新增", "success", lambda: _add_new(self)),
        ("删除", "danger-outline", lambda: _delete_selected(self)),
    ]:
        ttk.Button(btn_row1, text=txt, bootstyle=style, width=8, command=cmd).pack(
            side=tk.LEFT, padx=(0, 4)
        )

    btn_row2 = ttk.Frame(edit_frame)
    btn_row2.grid(row=5, column=0, columnspan=3, sticky=tk.EW, pady=(4, 0))
    ttk.Button(
        btn_row2, text="↑ 上移",
        bootstyle="secondary-outline", width=8,
        command=lambda: _move_selected(self, -1),
    ).pack(side=tk.LEFT, padx=(0, 4))
    ttk.Button(
        btn_row2, text="↓ 下移",
        bootstyle="secondary-outline", width=8,
        command=lambda: _move_selected(self, 1),
    ).pack(side=tk.LEFT)

    # ---- 生成器 ----
    gen_frame = ttk.LabelFrame(right, text="按曲线生成", padding=8)
    gen_frame.pack(fill=tk.X, pady=(10, 0))

    ttk.Label(gen_frame, text="类型").grid(row=0, column=0, sticky=tk.W)
    self._gamma_gen_type_var = tk.StringVar(value=next(iter(_GENERATORS)))
    ttk.Combobox(
        gen_frame, textvariable=self._gamma_gen_type_var,
        values=list(_GENERATORS.keys()), state="readonly", width=18,
    ).grid(row=0, column=1, sticky=tk.W, padx=(4, 0))

    ttk.Label(gen_frame, text="点数 N").grid(row=1, column=0, sticky=tk.W, pady=(6, 0))
    self._gamma_n_points_var = tk.StringVar(value="11")
    ttk.Entry(gen_frame, textvariable=self._gamma_n_points_var, width=10).grid(
        row=1, column=1, sticky=tk.W, padx=(4, 0), pady=(6, 0)
    )
    ttk.Button(
        gen_frame, text="生成",
        bootstyle="info-outline",
        command=lambda: _generate_by_curve(self),
    ).grid(row=2, column=0, columnspan=2, sticky=tk.EW, pady=(6, 0))

    # ---- 剪贴板粘贴 ----
    paste_frame = ttk.LabelFrame(right, text="批量粘贴", padding=8)
    paste_frame.pack(fill=tk.X, pady=(10, 0))
    ttk.Label(
        paste_frame, text="每行：R,G,B 或 R G B\n或：灰度% (如 50%)",
        style="Muted.TLabel", justify=tk.LEFT,
    ).pack(anchor=tk.W)
    ttk.Button(
        paste_frame, text="从剪贴板导入",
        bootstyle="secondary-outline",
        command=lambda: _paste_from_clipboard(self),
    ).pack(fill=tk.X, pady=(6, 0))

    # ---- 右侧：校验与保存（与编辑区放在一起） ----
    save_box = ttk.LabelFrame(right, text="校验与保存", padding=8)
    save_box.pack(fill=tk.X, pady=(10, 0))

    self._gamma_validate_label = ttk.Label(
        save_box, text="", style="Muted.TLabel", justify=tk.LEFT
    )
    self._gamma_validate_label.pack(anchor=tk.W)

    save_row = ttk.Frame(save_box)
    save_row.pack(fill=tk.X, pady=(6, 0))
    ttk.Button(
        save_row, text="保存改动到当前预设",
        bootstyle="primary",
        command=lambda: _save_to_current_preset(self),
    ).pack(side=tk.LEFT)
    ttk.Button(
        save_row, text="另存为新预设...",
        bootstyle="info-outline",
        command=lambda: _save_as_preset(self),
    ).pack(side=tk.RIGHT)

    # 注册并初始化
    self.register_panel("gamma_pattern", frame, None, "gamma_pattern_visible")
    _refresh_preset_combo(self)
    _load_initial(self)


def toggle_gamma_pattern_panel(self: "PQAutomationApp"):
    """切换面板显隐。"""
    self.show_panel("gamma_pattern")


# ============================================================
# 预设管理
# ============================================================

def _refresh_preset_combo(self: "PQAutomationApp", select: str | None = None):
    presets = pq_config.list_presets(_TEST_KIND)
    self._gamma_preset_list = presets
    names = [p["name"] for p in presets]
    labels = [
        f"{'🔒 ' if p['locked'] else ''}{p['name']}  ({p['point_count']}点)"
        for p in presets
    ]
    self._gamma_preset_combo["values"] = labels

    target = select or self._gamma_current_preset
    if target and target in names:
        self._gamma_preset_var.set(labels[names.index(target)])
    elif labels:
        self._gamma_preset_var.set(labels[0])

    _update_active_label(self)


def _selected_preset_name(self: "PQAutomationApp") -> str | None:
    label = self._gamma_preset_var.get()
    if not label:
        return None
    for p in self._gamma_preset_list:
        prefix = "🔒 " if p["locked"] else ""
        if label.startswith(prefix + p["name"]):
            return p["name"]
    return None


def _update_active_label(self: "PQAutomationApp"):
    active = pq_config.get_active_preset_name(_TEST_KIND)
    current = self._gamma_current_preset
    if active and current == active and not self._gamma_dirty:
        self._gamma_active_label.config(
            text=f"✔ 当前激活：{active}", style="SuccessState.TLabel"
        )
    elif active:
        extra = "（有未保存改动）" if self._gamma_dirty else ""
        self._gamma_active_label.config(
            text=f"● 激活：{active}  编辑中：{current or '-'}{extra}",
            style="WarningState.TLabel" if self._gamma_dirty else "Muted.TLabel",
        )
    else:
        self._gamma_active_label.config(text="● 未激活任何预设", style="Muted.TLabel")


def _on_preset_selected(self: "PQAutomationApp"):
    """选中下拉项时直接加载（含未保存提醒）。"""
    if self._gamma_dirty:
        if not messagebox.askyesno(
            "未保存改动", "当前预设有未保存改动，确定切换？切换后改动将丢失。"
        ):
            _refresh_preset_combo(self, select=self._gamma_current_preset)
            return
    _load_selected_preset(self)


def _load_selected_preset(self: "PQAutomationApp"):
    name = _selected_preset_name(self)
    if not name:
        return
    try:
        data = pq_config.load_preset(_TEST_KIND, name)
    except (FileNotFoundError, json.JSONDecodeError) as exc:
        messagebox.showerror("加载失败", str(exc))
        return
    self._gamma_current_preset = name
    self._gamma_preset_locked = bool((data.get("_meta") or {}).get("locked"))
    self._gamma_pattern_params = [
        list(map(int, rgb)) for rgb in (data.get("pattern_params") or [])
    ]
    self._gamma_dirty = False
    _refresh_tree(self)
    _update_meta_label(self, data)
    _update_active_label(self)


def _activate_selected_preset(self: "PQAutomationApp"):
    name = _selected_preset_name(self)
    if not name:
        messagebox.showinfo("提示", "请先选择一个预设")
        return
    if self._gamma_dirty and self._gamma_current_preset == name:
        if not messagebox.askyesno(
            "未保存改动", "当前编辑未保存，将使用磁盘上原始预设进行激活。继续？"
        ):
            return
    try:
        pq_config.activate_preset(_TEST_KIND, name)
    except Exception as exc:
        messagebox.showerror("激活失败", str(exc))
        return
    if hasattr(self, "log_gui"):
        self.log_gui.log(f"已激活灰阶预设：{name}", level="success")
    _update_active_label(self)
    messagebox.showinfo("成功", f"已应用预设 '{name}' 到运行时。")


def _new_preset(self: "PQAutomationApp"):
    name = simpledialog.askstring(
        "新建预设", "预设名（建议英文/数字/下划线）：",
        parent=self.gamma_pattern_frame,
    )
    if not name:
        return
    try:
        path = pq_config.save_preset(
            _TEST_KIND, name,
            {"pattern_params": _gen_even(11)},
            description="新建空白预设",
            generator="manual",
            overwrite=False,
        )
    except (FileExistsError, PermissionError, ValueError) as exc:
        messagebox.showerror("创建失败", str(exc))
        return
    _refresh_preset_combo(self, select=Path(path).stem)
    _load_selected_preset(self)


def _save_as_preset(self: "PQAutomationApp"):
    if not self._gamma_pattern_params:
        messagebox.showerror("错误", "当前列表为空，无法另存")
        return
    name = simpledialog.askstring(
        "另存为预设", "新预设名：", parent=self.gamma_pattern_frame,
        initialvalue=(self._gamma_current_preset or "untitled").lstrip("_") + "_copy",
    )
    if not name:
        return
    try:
        path = pq_config.save_preset(
            _TEST_KIND, name,
            {"pattern_params": self._gamma_pattern_params},
            description="用户另存",
            generator="manual",
            overwrite=False,
        )
    except (FileExistsError, PermissionError, ValueError) as exc:
        messagebox.showerror("保存失败", str(exc))
        return
    self._gamma_dirty = False
    _refresh_preset_combo(self, select=Path(path).stem)
    _load_selected_preset(self)


def _duplicate_preset(self: "PQAutomationApp"):
    src = _selected_preset_name(self)
    if not src:
        return
    new_name = simpledialog.askstring(
        "复制预设", "新名字：", parent=self.gamma_pattern_frame,
        initialvalue=src.lstrip("_") + "_copy",
    )
    if not new_name:
        return
    try:
        path = pq_config.duplicate_preset(_TEST_KIND, src, new_name)
    except (FileExistsError, FileNotFoundError, ValueError) as exc:
        messagebox.showerror("复制失败", str(exc))
        return
    _refresh_preset_combo(self, select=Path(path).stem)


def _rename_preset(self: "PQAutomationApp"):
    src = _selected_preset_name(self)
    if not src:
        return
    new_name = simpledialog.askstring(
        "重命名预设", "新名字：", parent=self.gamma_pattern_frame, initialvalue=src
    )
    if not new_name or new_name == src:
        return
    try:
        path = pq_config.rename_preset(_TEST_KIND, src, new_name)
    except (FileExistsError, FileNotFoundError, PermissionError, ValueError) as exc:
        messagebox.showerror("重命名失败", str(exc))
        return
    if self._gamma_current_preset == src:
        self._gamma_current_preset = Path(path).stem
    _refresh_preset_combo(self, select=Path(path).stem)


def _delete_preset(self: "PQAutomationApp"):
    src = _selected_preset_name(self)
    if not src:
        return
    if not messagebox.askyesno("确认", f"确定删除预设 '{src}' ？"):
        return
    try:
        pq_config.delete_preset(_TEST_KIND, src)
    except (FileNotFoundError, PermissionError) as exc:
        messagebox.showerror("删除失败", str(exc))
        return
    if self._gamma_current_preset == src:
        self._gamma_current_preset = None
        self._gamma_pattern_params = []
        _refresh_tree(self)
    _refresh_preset_combo(self)


def _import_preset(self: "PQAutomationApp"):
    fp = filedialog.askopenfilename(
        title="选择 Pattern JSON 文件",
        filetypes=[("JSON", "*.json"), ("All", "*.*")],
        initialdir=_get_settings_dir(self),
    )
    if not fp:
        return
    name = simpledialog.askstring(
        "导入预设", "保存为预设名：", parent=self.gamma_pattern_frame,
        initialvalue=Path(fp).stem,
    )
    if not name:
        return
    try:
        path = pq_config.import_preset_from_file(_TEST_KIND, fp, name=name)
    except (ValueError, json.JSONDecodeError, OSError) as exc:
        messagebox.showerror("导入失败", str(exc))
        return
    _refresh_preset_combo(self, select=Path(path).stem)
    _load_selected_preset(self)


def _export_preset(self: "PQAutomationApp"):
    src = _selected_preset_name(self)
    if not src:
        return
    fp = filedialog.asksaveasfilename(
        title="导出预设到文件",
        filetypes=[("JSON", "*.json")],
        defaultextension=".json",
        initialfile=f"{src}.json",
        initialdir=_get_settings_dir(self),
    )
    if not fp:
        return
    try:
        pq_config.export_preset_to_file(_TEST_KIND, src, fp)
    except Exception as exc:
        messagebox.showerror("导出失败", str(exc))
        return
    messagebox.showinfo("导出成功", f"已导出到\n{fp}")


def _save_to_current_preset(self: "PQAutomationApp"):
    name = self._gamma_current_preset
    if not name:
        messagebox.showinfo("提示", "没有当前预设，请先'新建'或'另存为'")
        return
    if self._gamma_preset_locked:
        messagebox.showwarning(
            "预设已锁定",
            f"'{name}' 是内置预设，不能直接覆盖。请使用'另存为'保存为新预设。",
        )
        return
    if len(self._gamma_pattern_params) < 2:
        messagebox.showerror("错误", "至少需要 2 个灰阶点")
        return
    try:
        existing_meta = (pq_config.load_preset(_TEST_KIND, name).get("_meta") or {})
    except Exception:
        existing_meta = {}
    try:
        pq_config.save_preset(
            _TEST_KIND, name,
            {"pattern_params": self._gamma_pattern_params},
            description=existing_meta.get("description", ""),
            generator=existing_meta.get("generator", "manual"),
            overwrite=True,
        )
    except (PermissionError, ValueError) as exc:
        messagebox.showerror("保存失败", str(exc))
        return
    self._gamma_dirty = False
    _refresh_preset_combo(self, select=name)
    if hasattr(self, "log_gui"):
        self.log_gui.log(f"已保存预设：{name}", level="success")
    messagebox.showinfo("成功", f"已保存到预设 '{name}'。\n如需生效请点'应用为当前'或'应用到运行时'。")


def _apply_current_to_runtime(self: "PQAutomationApp"):
    """把当前编辑结果直接写入 gray.json 并热加载（不必先保存为预设）。"""
    if len(self._gamma_pattern_params) < 2:
        messagebox.showerror("错误", "至少需要 2 个灰阶点")
        return
    pattern = {
        "pattern_mode": "SolidColor",
        "measurement_bit_depth": 8,
        "measurement_max_value": len(self._gamma_pattern_params) - 1,
        "pattern_params": [list(map(int, rgb)) for rgb in self._gamma_pattern_params],
    }
    path = Path(_get_settings_dir(self)) / "patterns" / "gray.json"
    try:
        pq_config.save_pattern_file(path, pattern)
        pq_config.reload_gray_pattern()
    except Exception as exc:
        messagebox.showerror("应用失败", str(exc))
        return

    # 若当前编辑与磁盘上同名预设内容一致，则把该预设标记为激活
    if self._gamma_current_preset:
        try:
            disk = pq_config.load_preset(_TEST_KIND, self._gamma_current_preset)
            if (disk.get("pattern_params") or []) == self._gamma_pattern_params:
                pq_config.activate_preset(_TEST_KIND, self._gamma_current_preset)
        except Exception:
            pass

    if hasattr(self, "log_gui"):
        self.log_gui.log(
            f"已应用 {len(self._gamma_pattern_params)} 点灰阶 pattern 到运行时",
            level="success",
        )
    _update_active_label(self)
    messagebox.showinfo("成功", "已应用到运行时，立即生效。")


# ============================================================
# 表格 / 编辑
# ============================================================

def _refresh_tree(self: "PQAutomationApp"):
    tree = self.gamma_pattern_tree
    tree.delete(*tree.get_children())
    for i, rgb in enumerate(self._gamma_pattern_params):
        try:
            r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
        except (TypeError, ValueError, IndexError):
            r = g = b = 0
        tag = f"row_{i}"
        bg = f"#{r:02x}{g:02x}{b:02x}"
        fg = _fg_for_bg([r, g, b])
        tree.tag_configure(tag, background=bg, foreground=fg)
        tree.insert(
            "", tk.END, iid=str(i),
            values=(i, _gray_pct_of([r, g, b]), r, g, b, _hex_of([r, g, b])),
            tags=(tag,),
        )
    _run_validation(self)


def _selected_index(self: "PQAutomationApp"):
    sel = self.gamma_pattern_tree.selection()
    if not sel:
        return None
    try:
        return int(sel[0])
    except ValueError:
        return None


def _on_select(self: "PQAutomationApp"):
    idx = _selected_index(self)
    if idx is None or idx >= len(self._gamma_pattern_params):
        return
    r, g, b = self._gamma_pattern_params[idx]
    self._gamma_edit_r_var.set(str(r))
    self._gamma_edit_g_var.set(str(g))
    self._gamma_edit_b_var.set(str(b))
    self._gamma_edit_pct_var.set(_gray_pct_of([r, g, b]))


def _parse_rgb_from_form(self: "PQAutomationApp"):
    try:
        r = int(self._gamma_edit_r_var.get().strip())
        g = int(self._gamma_edit_g_var.get().strip())
        b = int(self._gamma_edit_b_var.get().strip())
    except ValueError:
        messagebox.showerror("输入错误", "R/G/B 必须为整数")
        return None
    if not all(0 <= v <= 255 for v in (r, g, b)):
        messagebox.showerror("输入错误", "R/G/B 必须在 0~255 范围内")
        return None
    return [r, g, b]


def _fill_rgb_from_pct(self: "PQAutomationApp"):
    raw = self._gamma_edit_pct_var.get().strip().rstrip("%")
    try:
        pct = float(raw)
    except ValueError:
        messagebox.showerror("输入错误", "灰度 % 必须为数字")
        return
    pct = max(0.0, min(100.0, pct))
    v = int(round(pct / 100.0 * 255))
    self._gamma_edit_r_var.set(str(v))
    self._gamma_edit_g_var.set(str(v))
    self._gamma_edit_b_var.set(str(v))
    self._gamma_edit_pct_var.set(str(int(round(pct))))


def _mark_dirty(self: "PQAutomationApp"):
    self._gamma_dirty = True
    _update_active_label(self)


def _check_locked_or_prompt(self: "PQAutomationApp") -> bool:
    """锁定预设编辑前提示用户。返回 True 表示允许继续编辑。"""
    if not self._gamma_preset_locked:
        return True
    return messagebox.askyesno(
        "预设已锁定",
        "当前是内置预设，改动无法直接覆盖（保存时需'另存为'）。仍要编辑吗？",
    )


def _update_selected(self: "PQAutomationApp"):
    idx = _selected_index(self)
    if idx is None:
        messagebox.showinfo("提示", "请先在列表中选中一行")
        return
    rgb = _parse_rgb_from_form(self)
    if rgb is None:
        return
    if not _check_locked_or_prompt(self):
        return
    self._gamma_pattern_params[idx] = rgb
    _mark_dirty(self)
    _refresh_tree(self)
    try:
        self.gamma_pattern_tree.selection_set(str(idx))
    except Exception:
        pass


def _add_new(self: "PQAutomationApp"):
    rgb = _parse_rgb_from_form(self)
    if rgb is None:
        return
    if not _check_locked_or_prompt(self):
        return
    idx = _selected_index(self)
    insert_at = (idx + 1) if idx is not None else len(self._gamma_pattern_params)
    self._gamma_pattern_params.insert(insert_at, rgb)
    _mark_dirty(self)
    _refresh_tree(self)
    try:
        self.gamma_pattern_tree.selection_set(str(insert_at))
    except Exception:
        pass


def _delete_selected(self: "PQAutomationApp"):
    idx = _selected_index(self)
    if idx is None:
        messagebox.showinfo("提示", "请先在列表中选中一行")
        return
    if len(self._gamma_pattern_params) <= 2:
        messagebox.showwarning("提示", "至少保留 2 个灰阶点")
        return
    if not _check_locked_or_prompt(self):
        return
    del self._gamma_pattern_params[idx]
    _mark_dirty(self)
    _refresh_tree(self)


def _move_selected(self: "PQAutomationApp", delta: int):
    idx = _selected_index(self)
    if idx is None:
        return
    new_idx = idx + delta
    if new_idx < 0 or new_idx >= len(self._gamma_pattern_params):
        return
    if not _check_locked_or_prompt(self):
        return
    params = self._gamma_pattern_params
    params[idx], params[new_idx] = params[new_idx], params[idx]
    _mark_dirty(self)
    _refresh_tree(self)
    try:
        self.gamma_pattern_tree.selection_set(str(new_idx))
    except Exception:
        pass


def _generate_by_curve(self: "PQAutomationApp"):
    try:
        n = int(self._gamma_n_points_var.get().strip())
    except ValueError:
        messagebox.showerror("输入错误", "点数 N 必须为整数")
        return
    if n < 2 or n > 256:
        messagebox.showerror("输入错误", "点数 N 必须在 2~256 之间")
        return
    gen_name = self._gamma_gen_type_var.get()
    func = _GENERATORS.get(gen_name)
    if func is None:
        return
    if not _check_locked_or_prompt(self):
        return
    if not messagebox.askyesno("确认", f"将用 [{gen_name}] N={n} 替换当前列表，继续？"):
        return
    self._gamma_pattern_params = func(n)
    _mark_dirty(self)
    _refresh_tree(self)


def _paste_from_clipboard(self: "PQAutomationApp"):
    try:
        raw = self.gamma_pattern_frame.clipboard_get()
    except tk.TclError:
        messagebox.showinfo("提示", "剪贴板为空")
        return
    rows: list[list[int]] = []
    errors: list[str] = []
    for ln_no, line in enumerate(raw.splitlines(), 1):
        s = line.strip()
        if not s:
            continue
        if s.endswith("%"):
            try:
                pct = float(s.rstrip("%"))
                v = int(round(max(0, min(100, pct)) / 100.0 * 255))
                rows.append([v, v, v])
                continue
            except ValueError:
                errors.append(f"第 {ln_no} 行无法解析：{s}")
                continue
        parts = [p for p in s.replace(",", " ").replace(";", " ").split() if p]
        if len(parts) == 1:
            try:
                v = int(parts[0])
                if 0 <= v <= 255:
                    rows.append([v, v, v])
                    continue
            except ValueError:
                pass
            errors.append(f"第 {ln_no} 行无法解析：{s}")
            continue
        if len(parts) < 3:
            errors.append(f"第 {ln_no} 行字段不足：{s}")
            continue
        try:
            r, g, b = int(parts[0]), int(parts[1]), int(parts[2])
        except ValueError:
            errors.append(f"第 {ln_no} 行非整数：{s}")
            continue
        if not all(0 <= v <= 255 for v in (r, g, b)):
            errors.append(f"第 {ln_no} 行越界：{s}")
            continue
        rows.append([r, g, b])
    if not rows:
        messagebox.showerror("粘贴失败", "未识别到任何有效行\n" + "\n".join(errors[:5]))
        return
    if not _check_locked_or_prompt(self):
        return
    msg = f"识别到 {len(rows)} 行有效数据。"
    if errors:
        msg += f"\n忽略 {len(errors)} 行错误（示例）：\n" + "\n".join(errors[:3])
    msg += "\n\n是 = 替换当前列表 / 否 = 追加到末尾 / 取消"
    choice = messagebox.askyesnocancel("确认", msg)
    if choice is None:
        return
    if choice:
        self._gamma_pattern_params = rows
    else:
        self._gamma_pattern_params.extend(rows)
    _mark_dirty(self)
    _refresh_tree(self)


# ============================================================
# 元信息显示 & 校验
# ============================================================

def _update_meta_label(self: "PQAutomationApp", data: dict):
    meta = data.get("_meta") or {}
    parts = []
    if meta.get("locked"):
        parts.append("🔒 内置（只读，需另存为修改）")
    if meta.get("description"):
        parts.append(meta["description"])
    if meta.get("generator"):
        parts.append(f"生成器：{meta['generator']}")
    if meta.get("created"):
        parts.append(f"创建：{meta['created']}")
    self._gamma_meta_label.config(text="  |  ".join(parts) if parts else "")


def _run_validation(self: "PQAutomationApp"):
    params = self._gamma_pattern_params
    msgs: list[str] = []

    if len(params) < 2:
        msgs.append("⚠ 至少需要 2 个灰阶点")

    bad = [
        i for i, rgb in enumerate(params)
        if not all(0 <= int(v) <= 255 for v in rgb)
    ]
    if bad:
        msgs.append(f"⚠ 越界点 (索引): {bad}")

    non_gray = [
        i for i, (r, g, b) in enumerate(params)
        if not (int(r) == int(g) == int(b))
    ]
    if non_gray:
        msgs.append(f"ⓘ 非纯灰点 (R≠G≠B): {non_gray}（gamma 计算按 R 通道）")

    seen: dict[tuple, int] = {}
    dups: list[int] = []
    for i, rgb in enumerate(params):
        key = tuple(rgb)
        if key in seen:
            dups.append(i)
        else:
            seen[key] = i
    if dups:
        msgs.append(f"⚠ 重复点 (索引): {dups}")

    r_seq = [int(rgb[0]) for rgb in params]
    if len(r_seq) >= 2:
        is_desc = all(a >= b for a, b in zip(r_seq, r_seq[1:]))
        is_asc = all(a <= b for a, b in zip(r_seq, r_seq[1:]))
        if not (is_desc or is_asc):
            msgs.append("ⓘ R 通道非单调递增/递减")

    if not msgs:
        text = f"✔ 校验通过（共 {len(params)} 点）"
        style_name = "SuccessState.TLabel"
    else:
        text = f"共 {len(params)} 点  |  " + "  ".join(msgs)
        style_name = "WarningState.TLabel" if any(m.startswith("⚠") for m in msgs) else "Muted.TLabel"
    self._gamma_validate_label.config(text=text, style=style_name)


# ============================================================
# 初始加载
# ============================================================

def _load_initial(self: "PQAutomationApp"):
    """启动时优先加载激活预设；如无则加载当前 gray.json。"""
    active = pq_config.get_active_preset_name(_TEST_KIND)
    if active:
        try:
            data = pq_config.load_preset(_TEST_KIND, active)
            self._gamma_current_preset = active
            self._gamma_preset_locked = bool((data.get("_meta") or {}).get("locked"))
            self._gamma_pattern_params = [
                list(map(int, rgb)) for rgb in (data.get("pattern_params") or [])
            ]
            _refresh_tree(self)
            _refresh_preset_combo(self, select=active)
            _update_meta_label(self, data)
            _update_active_label(self)
            return
        except Exception:
            pass

    pattern = pq_config.get_pattern("gray")
    self._gamma_pattern_params = [
        list(map(int, rgb)) for rgb in (pattern.get("pattern_params") or [])
    ]
    self._gamma_current_preset = None
    self._gamma_preset_locked = False
    _refresh_tree(self)
    _update_meta_label(self, {})
    _update_active_label(self)


# ============================================================
# Mixin
# ============================================================

class GammaPatternPanelMixin:
    """挂到 PQAutomationApp 上的方法集合。"""

    create_gamma_pattern_panel = create_gamma_pattern_panel
    toggle_gamma_pattern_panel = toggle_gamma_pattern_panel
