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 drivers.UCD323_Function import UCDController
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 import custom_template_panel as _ctp
from app.views.panels import side_panels as _sp
from app.views.panels import cct_panel as _ccp
from app.views.panels import main_layout as _main
from app.views.panels import ai_image_panel as _aip
from app.views.panels import single_step_panel as _ssp
from app.views.panels import pantone_baseline_panel as _pbp
from app.views import panel_manager as PM
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_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 plot_accuracy as _plot_accuracy
from app.plots.plot_cct import plot_cct as _plot_cct
from app.plots.plot_contrast import plot_contrast as _plot_contrast
from app.plots.plot_eotf import plot_eotf as _plot_eotf
from app.plots.plot_gamma import plot_gamma as _plot_gamma
from app.plots.plot_gamut import plot_gamut as _plot_gamut
from app.views.chart_frame import (
    clear_chart as _cf_clear_chart,
    create_result_chart_frame as _cf_create_result_chart_frame,
    init_accuracy_chart as _cf_init_accuracy_chart,
    init_cct_chart as _cf_init_cct_chart,
    init_contrast_chart as _cf_init_contrast_chart,
    init_eotf_chart as _cf_init_eotf_chart,
    init_gamma_chart as _cf_init_gamma_chart,
    init_gamut_chart as _cf_init_gamut_chart,
    on_chart_tab_changed as _cf_on_chart_tab_changed,
    update_chart_tabs_state as _cf_update_chart_tabs_state,
)
from app.config_io import (
    clear_config_file as _cfg_clear_config_file,
    get_config_path as _cfg_get_config_path,
    load_pq_config as _cfg_load_pq_config,
    save_pq_config as _cfg_save_pq_config,
)
from app.tests.local_dimming import (
    clear_ld_records as _ld_clear_ld_records,
    measure_ld_luminance as _ld_measure_ld_luminance,
    save_local_dimming_results as _ld_save_local_dimming_results,
    send_ld_window as _ld_send_ld_window,
    start_local_dimming_test as _ld_start_local_dimming_test,
    stop_local_dimming_test as _ld_stop_local_dimming_test,
    update_ld_results as _ld_update_ld_results,
)
from app.services import PatternService
from app.device.connection import (
    check_com_connections as _dev_check_com_connections,
    check_port_connection as _dev_check_port_connection,
    disconnect_com_connections as _dev_disconnect_com_connections,
    enable_com_widgets as _dev_enable_com_widgets,
    get_available_com_ports as _dev_get_available_com_ports,
    get_available_ucd_ports as _dev_get_available_ucd_ports,
    refresh_com_ports as _dev_refresh_com_ports,
    update_connection_indicator as _dev_update_connection_indicator,
)
from app.runner.test_runner import (
    get_current_test_result as _run_get_current_test_result,
    new_pq_results as _run_new_pq_results,
    on_custom_template_test_completed as _run_on_custom_template_test_completed,
    on_test_completed as _run_on_test_completed,
    on_test_error as _run_on_test_error,
    run_custom_sdr_test as _run_run_custom_sdr_test,
    run_hdr_movie_test as _run_run_hdr_movie_test,
    run_screen_module_test as _run_run_screen_module_test,
    run_sdr_movie_test as _run_run_sdr_movie_test,
    run_test as _run_run_test,
    send_fix_pattern as _run_send_fix_pattern,
    test_cct as _run_test_cct,
    test_color_accuracy as _run_test_color_accuracy,
    test_contrast as _run_test_contrast,
    test_custom_sdr as _run_test_custom_sdr,
    test_eotf as _run_test_eotf,
    test_gamma as _run_test_gamma,
    test_gamut as _run_test_gamut,
)

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

