import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import csv
import os
import io

# 尽量按需导入 matplotlib 和 pandas（可选）
try:
    import matplotlib
    matplotlib.use('Agg')  # 先设后端，实际绘图用 FigureCanvasTkAgg
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    import matplotlib.pyplot as plt
    HAS_MPL = True
except Exception:
    HAS_MPL = False

try:
    import pandas as pd
    HAS_PANDAS = True
except Exception:
    HAS_PANDAS = False

class ScorePlotter(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("成绩走势图")
        self.geometry("900x600")
        self.data = None  # pandas DataFrame 或 list of dicts
        self.filename = None

        self._create_widgets()

    def _create_widgets(self):
        # 顶部按钮栏
        toolbar = ttk.Frame(self)
        toolbar.pack(side='top', fill='x', padx=6, pady=6)

        ttk.Button(toolbar, text="导入 CSV", command=self.load_csv).pack(side='left')
        ttk.Button(toolbar, text="导出 CSV", command=self.export_csv).pack(side='left', padx=4)
        ttk.Button(toolbar, text="导出图像 (PNG)", command=self.export_plot).pack(side='left', padx=4)

        ttk.Label(toolbar, text="视图:").pack(side='left', padx=(10,2))
        self.view_var = tk.StringVar(value='按学生')
        view_box = ttk.Combobox(toolbar, textvariable=self.view_var, values=['按学生', '按科目'], width=10, state='readonly')
        view_box.pack(side='left')

        ttk.Label(toolbar, text="图表类型:").pack(side='left', padx=(10,2))
        self.chart_var = tk.StringVar(value='折线图')
        chart_box = ttk.Combobox(toolbar, textvariable=self.chart_var, values=['折线图', '柱状图'], width=8, state='readonly')
        chart_box.pack(side='left')

        ttk.Label(toolbar, text="选择:").pack(side='left', padx=(10,2))
        self.select_var = tk.StringVar()
        self.select_cb = ttk.Combobox(toolbar, textvariable=self.select_var, values=[], width=20)
        self.select_cb.pack(side='left')
        ttk.Button(toolbar, text="绘制", command=self.plot).pack(side='left', padx=6)

        ttk.Button(toolbar, text="统计信息", command=self.show_stats).pack(side='right')

        # 中央分割：左侧表格，右侧画布
        main = ttk.Frame(self)
        main.pack(fill='both', expand=True, padx=6, pady=6)

        left = ttk.Frame(main)
        left.pack(side='left', fill='both', expand=True)

        self.tree = ttk.Treeview(left, columns=('c1',), show='headings')
        self.tree.pack(fill='both', expand=True, side='left')
        scrollbar = ttk.Scrollbar(left, orient='vertical', command=self.tree.yview)
        scrollbar.pack(side='right', fill='y')
        self.tree.config(yscrollcommand=scrollbar.set)

        right = ttk.Frame(main, width=400)
        right.pack(side='right', fill='both', expand=False)

        # 如果有 matplotlib，放置画布位置；若没有则显示提示标签
        if HAS_MPL:
            self.fig, self.ax = plt.subplots(figsize=(5,4))
            self.canvas = FigureCanvasTkAgg(self.fig, master=right)
            self.canvas.get_tk_widget().pack(fill='both', expand=True)
        else:
            ttk.Label(right, text="未安装 matplotlib，无法绘图。\n请运行 pip install matplotlib", foreground='red').pack(padx=10, pady=10)

    def load_csv(self):
        path = filedialog.askopenfilename(title="选择 CSV 文件", filetypes=[("CSV 文件", "*.csv"), ("所有文件", "*.*")])
        if not path:
            return
        try:
            with open(path, 'r', encoding='utf-8-sig') as f:
                reader = csv.reader(f)
                rows = list(reader)
            if not rows:
                messagebox.showerror("错误", "文件为空或格式不正确。")
                return
            headers = rows[0]
            data_rows = rows[1:]
            # 简单检查：第一列为姓名
            names = [r[0] for r in data_rows]
            subjects = headers[1:]
            # 保存为列表字典，同时尝试用 pandas DataFrame（若可用）
            records = []
            for r in data_rows:
                rec = {'姓名': r[0]}
                for i, s in enumerate(subjects, start=1):
                    try:
                        val = float(r[i]) if r[i] != '' else None
                    except Exception:
                        val = None
                    rec[s] = val
                records.append(rec)
            self.data = {'headers': headers, 'rows': records, 'subjects': subjects}
            self.filename = os.path.basename(path)
            self._populate_table()
            self._update_select_options()
            messagebox.showinfo("完成", f"已加载 {self.filename}（{len(records)} 行，{len(subjects)} 科目）")
        except Exception as e:
            messagebox.showerror("错误", f"加载失败：{e}")

    def _populate_table(self):
        # 清空 tree
        for col in self.tree['columns']:
            self.tree.heading(col, text='')
        self.tree.delete(*self.tree.get_children())
        if not self.data:
            return
        headers = self.data['headers']
        # 重建 columns
        cols = headers
        self.tree.config(columns=cols, show='headings')
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=100, anchor='center')
        for rec in self.data['rows']:
            row = [rec.get(headers[i], '') if i>0 else rec.get('姓名','') for i in range(len(headers))]
            # convert None to empty string
            row = [("" if v is None else v) for v in row]
            self.tree.insert('', tk.END, values=row)

    def _update_select_options(self):
        if not self.data:
            self.select_cb['values'] = []
            return
        if self.view_var.get() == '按学生':
            vals = [r['姓名'] for r in self.data['rows']]
        else:
            vals = self.data['subjects']
        self.select_cb['values'] = vals
        if vals:
            self.select_cb.set(vals[0])

    def plot(self):
        if not HAS_MPL:
            messagebox.showerror("错误", "未安装 matplotlib，无法绘图。")
            return
        if not self.data:
            messagebox.showinfo("提示", "请先导入 CSV 数据。")
            return
        key = self.select_var.get().strip()
        if not key:
            messagebox.showinfo("提示", "请选择一个学生或科目。")
            return

        view = self.view_var.get()
        chart = self.chart_var.get()

        self.ax.clear()

        try:
            if view == '按学生':
                # 找到该学生记录
                rec = next((r for r in self.data['rows'] if r['姓名'] == key), None)
                if not rec:
                    messagebox.showerror("错误", "未找到该学生。")
                    return
                subjects = self.data['subjects']
                vals = [rec.get(s) if rec.get(s) is not None else float('nan') for s in subjects]
                x = subjects
                if chart == '折线图':
                    self.ax.plot(x, vals, marker='o', linestyle='-')
                else:
                    self.ax.bar(x, vals)
                self.ax.set_title(f"{key} 的成绩")
                self.ax.set_xlabel("科目")
                self.ax.set_ylabel("分数")
                self.ax.set_ylim(0, 100)
            else:  # 按科目
                subject = key
                names = [r['姓名'] for r in self.data['rows']]
                vals = [r.get(subject) if r.get(subject) is not None else float('nan') for r in self.data['rows']]
                x = names
                if chart == '折线图':
                    self.ax.plot(x, vals, marker='o', linestyle='-')
                else:
                    self.ax.bar(x, vals)
                self.ax.set_title(f"{subject} 成绩分布")
                self.ax.set_xlabel("学生")
                self.ax.set_ylabel("分数")
                self.ax.set_ylim(0, 100)
                self.ax.tick_params(axis='x', rotation=45)
            self.fig.tight_layout()
            self.canvas.draw()
        except Exception as e:
            messagebox.showerror("绘图错误", str(e))

    def show_stats(self):
        if not self.data:
            messagebox.showinfo("提示", "请先导入数据。")
            return
        # 计算各科目平均分、最高、最低
        subjects = self.data['subjects']
        rows = self.data['rows']
        stats = []
        for s in subjects:
            vals = [r[s] for r in rows if isinstance(r.get(s), (int, float))]
            if not vals:
                avg = high = low = None
            else:
                avg = sum(vals)/len(vals)
                high = max(vals)
                low = min(vals)
            stats.append((s, avg, high, low, len(vals)))
        # 弹出简单窗口显示
        txt = io.StringIO()
        txt.write("科目\t平均\t最高\t最低\t有效人数\n")
        for s, avg, high, low, cnt in stats:
            txt.write(f"{s}\t{'' if avg is None else round(avg,2)}\t{'' if high is None else high}\t{'' if low is None else low}\t{cnt}\n")
        top = tk.Toplevel(self)
        top.title("统计信息")
        text = tk.Text(top, width=60, height=20)
        text.pack(fill='both', expand=True)
        text.insert('1.0', txt.getvalue())
        text.configure(state='disabled')

    def export_csv(self):
        if not self.data:
            messagebox.showinfo("提示", "没有数据可导出。")
            return
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV 文件","*.csv"),("所有文件","*.*")])
        if not path:
            return
        try:
            headers = self.data['headers']
            with open(path, 'w', encoding='utf-8-sig', newline='') as f:
                writer = csv.writer(f)
                writer.writerow(headers)
                for r in self.data['rows']:
                    row = [r.get('姓名','')]
                    for s in self.data['subjects']:
                        v = r.get(s)
                        row.append("" if v is None else v)
                    writer.writerow(row)
            messagebox.showinfo("导出成功", f"已保存到 {path}")
        except Exception as e:
            messagebox.showerror("错误", f"导出失败：{e}")

    def export_plot(self):
        if not HAS_MPL:
            messagebox.showerror("错误", "未安装 matplotlib，无法导出图像。")
            return
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG 图片","*.png"),("所有文件","*.*")])
        if not path:
            return
        try:
            self.fig.savefig(path, dpi=150)
            messagebox.showinfo("导出成功", f"图像已保存到 {path}")
        except Exception as e:
            messagebox.showerror("错误", f"保存失败：{e}")

if __name__ == "__main__":
    app = ScorePlotter()
    app.mainloop() 