#!/usr/bin/env python3
"""
简单但功能齐全的桌面计算器（Tkinter）
保存为 calc_gui.py 后运行: python calc_gui.py
"""

import tkinter as tk
from tkinter import ttk, messagebox
from tkinter.scrolledtext import ScrolledText
import ast
import math

# ---------- 安全求值（仅允许数学表达式） ----------
_SAFE_NAMES = {
    # 常用数学函数
    'sin': math.sin, 'cos': math.cos, 'tan': math.tan,
    'asin': math.asin, 'acos': math.acos, 'atan': math.atan,
    'sinh': math.sinh, 'cosh': math.cosh, 'tanh': math.tanh,
    'exp': math.exp, 'log': math.log, 'log10': math.log10,
    'sqrt': math.sqrt, 'pow': pow, 'abs': abs, 'round': round,
    # 常数
    'pi': math.pi, 'e': math.e
}

_ALLOWED_NODES = (
    ast.Expression, ast.BinOp, ast.UnaryOp, ast.Num, ast.Constant, ast.Call,
    ast.Name, ast.Load, ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Pow, ast.Mod,
    ast.FloorDiv, ast.UAdd, ast.USub, ast.Tuple, ast.List
)


def safe_eval(expr: str):
    """
    使用 AST 安全解析并计算表达式。
    支持 ^ 作为幂运算（会在前端被替换成 **）。
    """
    if not expr:
        raise ValueError("空表达式")
    # 替换常见符号
    expr = expr.replace('^', '**')
    try:
        node = ast.parse(expr, mode='eval')
    except Exception as e:
        raise ValueError(f"解析错误: {e}")

    for n in ast.walk(node):
        if not isinstance(n, _ALLOWED_NODES):
            raise ValueError(f"不允许的表达式或节点: {type(n).__name__}")
        if isinstance(n, ast.Call):
            if not (isinstance(n.func, ast.Name) and n.func.id in _SAFE_NAMES):
                raise ValueError(f"调用未允许的函数: {ast.dump(n)}")
        if isinstance(n, ast.Name):
            if n.id not in _SAFE_NAMES:
                raise ValueError(f"未允许的名称: {n.id}")
    compiled = compile(node, "<safe>", "eval")
    return eval(compiled, {"__builtins__": {}}, _SAFE_NAMES)

# ---------- GUI 应用 ----------


