import ttkbootstrap as ttk
import tkinter as tk
from tkinter import messagebox, filedialog
import threading
import time
import os
import datetime
import traceback
import matplotlib.pyplot as plt
from app_version import APP_NAME, APP_VERSION, get_app_title
from app.ucd import (
    ConnectionChanged,
    DeviceKind,
    EventBus,
    PatternService,
    SignalService,
    UCD323Device,
)
from app.pq.pq_config import PQConfig
from app.pq.pq_result import PQResultStore
from app.export import (
    save_result_images as _save_result_images_impl,
    export_excel_report as _export_excel_report_impl,
    EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG,
)
from app.views.panels.custom_template_panel import CustomTemplatePanelMixin
from app.views.panels.side_panels import SidePanelsMixin
from app.views.panels.cct_panel import CctPanelMixin
from app.views.panels.main_layout import MainLayoutMixin
from app.views.panels.ai_image_panel import AIImagePanelMixin
from app.views.panels.single_step_panel import SingleStepPanelMixin
from app.views.panels.pantone_baseline_panel import PantoneBaselinePanelMixin
from app.views.panels.gamma_pattern_panel import GammaPatternPanelMixin
from app.views.panels.calman_panel import CalmanPanelMixin
from app.views.panel_manager import PanelManagerMixin
from app.logging_setup import setup_logging, attach_gui_handler

# Step 0/1 重构：资源工具和纯算法已迁移到 app/ 包，这里重新导入以保持
# 对原函数名/方法名的向后兼容（老代码内部仍用 self.calculate_* 调用）。
from app.resources import (
    backgroud_style_set,
    get_resource_path,
    load_icon,
)
from app.tests.color_accuracy import (
    calculate_accuracy_delta_e_2000 as _calc_accuracy_delta_e_2000,
    calculate_color_accuracy as _calc_color_accuracy,
    calculate_delta_e_2000 as _calc_delta_e_2000,
    get_accuracy_color_standards as _get_accuracy_color_standards,
)
from app.tests.eotf import calculate_pq_curve as _calc_pq_curve
from app.tests.gamma import calculate_gamma as _calc_gamma
from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage
from app.plots.plot_accuracy import PlotAccuracyMixin
from app.plots.plot_cct import PlotCctMixin
from app.plots.plot_contrast import PlotContrastMixin
from app.plots.plot_eotf import PlotEotfMixin
from app.plots.plot_gamma import PlotGammaMixin
from app.plots.plot_gamut import PlotGamutMixin
from app.views.chart_frame import ChartFrameMixin
from app.config_io import ConfigIOMixin
from app.tests.local_dimming import LocalDimmingMixin
from app.device.connection import DeviceConnectionMixin, ConnectionController
from app.runner.test_runner import TestRunnerMixin

plt.rcParams["font.family"] = ["sans-serif"]
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]


class _LegacyUcdConnected:
    """Shim for legacy ``self.ucd.status`` checks after the UCD service refactor."""

    status = True


_LEGACY_UCD_CONNECTED = _LegacyUcdConnected()


