"""
Materials Viewer - Materials Project 材料查询工具

功能：
    - 根据化学式查询 Materials Project 数据库
    - 显示材料信息：ID、空间群、能量、形成能、带隙
    - 支持点击表头排序
    - 支持带隙范围筛选
    - 支持下载 CIF 和 POSCAR 格式
    - 支持3D结构可视化（VESTA/ASE）
    - 支持批量下载
    - API Key 和 VESTA 路径自动保存

环境变量：
    MP_API_KEY: Materials Project API Key

作者：AI Assistant
日期：2026-03-19
"""

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import os
import json
import re
import base64
import atexit
import tempfile
import logging
from pathlib import Path
from typing import Dict, Tuple, List, Optional
from pymatgen.ext.matproj import MPRester
from pymatgen.io.vasp import Poscar

# 常量定义（在类定义之前）
CONFIG_DIR_NAME = ".materials_viewer"
LOG_FILE_NAME = "app.log"

# 配置日志
log_dir = Path.home() / CONFIG_DIR_NAME
log_dir.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler(log_dir / LOG_FILE_NAME, encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

# API Key：从环境变量读取（程序启动后会从配置文件加载）
API_KEY = os.getenv("MP_API_KEY", "")


class App:
    """Materials Viewer 主应用类"""

    # 类常量
    WINDOW_TITLE = "Materials Viewer"
    WINDOW_SIZE = "900x650"
    DEFAULT_GAP_MIN = 0.05
    DEFAULT_GAP_MAX = 10.0
    CONFIG_DIR_NAME = ".materials_viewer"
    CONFIG_FILE_NAME = "config.json"
    LOG_FILE_NAME = "app.log"

    # 列宽配置
    COL_WIDTHS = {
        "下载CIF": 60,
        "下载POSCAR": 70,
        "ID": 100,
        "空间群": 100,
        "能量": 100,
        "形成能": 100,
        "带隙": 80
    }

    # VESTA 默认路径
    VESTA_DEFAULT_PATHS = [
        r"C:\Program Files\VESTA\VESTA.exe",
        r"C:\Program Files (x86)\VESTA\VESTA.exe",
        r"C:\Users\{}\AppData\Local\VESTA\VESTA.exe",
    ]

    def __init__(self, root: tk.Tk) -> None:
        """初始化应用

        Args:
            root: Tkinter 根窗口
        """
        self.root = root
        self.root.title(self.WINDOW_TITLE)
        self.root.geometry(self.WINDOW_SIZE)
        self.root.configure(bg="#f5f5f5")

        # 数据存储
        self.data: List[Tuple[str, Dict]] = []
        self.sort_col: Optional[str] = None
        self.sort_reverse: bool = False

        # 临时文件列表，用于程序退出时清理
        self._temp_files: List[str] = []
        atexit.register(self._cleanup_temp_files)

        # 标题
        tk.Label(root, text="材料查询工具", font=("Microsoft YaHei", 18, "bold"), bg="#f5f5f5", fg="#333").pack(pady=15)

        # 搜索框
        f = tk.Frame(root, bg="#f5f5f5")
        f.pack(pady=15)
        tk.Label(f, text="化学式:", font=("Microsoft YaHei", 11), bg="#f5f5f5").pack(side="left")
        self.e = tk.Entry(f, width=15, font=("Microsoft YaHei", 11), relief="solid", bd=1)
        self.e.pack(side="left", padx=10)
        self.e.insert(0, "BaTiO3")
        # 绑定回车键触发查询
        self.e.bind("<Return>", lambda event: self.search())
        tk.Button(f, text="🔍 查询", command=self.search, bg="#4CAF50", fg="white",
                  font=("Microsoft YaHei", 10), relief="flat", padx=15, pady=5,
                  cursor="hand2", activebackground="#45a049").pack(side="left")
        tk.Button(f, text="⬇ 下载全部", command=self.download, bg="#2196F3", fg="white",
                  font=("Microsoft YaHei", 10), relief="flat", padx=15, pady=5,
                  cursor="hand2", activebackground="#1976D2").pack(side="left", padx=10)
        tk.Button(f, text="� 使用说明", command=self.show_help, bg="#9C27B0", fg="white",
                  font=("Microsoft YaHei", 10), relief="flat", padx=15, pady=5,
                  cursor="hand2", activebackground="#7B1FA2").pack(side="left")
        tk.Button(f, text="� 输入API Key", command=self.input_api_key, bg="#FF9800", fg="white",
                  font=("Microsoft YaHei", 10), relief="flat", padx=15, pady=5,
                  cursor="hand2", activebackground="#F57C00").pack(side="left", padx=10)
        tk.Button(f, text="⚙️ 设置VESTA路径", command=self.set_vesta_path, bg="#607D8B", fg="white",
                  font=("Microsoft YaHei", 10), relief="flat", padx=15, pady=5,
                  cursor="hand2", activebackground="#455A64").pack(side="left")

        # 筛选条件框
        filter_frame = tk.Frame(root, bg="#f5f5f5")
        filter_frame.pack(pady=5)

        # 带隙筛选
        tk.Label(filter_frame, text="带隙范围:", font=("Microsoft YaHei", 10), bg="#f5f5f5").pack(side="left")
        self.gap_min = tk.Entry(filter_frame, width=8, font=("Microsoft YaHei", 10), relief="solid", bd=1)
        self.gap_min.pack(side="left", padx=5)
        self.gap_min.insert(0, str(self.DEFAULT_GAP_MIN))
        tk.Label(filter_frame, text="-", font=("Microsoft YaHei", 10), bg="#f5f5f5").pack(side="left")
        self.gap_max = tk.Entry(filter_frame, width=8, font=("Microsoft YaHei", 10), relief="solid", bd=1)
        self.gap_max.pack(side="left", padx=5)
        self.gap_max.insert(0, str(self.DEFAULT_GAP_MAX))
        tk.Label(filter_frame, text="eV", font=("Microsoft YaHei", 10), bg="#f5f5f5").pack(side="left")

        # 空间群筛选
        tk.Label(filter_frame, text="  空间群:", font=("Microsoft YaHei", 10), bg="#f5f5f5").pack(side="left", padx=(20, 5))
        self.sg_filter = tk.Entry(filter_frame, width=10, font=("Microsoft YaHei", 10), relief="solid", bd=1)
        self.sg_filter.pack(side="left", padx=5)
        self.sg_filter.insert(0, "")
        tk.Label(filter_frame, text="(如: 99 或 P4mm)", font=("Microsoft YaHei", 9), bg="#f5f5f5", fg="#666").pack(side="left")

        # 筛选按钮
        tk.Button(filter_frame, text="应用筛选", command=self.apply_filter, bg="#00BCD4", fg="white",
                  font=("Microsoft YaHei", 9), relief="flat", padx=10, pady=2,
                  cursor="hand2", activebackground="#0097A7").pack(side="left", padx=10)
        tk.Button(filter_frame, text="重置", command=self.reset_filter, bg="#9E9E9E", fg="white",
                  font=("Microsoft YaHei", 9), relief="flat", padx=10, pady=2,
                  cursor="hand2", activebackground="#757575").pack(side="left")

        # VESTA路径存储和配置文件路径
        self.config_dir = Path.home() / self.CONFIG_DIR_NAME
        self.config_file = self.config_dir / self.CONFIG_FILE_NAME
        config = self._load_config()
        self.vesta_path = config.get("vesta_path", None)

        # 加载保存的 API Key（优先级：环境变量 > 配置文件）
        global API_KEY
        if not API_KEY and config.get("api_key"):
            API_KEY = config.get("api_key")

        # 表格样式
        style = ttk.Style()
        style.theme_use("clam")
        style.configure("Custom.Treeview",
                        font=("Microsoft YaHei", 10),
                        rowheight=32,
                        background="white",
                        fieldbackground="white",
                        foreground="#333")
        style.configure("Custom.Treeview.Heading",
                        font=("Microsoft YaHei", 10, "bold"),
                        background="#e3f2fd",
                        foreground="#1565c0",
                        relief="flat")
        style.layout("Custom.Treeview", [
            ('Custom.Treeview.treearea', {'sticky': 'nswe'})
        ])
        style.configure("Custom.Treeview",
                        relief="solid",
                        borderwidth=1)
        style.map("Custom.Treeview.Heading",
                  background=[("active", "#bbdefb")])
        style.map("Custom.Treeview",
                  background=[("selected", "#90caf9")],
                  foreground=[("selected", "#000")])

        # 表格容器
        frame = tk.Frame(root, bg="#f5f5f5")
        frame.pack(fill="both", expand=True, padx=20, pady=10)

        # 表格
        self.tree = ttk.Treeview(frame, columns=("下载CIF","下载POSCAR","ID","空间群","能量","形成能","带隙"),
                                  show="headings", style="Custom.Treeview")

        # 交替行颜色配置
        self.tree.tag_configure("even", background="#f8f9fa")
        self.tree.tag_configure("odd", background="white")
        self.columns = ("下载CIF","下载POSCAR","ID","空间群","能量","形成能","带隙")

        # 列配置（使用类常量）
        for c in self.columns:
            self.tree.heading(c, text=c, command=lambda col=c: self.sort_by_column(col))
            self.tree.column(c, width=self.COL_WIDTHS.get(c, 80), anchor="center")

        self.tree.column("下载CIF", anchor="center")
        self.tree.column("下载POSCAR", anchor="center")
        self.tree.column("ID", anchor="w")
        self.tree.column("空间群", anchor="center")

        # 滚动条
        vsb = ttk.Scrollbar(frame, orient="vertical", command=self.tree.yview)
        hsb = ttk.Scrollbar(frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)

        self.tree.grid(row=0, column=0, sticky="nsew")
        vsb.grid(row=0, column=1, sticky="ns")
        hsb.grid(row=1, column=0, sticky="ew")

        frame.grid_rowconfigure(0, weight=1)
        frame.grid_columnconfigure(0, weight=1)

        self.tree.bind("<Double-1>", self.on_double_click)

        # 状态栏
        self.status = tk.Label(root, text="就绪", font=("Microsoft YaHei", 9), bg="#e0e0e0", anchor="w", padx=10)
        self.status.pack(fill="x", side="bottom")

    def on_double_click(self, event):
        region = self.tree.identify_region(event.x, event.y)
        if region == "cell":
            column = self.tree.identify_column(event.x)
            item = self.tree.identify_row(event.y)
            if item and column in ("#1", "#2"):
                values = self.tree.item(item, "values")
                if values:
                    material_id = values[2]
                    fmt = "cif" if column == "#1" else "poscar"
                    self.download_single(material_id, fmt)
            elif item and column == "#3":  # 点击ID列显示结构可视化
                values = self.tree.item(item, "values")
                if values:
                    material_id = values[2]
                    self.visualize_structure(material_id)

    def _encrypt(self, text: str) -> str:
        """简单加密文本（Base64）"""
        if not text:
            return ""
        try:
            return base64.b64encode(text.encode()).decode()
        except Exception:
            return ""

    def _decrypt(self, encrypted: str) -> str:
        """解密文本"""
        if not encrypted:
            return ""
        try:
            return base64.b64decode(encrypted.encode()).decode()
        except Exception:
            return ""

    def _load_config(self):
        """加载配置文件"""
        try:
            if self.config_file.exists():
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                    # 解密 API Key
                    if config.get("api_key"):
                        config["api_key"] = self._decrypt(config["api_key"])
                    logger.info("配置文件加载成功")
                    return config
        except Exception as ex:
            logger.warning(f"加载配置文件失败: {ex}")
        return {}

    def _save_config(self, config):
        """保存配置文件"""
        try:
            self.config_dir.mkdir(parents=True, exist_ok=True)
            # 加密 API Key
            config_to_save = config.copy()
            if config_to_save.get("api_key"):
                config_to_save["api_key"] = self._encrypt(config_to_save["api_key"])
            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(config_to_save, f, ensure_ascii=False, indent=2)
            logger.info("配置文件保存成功")
        except Exception as ex:
            logger.error(f"保存配置失败: {ex}")

    def download_single(self, material_id, fmt="cif"):
        """下载单个材料"""
        # 预先生成文件名
        if fmt == "cif":
            filename = f"{material_id}.cif"
        else:
            filename = f"POSCAR_{material_id}"

        # 显示保存对话框，预设文件名
        from tkinter import filedialog
        filepath = filedialog.asksaveasfilename(
            initialfile=filename,
            defaultextension=".cif" if fmt == "cif" else "",
            filetypes=[("CIF files", "*.cif"), ("All files", "*.*")] if fmt == "cif" else [("POSCAR files", "POSCAR*"), ("All files", "*.*")]
        )
        if not filepath:
            logger.info(f"用户取消下载: {material_id}")
            return

        try:
            m = MPRester(API_KEY)
            s = m.get_structure_by_material_id(material_id)
            if fmt == "cif":
                s.to(filename=filepath, fmt="cif")
                logger.info(f"下载成功: {material_id} -> {filepath}")
                messagebox.showinfo("完成", f"{filename} 下载完成!")
            else:
                from pymatgen.io.vasp import Poscar
                poscar = Poscar(s)
                poscar.write_file(filepath)
                logger.info(f"下载成功: {material_id} -> {filepath}")
                messagebox.showinfo("完成", f"{filename} 下载完成!")
        except Exception as ex:
            logger.error(f"下载失败: {material_id}, 错误: {ex}")
            self._handle_error(ex, f"下载 {material_id} 失败")

    def sort_by_column(self, col):
        if col in ("下载CIF", "下载POSCAR"):
            return
        if self.sort_col == col:
            self.sort_reverse = not self.sort_reverse
        else:
            self.sort_col = col
            self.sort_reverse = False
        self._show(self.data, resort=True)

    def _get_sort_key(self, item):
        k, v = item
        col = self.sort_col
        if col == "ID":
            match = k.replace('-GGA', '').replace('mp-', '')
            try:
                return int(match)
            except ValueError:
                return k
        elif col == "空间群":
            return v.get('sg', '')
        elif col == "能量":
            return v.get('e', 0)
        elif col == "形成能":
            return v.get('f', 0)
        elif col == "带隙":
            return v.get('g', 0)
        return k

    def _validate_formula(self, formula: str) -> bool:
        """验证化学式格式

        Args:
            formula: 化学式字符串

        Returns:
            格式正确返回 True
        """
        if not formula:
            return False
        # 化学式格式：元素符号(首字母大写，可选小写) + 可选数字
        # 支持多元素，如 BaTiO3, H2O, C6H12O6
        pattern = r'^([A-Z][a-z]?\d*)+$'
        return bool(re.match(pattern, formula))

    def search(self):
        formula = self.e.get().strip()
        if not formula:
            return
        # 验证化学式格式
        if not self._validate_formula(formula):
            logger.warning(f"化学式格式无效: {formula}")
            messagebox.showwarning("格式错误", "化学式格式不正确\n\n正确示例：BaTiO3, H2O, SiO2\n\n规则：\n• 元素符号首字母大写\n• 可选小写字母\n• 可选数字")
            return
        logger.info(f"开始查询材料: {formula}")
        self.status.config(text=f"正在查询 {formula}...")
        threading.Thread(target=self._search, args=(formula,)).start()

    def _search(self, formula):
        try:
            m = MPRester(API_KEY)
            r = m.summary_search(formula=formula)
            e = m.get_entries(formula)
            d = {}
            # 从 summary_search 获取基本信息
            for i in r:
                symmetry = i.get('symmetry', {})
                sg_symbol = symmetry.get('symbol', '')
                sg_number = symmetry.get('number', '')
                d[i['material_id']] = {
                    'e': i['energy_per_atom'],
                    'f': i['formation_energy_per_atom'],
                    'g': i['band_gap'],
                    's': i['is_stable'],
                    'sg': sg_symbol,
                    'sg_num': sg_number
                }
            # 从 get_entries 补充空间群信息（如果 summary 中没有）
            for i in e:
                mid = i.entry_id.replace('-GGA', '')
                if mid in d and not d[mid]['sg']:
                    try:
                        sg_info = i.structure.get_space_group_info()
                        if sg_info:
                            d[mid]['sg'] = sg_info[0]
                            d[mid]['sg_num'] = sg_info[1] if len(sg_info) > 1 else ''
                    except Exception as sg_ex:
                        logger.warning(f"获取 {mid} 空间群失败: {sg_ex}")
                        d[mid]['sg'] = 'N/A'
                        d[mid]['sg_num'] = ''
            logger.info(f"查询成功: {formula}, 找到 {len(d)} 个材料")
            self.root.after(0, lambda: self._show(d, formula))
        except Exception as ex:
            error_msg = str(ex)
            logger.error(f"查询失败: {formula}, 错误: {error_msg}")
            self.root.after(0, lambda msg=error_msg: self._show_error(msg))

    def _show_error(self, msg):
        """显示错误信息"""
        self.status.config(text=f"查询失败: {msg}")
        messagebox.showerror("错误", msg)

    def _handle_error(self, ex, context=""):
        """统一处理异常"""
        error_msg = str(ex)
        user_friendly_msg = error_msg

        # 分类处理常见错误
        if "API_KEY" in error_msg or "api key" in error_msg.lower():
            user_friendly_msg = "API Key 无效或未设置。请设置环境变量 MP_API_KEY"
        elif "Connection" in error_msg or "timeout" in error_msg.lower():
            user_friendly_msg = "网络连接失败，请检查网络"
        elif "Permission" in error_msg:
            user_friendly_msg = "权限不足，无法写入文件"
        elif "No such file" in error_msg:
            user_friendly_msg = "目录不存在"
        elif "not found" in error_msg.lower():
            user_friendly_msg = "未找到该材料"

        if context:
            user_friendly_msg = f"{context}\n{user_friendly_msg}"

        self.status.config(text=f"错误: {error_msg[:50]}...")
        messagebox.showerror("错误", user_friendly_msg)

    def input_api_key(self):
        """弹出对话框输入 API Key"""
        dialog = tk.Toplevel(self.root)
        dialog.title("输入 API Key")
        dialog.geometry("400x180")
        dialog.configure(bg="#f5f5f5")
        dialog.transient(self.root)
        dialog.grab_set()

        tk.Label(dialog, text="请输入 Materials Project API Key:",
                 font=("Microsoft YaHei", 11), bg="#f5f5f5").pack(pady=10)

        entry = tk.Entry(dialog, width=40, font=("Microsoft YaHei", 10), show="*")
        entry.pack(pady=5)
        entry.insert(0, API_KEY)

        # 是否保存到配置文件的选项
        save_var = tk.BooleanVar(value=True)
        tk.Checkbutton(dialog, text="保存 API Key（下次启动自动加载）",
                       variable=save_var, bg="#f5f5f5",
                       font=("Microsoft YaHei", 9)).pack(pady=5)

        def save_key():
            global API_KEY
            new_key = entry.get().strip()
            if new_key:
                API_KEY = new_key
                # 保存到配置文件
                if save_var.get():
                    config = self._load_config()
                    config["api_key"] = new_key
                    self._save_config(config)
                    self.status.config(text="API Key 已更新并保存")
                else:
                    # 如果不保存，清除配置文件中的 api_key
                    config = self._load_config()
                    config.pop("api_key", None)
                    self._save_config(config)
                    self.status.config(text="API Key 已更新（未保存）")
                dialog.destroy()
            else:
                messagebox.showwarning("提示", "API Key 不能为空")

        tk.Button(dialog, text="确定", command=save_key,
                  bg="#4CAF50", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=30, pady=5).pack(pady=10)

    def set_vesta_path(self):
        """设置 VESTA 程序路径"""
        dialog = tk.Toplevel(self.root)
        dialog.title("设置 VESTA 路径")
        dialog.geometry("500x200")
        dialog.configure(bg="#f5f5f5")
        dialog.transient(self.root)
        dialog.grab_set()

        tk.Label(dialog, text="请选择 VESTA 可执行文件路径:",
                 font=("Microsoft YaHei", 11), bg="#f5f5f5").pack(pady=10)

        # 路径输入框
        path_frame = tk.Frame(dialog, bg="#f5f5f5")
        path_frame.pack(pady=5, padx=20, fill="x")

        path_entry = tk.Entry(path_frame, width=45, font=("Microsoft YaHei", 10))
        path_entry.pack(side="left", fill="x", expand=True)
        if self.vesta_path:
            path_entry.insert(0, self.vesta_path)

        def browse_file():
            from tkinter import filedialog
            file_path = filedialog.askopenfilename(
                title="选择 VESTA 可执行文件",
                filetypes=[("可执行文件", "*.exe"), ("所有文件", "*.*")]
            )
            if file_path:
                path_entry.delete(0, tk.END)
                path_entry.insert(0, file_path)

        tk.Button(path_frame, text="浏览...", command=browse_file,
                  bg="#2196F3", fg="white", font=("Microsoft YaHei", 9),
                  relief="flat", padx=10).pack(side="left", padx=5)

        # 提示信息
        tk.Label(dialog, text="提示：VESTA 默认路径通常是:\n"
                              "C:\\Program Files\\VESTA\\VESTA.exe",
                 font=("Microsoft YaHei", 9), bg="#f5f5f5", fg="#666",
                 justify="left").pack(pady=10)

        def save_path():
            path = path_entry.get().strip()
            if path and os.path.exists(path):
                self.vesta_path = path
                # 保存到配置文件
                config = self._load_config()
                config["vesta_path"] = path
                self._save_config(config)
                self.status.config(text=f"VESTA 路径已设置并保存: {path}")
                dialog.destroy()
            elif path:
                messagebox.showwarning("警告", "指定的文件不存在，请检查路径")
            else:
                self.vesta_path = None
                # 清除配置文件中的路径
                config = self._load_config()
                config.pop("vesta_path", None)
                self._save_config(config)
                self.status.config(text="VESTA 路径已清除，将使用自动查找")
                dialog.destroy()

        tk.Button(dialog, text="确定", command=save_path,
                  bg="#4CAF50", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=30, pady=5).pack(pady=10)

    def visualize_structure(self, material_id):
        """可视化晶体结构（优先使用 VESTA，否则使用 ASE）"""
        # 询问用户使用哪种工具
        dialog = tk.Toplevel(self.root)
        dialog.title("选择可视化工具")
        dialog.geometry("350x180")
        dialog.configure(bg="#f5f5f5")
        dialog.transient(self.root)
        dialog.grab_set()

        tk.Label(dialog, text=f"选择查看 {material_id} 结构的工具",
                 font=("Microsoft YaHei", 11, "bold"), bg="#f5f5f5").pack(pady=15)

        btn_frame = tk.Frame(dialog, bg="#f5f5f5")
        btn_frame.pack(pady=10)

        def use_vesta():
            dialog.destroy()
            self._visualize_with_vesta(material_id)

        def use_ase():
            dialog.destroy()
            self._visualize_with_ase(material_id)

        tk.Button(btn_frame, text="VESTA", command=use_vesta,
                  bg="#E91E63", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=25, pady=8).pack(side="left", padx=10)
        tk.Button(btn_frame, text="ASE", command=use_ase,
                  bg="#2196F3", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=25, pady=8).pack(side="left", padx=10)

    def _cleanup_temp_files(self):
        """清理所有临时文件"""
        for f in self._temp_files[:]:
            try:
                if os.path.exists(f):
                    os.unlink(f)
                    self._temp_files.remove(f)
            except Exception:
                pass

    def _visualize_with_vesta(self, material_id):
        """使用 VESTA 打开结构"""
        import subprocess

        try:
            self.status.config(text=f"正在准备 {material_id} 结构文件...")

            # 获取结构
            m = MPRester(API_KEY)
            structure = m.get_structure_by_material_id(material_id)

            # 创建临时 CIF 文件（使用 mkstemp 更安全）
            fd, cif_path = tempfile.mkstemp(suffix='.cif', prefix=f'{material_id}_')
            os.close(fd)
            self._temp_files.append(cif_path)
            structure.to(filename=cif_path, fmt="cif")

            # 查找 VESTA 可执行文件（优先使用用户设置的路径）
            vesta_exe = None

            # 1. 检查用户设置的路径
            if self.vesta_path and os.path.exists(self.vesta_path):
                vesta_exe = self.vesta_path

            # 2. 自动查找常见路径
            if not vesta_exe:
                for path_template in self.VESTA_DEFAULT_PATHS:
                    path = path_template.format(os.getenv('USERNAME'))
                    if os.path.exists(path):
                        vesta_exe = path
                        break

            # 3. 尝试从环境变量查找
            if not vesta_exe:
                import shutil
                vesta_exe = shutil.which("VESTA")

            if not vesta_exe:
                error_msg = f"未找到 VESTA 程序。\n"
                if self.vesta_path:
                    error_msg += f"已设置路径: {self.vesta_path}\n但该路径无效。\n"
                error_msg += "请使用 '⚙️ 设置VESTA路径' 按钮重新指定。\n"
                error_msg += "将使用 ASE 作为替代方案。"
                messagebox.showerror("错误", error_msg)
                self._visualize_with_ase(material_id)
                return

            # 启动 VESTA
            subprocess.Popen([vesta_exe, cif_path], shell=False)
            self.status.config(text=f"{material_id} 已在 VESTA 中打开")

            # 延迟删除临时文件（给 VESTA 足够时间加载）
            def cleanup():
                import time
                time.sleep(10)  # 增加等待时间确保 VESTA 完全加载
                try:
                    if os.path.exists(cif_path):
                        os.unlink(cif_path)
                        if cif_path in self._temp_files:
                            self._temp_files.remove(cif_path)
                except Exception:
                    pass

            threading.Thread(target=cleanup, daemon=True).start()

        except Exception as ex:
            self._handle_error(ex, f"使用 VESTA 打开 {material_id} 失败")

    def _visualize_with_ase(self, material_id):
        """使用 ASE GUI 可视化晶体结构（支持鼠标交互）"""
        try:
            from ase import Atoms
            from ase.visualize import view

            self.status.config(text=f"正在获取 {material_id} 结构...")

            # 获取结构
            m = MPRester(API_KEY)
            structure = m.get_structure_by_material_id(material_id)

            # 转换为 ASE Atoms
            atoms = Atoms(
                symbols=[str(site.specie) for site in structure],
                positions=structure.cart_coords,
                cell=structure.lattice.matrix,
                pbc=True
            )

            # 在独立线程中打开 ASE GUI（避免阻塞主界面）
            def open_viewer():
                try:
                    view(atoms)
                except Exception as e:
                    self.root.after(0, lambda: self._handle_error(e, "打开3D视图失败"))

            viewer_thread = threading.Thread(target=open_viewer)
            viewer_thread.daemon = True
            viewer_thread.start()

            self.status.config(text=f"{material_id} 3D视图已打开（支持鼠标旋转、缩放）")

        except ImportError:
            messagebox.showerror("错误", "请先安装 ASE 库:\npip install ase")
        except Exception as ex:
            self._handle_error(ex, f"可视化 {material_id} 失败")

    def show_help(self):
        """显示使用说明书"""
        help_text = """
Materials Viewer 使用说明

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📌 首次使用必读
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1️⃣ 设置 API Key
   • 点击"🔑 输入API Key"按钮
   • 输入 Materials Project API Key
   • 勾选"保存 API Key"（推荐）
   • 点击确定

   💡 获取 API Key：
   https://materialsproject.org/dashboard

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔍 查询材料
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2️⃣ 基本查询
   • 在"化学式"输入框输入材料（如 BaTiO3）
   • 点击"🔍 查询"按钮或按回车键
   • 等待结果显示在表格中

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 表格操作
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3️⃣ 排序
   • 点击表头（ID、空间群、能量等）排序
   • 再次点击切换升序/降序

4️⃣ 带隙筛选
   • 设置带隙范围（最小值-最大值，单位eV）
   • 点击"应用筛选"按钮
   • 点击"重置"恢复显示全部材料

5️⃣ 下载单个材料
   • 双击"下载CIF"列 → 下载 CIF 文件
   • 双击"下载POSCAR"列 → 下载 POSCAR 文件
   • 弹出对话框可修改文件名和保存位置

6️⃣ 3D结构可视化
   • 双击"ID"列（材料编号）
   • 选择可视化工具：
     - VESTA：专业晶体可视化软件（推荐）
     - ASE：Python 3D可视化库

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 批量操作
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

7️⃣ 批量下载
   • 点击"⬇ 下载全部"按钮
   • 选择格式（CIF 或 POSCAR）
   • 选择保存目录

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚙️ 高级设置
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

8️⃣ 设置 VESTA 路径
   • 点击"⚙️ 设置VESTA路径"
   • 浏览选择 VESTA.exe 文件
   • 路径将自动保存，下次启动生效

   💡 VESTA 默认路径：
   C:\\Program Files\\VESTA\\VESTA.exe

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🖱️ 鼠标操作（VESTA 3D视图）
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   • 左键拖动：旋转结构
   • 右键拖动：平移结构
   • 滚轮：缩放结构
   • 双击：重置视角

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ 常见问题
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q: API Key 无效？
A: 访问官网获取新 Key，或检查网络连接

Q: 查询不到材料？
A: 检查化学式拼写，或尝试简化形式

Q: VESTA 无法打开？
A: 使用"⚙️ 设置VESTA路径"手动指定位置

Q: 提示缺少 ASE？
A: 运行命令安装：pip install ase

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💾 配置文件位置
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   Windows: %USERPROFILE%\\.materials_viewer\\config.json

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"""

        dialog = tk.Toplevel(self.root)
        dialog.title("使用说明")
        dialog.geometry("550x600")
        dialog.configure(bg="#f5f5f5")
        dialog.transient(self.root)
        dialog.grab_set()

        # 标题
        tk.Label(dialog, text="📖 Materials Viewer 使用说明",
                 font=("Microsoft YaHei", 14, "bold"),
                 bg="#f5f5f5", fg="#333").pack(pady=10)

        # 文本框
        text_widget = tk.Text(dialog, font=("Microsoft YaHei", 10),
                              wrap="word", padx=10, pady=10,
                              bg="white", fg="#333",
                              relief="solid", bd=1)
        text_widget.pack(fill="both", expand=True, padx=15, pady=5)
        text_widget.insert("1.0", help_text)
        text_widget.config(state="disabled")  # 只读

        # 滚动条
        scrollbar = ttk.Scrollbar(text_widget, command=text_widget.yview)
        scrollbar.pack(side="right", fill="y")
        text_widget.config(yscrollcommand=scrollbar.set)

        # 关闭按钮
        tk.Button(dialog, text="关闭", command=dialog.destroy,
                  bg="#4CAF50", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=30, pady=5).pack(pady=10)

    def _show(self, d, formula=None, resort=False, filtered_data=None):
        if filtered_data is not None:
            self.data = filtered_data
        elif resort:
            self.data = sorted(self.data, key=self._get_sort_key, reverse=self.sort_reverse)
        else:
            self.data = sorted(d.items(), key=lambda x: x[1]['e'])
            # 保存原始数据用于筛选
            self._original_data = self.data.copy()
        self.tree.delete(*self.tree.get_children())
        for idx, (k, v) in enumerate(self.data):
            tag = "even" if idx % 2 == 0 else "odd"
            # 格式化空间群显示：符号 + 序号
            sg_symbol = v.get('sg', '')
            sg_num = v.get('sg_num', '')
            if sg_symbol and sg_num:
                sg_display = f"{sg_symbol} ({sg_num})"
            elif sg_symbol:
                sg_display = sg_symbol
            else:
                sg_display = 'N/A'
            self.tree.insert("", "end", values=("C", "P", k, sg_display, f"{v['e']:.3f}", f"{v['f']:.3f}", f"{v['g']:.2f}"), tags=(tag,))
        if formula:
            self.status.config(text=f"找到 {len(self.data)} 个 {formula} 材料")

    def apply_filter(self):
        """应用带隙和空间群筛选"""
        try:
            min_gap = float(self.gap_min.get().strip() or 0)
            max_gap = float(self.gap_max.get().strip() or 10)
        except ValueError:
            messagebox.showwarning("提示", "请输入有效的数字")
            return

        if not hasattr(self, '_original_data') or not self._original_data:
            messagebox.showwarning("提示", "请先查询材料")
            return

        # 获取空间群筛选条件
        sg_filter = self.sg_filter.get().strip()

        # 筛选数据
        filtered = []
        for k, v in self._original_data:
            # 带隙筛选
            if not (min_gap <= v['g'] <= max_gap):
                continue

            # 空间群筛选
            if sg_filter:
                sg_symbol = v.get('sg', '')
                sg_num = str(v.get('sg_num', ''))
                # 匹配空间群符号或序号
                if sg_filter.lower() not in sg_symbol.lower() and sg_filter != sg_num:
                    continue

            filtered.append((k, v))

        # 更新状态文本
        filter_desc = f"带隙 {min_gap}-{max_gap} eV"
        if sg_filter:
            filter_desc += f", 空间群 {sg_filter}"

        self._show(None, filtered_data=filtered)
        self.status.config(text=f"筛选结果: {len(filtered)} 个材料 ({filter_desc})")

    def reset_filter(self):
        """重置筛选"""
        self.gap_min.delete(0, tk.END)
        self.gap_min.insert(0, str(self.DEFAULT_GAP_MIN))
        self.gap_max.delete(0, tk.END)
        self.gap_max.insert(0, str(self.DEFAULT_GAP_MAX))
        self.sg_filter.delete(0, tk.END)
        if hasattr(self, '_original_data') and self._original_data:
            self._show(None, filtered_data=self._original_data)
            self.status.config(text=f"显示全部 {len(self._original_data)} 个材料")

    def download(self):
        """批量下载所有材料"""
        if not self.data:
            messagebox.showwarning("提示", "没有可下载的数据")
            return

        # 选择下载格式
        dialog = tk.Toplevel(self.root)
        dialog.title("选择下载格式")
        dialog.geometry("300x150")
        dialog.configure(bg="#f5f5f5")
        dialog.transient(self.root)
        dialog.grab_set()

        tk.Label(dialog, text="选择批量下载格式", font=("Microsoft YaHei", 12, "bold"),
                 bg="#f5f5f5").pack(pady=15)

        btn_frame = tk.Frame(dialog, bg="#f5f5f5")
        btn_frame.pack(pady=10)

        def do_download(fmt):
            dialog.destroy()
            folder = filedialog.askdirectory()
            if not folder:
                return
            try:
                m = MPRester(API_KEY)
                count = 0
                for k, v in self.data:
                    s = m.get_structure_by_material_id(k)
                    if fmt == "cif":
                        s.to(filename=os.path.join(folder, f"{k}.cif"), fmt="cif")
                    else:
                        from pymatgen.io.vasp import Poscar
                        poscar = Poscar(s)
                        poscar.write_file(os.path.join(folder, f"POSCAR_{k}"))
                    count += 1
                messagebox.showinfo("完成", f"成功下载 {count} 个文件!")
            except Exception as ex:
                self._handle_error(ex, "批量下载失败")

        tk.Button(btn_frame, text="CIF 格式", command=lambda: do_download("cif"),
                  bg="#4CAF50", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=20, pady=5).pack(side="left", padx=10)
        tk.Button(btn_frame, text="POSCAR 格式", command=lambda: do_download("poscar"),
                  bg="#2196F3", fg="white", font=("Microsoft YaHei", 10),
                  relief="flat", padx=20, pady=5).pack(side="left", padx=10)

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    root.mainloop()