
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.colors import LinearSegmentedColormap
import tkinter as tk
from tkinter import ttk, messagebox
import time


class MandelbrotViewer:
    def __init__(self, root):
        self.root = root
        self.root.title("曼德勃罗集 - 鼠标中心缩放版")
        self.root.geometry("1200x800")

        # --- 核心状态变量 ---
        self.width = 800
        self.height = 600
        self.max_iter = 200
        self.zoom = 1.0
        # 初始中心点 (曼德勃罗集主要位于实轴-0.5附近)
        self.center_x = -0.5
        self.center_y = 0.0

        # --- UI 初始化 ---
        self.setup_ui()

        # --- Matplotlib 设置 ---
        self.fig, self.ax = plt.subplots(figsize=(8, 6))
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

        # 绑定鼠标事件
        self.canvas.mpl_connect('scroll_event', self.on_scroll_zoom)
        self.canvas.mpl_connect('button_press_event', self.on_click_pan)

        # 初始绘制
        self.plot_mandelbrot()

    def setup_ui(self):
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 左侧控制面板
        control_frame = ttk.LabelFrame(main_frame, text="控制中心", padding=10)
        control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)

        # 1. 数字输入控制区
        input_frame = ttk.LabelFrame(control_frame, text="数字指令控制", padding=5)
        input_frame.pack(fill=tk.X, pady=5)

        ttk.Label(input_frame, text="缩放倍数 (Zoom):").pack(anchor=tk.W)
        self.zoom_entry = ttk.Entry(input_frame)
        self.zoom_entry.insert(0, "1.0")
        self.zoom_entry.pack(fill=tk.X, pady=2)

        ttk.Label(input_frame, text="最大迭代 (Iter):").pack(anchor=tk.W)
        self.iter_entry = ttk.Entry(input_frame)
        self.iter_entry.insert(0, "200")
        self.iter_entry.pack(fill=tk.X, pady=2)

        ttk.Button(input_frame, text="应用数字设置",
                   command=self.apply_numeric_settings).pack(fill=tk.X, pady=5)

        # 2. 快捷操作
        btn_frame = ttk.Frame(control_frame)
        btn_frame.pack(fill=tk.X, pady=10)
        ttk.Button(btn_frame, text="重置视图", command=self.reset_view).pack(
            side=tk.LEFT, padx=2)
        ttk.Button(btn_frame, text="保存图像", command=self.save_image).pack(
            side=tk.LEFT, padx=2)

        # 3. 状态显示
        self.status_var = tk.StringVar(value="就绪 | 提示: 滚轮以鼠标为中心缩放")
        ttk.Label(control_frame, textvariable=self.status_var,
                  foreground="green").pack(anchor=tk.W, pady=10)

        info_text = ("操作指南:\n"
                     "1. 鼠标滚轮: 以指针位置为中心缩放\n"
                     "2. 左键拖拽: 平移视图\n"
                     "3. 左侧输入: 精确控制参数")
        ttk.Label(control_frame, text=info_text, justify=tk.LEFT,
                  font=("Arial", 9)).pack(anchor=tk.W, pady=10)

        # 右侧绘图容器
        self.plot_frame = ttk.Frame(main_frame)
        self.plot_frame.pack(side=tk.RIGHT, fill=tk.BOTH,
                             expand=True, padx=10, pady=10)

    def apply_numeric_settings(self):
        """根据输入框的数字更新视图"""
        try:
            new_zoom = float(self.zoom_entry.get())
            new_iter = int(self.iter_entry.get())

            if new_zoom <= 0:
                raise ValueError("缩放倍数必须大于0")
            if new_iter < 50 or new_iter > 5000:
                raise ValueError("迭代次数建议在 50-5000 之间")

            self.zoom = new_zoom
            self.max_iter = new_iter
            self.plot_mandelbrot()
            self.status_var.set(
                f"已应用: Zoom={self.zoom:.2f}, Iter={self.max_iter}")
        except Exception as e:
            messagebox.showerror("输入错误", f"无效的数字输入: {e}")

    def reset_view(self):
        self.center_x = -0.5
        self.center_y = 0.0
        self.zoom = 1.0
        self.max_iter = 200
        self.zoom_entry.delete(0, tk.END)
        self.zoom_entry.insert(0, "1.0")
        self.iter_entry.delete(0, tk.END)
        self.iter_entry.insert(0, "200")
        self.plot_mandelbrot()
        self.status_var.set("视图已重置")

    def save_image(self):
        file_path = tk.filedialog.asksaveasfilename(
            defaultextension=".png", filetypes=[("PNG", "*.png")])
        if file_path:
            self.fig.savefig(file_path, dpi=300, bbox_inches='tight')
            messagebox.showinfo("成功", "图像已保存")

    def on_click_pan(self, event):
        """左键点击平移中心"""
        if event.inaxes != self.ax:
            return
        if event.button == 1:  # 左键
            self.center_x = event.xdata
            self.center_y = event.ydata
            self.plot_mandelbrot()
            self.status_var.set(
                f"中心移至: ({self.center_x:.4f}, {self.center_y:.4f})")

    def on_scroll_zoom(self, event):
        """
        核心逻辑: 以鼠标位置为中心进行缩放
        原理: 
        1. 获取鼠标在数据坐标系中的位置 (mx, my)
        2. 计算缩放前的范围
        3. 应用缩放因子
        4. 调整中心点，使得 (mx, my) 在缩放后仍然处于屏幕的相同相对位置
        """
        if event.inaxes != self.ax:
            return

        # 获取鼠标在数据坐标中的位置
        mx, my = event.xdata, event.ydata
        if mx is None or my is None:
            return

        # 确定缩放因子
        scale_factor = 1.1 if event.button == 'up' else 0.9

        # 记录旧的缩放比例
        old_zoom = self.zoom

        # 更新缩放比例
        self.zoom *= scale_factor

        # 关键数学推导: 保持鼠标指向的点 (mx, my) 在屏幕上的像素位置不变
        # 新中心 = 鼠标点 - (鼠标点 - 旧中心) * (旧缩放 / 新缩放)
        # 简化理解: 距离鼠标点的偏移量需要随着缩放比例反向调整

        ratio = old_zoom / self.zoom

        self.center_x = mx - (mx - self.center_x) * ratio
        self.center_y = my - (my - self.center_y) * ratio

        # 更新UI显示
        self.zoom_entry.delete(0, tk.END)
        self.zoom_entry.insert(0, f"{self.zoom:.4f}")

        self.plot_mandelbrot()
        self.status_var.set(
            f"Zoom: {self.zoom:.4f} | 中心: ({self.center_x:.4f}, {self.center_y:.4f})")

    def compute_mandelbrot_array(self):
        """使用 NumPy 向量化计算曼德勃罗集"""
        # 计算当前视图的边界
        # r 是半高对应的复平面半径
        r = 2.5 / self.zoom
        xmin = self.center_x - r * self.width / self.height
        xmax = self.center_x + r * self.width / self.height
        ymin = self.center_y - r
        ymax = self.center_y + r

        # 生成网格
        x = np.linspace(xmin, xmax, self.width)
        y = np.linspace(ymin, ymax, self.height)
        X, Y = np.meshgrid(x, y)
        C = X + 1j * Y

        # 初始化 Z 和 掩码
        Z = np.zeros_like(C)
        M = np.zeros(C.shape, dtype=int)  # 存储发散所需的迭代次数
        mask = np.ones(C.shape, dtype=bool)  # 标记哪些点还在集合内或未发散

        # 迭代
        for i in range(self.max_iter):
            Z[mask] = Z[mask]**2 + C[mask]
            # 找出本次迭代中发散的点
            diverged = np.abs(Z) > 2
            # 记录这些点是第几次迭代发散的
            M[mask & diverged] = i
            # 从掩码中移除已发散的点 (不再计算它们)
            mask &= ~diverged

        # 未发散的点标记为 max_iter
        M[mask] = self.max_iter
        return M, (xmin, xmax, ymin, ymax)

    def plot_mandelbrot(self):
        start_time = time.time()
        self.root.update_idletasks()  # 刷新UI显示"计算中"

        M, extent = self.compute_mandelbrot_array()

        self.ax.clear()
        # 使用 imshow 绘制，origin='lower' 使虚轴向上为正
        self.ax.imshow(M, extent=extent, cmap='magma', origin='lower')
        self.ax.set_title(f"Mandelbrot Set (Zoom: {self.zoom:.2f})")
        self.ax.set_xlabel("Re(c)")
        self.ax.set_ylabel("Im(c)")

        # 隐藏刻度以获得更沉浸的体验，或者保留以便观察坐标
        # self.ax.set_xticks([])
        # self.ax.set_yticks([])

        self.canvas.draw()

        elapsed = time.time() - start_time
        print(f"Render time: {elapsed:.3f}s")


if __name__ == "__main__":
    root = tk.Tk()
    app = MandelbrotViewer(root)
    root.mainloop()