class PQAutomationApp:
    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色度计
        self.ucd = UCDController()  # 信号发生器

        # 初始化测试状态
        self.testing = False
        self.test_thread = None
        # 采集节奏参数：默认在稳定性与速度之间取平衡，可按现场情况再微调。
        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()

        # 加载上次保存的设置
        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=180)
        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, bootstyle="primary")
        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
        )

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

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

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

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

        # 创建右侧结果显示区域
        self.result_frame = ttk.LabelFrame(self.control_frame_middle, text="测试结果")
        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()
        # 创建测试类型选择区域
        self.create_test_type_frame()
        # 创建操作按钮区域
        self.create_operation_frame()
        # 创建结果图表区域
        self.create_result_chart_frame()
        # 创建客户模板结果显示区域（黑底表格）
        self.create_custom_template_result_panel()
        # 在所有控件创建完成后，统一初始化测试类型
        self.root.after(100, self.initialize_default_test_type)

        # 状态栏
        self.status_var = tk.StringVar(value="就绪")
        self.status_bar = ttk.Label(
            root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W
        )
        self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)

    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")

    get_config_path = _cfg_get_config_path
    load_pq_config = _cfg_load_pq_config
    save_pq_config = _cfg_save_pq_config
    
    register_panel = PM.register_panel
    show_panel = PM.show_panel
    hide_all_panels = PM.hide_all_panels

    init_gamut_chart = _cf_init_gamut_chart
    init_gamma_chart = _cf_init_gamma_chart
    init_eotf_chart = _cf_init_eotf_chart
    init_cct_chart = _cf_init_cct_chart
    init_contrast_chart = _cf_init_contrast_chart
    init_accuracy_chart = _cf_init_accuracy_chart
    clear_chart = _cf_clear_chart
    create_result_chart_frame = _cf_create_result_chart_frame
    on_chart_tab_changed = _cf_on_chart_tab_changed

    create_floating_config_panel = _main.create_floating_config_panel
    create_test_items_content = _main.create_test_items_content
    create_signal_format_content = _main.create_signal_format_content
    create_connection_content = _main.create_connection_content
    create_operation_frame = _main.create_operation_frame
    create_test_type_frame = _main.create_test_type_frame
    update_config_info_display = _main.update_config_info_display
    on_screen_module_timing_changed = _main.on_screen_module_timing_changed
    update_test_items = _main.update_test_items
    on_test_type_change = _main.on_test_type_change

    create_cct_params_frame = _ccp.create_cct_params_frame
    on_sdr_cct_param_focus_out = _ccp.on_sdr_cct_param_focus_out
    save_sdr_cct_params = _ccp.save_sdr_cct_params
    on_hdr_cct_param_focus_out = _ccp.on_hdr_cct_param_focus_out
    save_hdr_cct_params = _ccp.save_hdr_cct_params
    recalculate_cct = _ccp.recalculate_cct
    recalculate_gamut = _ccp.recalculate_gamut
    on_cct_param_focus_out = _ccp.on_cct_param_focus_out
    save_cct_params = _ccp.save_cct_params
    reload_cct_params = _ccp.reload_cct_params
    toggle_cct_params_frame = _ccp.toggle_cct_params_frame
    on_screen_gamut_ref_changed = _ccp.on_screen_gamut_ref_changed
    on_sdr_gamut_ref_changed = _ccp.on_sdr_gamut_ref_changed
    on_hdr_gamut_ref_changed = _ccp.on_hdr_gamut_ref_changed

    get_available_ucd_ports = _dev_get_available_ucd_ports
    get_available_com_ports = _dev_get_available_com_ports
    refresh_com_ports = _dev_refresh_com_ports
    check_com_connections = _dev_check_com_connections
    update_connection_indicator = _dev_update_connection_indicator
    check_port_connection = _dev_check_port_connection
    enable_com_widgets = _dev_enable_com_widgets
    disconnect_com_connections = _dev_disconnect_com_connections

    create_custom_template_result_panel = _ctp.create_custom_template_result_panel
    show_custom_result_context_menu = _ctp.show_custom_result_context_menu
    set_custom_result_table_locked = _ctp.set_custom_result_table_locked
    start_custom_row_single_step = _ctp.start_custom_row_single_step
    copy_custom_result_table = _ctp.copy_custom_result_table
    clear_custom_template_results = _ctp.clear_custom_template_results
    auto_expand_custom_result_view = _ctp.auto_expand_custom_result_view
    append_custom_template_result = _ctp.append_custom_template_result
    start_custom_template_test = _ctp.start_custom_template_test
    update_custom_button_visibility = _ctp.update_custom_button_visibility

    create_log_panel = _sp.create_log_panel
    create_local_dimming_panel = _sp.create_local_dimming_panel
    toggle_local_dimming_panel = _sp.toggle_local_dimming_panel
    toggle_log_panel = _sp.toggle_log_panel
    update_sidebar_selection = _sp.update_sidebar_selection

    # ---- AI 图片对话面板 ----
    create_ai_image_panel = _aip.create_ai_image_panel
    toggle_ai_image_panel = _aip.toggle_ai_image_panel
    reload_ai_image_list = _aip.reload_ai_image_list

    # ---- 单步调试面板 ----
    create_single_step_panel = _ssp.create_single_step_panel
    toggle_single_step_panel = _ssp.toggle_single_step_panel

    # ---- Pantone 认证摸底测试面板 ----
    create_pantone_baseline_panel = _pbp.create_pantone_baseline_panel
    toggle_pantone_baseline_panel = _pbp.toggle_pantone_baseline_panel

    # ---- 单步调试面板（统一实现，委托到 side_panels 模块） ----
    _toggle_debug_panel = _sp._toggle_debug_panel
    toggle_screen_debug_panel = _sp.toggle_screen_debug_panel
    toggle_sdr_debug_panel = _sp.toggle_sdr_debug_panel
    toggle_hdr_debug_panel = _sp.toggle_hdr_debug_panel

    clear_config_file = _cfg_clear_config_file
    start_local_dimming_test = _ld_start_local_dimming_test
    update_ld_results = _ld_update_ld_results
    stop_local_dimming_test = _ld_stop_local_dimming_test
    send_ld_window = _ld_send_ld_window
    measure_ld_luminance = _ld_measure_ld_luminance
    clear_ld_records = _ld_clear_ld_records
    save_local_dimming_results = _ld_save_local_dimming_results

    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"]
        if include_gamut:
            attrs += ["recalc_gamut_btn", "sdr_recalc_gamut_btn", "hdr_recalc_gamut_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,
            }
            target_tab = tab_mapping.get(test_type, 0)

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

            self.signal_tabs.select(target_tab)
            self.signal_tabs.update()
            self.root.update_idletasks()

            if target_tab == 0:
                self.screen_module_signal_frame.tkraise()
            elif target_tab == 1:
                self.sdr_signal_frame.tkraise()
            elif target_tab == 2:
                self.hdr_signal_frame.tkraise()

            for i in range(3):
                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 _switch_chart_tabs_by_test_type(self, test_type):
        """按测试类型切换 Gamma/EOTF 与客户模板结果 Tab。"""
        if not hasattr(self, "chart_notebook"):
            return

        try:
            current_tabs = list(self.chart_notebook.tabs())
            gamma_tab_id = str(self.gamma_chart_frame)
            eotf_tab_id = str(self.eotf_chart_frame)

            if test_type == "hdr_movie":
                if gamma_tab_id in current_tabs:
                    gamma_index = current_tabs.index(gamma_tab_id)
                    self.chart_notebook.forget(gamma_index)
                if eotf_tab_id not in current_tabs:
                    self.chart_notebook.insert(1, self.eotf_chart_frame, text="EOTF 曲线")
            else:
                if eotf_tab_id in current_tabs:
                    eotf_index = current_tabs.index(eotf_tab_id)
                    self.chart_notebook.forget(eotf_index)
                if gamma_tab_id not in current_tabs:
                    self.chart_notebook.insert(1, self.gamma_chart_frame, text="Gamma 曲线")

            custom_tab_id = str(self.custom_template_tab_frame)
            current_tabs = list(self.chart_notebook.tabs())

            if test_type == "sdr_movie":
                if custom_tab_id not in current_tabs:
                    self.chart_notebook.add(self.custom_template_tab_frame, text="客户模板结果显示")
            else:
                if custom_tab_id in current_tabs:
                    self.chart_notebook.forget(self.custom_template_tab_frame)

            self.chart_notebook.update_idletasks()
        except Exception as e:
            if hasattr(self, "log_gui"):
                self.log_gui.log(f"切换 Gamma/EOTF Tab 失败: {str(e)}", level="error")

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

        # 更新测试项目和侧边栏
        self.update_test_items()
        self.update_sidebar_selection()
        self.on_test_type_change()
        self._switch_signal_format_tabs(test_type)
        self._switch_chart_tabs_by_test_type(test_type)

    def _check_start_preconditions(self):
        """检查开始测试前置条件：设备连接 & 未在测试中。"""
        if self.ca is None or self.ucd is None:
            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 测试，请设置正确的图像模式"
        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,
            )

            # 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)}")

    new_pq_results = _run_new_pq_results
    run_test = _run_run_test
    run_screen_module_test = _run_run_screen_module_test
    run_custom_sdr_test = _run_run_custom_sdr_test
    run_sdr_movie_test = _run_run_sdr_movie_test
    run_hdr_movie_test = _run_run_hdr_movie_test
    send_fix_pattern = _run_send_fix_pattern
    test_custom_sdr = _run_test_custom_sdr
    test_gamut = _run_test_gamut
    test_gamma = _run_test_gamma
    test_eotf = _run_test_eotf
    test_cct = _run_test_cct
    test_contrast = _run_test_contrast
    test_color_accuracy = _run_test_color_accuracy
 
    calculate_delta_e_2000 = staticmethod(_calc_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)

    plot_gamut = _plot_gamut
    plot_gamma = _plot_gamma
    plot_eotf = _plot_eotf
    plot_cct = _plot_cct
    plot_contrast = _plot_contrast
    plot_accuracy = _plot_accuracy

    on_test_completed = _run_on_test_completed
    on_custom_template_test_completed = _run_on_custom_template_test_completed
    get_current_test_result = _run_get_current_test_result
    on_test_error = _run_on_test_error

    update_chart_tabs_state = _cf_update_chart_tabs_state
    
    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测试"
        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值
            self.config.set_current_timing(self.screen_module_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()

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

            # 断开设备连接
            if self.ucd.status:
                self.ucd.close()
            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()
        # root = tk.Tk()
        root = ttk.Window(themename="yeti")
        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()
