#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数学公式浏览器（Tkinter + matplotlib mathtext）
- 左侧：公式列表（可搜索）
- 右侧：同一窗口显示公式（渲染）与释义/背景/好的点（即时切换，不弹新窗口）
- 可在同目录放置 formulas.json 覆盖内置示例
JSON 每项示例格式：
[
  {
    "title": "欧拉恒等式",
    "latex": "e^{i\\pi} + 1 = 0",
    "description": "简短释义...",
    "background": "关于欧拉或背景...",
    "commentary": "好的点或赏析..."
  }, ...
]
"""
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import json
import os
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from tkinter.scrolledtext import ScrolledText

# matplotlib for rendering math text
import matplotlib
matplotlib.use("TkAgg")

# ----------------- 内置公式示例 -----------------
SAMPLE_FORMULAS = [
    {
        "title": "勾股定理（Pythagorean theorem）",
        "latex": "a^2 + b^2 = c^2",
        "description": "在直角三角形中，斜边 c 的平方等于两条直角边 a、b 的平方和。",
        "background": "古希腊与古中国等多处古代数学文献都有类似表述，是几何学中最基础且常用的定理之一。",
        "commentary": "直接且直观，广泛应用于几何、向量长度、距离计算等。"
    },
    {
        "title": "二次方程求根公式（Quadratic formula）",
        "latex": "x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}",
        "description": "求 ax^2 + bx + c = 0 的解。",
        "background": "可由配方法推导而来，是代数学的标准公式。",
        "commentary": "能直接给出根的显式表达式；判别式 b^2-4ac 决定根的实虚与重根情况。"
    },
    {
        "title": "二项式定理（Binomial theorem）",
        "latex": "(x+y)^n = \\sum_{k=0}^n {n \\choose k} x^{n-k} y^k",
        "description": "展开 (x+y)^n 的通式，包含二项系数。",
        "background": "二项式系数与组合数、帕斯卡三角形相关，是组合与代数的桥梁。",
        "commentary": "可用于多项式展开、概率计算与级数推导。"
    },
    {
        "title": "泰勒展开（Taylor series）",
        "latex": "f(x)=\\sum_{n=0}^{\\infty} \\frac{f^{(n)}(a)}{n!}(x-a)^n",
        "description": "把光滑函数在点 a 附近展开为幂级数。",
        "background": "函数逼近与数值计算的基础，工程和物理计算中广泛使用。",
        "commentary": "通过有限项能近似函数，误差由余项估计控制。"
    },
    {
        "title": "欧拉恒等式（Euler's identity）",
        "latex": "e^{i\\pi} + 1 = 0",
        "description": "复指数函数的重要恒等式，将 e、i、π、1、0 五个基本常数联系在一起。",
        "background": "是复分析与三角函数的深刻联系的体现，被誉为最美的数学公式之一。",
        "commentary": "简洁优美，内含指数函数、复数与三角函数的核心关系。"
    },
    {
        "title": "导数定义（Definition of derivative）",
        "latex": "f'(x)=\\lim_{h\\to 0} \\frac{f(x+h)-f(x)}{h}",
        "description": "函数导数的极限定义，表示函数在某点的瞬时变化率。",
        "background": "微积分基本概念，后续导数运算法则从此定义推导。",
        "commentary": "概念层面非常重要，是连续变化与速率分析的基石。"
    },
    {
        "title": "积分基本定理（Fundamental theorem of calculus）",
        "latex": "\\int_a^b f(x)\\,dx = F(b)-F(a),\\quad F'=f",
        "description": "不定积分的原函数在定积分计算中的作用。",
        "background": "连接微分与积分两大核心概念，是微积分理论的桥梁。",
        "commentary": "极大简化了定积分的计算，并揭示了微分与积分的内在关系。"
    },
    {
        "title": "复数模与辐角表示（Polar form）",
        "latex": "z = r(\\cos\\theta + i\\sin\\theta)=re^{i\\theta}",
        "description": "将复数表示为模 r 与辐角 θ 的形式，便于乘除与幂运算。",
        "background": "欧拉公式连接指数与三角函数，使复数运算更直观。",
        "commentary": "在电路、信号处理与振动分析中非常实用。"
    },
    {
        "title": "等差数列求和（Arithmetic series）",
        "latex": "S_n=\\frac{n}{2}(a_1+a_n)",
        "description": "等差数列前 n 项和的公式。",
        "background": "高效计算等差序列和的经典公式，常用于速算与证明。",
        "commentary": "简单实用，常见于初等代数与竞赛题目。"
    },
    {
        "title": "几何体体积 - 圆柱（Cylinder volume）",
        "latex": "V=\\pi r^2 h",
        "description": "半径为 r、高为 h 的圆柱体积。",
        "background": "基本几何公式之一，源自平面面积与高的乘积。",
        "commentary": "直观且常用，工程与几何问题常见。"
    },
    # 你可以在此处继续补充更多公式
]

# ----------------- GUI 应用 -----------------


class FormulasBrowser(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("数学公式浏览器（同窗切换显示）")
        self.geometry("1000x640")
        self.minsize(800, 500)

        self.formulas = []
        self.load_data(SAMPLE_FORMULAS)
        self.try_load_external_json()
        self._build_ui()

    def try_load_external_json(self):
        fn = os.path.join(os.path.dirname(__file__), "formulas.json")
        if os.path.exists(fn):
            try:
                with open(fn, "r", encoding="utf-8") as f:
                    data = json.load(f)
                if isinstance(data, list) and data:
                    self.load_data(data)
                    messagebox.showinfo("已加载", "检测到 formulas.json 并已加载公式数据。")
            except Exception as e:
                messagebox.showwarning("加载失败", f"尝试加载 formulas.json 时出错：{e}")

    def load_data(self, lst):
        # 期望 lst 为由 dict 构成的列表
        self.formulas = []
        for it in lst:
            # 保证字段存在并默认 latex 为 plain text
            title = it.get("title", "（无标题）")
            latex = it.get("latex", it.get("formula", "")) or ""
            description = it.get("description", "")
            background = it.get("background", "")
            commentary = it.get("commentary", "")
            self.formulas.append({
                "title": title,
                "latex": latex,
                "description": description,
                "background": background,
                "commentary": commentary
            })
        # prepare display items
        self.items = [f"{p['title']}" for p in self.formulas]
        self.filtered_indexes = list(range(len(self.items)))

    def _build_ui(self):
        # Top: search bar and load button
        top = ttk.Frame(self, padding=6)
        top.pack(side=tk.TOP, fill=tk.X)

        ttk.Label(top, text="搜索（标题/公式/注释）:").pack(side=tk.LEFT)
        self.search_var = tk.StringVar()
        entry = ttk.Entry(top, textvariable=self.search_var, width=50)
        entry.pack(side=tk.LEFT, padx=6)
        entry.bind("<KeyRelease>", lambda e: self.on_search())

        ttk.Button(top, text="从文件加载 formulas.json",
                   command=self.load_from_file).pack(side=tk.RIGHT)

        # Main panes
        main = ttk.Frame(self)
        main.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)

        # Left: listbox
        left = ttk.Frame(main, width=300)
        left.pack(side=tk.LEFT, fill=tk.Y)
        ttk.Label(left, text="公式列表", font=(
            "Segoe UI", 10, "bold")).pack(anchor=tk.W)
        self.listbox = tk.Listbox(left, exportselection=False)
        self.listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.listbox.bind("<<ListboxSelect>>", lambda e: self.on_select())
        lb_scroll = ttk.Scrollbar(
            left, orient=tk.VERTICAL, command=self.listbox.yview)
        lb_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.listbox.config(yscrollcommand=lb_scroll.set)

        # Right: formula render + texts
        right = ttk.Frame(main)
        right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # Title label
        self.title_var = tk.StringVar(value="请选择左侧的公式")
        ttk.Label(right, textvariable=self.title_var, font=(
            "Segoe UI", 12, "bold")).pack(anchor=tk.W)

        # Figure for math rendering
        fig_frame = ttk.Frame(right)
        fig_frame.pack(fill=tk.BOTH, expand=False, pady=(6, 4))
        self.fig = Figure(figsize=(6, 2), dpi=120)
        self.ax = self.fig.add_subplot(111)
        self.ax.axis('off')
        self.canvas = FigureCanvasTkAgg(self.fig, master=fig_frame)
        self.canvas_widget = self.canvas.get_tk_widget()
        self.canvas_widget.pack(fill=tk.BOTH, expand=True)

        # Lower: tabs or stacked text areas for description/background/commentary
        info_frame = ttk.Frame(right)
        info_frame.pack(fill=tk.BOTH, expand=True, pady=(6, 0))

        # We'll show three sections vertically
        ttk.Label(info_frame, text="释义 / 注释：",
                  font=("Segoe UI", 10, "bold")).pack(anchor=tk.W)
        self.desc = ScrolledText(info_frame, height=6, wrap='word')
        self.desc.pack(fill=tk.BOTH, expand=False, pady=(0, 6))
        self.desc.config(state='disabled')

        bottom_row = ttk.Frame(info_frame)
        bottom_row.pack(fill=tk.BOTH, expand=True)
        left_col = ttk.Frame(bottom_row)
        left_col.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 4))
        right_col = ttk.Frame(bottom_row)
        right_col.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(4, 0))

        ttk.Label(left_col, text="背景：", font=(
            "Segoe UI", 10, "bold")).pack(anchor=tk.W)
        self.bg = ScrolledText(left_col, wrap='word')
        self.bg.pack(fill=tk.BOTH, expand=True)
        self.bg.config(state='disabled')

        ttk.Label(right_col, text="好的点 / 赏析：",
                  font=("Segoe UI", 10, "bold")).pack(anchor=tk.W)
        self.comm = ScrolledText(right_col, wrap='word')
        self.comm.pack(fill=tk.BOTH, expand=True)
        self.comm.config(state='disabled')

        # Bottom buttons
        btns = ttk.Frame(self)
        btns.pack(side=tk.BOTTOM, fill=tk.X)
        ttk.Button(btns, text="复制 LaTeX 到剪贴板", command=self.copy_latex).pack(
            side=tk.LEFT, padx=6, pady=6)
        ttk.Label(btns, text="（渲染由 matplotlib mathtext，支持常用 LaTeX 表达式）").pack(
            side=tk.LEFT, padx=6)

        # Fill listbox
        self.refresh_listbox()

    def refresh_listbox(self):
        self.listbox.delete(0, tk.END)
        for idx in self.filtered_indexes:
            self.listbox.insert(tk.END, self.items[idx])
        self.title("数学公式浏览器")
        if self.filtered_indexes:
            # select first by default
            self.listbox.selection_clear(0, tk.END)
            self.listbox.selection_set(0)
            self.on_select()
        else:
            self.clear_display()

    def on_search(self):
        q = self.search_var.get().strip().lower()
        if not q:
            self.filtered_indexes = list(range(len(self.items)))
        else:
            res = []
            for i, p in enumerate(self.formulas):
                if (q in p['title'].lower() or
                    q in p['latex'].lower() or
                    q in (p['description'] or "").lower() or
                    q in (p['background'] or "").lower() or
                        q in (p['commentary'] or "").lower()):
                    res.append(i)
            self.filtered_indexes = res
        self.refresh_listbox()

    def get_selected_formula(self):
        sel = self.listbox.curselection()
        if not sel:
            return None
        idx_in_filtered = sel[0]
        real_idx = self.filtered_indexes[idx_in_filtered]
        return self.formulas[real_idx]

    def on_select(self):
        formula = self.get_selected_formula()
        if formula:
            self.show_formula_inline(formula)

    def show_formula_inline(self, f):
        title = f.get("title", "")
        latex = f.get("latex", "")
        desc = f.get("description", "（暂无释义）")
        bg = f.get("background", "（暂无背景）")
        comm = f.get("commentary", "（暂无赏析）")

        self.title_var.set(title)

        # render latex using matplotlib mathtext
        self.ax.clear()
        self.ax.axis('off')
        # Place the formula centered; if latex empty show text
        try:
            if latex.strip():
                # mathtext expects $...$ or plain; ensure wrapped in $...$
                s = latex.strip()
                if not (s.startswith("$") and s.endswith("$")):
                    s = "$" + s + "$"
                # add as text centered
                self.ax.text(0.5, 0.5, s, fontsize=28,
                             ha='center', va='center')
            else:
                self.ax.text(0.5, 0.5, "(无 LaTeX 表达式)",
                             fontsize=14, ha='center', va='center')
            self.fig.tight_layout()
            self.canvas.draw()
        except Exception as e:
            # 避免渲染错误导致程序崩溃
            self.ax.clear()
            self.ax.axis('off')
            self.ax.text(0.5, 0.5, "渲染出错：" + str(e),
                         fontsize=10, ha='center', va='center')
            self.canvas.draw()

        # fill text areas
        self.desc.config(state='normal')
        self.desc.delete('1.0', tk.END)
        self.desc.insert(tk.END, desc)
        self.desc.config(state='disabled')

        self.bg.config(state='normal')
        self.bg.delete('1.0', tk.END)
        self.bg.insert(tk.END, bg)
        self.bg.config(state='disabled')

        self.comm.config(state='normal')
        self.comm.delete('1.0', tk.END)
        self.comm.insert(tk.END, comm)
        self.comm.config(state='disabled')

    def clear_display(self):
        self.title_var.set("无可显示公式")
        self.ax.clear()
        self.ax.axis('off')
        self.canvas.draw()
        for w in (self.desc, self.bg, self.comm):
            w.config(state='normal')
            w.delete('1.0', tk.END)
            w.insert(tk.END, "")
            w.config(state='disabled')

    def copy_latex(self):
        f = self.get_selected_formula()
        if not f:
            messagebox.showinfo("提示", "请先选择一个公式")
            return
        latex = f.get("latex", "")
        try:
            self.clipboard_clear()
            self.clipboard_append(latex)
            messagebox.showinfo("已复制", "LaTeX 表达式已复制到剪贴板")
        except Exception as e:
            messagebox.showwarning("复制失败", str(e))

    def load_from_file(self):
        fn = filedialog.askopenfilename(title="选择 formulas.json（UTF-8）",
                                        filetypes=[("JSON 文件", "*.json"), ("所有文件", "*.*")])
        if not fn:
            return
        try:
            with open(fn, "r", encoding="utf-8") as f:
                data = json.load(f)
            if not isinstance(data, list):
                raise ValueError("JSON 文件应为公式对象列表")
            self.load_data(data)
            self.on_search()
            messagebox.showinfo("加载完成", f"已加载 {len(self.formulas)} 条公式")
        except Exception as e:
            messagebox.showerror("加载失败", str(e))


if __name__ == "__main__":
    app = FormulasBrowser()
    app.mainloop()
