# score_trend_gui.py
# 成绩走势图 GUI 程序
# 运行：python score_trend_gui.py
# 依赖：matplotlib, pandas (pandas 可选但推荐)
# pip install matplotlib pandas

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from tkinter.scrolledtext import ScrolledText
import csv
import io
import os
from datetime import datetime

try:
    import pandas as pd
except Exception:
    pd = None

import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

class ScoreTrendApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("成绩走势图")
        self.geometry("1000x650")
        self.records = []  # 每条记录为 dict: {'student','subject','date','score'}
        self._build_ui()

    def _build_ui(self):
        # 顶部按钮
        top = ttk.Frame(self)
        top.pack(side="top", fill="x", padx=6, pady=6)

        ttk.Button(top, text="导入 CSV", command=self.import_csv).pack(side="left")
        ttk.Button(top, text="导出 CSV", command=self.export_csv).pack(side="left", padx=6)
        ttk.Button(top, text="保存图像(PNG)", command=self.save_fig).pack(side="left", padx=6)
        ttk.Button(top, text="清空全部", command=self.clear_all).pack(side="left", padx=6)

        # 中间主分隔：左为数据录入与列表，右为图表与选项
        mid = ttk.Frame(self)
        mid.pack(fill="both", expand=True, padx=6, pady=6)

        # 左侧：数据录入与表格
        left = ttk.Frame(mid, width=360)
        left.pack(side="left", fill="y")
        # 录入区
        entry_frame = ttk.LabelFrame(left, text="添加 / 编辑 记录")
        entry_frame.pack(fill="x", padx=4, pady=4)

        ttk.Label(entry_frame, text="学生:").grid(row=0, column=0, sticky="e")
        self.e_student = ttk.Entry(entry_frame, width=20)
        self.e_student.grid(row=0, column=1, padx=4, pady=3)

        ttk.Label(entry_frame, text="科目:").grid(row=1, column=0, sticky="e")
        self.e_subject = ttk.Entry(entry_frame, width=20)
        self.e_subject.grid(row=1, column=1, padx=4, pady=3)

        ttk.Label(entry_frame, text="日期/序号:").grid(row=2, column=0, sticky="e")
        self.e_date = ttk.Entry(entry_frame, width=20)
        self.e_date.grid(row=2, column=1, padx=4, pady=3)
        ttk.Label(entry_frame, text="（可填 2023-09-01 或 考试1）", foreground="gray").grid(row=2, column=2, sticky="w")

        ttk.Label(entry_frame, text="分数:").grid(row=3, column=0, sticky="e")
        self.e_score = ttk.Entry(entry_frame, width=20)
        self.e_score.grid(row=3, column=1, padx=4, pady=3)

        btn_frame = ttk.Frame(entry_frame)
        btn_frame.grid(row=4, column=0, columnspan=3, pady=6)
        ttk.Button(btn_frame, text="添加记录", command=self.add_record).pack(side="left", padx=4)
        ttk.Button(btn_frame, text="更新选中", command=self.update_selected).pack(side="left", padx=4)
        ttk.Button(btn_frame, text="删除选中", command=self.delete_selected).pack(side="left", padx=4)

        # 列表区
        list_frame = ttk.LabelFrame(left, text="成绩列表")
        list_frame.pack(fill="both", expand=True, padx=4, pady=4)
        cols = ("student","subject","date","score")
        self.tree = ttk.Treeview(list_frame, columns=cols, show="headings", selectmode="browse")
        for c in cols:
            self.tree.heading(c, text=c)
            self.tree.column(c, width=80, anchor="center")
        self.tree.pack(fill="both", expand=True)
        self.tree.bind("<<TreeviewSelect>>", self.on_tree_select)

        # 右侧：图表与选项
        right = ttk.Frame(mid)
        right.pack(side="right", fill="both", expand=True)

        # 控制选项
        ctrl = ttk.LabelFrame(right, text="绘图选项")
        ctrl.pack(fill="x", padx=4, pady=4)

        self.mode = tk.StringVar(value="by_student")
        ttk.Radiobutton(ctrl, text="按学生查看多科成绩", variable=self.mode, value="by_student").grid(row=0, column=0, sticky="w", padx=6, pady=3)
        ttk.Radiobutton(ctrl, text="按科目比较多名学生", variable=self.mode, value="by_subject").grid(row=1, column=0, sticky="w", padx=6, pady=3)

        ttk.Label(ctrl, text="学生名:").grid(row=0, column=1, sticky="e")
        self.sel_student = ttk.Combobox(ctrl, values=[], width=18)
        self.sel_student.grid(row=0, column=2, padx=4)
        ttk.Label(ctrl, text="科目:").grid(row=1, column=1, sticky="e")
        self.sel_subject = ttk.Combobox(ctrl, values=[], width=18)
        self.sel_subject.grid(row=1, column=2, padx=4)

        ttk.Button(ctrl, text="绘制图表", command=self.plot).grid(row=0, column=3, rowspan=2, padx=8)

        # 图表区域
        fig_frame = ttk.Frame(right)
        fig_frame.pack(fill="both", expand=True, padx=4, pady=4)
        self.fig = Figure(figsize=(6,4), dpi=100)
        self.ax = self.fig.add_subplot(111)
        self.canvas = FigureCanvasTkAgg(self.fig, master=fig_frame)
        self.canvas.get_tk_widget().pack(fill="both", expand=True)

        # 状态栏
        self.status = tk.StringVar(value="就绪")
        ttk.Label(self, textvariable=self.status, relief="sunken", anchor="w").pack(side="bottom", fill="x")

    # ---------- 数据操作 ----------
    def add_record(self):
        student = self.e_student.get().strip()
        subject = self.e_subject.get().strip()
        date = self.e_date.get().strip()
        score = self.e_score.get().strip()
        if not student or not subject or not date or not score:
            messagebox.showwarning("输入错误", "请完整填写学生 / 科目 / 日期(或序号) / 分数。")
            return
        try:
            score_v = float(score)
        except ValueError:
            messagebox.showwarning("输入错误", "分数应为数字。")
            return
        rec = {"student": student, "subject": subject, "date": date, "score": score_v}
        self.records.append(rec)
        self._refresh_tree()
        self._refresh_selectors()
        self.status.set(f"已添加记录：{student} {subject} {date} {score_v}")

    def _refresh_tree(self):
        for it in self.tree.get_children():
            self.tree.delete(it)
        for i, r in enumerate(self.records):
            self.tree.insert("", "end", iid=str(i), values=(r["student"], r["subject"], r["date"], r["score"]))

    def on_tree_select(self, event=None):
        sel = self.tree.selection()
        if not sel:
            return
        idx = int(sel[0])
        r = self.records[idx]
        self.e_student.delete(0, "end"); self.e_student.insert(0, r["student"])
        self.e_subject.delete(0, "end"); self.e_subject.insert(0, r["subject"])
        self.e_date.delete(0, "end"); self.e_date.insert(0, r["date"])
        self.e_score.delete(0, "end"); self.e_score.insert(0, str(r["score"]))
        self.status.set(f"选中记录 {idx+1}")

    def update_selected(self):
        sel = self.tree.selection()
        if not sel:
            messagebox.showinfo("提示", "请选择要更新的记录（左侧列表）。")
            return
        idx = int(sel[0])
        student = self.e_student.get().strip()
        subject = self.e_subject.get().strip()
        date = self.e_date.get().strip()
        score = self.e_score.get().strip()
        if not student or not subject or not date or not score:
            messagebox.showwarning("输入错误", "请完整填写学生 / 科目 / 日期(或序号) / 分数。")
            return
        try:
            score_v = float(score)
        except ValueError:
            messagebox.showwarning("输入错误", "分数应为数字。")
            return
        self.records[idx] = {"student": student, "subject": subject, "date": date, "score": score_v}
        self._refresh_tree()
        self._refresh_selectors()
        self.status.set(f"已更新记录 {idx+1}")

    def delete_selected(self):
        sel = self.tree.selection()
        if not sel:
            messagebox.showinfo("提示", "请选择要删除的记录（左侧列表）。")
            return
        idx = int(sel[0])
        r = self.records.pop(idx)
        self._refresh_tree()
        self._refresh_selectors()
        self.status.set(f"已删除记录：{r['student']} {r['subject']} {r['date']}")

    def clear_all(self):
        if messagebox.askyesno("确认", "确定清空所有记录？此操作不可撤销。"):
            self.records.clear()
            self._refresh_tree()
            self._refresh_selectors()
            self.ax.clear()
            self.canvas.draw()
            self.status.set("已清空所有数据。")

    # ---------- 导入/导出 ----------
    def import_csv(self):
        path = filedialog.askopenfilename(filetypes=[("CSV files","*.csv"),("All files","*.*")])
        if not path:
            return
        try:
            if pd:
                df = pd.read_csv(path, dtype=str)
                # 兼容列名
                cols = [c.lower() for c in df.columns]
                if not all(x in cols for x in ("student","subject","date","score")):
                    messagebox.showerror("格式错误", "CSV 文件需包含列：student,subject,date,score")
                    return
                # 使用原列名映射
                colmap = {c.lower(): c for c in df.columns}
                self.records = []
                for _, row in df.iterrows():
                    s = row[colmap["student"]]
                    sub = row[colmap["subject"]]
                    d = row[colmap["date"]]
                    sc = row[colmap["score"]]
                    try:
                        scv = float(sc)
                    except:
                        continue
                    self.records.append({"student": str(s), "subject": str(sub), "date": str(d), "score": scv})
            else:
                # 使用 csv 模块手动读取
                with open(path, newline='', encoding='utf-8') as f:
                    reader = csv.DictReader(f)
                    if not all(x in reader.fieldnames for x in ("student","subject","date","score")):
                        messagebox.showerror("格式错误", "CSV 文件需包含列：student,subject,date,score")
                        return
                    self.records = []
                    for row in reader:
                        try:
                            scv = float(row["score"])
                        except:
                            continue
                        self.records.append({"student": row["student"], "subject": row["subject"], "date": row["date"], "score": scv})
            self._refresh_tree()
            self._refresh_selectors()
            self.status.set(f"已导入 {len(self.records)} 条记录")
        except Exception as e:
            messagebox.showerror("导入失败", f"读取 CSV 失败：{e}")

    def export_csv(self):
        if not self.records:
            messagebox.showinfo("提示", "当前无数据可导出。")
            return
        path = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV files","*.csv"),("All files","*.*")])
        if not path:
            return
        try:
            with open(path, "w", newline='', encoding='utf-8') as f:
                writer = csv.DictWriter(f, fieldnames=["student","subject","date","score"])
                writer.writeheader()
                for r in self.records:
                    writer.writerow(r)
            self.status.set(f"已导出 CSV：{os.path.basename(path)}")
        except Exception as e:
            messagebox.showerror("导出失败", f"写入 CSV 失败：{e}")

    # ---------- 绘图 ----------
    def _parse_dates_for_series(self, dates):
        # 尝试把字符串日期解析为 datetime，对不能解析的按原序号返回 None
        parsed = []
        all_parsed = True
        for d in dates:
            if isinstance(d, (int,float)):
                parsed.append(d)
                all_parsed = False
                continue
            s = str(d).strip()
            if not s:
                parsed.append(None)
                all_parsed = False
                continue
            # 尝试常见日期格式
            for fmt in ("%Y-%m-%d","%Y/%m/%d","%Y.%m.%d","%Y%m%d"):
                try:
                    dt = datetime.strptime(s, fmt)
                    parsed.append(dt)
                    break
                except:
                    dt = None
            else:
                # 尝试像 "考试1" 或纯数字
                if s.isdigit():
                    parsed.append(int(s))
                    all_parsed = False
                else:
                    import re
                    m = re.search(r"(\d{4})[^\d]?(\d{1,2})[^\d]?(\d{1,2})", s)
                    if m:
                        try:
                            dt = datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)))
                            parsed.append(dt)
                            continue
                        except:
                            parsed.append(None)
                            all_parsed = False
                    else:
                        parsed.append(None)
                        all_parsed = False
        return parsed, all_parsed

    def plot(self):
        mode = self.mode.get()
        if not self.records:
            messagebox.showinfo("提示", "请先添加一些成绩记录再绘图。")
            return
        self.ax.clear()
        if mode == "by_student":
            student = self.sel_student.get().strip()
            if not student:
                messagebox.showwarning("参数缺失", "请选择或输入要查看的学生名（上拉选择或直接输入）。")
                return
            # 按科目分组：每个科目为一条曲线
            data = [r for r in self.records if r["student"] == student]
            if not data:
                messagebox.showinfo("无数据", f"没有找到学生 {student} 的记录。")
                return
            # 按科目分类
            from collections import defaultdict
            grp = defaultdict(list)
            for r in data:
                grp[r["subject"]].append(r)
            colors = None
            for subj, items in grp.items():
                items_sorted = sorted(items, key=lambda x: x["date"])
                dates = [it["date"] for it in items_sorted]
                scores = [it["score"] for it in items_sorted]
                parsed, all_parsed = self._parse_dates_for_series(dates)
                if all_parsed:
                    # datetime 可排序并用于 x 轴
                    xs = parsed
                    self.ax.plot_date(xs, scores, '-o', label=subj)
                else:
                    # 使用索引位置作为 x
                    xs = list(range(1, len(scores)+1))
                    self.ax.plot(xs, scores, '-o', label=subj)
                    # 设置 x 刻度为原始日期标签
                    self.ax.set_xticks(xs)
                    self.ax.set_xticklabels([str(d) for d in dates], rotation=30)
            self.ax.set_title(f"{student} 的成绩走势（按科目）")
            self.ax.set_xlabel("考试（日期/序号）")
            self.ax.set_ylabel("分数")
            self.ax.legend()
            self.ax.grid(True)
        else:  # by_subject
            subject = self.sel_subject.get().strip()
            if not subject:
                messagebox.showwarning("参数缺失", "请选择或输入要比较的科目（上拉选择或直接输入）。")
                return
            data = [r for r in self.records if r["subject"] == subject]
            if not data:
                messagebox.showinfo("无数据", f"没有找到科目 {subject} 的记录。")
                return
            # 按学生分组：每个学生为一条曲线
            from collections import defaultdict
            grp = defaultdict(list)
            for r in data:
                grp[r["student"]].append(r)
            for student, items in grp.items():
                items_sorted = sorted(items, key=lambda x: x["date"])
                dates = [it["date"] for it in items_sorted]
                scores = [it["score"] for it in items_sorted]
                parsed, all_parsed = self._parse_dates_for_series(dates)
                if all_parsed:
                    xs = parsed
                    self.ax.plot_date(xs, scores, '-o', label=student)
                else:
                    xs = list(range(1, len(scores)+1))
                    self.ax.plot(xs, scores, '-o', label=student)
                    self.ax.set_xticks(xs)
                    self.ax.set_xticklabels([str(d) for d in dates], rotation=30)
            self.ax.set_title(f"科目 {subject} 的学生成绩比较")
            self.ax.set_xlabel("考试（日期/序号）")
            self.ax.set_ylabel("分数")
            self.ax.legend()
            self.ax.grid(True)

        self.fig.tight_layout()
        self.canvas.draw()
        self.status.set("绘图完成。")

    # ---------- 保存图像 ----------
    def save_fig(self):
        path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG 图片","*.png"),("All files","*.*")])
        if not path:
            return
        try:
            self.fig.savefig(path)
            self.status.set(f"已保存图像：{os.path.basename(path)}")
        except Exception as e:
            messagebox.showerror("保存失败", f"保存图像失败：{e}")

    # ---------- 其它 ----------
    def _refresh_selectors(self):
        students = sorted({r["student"] for r in self.records})
        subjects = sorted({r["subject"] for r in self.records})
        self.sel_student['values'] = students
        self.sel_subject['values'] = subjects

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