# score_plot_app.py
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
import json
import os
import math
from collections import defaultdict
from datetime import datetime

# 可选功能：导出为 PNG 需要 Pillow
try:
    from PIL import ImageGrab, Image
    PIL_AVAILABLE = True
except Exception:
    PIL_AVAILABLE = False

DATA_FILE = "scores_data.json"

# ---------- 辅助函数 ----------
def clamp(x, a, b):
    return max(a, min(b, x))

def smooth_points(points, factor=0.2, iterations=2):
    # 简单的 Chaikin 曲线细分平滑（不改变端点）
    if len(points) < 3:
        return points[:]
    pts = points[:]
    for _ in range(iterations):
        new_pts = [pts[0]]
        for i in range(len(pts)-1):
            x0,y0 = pts[i]
            x1,y1 = pts[i+1]
            q = ( (1-factor)*x0 + factor*x1, (1-factor)*y0 + factor*y1 )
            r = ( factor*x0 + (1-factor)*x1, factor*y0 + (1-factor)*y1 )
            new_pts.append(q)
            new_pts.append(r)
        new_pts.append(pts[-1])
        pts = new_pts
    return pts

# ---------- 主应用 ----------
class ScorePlotApp:
    def __init__(self, master):
        self.master = master
        master.title("有趣的成绩走势图")
        master.geometry("1000x650")

        self.data = defaultdict(list)  # name -> list of (timestamp_str, score)
        self.load_data_if_exists()

        self.create_widgets()
        self.redraw_canvas()

    def load_data_if_exists(self):
        if os.path.exists(DATA_FILE):
            try:
                with open(DATA_FILE, 'r', encoding='utf-8') as f:
                    raw = json.load(f)
                for name, entries in raw.items():
                    # entries: list of {"time":..., "score":...}
                    self.data[name] = [(e.get("time"), float(e.get("score"))) for e in entries]
            except Exception:
                pass

    def save_data(self, path=None):
        if path is None:
            path = DATA_FILE
        try:
            out = {}
            for name, lst in self.data.items():
                out[name] = [{"time": t, "score": s} for (t,s) in lst]
            with open(path, 'w', encoding='utf-8') as f:
                json.dump(out, f, ensure_ascii=False, indent=2)
            messagebox.showinfo("保存成功", f"数据已保存到 {path}")
        except Exception as e:
            messagebox.showerror("保存失败", str(e))

    def create_widgets(self):
        # 顶部控制区
        top_frame = ttk.Frame(self.master, padding=6)
        top_frame.pack(side=tk.TOP, fill=tk.X)

        ttk.Label(top_frame, text="姓名：").pack(side=tk.LEFT)
        self.name_var = tk.StringVar()
        ttk.Entry(top_frame, textvariable=self.name_var, width=15).pack(side=tk.LEFT, padx=(0,6))

        ttk.Label(top_frame, text="成绩：").pack(side=tk.LEFT)
        self.score_var = tk.StringVar()
        ttk.Entry(top_frame, textvariable=self.score_var, width=8).pack(side=tk.LEFT, padx=(0,6))

        ttk.Button(top_frame, text="添加/记录", command=self.add_score).pack(side=tk.LEFT, padx=(0,6))
        ttk.Button(top_frame, text="删除最新", command=self.pop_latest).pack(side=tk.LEFT, padx=(0,6))

        ttk.Button(top_frame, text="显示学生折线", command=self.show_student_prompt).pack(side=tk.LEFT, padx=(4,6))
        ttk.Button(top_frame, text="显示分布", command=self.show_distribution).pack(side=tk.LEFT, padx=(0,6))
        ttk.Button(top_frame, text="比较多名学生", command=self.compare_students_prompt).pack(side=tk.LEFT, padx=(0,6))

        ttk.Button(top_frame, text="保存数据", command=lambda: self.save_data()).pack(side=tk.RIGHT, padx=(6,0))
        ttk.Button(top_frame, text="另存为...", command=self.save_as).pack(side=tk.RIGHT)

        # 左侧列表区：学生与简单统计
        left_frame = ttk.Frame(self.master, width=260, padding=6)
        left_frame.pack(side=tk.LEFT, fill=tk.Y)
        left_frame.pack_propagate(False)

        ttk.Label(left_frame, text="学生列表", font=("宋体",12,"bold")).pack(anchor=tk.W)
        self.student_listbox = tk.Listbox(left_frame)
        self.student_listbox.pack(fill=tk.BOTH, expand=True, pady=(6,6))
        self.student_listbox.bind("<<ListboxSelect>>", lambda e: self.on_student_select())

        btn_frame = ttk.Frame(left_frame)
        btn_frame.pack(fill=tk.X)
        ttk.Button(btn_frame, text="删除学生", command=self.delete_student).pack(side=tk.LEFT, padx=(0,6))
        ttk.Button(btn_frame, text="清空所有", command=self.clear_all).pack(side=tk.LEFT)

        self.stats_label = ttk.Label(left_frame, text="", wraplength=240, foreground="gray")
        self.stats_label.pack(pady=(6,0))

        # 右侧画布区：绘图与图例
        right_frame = ttk.Frame(self.master, padding=6)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        self.canvas = tk.Canvas(right_frame, bg="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas.bind("<Configure>", lambda e: self.redraw_canvas())

        bottom_frame = ttk.Frame(right_frame)
        bottom_frame.pack(fill=tk.X)
        ttk.Button(bottom_frame, text="导出图片(PNG)", command=self.export_png).pack(side=tk.LEFT)
        ttk.Button(bottom_frame, text="示例数据", command=self.load_sample).pack(side=tk.LEFT, padx=(6,0))
        ttk.Button(bottom_frame, text="重置视图", command=self.redraw_canvas).pack(side=tk.LEFT, padx=(6,0))

        self.update_student_listbox()

    # ---------- 数据与界面操作 ----------
    def update_student_listbox(self):
        self.student_listbox.delete(0, tk.END)
        for name in sorted(self.data.keys()):
            lst = self.data[name]
            if lst:
                scores = [s for (_,s) in lst]
                avg = sum(scores)/len(scores)
                txt = f"{name}  ({len(scores)} 次) 平均 {avg:.1f}"
            else:
                txt = f"{name} (0 次)"
            self.student_listbox.insert(tk.END, txt)
        self.update_stats_label()

    def update_stats_label(self):
        all_scores = [s for lst in self.data.values() for (_,s) in lst]
        if not all_scores:
            self.stats_label.config(text="当前无成绩记录。")
            return
        n = len(all_scores)
        avg = sum(all_scores)/n
        mn = min(all_scores); mx = max(all_scores)
        text = f"共记录 {n} 次成绩，平均 {avg:.2f}，最低 {mn:.1f}，最高 {mx:.1f}"
        self.stats_label.config(text=text)

    def add_score(self):
        name = self.name_var.get().strip()
        score_str = self.score_var.get().strip()
        if not name:
            messagebox.showwarning("输入错误", "请输入姓名。")
            return
        try:
            score = float(score_str)
            if math.isnan(score) or score < 0 or score > 150:
                raise ValueError()
        except Exception:
            messagebox.showwarning("输入错误", "请输入合法的成绩（0-150）。")
            return
        t = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.data[name].append((t, score))
        self.name_var.set("")
        self.score_var.set("")
        self.update_student_listbox()
        self.redraw_canvas()

    def pop_latest(self):
        name = self.name_var.get().strip()
        if not name:
            messagebox.showwarning("输入错误", "请输入要删除最新成绩的学生姓名。")
            return
        if name not in self.data or not self.data[name]:
            messagebox.showinfo("提示", "该学生无成绩记录。")
            return
        self.data[name].pop()
        self.update_student_listbox()
        self.redraw_canvas()

    def delete_student(self):
        sel = self.student_listbox.curselection()
        if not sel:
            messagebox.showinfo("提示", "请选择学生。")
            return
        idx = sel[0]
        name = sorted(self.data.keys())[idx]
        if messagebox.askyesno("删除学生", f"确定删除学生 {name} 及其所有成绩？"):
            del self.data[name]
            self.update_student_listbox()
            self.redraw_canvas()

    def clear_all(self):
        if messagebox.askyesno("清空", "确定要清空所有学生与成绩记录？"):
            self.data.clear()
            self.update_student_listbox()
            self.redraw_canvas()

    def on_student_select(self):
        sel = self.student_listbox.curselection()
        if not sel:
            return
        idx = sel[0]
        name = sorted(self.data.keys())[idx]
        lst = self.data.get(name, [])
        # 显示该学生的成绩概况弹窗
        if not lst:
            messagebox.showinfo("学生信息", f"{name} 当前无成绩记录。")
            return
        scores = [s for (_,s) in lst]
        info = f"{name} 的成绩（{len(scores)} 次）\n平均：{sum(scores)/len(scores):.2f}\n最低：{min(scores):.1f}\n最高：{max(scores):.1f}\n最近五次：\n"
        info += "\n".join(f"{t}  {s}" for t,s in lst[-5:])
        messagebox.showinfo("学生信息", info)

    # ---------- 绘图 ----------
    def redraw_canvas(self, mode="line", compare_names=None):
        # mode: "line" 或 "distribution"
        self.canvas.delete("all")
        w = self.canvas.winfo_width()
        h = self.canvas.winfo_height()
        if w < 50 or h < 50:
            return

        # 背景网格
        self.canvas.create_rectangle(0,0,w,h, fill="#fbfbfd", outline="")
        for i in range(6):
            y = h * i / 6
            self.canvas.create_line(50, y, w-20, y, fill="#eee")
        # 左侧及底部边距
        left = 60; right = w-20; top = 20; bottom = h-40
        # 标题
        self.canvas.create_text(w/2, 12, text="成绩走势图", font=("Arial", 14, "bold"))

        # 如果是 distribution 模式
        if mode == "distribution":
            all_scores = [s for lst in self.data.values() for (_,s) in lst]
            if not all_scores:
                self.canvas.create_text(w/2, h/2, text="无成绩数据可显示分布", fill="gray")
                return
            # 画直方图（区间 0-100 按 10 分段 + 100+）
            bins = [0]*11
            for s in all_scores:
                idx = int(min(10, max(0, s//10)))
                bins[idx]+=1
            maxc = max(bins)
            bw = (right-left) / len(bins) * 0.9
            gap = ((right-left) - bw*len(bins)) / (len(bins)-1) if len(bins)>1 else 0
            for i,c in enumerate(bins):
                x = left + i*(bw+gap)
                bar_h = 0 if maxc==0 else (c/maxc)*(bottom-top)
                self.canvas.create_rectangle(x, bottom-bar_h, x+bw, bottom, fill="#7db9f6", outline="#5a9bd8")
                label = f"{i*10}-{i*10+9}" if i<10 else "100+"
                self.canvas.create_text(x+bw/2, bottom+12, text=label, font=("Arial",9))
                self.canvas.create_text(x+bw/2, bottom-bar_h-8, text=str(c), font=("Arial",9))
            return

        # 默认绘制折线。若 compare_names 给定则绘制这些学生，否则只绘制所有学生平均线
        if compare_names:
            names = compare_names
        else:
            # 计算每次“时间点”的全班平均（按时间顺序）
            # 合并时间轴：取所有时间点的排序并对每个时间点求平均
            times = sorted({t for lst in self.data.values() for (t,_) in lst})
            if not times:
                self.canvas.create_text(w/2, h/2, text="无成绩数据。可点击“示例数据”快速填充", fill="gray")
                return
            avg_points = []
            for t in times:
                vals = [s for lst in self.data.values() for (tt,s) in lst if tt==t]
                if vals:
                    avg_points.append((t, sum(vals)/len(vals)))
            # 将 avg_points 转换为绘制数据
            names = []
            to_plot = {"全班平均": avg_points}

            # fall through to plotting below
        # 如果 compare_names，则构造 to_plot
        if compare_names:
            to_plot = {}
            for name in names:
                lst = self.data.get(name, [])
                to_plot[name] = lst[:]
        # 绘制每条曲线
        colors = ["#e74c3c","#3498db","#f1c40f","#2ecc71","#9b59b6","#e67e22","#34495e"]
        color_map = {}
        i = 0
        # 先找到当前最大和最小分数用于 Y 轴比例
        all_values = [s for lst in to_plot.values() for (_,s) in lst]
        if not all_values:
            self.canvas.create_text(w/2, h/2, text="所选学生无成绩可绘制。", fill="gray")
            return
        ymin = min(all_values); ymax = max(all_values)
        if ymin==ymax:
            ymin = ymin-5; ymax = ymax+5
        # Y 轴刻度
        for k in range(6):
            yv = ymin + (ymax-ymin)*(5-k)/5
            y = top + (bottom-top)*(k/5)
            self.canvas.create_text(40, y, text=f"{yv:.0f}", font=("Arial",9))
            self.canvas.create_line(left, y, right, y, fill="#eee")
        # 绘制每条线
        legend_x = left + 6
        legend_y = top + 6
        for name, lst in to_plot.items():
            pts = []
            # x 按序号分布
            for idx, (t,s) in enumerate(lst):
                x = left + (right-left) * (idx/(max(1, len(lst)-1)))
                y = bottom - (s - ymin) / (ymax-ymin) * (bottom-top)
                pts.append((x,y))
            # 平滑处理
            smooth = smooth_points(pts, factor=0.25, iterations=2)
            col = colors[i % len(colors)]
            color_map[name]=col
            i+=1
            # 绘制曲线（多段线）
            if len(smooth) >= 2:
                for a,b in zip(smooth[:-1], smooth[1:]):
                    self.canvas.create_line(a[0], a[1], b[0], b[1], fill=col, width=2, smooth=True)
            # 标记原始点
            for (x,y),(t,s) in zip(pts,lst):
                radius = 4
                self.canvas.create_oval(x-radius, y-radius, x+radius, y+radius, fill=col, outline="")
                # 鼠标悬停提示（简单：文本）
                self.canvas.create_text(x, y-10, text=f"{s:.0f}", font=("Arial",8), fill="#333")
            # 图例
            self.canvas.create_rectangle(legend_x-6, legend_y-10, legend_x+120, legend_y+12, fill="", outline="")
            self.canvas.create_oval(legend_x, legend_y-2, legend_x+10, legend_y+8, fill=col, outline="")
            self.canvas.create_text(legend_x+22, legend_y+3, text=name, anchor=tk.W)
            legend_y += 18

    # ---------- 交互弹窗 ----------
    def show_student_prompt(self):
        # 询问要显示哪位学生，默认用当前选中或弹出输入
        sel = self.student_listbox.curselection()
        default = ""
        if sel:
            default = sorted(self.data.keys())[sel[0]]
        name = simpledialog.askstring("显示学生", "请输入学生姓名（可留空查看全班平均）：", initialvalue=default, parent=self.master)
        if name is None:
            return
        name = name.strip()
        if name == "":
            # 显示全班平均线
            self.redraw_canvas(mode="line", compare_names=None)
            return
        if name not in self.data:
            messagebox.showinfo("未找到", "该学生不存在，可先添加成绩。")
            return
        self.redraw_canvas(mode="line", compare_names=[name])

    def compare_students_prompt(self):
        # 弹出一个选择多个学生的对话（简单实现用逗号分隔）
        names = sorted(self.data.keys())
        if not names:
            messagebox.showinfo("提示", "无学生数据。")
            return
        choice = simpledialog.askstring("比较学生", "请输入要比较的学生姓名，用逗号分隔（如：张三,李四）：", parent=self.master)
        if choice is None:
            return
        sel_names = [s.strip() for s in choice.split(",") if s.strip()]
        sel_names = [s for s in sel_names if s in self.data]
        if not sel_names:
            messagebox.showinfo("提示", "未选择有效学生。")
            return
        self.redraw_canvas(mode="line", compare_names=sel_names)

    def show_distribution(self):
        self.redraw_canvas(mode="distribution")

    # ---------- 文件操作 ----------
    def save_as(self):
        path = filedialog.asksaveasfilename(title="另存为 JSON", defaultextension=".json", filetypes=[("JSON 文件","*.json"),("所有","*.*")])
        if not path:
            return
        self.save_data(path)

    def export_png(self):
        if not PIL_AVAILABLE:
            messagebox.showwarning("功能不可用", "导出 PNG 需要安装 pillow：pip install pillow")
            return
        # 截取画布在屏幕上的区域
        x = self.canvas.winfo_rootx()
        y = self.canvas.winfo_rooty()
        w = self.canvas.winfo_width()
        h = self.canvas.winfo_height()
        bbox = (x, y, x+w, y+h)
        try:
            img = ImageGrab.grab(bbox)
            path = filedialog.asksaveasfilename(title="导出图片", defaultextension=".png", filetypes=[("PNG 图片","*.png")])
            if not path:
                return
            img.save(path)
            messagebox.showinfo("导出成功", f"已保存到 {path}")
        except Exception as e:
            messagebox.showerror("导出失败", str(e))

    # ---------- 示例数据 ----------
    def load_sample(self):
        # 清空并填充若干示例学生及成绩（便于体验）
        self.data.clear()
        import random, time
        names = ["张三","李四","王五","赵六"]
        for n in names:
            count = random.randint(4,8)
            t0 = datetime.now()
            for i in range(count):
                t = (t0).strftime("%Y-%m-%d %H:%M:%S")
                score = clamp(50+random.gauss(0,15)+i*2, 0, 120)
                self.data[n].append((t, round(score,1)))
                # 假造时间序列
                t0 = t0.replace(second=(t0.second+1)%60)
        self.update_student_listbox()
        self.redraw_canvas()

# ---------- 启动 ----------
if __name__ == "__main__":
    root = tk.Tk()
    app = ScorePlotApp(root)
    root.mainloop()