class PQAutomationApp(
    ConfigIOMixin,
    ChartFrameMixin,
    MainLayoutMixin,
    CctPanelMixin,
    DeviceConnectionMixin,
    CustomTemplatePanelMixin,
    SidePanelsMixin,
    AIImagePanelMixin,
    SingleStepPanelMixin,
    PantoneBaselinePanelMixin,
    GammaPatternPanelMixin,
    CalmanPanelMixin,
    LocalDimmingMixin,
    PanelManagerMixin,
    TestRunnerMixin,
    PlotGamutMixin,
    PlotGammaMixin,
    PlotEotfMixin,
    PlotCctMixin,
    PlotContrastMixin,
    PlotAccuracyMixin,
):
    def __init__(self, root):
        self.root = root
        self.root.title(get_app_title())
        self.root.geometry("900x650")
        self.root.minsize(900, 650)
        self.root.state("zoomed")
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.app_name = APP_NAME
        self.app_version = APP_VERSION

        self.config_cleared = False

        # 初始化设备连接状态
        self.ca = None  # CA410色度计

        # UCD：EventBus + 设备抽象 + 服务层；上层统一走 signal_service / ucd_device。
        self.event_bus = EventBus()
        self.ucd_device = UCD323Device(self.event_bus)
        self.signal_service = SignalService(self.ucd_device, self.event_bus)

        # 连接控制器：统一管理 CA/UCD 生命周期。
        # 旧的 check_com_connections / disconnect_com_connections 等模块级
        # 函数仍以类属性形式挂在 PQAutomationApp 上，内部全部委托给本对象。
        self.connection = ConnectionController(self)

        # 初始化测试状态
        self.testing = False
        self.test_thread = None
        # 采集节奏参数由 settings/pq_config.json → acquisition_timing 加载（见 load_pq_config）。
        self.pattern_settle_time = 0.4
        self.pattern_progress_log_step = 5

        # 创建主框架
        self.main_frame = ttk.Frame(root)
        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        backgroud_style_set()

        # 创建配置对象
        self.config = PQConfig()
        self.pattern_service = PatternService(self)
        # 结果管理器：按 test_type 保留每次测试结果，始终存在，避免未初始化错误
        self.results = PQResultStore()
        # 图表快照：按 test_type 缓存最近一次各图表的绘制参数，切换类型时可恢复
        self._chart_snapshots: dict = {}  # {test_type: {chart_name: args_tuple}}

        # 加载上次保存的设置
        self.config_file = self.get_config_path()
        self.load_pq_config()

        #  如果加载的配置不是屏模组，强制切换为屏模组
        if self.config.current_test_type != "screen_module":
            self.config.set_current_test_type("screen_module")

        # 初始化侧边栏功能显示状态 - 使用统一的页面管理
        self.current_panel = None  # 当前显示的面板名称
        self.panels = {}  # 存储所有面板的信息
        self.log_visible = False

        # 创建左侧面板
        self.left_frame = ttk.Frame(self.main_frame, width=208)
        self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=0, pady=5)
        self.left_frame.pack_propagate(False)

        # 创建左侧导航栏
        self.sidebar_frame = ttk.Frame(self.left_frame, style="Sidebar.TFrame")
        self.sidebar_frame.pack(fill=tk.BOTH, expand=True, padx=0, pady=5)
        # self.sidebar_frame.pack_propagate(False)

        # 创建右侧内容区域
        self.content_frame = ttk.Frame(self.main_frame)
        self.content_frame.pack(
            side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5
        )

        # 叠层容器：主界面与工具面板同尺寸叠放，切换时用 lift 而非 pack_forget。
        self.view_stack = ttk.Frame(self.content_frame)
        self.view_stack.pack(fill=tk.BOTH, expand=True)
        self.main_view = ttk.Frame(self.view_stack)
        self._ld_embedded = False
        self._view_stack_ready = False

        # 创建右侧内容区域的上中下三个分区
        self.control_frame_top = ttk.Frame(self.main_view)
        self.control_frame_top.pack(
            side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
        )

        self.control_frame_middle = ttk.Frame(self.main_view)
        self.control_frame_middle.pack(
            side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
        )

        self.control_frame_bottom = ttk.Frame(self.main_view)
        self.control_frame_bottom.pack(
            side=tk.TOP, fill=tk.BOTH, expand=True, padx=0, pady=5
        )

        # 创建右上角悬浮配置框
        self.create_floating_config_panel()

        # 创建右侧结果显示区域（无边框，纯 Frame，让图表占满）
        self.result_frame = ttk.Frame(self.control_frame_middle)
        self.result_frame.pack(
            side=tk.BOTTOM, fill=tk.BOTH, expand=True, padx=0, pady=5
        )

        # 创建日志显示区域
        self.create_log_panel()
        # 创建 Local Dimming 面板
        self.create_local_dimming_panel()
        # 创建 AI 图片对话面板
        self.create_ai_image_panel()
        # # 创建单步调试面板
        # self.create_single_step_panel()
        # 创建 Pantone 认证摸底测试面板
        self.create_pantone_baseline_panel()
        # 创建 Gamma 测试图案配置面板
        self.create_gamma_pattern_panel()
        # 创建 CALMAN 风格灰阶测试面板
        self.create_calman_panel()
        # 创建测试类型选择区域
        self.create_test_type_frame()
        self._setup_connection_event_handlers()
        self.refresh_connection_indicators()
        # 创建操作按钮区域
        self.create_operation_frame()
        # 创建结果图表区域
        self.create_result_chart_frame()
        # 创建客户模板结果显示区域（黑底表格）
        self.create_custom_template_result_panel()
        self.setup_view_stack()
        # 在所有控件创建完成后，统一初始化测试类型
        self.root.after(100, self.initialize_default_test_type)

        # 状态栏（现代化扁平条，跟随 ttkbootstrap 主题）
        self.status_var = tk.StringVar(value="\u25cf  就绪")
        status_container = ttk.Frame(root, style="StatusBar.TFrame")
        status_container.pack(side=tk.BOTTOM, fill=tk.X)
        self.status_bar = ttk.Label(
            status_container,
            textvariable=self.status_var,
            style="StatusBar.TLabel",
            anchor=tk.W,
        )
        self.status_bar.pack(side=tk.LEFT, fill=tk.X, expand=True)
        # 右侧版本号
        ttk.Label(
            status_container,
            text=f"v{APP_VERSION}",
            style="StatusBarAccent.TLabel",
            anchor=tk.E,
        ).pack(side=tk.RIGHT)

    @property
    def is_ucd_connected(self) -> bool:
        """UCD323 是否已连接（统一入口，替代旧的 ``self.ucd.status``）。"""
        return self.signal_service.is_connected

    @property
    def ucd(self):
        """Legacy UCD handle: ``None`` when disconnected, truthy object with ``.status`` when connected."""
        if not self.is_ucd_connected:
            return None
        return _LEGACY_UCD_CONNECTED

    def _setup_connection_event_handlers(self) -> None:
        """订阅连接事件，驱动 UCD / CA 指示灯（替代轮询 controller.status）。"""

        def on_connection_changed(evt: ConnectionChanged) -> None:
            if evt.device is DeviceKind.UCD:
                indicator = getattr(self, "ucd_status_indicator", None)
            elif evt.device is DeviceKind.CA:
                indicator = getattr(self, "ca_status_indicator", None)
            else:
                return
            if indicator is None:
                return
            state = "green" if evt.connected else "gray"
            self._dispatch_ui(self.update_connection_indicator, indicator, state)

        self.event_bus.subscribe(ConnectionChanged, on_connection_changed)

    def _dispatch_ui(self, fn, *args, **kwargs):
        """把 ``fn(*args, **kwargs)`` 调度到 Tk 主线程执行。

        统一替代散落各处的 ``self.root.after(0, lambda: ...)`` 写法：
        - 自动捕获异常并记录日志，避免工作线程静默丢失 UI 更新失败；
        - 参数用位置/关键字传入，绕开 ``lambda`` 闭包捕获变量的常见坑；
        - 允许在 UI 销毁（如关闭窗口）后安全失败。
        """
        def _runner():
            try:
                fn(*args, **kwargs)
            except Exception as exc:
                log = getattr(self, "log_gui", None)
                if log is not None:
                    try:
                        log.log(f"UI 调度异常: {exc}")
                    except Exception:
                        pass
        try:
            self.root.after(0, _runner)
        except Exception:
            pass

    def initialize_default_test_type(self):
        try:
            # 初始设置为屏模组
            self.change_test_type("screen_module")
        except Exception as e:
            if hasattr(self, "log_gui"):
                self.log_gui.log(f"初始化默认测试类型失败: {str(e)}", level="error")

    def _save_current_cct_params(self, swallow_errors=True):
        """按当前测试类型分发保存对应的 CCT 参数。"""
        try:
            current_type = self.config.current_test_type
            if current_type == "screen_module":
                self.save_cct_params()
            elif current_type == "sdr_movie":
                self.save_sdr_cct_params()
            elif current_type == "hdr_movie" and hasattr(self, "save_hdr_cct_params"):
                self.save_hdr_cct_params()
        except Exception as e:
            if not swallow_errors:
                raise
            if hasattr(self, "log_gui"):
                self.log_gui.log(f"保存参数失败: {str(e)}", level="error")

    def _save_cct_params_before_test_type_switch(self):
        """切换测试类型前，按当前类型保存色度参数。"""
        if not hasattr(self, "cct_x_ideal_var"):
            return
        self._save_current_cct_params()

    def _set_gamut_combos_state(self, state, success_msg=None, error_prefix="色域参考标准状态切换失败"):
        """统一切换三个色域参考下拉框的状态。"""
        try:
            for attr in ("screen_gamut_combo", "sdr_gamut_combo", "hdr_gamut_combo"):
                combo = getattr(self, attr, None)
                if combo is not None:
                    combo.configure(state=state)
            if success_msg and hasattr(self, "log_gui"):
                self.log_gui.log(success_msg, level="success")
        except Exception as e:
            if hasattr(self, "log_gui"):
                self.log_gui.log(f"{error_prefix}: {str(e)}", level="error")

    def _hide_recalc_buttons(self, include_gamut=False):
        """隐藏重新计算按钮。include_gamut=True 时同时隐藏色域重算按钮。"""
        attrs = ["recalc_cct_btn", "sdr_recalc_cct_btn", "hdr_recalc_cct_btn"]
        hidden = 0
        for attr in attrs:
            btn = getattr(self, attr, None)
            if btn is None:
                continue
            try:
                btn.grid_remove()
                hidden += 1
            except Exception:
                pass
        return hidden

    def _disable_debug_panel(self):
        """禁用并隐藏单步调试面板（统一实现）。"""
        if hasattr(self, "debug_panel"):
            try:
                self.debug_panel.disable_all_debug()
                self.log_gui.log("单步调试已禁用", level="success")
            except Exception as e:
                self.log_gui.log(f"禁用单步调试失败: {str(e)}", level="error")
        if hasattr(self, "debug_container"):
            try:
                self.debug_container.pack_forget()
                self.log_gui.log("单步调试面板已隐藏", level="success")
            except Exception as e:
                self.log_gui.log(f"隐藏调试面板失败: {str(e)}", level="error")

    def _set_config_panel_btn_state(self, state):
        """统一设置配置面板按钮状态（disabled/normal）。"""
        if not hasattr(self, "config_panel_frame"):
            return
        try:
            self.config_panel_frame.btn.configure(state=state)
        except Exception:
            pass

    def _apply_current_test_type(self, test_type):
        """更新 UI 变量与配置中的当前测试类型。"""
        self.test_type_var.set(test_type)
        if hasattr(self, "config") and hasattr(self.config, "set_current_test_type"):
            success = self.config.set_current_test_type(test_type)
            if not success and hasattr(self, "log_gui"):
                self.log_gui.log(f"切换测试类型失败: {test_type}", level="error")

    def _switch_signal_format_tabs(self, test_type):
        """切换信号格式 Tab 到目标测试类型。"""
        if not hasattr(self, "signal_tabs"):
            if hasattr(self, "log_gui"):
                self.log_gui.log("signal_tabs 尚未创建", level="error")
            return

        try:
            tab_mapping = {
                "screen_module": 0,
                "sdr_movie": 1,
                "hdr_movie": 2,
                "local_dimming": 3,
            }
            target_tab = tab_mapping.get(test_type, 0)

            for i in range(4):
                self.signal_tabs.tab(i, state="normal")

            self.signal_tabs.select(target_tab)

            for i in range(4):
                if i != target_tab:
                    self.signal_tabs.tab(i, state="disabled")

            if hasattr(self, "log_gui"):
                tab_names = ["屏模组测试", "SDR测试", "HDR"]
        except Exception as e:
            if hasattr(self, "log_gui"):
                self.log_gui.log(f"切换信号格式失败: {str(e)}", level="error")

    def _sync_custom_template_tab_visibility(self, test_type):
        """按测试类型与客户模板结果状态同步客户模板 Tab 可见性。"""
        if not hasattr(self, "_set_custom_template_tab_visible"):
            return

        # 客户模板结果 Tab 只属于 SDR Movie
        if test_type != "sdr_movie":
            self._set_custom_template_tab_visible(False)
            return

        has_custom_rows = False
        tree = getattr(self, "custom_result_tree", None)
        if tree is not None:
            try:
                has_custom_rows = len(tree.get_children()) > 0
            except Exception:
                has_custom_rows = False

        # SDR 下仅在客户模板测试进行中，或已有客户模板结果时显示。
        show_tab = has_custom_rows or (
            getattr(self, "testing", False)
            and getattr(self, "test_type_var", None) is not None
            and self.test_type_var.get() == "sdr_movie"
        )
        self._set_custom_template_tab_visible(show_tab)

    def change_test_type(self, test_type):
        """切换测试类型"""
        # 切换测试类型时，自动隐藏日志面板
        if self.current_panel in (
            "log",
        ):
            self.hide_all_panels()
        self._save_cct_params_before_test_type_switch()
        self._apply_current_test_type(test_type)

        # 更新测试项目和侧边栏
        self.update_test_items()
        if hasattr(self, "refresh_connection_indicators"):
            try:
                self.refresh_connection_indicators()
            except Exception:
                pass
        self.update_sidebar_selection()
        self.on_test_type_change()
        self._switch_signal_format_tabs(test_type)
        self._sync_custom_template_tab_visibility(test_type)
        self.sync_gamut_toolbar()
        self._schedule_chart_restore(test_type)

    def _schedule_chart_restore(self, test_type: str) -> None:
        """延迟恢复图表，避免阻塞测试类型切换时的 UI 响应。"""
        job = getattr(self, "_chart_restore_job", None)
        if job is not None:
            try:
                self.root.after_cancel(job)
            except Exception:
                pass
        self._chart_restore_job = self.root.after_idle(
            lambda tt=test_type: self._restore_charts_for_type(tt)
        )

    def _save_chart_snapshot(self, test_type: str, chart_name: str, args: tuple):
        """保存某次绘图的参数，以便切换测试类型时可以重绘。"""
        if test_type not in self._chart_snapshots:
            self._chart_snapshots[test_type] = {}
        self._chart_snapshots[test_type][chart_name] = args

    def _has_saveable_results(self, test_type: str) -> bool:
        """判断指定测试类型是否有可保存的历史结果。"""
        snapshots = self._chart_snapshots.get(test_type) or {}
        return bool(snapshots) or (
            hasattr(self, "results") and self.results.has(test_type)
        )

    def _sync_save_button_state(self):
        """根据当前测试类型是否有可保存结果，同步保存按钮状态。"""
        if not hasattr(self, "save_btn"):
            return
        if getattr(self, "testing", False):
            self.save_btn.config(state=tk.DISABLED)
            return
        test_type = self.test_type_var.get() if hasattr(self, "test_type_var") else None
        if not test_type:
            return
        state = tk.NORMAL if self._has_saveable_results(test_type) else tk.DISABLED
        self.save_btn.config(state=state)

    def _restore_charts_for_type(self, test_type: str):
        """
        切换测试类型后恢复图表显示：
        - 该类型有历史结果 → 切换活跃结果 + 重绘所有已缓存图表 + 启用保存按钮
        - 该类型无历史结果 → 清空图表 + 禁用保存按钮
        """
        self.results.set_active(test_type)
        if not self._has_saveable_results(test_type):
            self.clear_chart()
            self._sync_save_button_state()
            return
        snapshots = self._chart_snapshots.get(test_type) or {}
        for chart_name, args in snapshots.items():
            plot_fn = getattr(self, f"plot_{chart_name}", None)
            if plot_fn:
                try:
                    plot_fn(*args)
                except Exception:
                    pass
        self._sync_save_button_state()

    def _check_start_preconditions(self):
        """检查开始测试前置条件：设备连接 & 未在测试中。"""
        if self.ca is None or not self.signal_service.is_connected:
            messagebox.showerror("错误", "请先连接CA410和信号发生器")
            return False
        if self.testing:
            messagebox.showinfo("提示", "测试已在进行中")
            return False
        return True

    def _collapse_config_panel_for_test(self):
        """收起配置项并禁用其按钮。"""
        if not hasattr(self, "config_panel_frame"):
            return
        try:
            if self.config_panel_frame.winfo_viewable():
                self.config_panel_frame.btn.invoke()
                self.root.update_idletasks()
                time.sleep(0.2)
        except Exception:
            pass
        self._set_config_panel_btn_state("disabled")

    def _set_ui_testing_state(self):
        """切换主 UI 到测试中状态（按钮禁用、状态栏、清空日志/图表）。"""
        self.testing = True
        self.start_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)
        self.save_btn.config(state=tk.DISABLED)
        self.clear_config_btn.config(state=tk.DISABLED)
        self.status_var.set("测试进行中...")
        self.log_gui.clear_log()
        self.clear_chart()

    def _prepare_ui_for_test_start(self):
        """为开始测试准备 UI（禁用调试、收起配置、禁用色域、隐藏重算按钮、切换状态）。"""
        self._disable_debug_panel()
        self._collapse_config_panel_for_test()
        self._set_gamut_combos_state("disabled", error_prefix="禁用色域参考标准失败")
        self._hide_recalc_buttons(include_gamut=False)
        self._set_ui_testing_state()

    def _rollback_test_start(self):
        """用户取消测试时恢复 UI 状态。"""
        self.testing = False
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)
        self.clear_config_btn.config(state=tk.NORMAL)
        self.status_var.set("测试已取消")
        self._set_config_panel_btn_state("normal")

    def _build_test_start_message(self, test_type):
        """根据测试类型生成确认弹框提示文案。"""
        if test_type == "screen_module":
            return "开始屏模组性能测试，请 byPass All PQ"
        if test_type == "sdr_movie":
            return "开始 SDR Movie 测试，请设置正确的图像模式"
        if test_type == "hdr_movie":
            return "开始 HDR Movie 测试，请设置正确的图像模式"
        if test_type == "local_dimming":
            return "Local Dimming 为手动模式，请在右侧面板发送图案并采集数据"
        return f"开始{self.get_test_type_name(test_type)}测试"

    def _launch_test_thread(self, test_type, test_items):
        """在新线程中执行测试。"""
        self.test_thread = threading.Thread(
            target=self.run_test, args=(test_type, test_items)
        )
        self.test_thread.daemon = True
        self.test_thread.start()

    def start_test(self):
        """开始测试"""
        if not self._check_start_preconditions():
            return

        test_type = self.test_type_var.get()
        test_items = self.get_selected_test_items()
        if not test_items:
            messagebox.showinfo("提示", "请至少选择一个测试项目")
            return

        self._prepare_ui_for_test_start()

        message = self._build_test_start_message(test_type)
        if not messagebox.askyesno("确认测试", message):
            self._rollback_test_start()
            return

        self._launch_test_thread(test_type, test_items)

    def _confirm_stop_test(self):
        """弹出确认停止测试对话框。"""
        return messagebox.askyesno(
            "确认停止测试",
            "测试正在进行中，确定要停止吗？\n\n 停止后将放弃本次测试的所有数据，无法保存。",
            icon="warning",
        )

    def _signal_stop_and_update_ui(self):
        """设置停止标志并立即更新 UI 以反馈给用户。"""
        self.testing = False
        self.log_gui.log("=" * 50, level="separator")
        self.log_gui.log("正在停止测试...", level="info")
        self.stop_btn.config(state=tk.DISABLED)
        self.status_var.set("正在停止测试，请稍候...")
        self.root.update()

    def _wait_for_test_thread(self, timeout_seconds=5):
        """等待测试线程结束，最多 timeout_seconds 秒，同时保持 UI 响应。"""
        if not (self.test_thread and self.test_thread.is_alive()):
            return
        self.log_gui.log("等待测试线程结束...", level="info")
        for _ in range(int(timeout_seconds * 10)):
            if not self.test_thread.is_alive():
                break
            time.sleep(0.1)
            self.root.update()
        if self.test_thread.is_alive():
            self.log_gui.log("测试线程未能正常结束，将在后台继续等待", level="error")
        else:
            self.log_gui.log("测试线程已结束", level="success")

    def _clear_test_data(self):
        """清空测试结果对象与中间数据缓存。"""
        try:
            self.log_gui.log("清理测试数据...", level="info")
            if hasattr(self, "results") and self.results is not None:
                self.results.clear()
                self.log_gui.log("  测试结果对象已清空", level="success")
            for attr in [
                "gamut_results",
                "gamma_results",
                "cct_results",
                "contrast_results",
                "accuracy_results",
            ]:
                if hasattr(self, attr):
                    setattr(self, attr, None)
            self.log_gui.log("  所有中间数据已清空", level="success")
        except Exception as e:
            self.log_gui.log(f"清理数据时出错: {str(e)}", level="error")

    def _clear_charts_and_tables(self):
        """清空图表与客户模板结果表格，并跳转到色域图 Tab。"""
        try:
            self.clear_chart()
            self.log_gui.log("图表已清空", level="success")
        except Exception as e:
            self.log_gui.log(f"清空图表时出错: {str(e)}", level="error")
        try:
            self.clear_custom_template_results()
            self.log_gui.log("客户模板结果表格已清空", level="success")
        except Exception as e:
            self.log_gui.log(f"清空客户模板结果表格失败: {str(e)}", level="error")
        try:
            if hasattr(self, "chart_notebook"):
                self.chart_notebook.select(self.gamut_chart_frame)
                self.root.update_idletasks()
        except Exception as e:
            self.log_gui.log(f"跳转到色域图失败: {str(e)}", level="error")

    def _restore_ui_after_stop(self):
        """恢复主按钮与状态栏到非测试态。"""
        self.set_custom_result_table_locked(False)
        self.start_btn.config(state=tk.NORMAL)
        self.stop_btn.config(state=tk.DISABLED)
        self.save_btn.config(state=tk.DISABLED)
        self.clear_config_btn.config(state=tk.NORMAL)
        if hasattr(self, "custom_btn"):
            self.custom_btn.config(state=tk.NORMAL)
        self.status_var.set("测试已停止 - 数据已清空")
        if hasattr(self, "config_panel_frame"):
            self._set_config_panel_btn_state("normal")

    def _finalize_stop_test(self):
        """延迟执行的清理流程总入口（由 root.after 调用）。"""
        self._clear_test_data()
        self._clear_charts_and_tables()
        self._restore_ui_after_stop()
        self._set_gamut_combos_state(
            "disabled",
            success_msg="色域参考标准已禁用",
            error_prefix="禁用色域参考标准失败",
        )
        hidden = self._hide_recalc_buttons(include_gamut=True)
        if hidden > 0:
            self.log_gui.log(f"已隐藏 {hidden} 个重新计算按钮", level="success")
        self._disable_debug_panel()
        self.log_gui.log("=" * 50, level="separator")
        self.log_gui.log("测试已停止，所有数据已清空", level="success")
        messagebox.showinfo(
            "测试已停止",
            "测试已停止，本次测试数据已清空。\n\n可以重新开始新的测试。",
        )

    def stop_test(self):
        """停止测试 - 放弃本次所有数据（完全集成版）"""
        if not self.testing:
            return
        if not self._confirm_stop_test():
            return
        self._signal_stop_and_update_ui()
        self._wait_for_test_thread(timeout_seconds=5)
        self.root.after(1000, self._finalize_stop_test)

    # ==================== 保存测试结果 ====================
    def save_results(self):
        """保存测试结果（图片 + Excel）。实现委派给 app.export。"""
        save_dir = filedialog.askdirectory(title="选择保存测试结果的目录")
        if not save_dir:
            return

        try:
            timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
            test_type = self.get_test_type_name(self.test_type_var.get())
            result_dir = os.path.join(save_dir, f"{test_type}_{timestamp}")
            os.makedirs(result_dir, exist_ok=True)

            current_test_type = self.test_type_var.get()
            selected_items = self.get_selected_test_items()
            log = self.log_gui.log

            log(f"保存测试类型: {current_test_type}", level="info")
            log(f"已选测试项: {selected_items}", level="info")

            # 1) 图片
            _save_result_images_impl(
                result_dir, current_test_type, selected_items,
                lambda attr: getattr(self, attr, None),
                log,
                app=self,
            )

            # 2) Excel
            if (current_test_type in _EXCEL_EXPORT_CONFIG
                    and hasattr(self, "results") and self.results):
                _export_excel_report_impl(
                    result_dir, current_test_type, selected_items,
                    self.results, log,
                )

            # 3) 成功提示
            log("=" * 50, level="separator")
            log(f"测试结果已保存到目录: {result_dir}", level="success")
            messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}")

        except Exception as e:
            self.log_gui.log(f"保存测试结果失败: {str(e)}", level="error")
            import traceback

            self.log_gui.log(traceback.format_exc(), level="error")
            messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")

    # 纯算法函数：作为 staticmethod 保留在主类（不依赖 self，且 calculate_xxx
    # 的命名空间由历史代码以 self.calculate_xxx 调用）。
    calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000)
    calculate_accuracy_delta_e_2000 = staticmethod(_calc_accuracy_delta_e_2000)
    get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards)
    calculate_gamut_coverage = staticmethod(_calc_gamut_coverage)
    calculate_gamma = staticmethod(_calc_gamma)
    calculate_color_accuracy = staticmethod(_calc_color_accuracy)
    calculate_pq_curve = staticmethod(_calc_pq_curve)

    def get_test_type_name(self, test_type):
        """获取测试类型的显示名称"""
        if test_type == "screen_module":
            return "屏模组性能测试"
        elif test_type == "sdr_movie":
            return "SDR Movie测试"
        elif test_type == "hdr_movie":
            return "HDR Movie测试"
        elif test_type == "local_dimming":
            return "Local Dimming"
        return test_type

    def get_selected_test_items(self):
        """获取当前选中的测试项"""
        selected_items = []
        for var_name, var in self.test_vars.items():
            if var.get():
                selected_items.append(var_name.split("_")[-1])
        return selected_items

    def update_config(self, event=None):
        """更新配置"""
        try:
            self.config.set_device_config(
                self.ca_com_var.get(),
                self.ucd_list_var.get(),
                self.ca_channel_var.get(),
            )

            # 保存当前选中的测试项到配置
            self.config.set_current_test_items(self.get_selected_test_items())
            # 按当前测试类型保存对应 timing，避免误覆盖其它测试类型配置。
            if self.config.current_test_type == "screen_module":
                self.config.set_current_timing(self.screen_module_timing_var.get())
            elif (
                self.config.current_test_type == "sdr_movie"
                and hasattr(self, "sdr_timing_var")
            ):
                self.config.set_current_timing(self.sdr_timing_var.get())
            elif (
                self.config.current_test_type == "local_dimming"
                and hasattr(self, "local_dimming_timing_var")
            ):
                self.config.set_current_timing(self.local_dimming_timing_var.get())
            # 自动保存配置到文件
            self.save_pq_config()

        except Exception as e:
            self.log_gui.log(f"更新配置失败: {str(e)}", level="error")

    def update_config_and_tabs(self):
        """更新配置并同步Tab状态"""
        self.update_config()
        self.update_chart_tabs_state()

        # 根据当前测试类型保存对应参数
        if "cct" in self.get_selected_test_items():
            self._save_current_cct_params()

        # 控制参数框的显示
        self.toggle_cct_params_frame()

        # 同步刷新顶部 header 折叠预览（现代化布局新增）
        if hasattr(self, "refresh_config_preview"):
            try:
                self.refresh_config_preview()
            except Exception:
                pass

    def on_closing(self):
        """窗口关闭时的处理"""
        try:
            # 检查是否清理了配置
            if not self.config_cleared:
                # 保存配置
                self.save_pq_config()
            else:
                print("配置已清理，不再保存")

            # 断开设备连接
            if self.signal_service.is_connected:
                self.connection.disconnect_ucd()
            if self.ca is not None:
                self.ca.close()

            # 关闭窗口
            self.root.destroy()

        except Exception as e:
            print(f"关闭窗口时出错: {str(e)}")
            self.root.destroy()


def main():
    try:
        setup_logging()
        # 先以浅色主题启动 Window，再根据用户偏好（含自定义 Calman 深色主题）切换
        root = ttk.Window(themename="yeti")
        from app.views.theme_manager import apply_initial_theme
        apply_initial_theme()
        app = PQAutomationApp(root)
        # GUI 创建完成后，把 logging 记录同步到日志面板
        if hasattr(app, "log_gui"):
            attach_gui_handler(app.log_gui)
        root.mainloop()
    except Exception as e:
        print("程序发生错误:", e)
        traceback.print_exc()
    finally:
        pass

if __name__ == "__main__":
    main()