class Calculator(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Python 计算器")
        self.resizable(False, False)
        self._make_widgets()
        self.memory = 0.0
        self.history = []

    def _make_widgets(self):
        frm = ttk.Frame(self, padding=8)
        frm.grid(row=0, column=0)

        # 显示文本
        self.entry_var = tk.StringVar()
        entry = ttk.Entry(frm, textvariable=self.entry_var, font=(
            "Consolas", 18), justify='right', width=22)
        entry.grid(row=0, column=0, columnspan=6, pady=(0, 8))
        entry.focus_set()

        # 历史显示（滚动）
        self.history_box = ScrolledText(
            frm, width=34, height=8, state='disabled', font=("Consolas", 10))
        self.history_box.grid(row=1, column=0, columnspan=6, pady=(0, 8))

        # 按钮布局
        buttons = [
            ('MC', self.mem_clear), ('MR', self.mem_recall), ('M+', self.mem_add), ('M-',
                                                                                    self.mem_sub), ('←', self.backspace), ('C', self.clear),
            ('7', lambda: self.insert('7')), ('8', lambda: self.insert('8')), ('9', lambda: self.insert(
                '9')), ('/', lambda: self.insert('/')), ('sqrt', lambda: self.insert('sqrt(')), ('^', lambda: self.insert('^')),
            ('4', lambda: self.insert('4')), ('5', lambda: self.insert('5')), ('6', lambda: self.insert(
                '6')), ('*', lambda: self.insert('*')), ('x^2', self.square), ('1/x', self.reciprocal),
            ('1', lambda: self.insert('1')), ('2', lambda: self.insert('2')), ('3', lambda: self.insert(
                '3')), ('-', lambda: self.insert('-')), ('(', lambda: self.insert('(')), (')', lambda: self.insert(')')),
            ('0', lambda: self.insert('0')), ('.', lambda: self.insert('.')), ('±',
                                                                               self.negate), ('+', lambda: self.insert('+')), ('%', self.percent), ('=', self.calculate),
        ]

        r = 2
        c = 0
        for (text, cmd) in buttons:
            b = ttk.Button(frm, text=text, width=6, command=cmd)
            b.grid(row=r, column=c, padx=2, pady=2)
            c += 1
            if c > 5:
                c = 0
                r += 1

        # 额外科学函数按钮行
        funcs = [('sin', 'sin('), ('cos', 'cos('), ('tan', 'tan('),
                 ('log', 'log('), ('ln', 'log('), ('pi', 'pi')]
        r += 1
        c = 0
        for label, to_ins in funcs:
            b = ttk.Button(frm, text=label, width=6,
                           command=lambda s=to_ins: self.insert(s))
            b.grid(row=r, column=c, padx=2, pady=4)
            c += 1

        # 绑定键盘
        self.bind_all('<Return>', lambda e: self.calculate())
        self.bind_all('<KP_Enter>', lambda e: self.calculate())
        self.bind_all('<BackSpace>', lambda e: self.backspace())
        self.bind_all('<Escape>', lambda e: self.clear())
        for k in '0123456789.+-*/()%^':
            self.bind_all(k, lambda e, ch=k: self.insert(ch))

    # 功能实现
    def insert(self, s):
        cur = self.entry_var.get()
        self.entry_var.set(cur + s)

    def clear(self):
        self.entry_var.set('')

    def backspace(self):
        cur = self.entry_var.get()
        if cur:
            self.entry_var.set(cur[:-1])

    def negate(self):
        cur = self.entry_var.get().strip()
        if not cur:
            return
        try:
            # 尝试计算当前表达式值并取相反数（安全）
            val = safe_eval(cur)
            self.entry_var.set(str(-val))
        except Exception:
            # 若无法直接计算（表达式不完整），则尝试在前面插入负号
            if cur.startswith('-'):
                self.entry_var.set(cur[1:])
            else:
                self.entry_var.set('-' + cur)

    def percent(self):
        cur = self.entry_var.get().strip()
        if not cur:
            return
        try:
            val = safe_eval(cur)
            self.entry_var.set(str(val / 100.0))
        except Exception as e:
            messagebox.showerror("错误", f"无法计算百分比: {e}")

    def square(self):
        cur = self.entry_var.get().strip()
        if not cur:
            return
        try:
            val = safe_eval(cur)
            self.entry_var.set(str(val * val))
        except Exception as e:
            messagebox.showerror("错误", f"无法平方: {e}")

    def reciprocal(self):
        cur = self.entry_var.get().strip()
        if not cur:
            return
        try:
            val = safe_eval(cur)
            if val == 0:
                raise ZeroDivisionError("除以零")
            self.entry_var.set(str(1.0 / val))
        except Exception as e:
            messagebox.showerror("错误", f"无法求倒数: {e}")

    def calculate(self):
        expr = self.entry_var.get().strip()
        if not expr:
            return
        try:
            result = safe_eval(expr)
            # 显示结果并记录历史
            self.entry_var.set(str(result))
            self._add_history(expr, result)
        except Exception as e:
            messagebox.showerror("计算错误", str(e))

    def _add_history(self, expr, result):
        self.history.append((expr, result))
        self.history_box.config(state='normal')
        self.history_box.insert('end', f"{expr} = {result}\n")
        self.history_box.see('end')
        self.history_box.config(state='disabled')

    # 内存功能
    def mem_clear(self):
        self.memory = 0.0
        messagebox.showinfo("内存", "已清除内存")

    def mem_recall(self):
        self.entry_var.set(str(self.memory))

    def mem_add(self):
        cur = self.entry_var.get().strip()
        if not cur:
            return
        try:
            val = safe_eval(cur)
            self.memory += val
            messagebox.showinfo("内存", f"内存已更新：{self.memory}")
        except Exception as e:
            messagebox.showerror("错误", f"无法加入内存: {e}")

    def mem_sub(self):
        cur = self.entry_var.get().strip()
        if not cur:
            return
        try:
            val = safe_eval(cur)
            self.memory -= val
            messagebox.showinfo("内存", f"内存已更新：{self.memory}")
        except Exception as e:
            messagebox.showerror("错误", f"无法从内存减去: {e}")


if __name__ == "__main__":
    app = Calculator()
    app.mainloop()
