# -*- coding: utf-8 -*-
"""
计算器 —— 基于 Python 标准库 tkinter 的桌面计算器

功能：
- 四则运算（+、-、×、÷）、括号、百分号（%）、正负号（±）
- 输入过程中实时预览计算结果
- 键盘输入：数字、+ - * / ( ) . %，回车 = 计算，退格删除，Esc 清空
- 错误提示：除 0、括号不匹配、表达式不完整等

运行：
    python3 计算器.py               # 启动图形界面
    python3 计算器.py --selftest    # 无界面自检（用于验证）

依赖：仅 Python 标准库（tkinter）。Windows / macOS 自带；Linux 需安装 python3-tk。
"""

import re
import sys
from decimal import Decimal, DivisionByZero, InvalidOperation, getcontext

getcontext().prec = 40  # 计算精度：40 位有效数字

try:
    import tkinter as tk
    from tkinter import ttk
    _TK_AVAILABLE = True
except ImportError:
    tk = None
    ttk = None
    _TK_AVAILABLE = False


# ============================ 表达式求值（安全解析器，不使用 eval） ============================

class EvalError(Exception):
    """表达式求值错误"""


# 数字（含小数、科学计数法）、括号、运算符
TOKEN_RE = re.compile(r"\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?|[()+\-*/%]")
NUMBER_RE = re.compile(r"\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?")


def tokenize(expr: str) -> list:
    """把表达式拆成 token 列表；遇到非法字符抛 EvalError"""
    tokens, i = [], 0
    while i < len(expr):
        ch = expr[i]
        if ch.isspace():
            i += 1
            continue
        m = TOKEN_RE.match(expr, i)
        if not m:
            raise EvalError("包含无法识别的字符")
        tokens.append(m.group(0))
        i = m.end()
    return tokens


class Parser:
    """递归下降解析器：+ - * / ( ) 以及一元正负号、后缀百分号"""

    def __init__(self, tokens):
        self.tokens = tokens
        self.pos = 0

    def peek(self):
        return self.tokens[self.pos] if self.pos < len(self.tokens) else None

    def consume(self):
        tok = self.peek()
        self.pos += 1
        return tok

    def parse(self) -> Decimal:
        if not self.tokens:
            raise EvalError("表达式为空")
        value = self.expr()
        if self.pos != len(self.tokens):
            raise EvalError("表达式不完整")
        return value

    def expr(self) -> Decimal:
        value = self.term()
        while self.peek() in ("+", "-"):
            op = self.consume()
            rhs = self.term()
            value = value + rhs if op == "+" else value - rhs
        return value

    def term(self) -> Decimal:
        value = self.factor()
        while self.peek() in ("*", "/"):
            op = self.consume()
            rhs = self.factor()
            if op == "*":
                value *= rhs
            else:
                if rhs == 0:
                    raise EvalError("不能除以 0")
                value /= rhs
        return value

    def factor(self) -> Decimal:
        tok = self.peek()
        if tok is None:
            raise EvalError("表达式不完整")
        if tok == "+":
            self.consume()
            return self.factor()
        if tok == "-":
            self.consume()
            return -self.factor()
        if tok == "(":
            self.consume()
            value = self.expr()
            if self.consume() != ")":
                raise EvalError("括号不匹配")
            return value
        if NUMBER_RE.fullmatch(tok):
            self.consume()
            value = Decimal(tok)
            if self.peek() == "%":
                self.consume()
                value /= 100
            return value
        raise EvalError("无效的输入")


def evaluate(expr: str) -> Decimal:
    """计算表达式；出错抛 EvalError"""
    try:
        return Parser(tokenize(expr)).parse()
    except EvalError:
        raise
    except (DivisionByZero, InvalidOperation):
        raise EvalError("数值超出可计算范围") from None


def format_number(value: Decimal) -> str:
    """把 Decimal 结果格式化为适合显示的字符串（去掉小数尾零，如 20.0 -> 20）"""
    if not value.is_finite():
        return "错误"
    if value == value.to_integral_value():
        return format(value.quantize(Decimal(1)), "f")
    return format(value, ".12g")


# ============================ 图形界面 ============================

class CalculatorApp:
    # 配色
    BG = "#f2f4f8"
    DISPLAY_BG = "#ffffff"
    TEXT_DARK = "#1f2430"
    TEXT_GRAY = "#8a94a6"
    NUM_BG = "#ffffff"
    NUM_ACTIVE = "#e6ebf2"
    OP_BG = "#e3e9f2"
    OP_ACTIVE = "#d3dce8"
    FUNC_BG = "#ffe3df"
    FUNC_ACTIVE = "#ffd0ca"
    FUNC_FG = "#c0392b"
    EQ_BG = "#3d6df2"
    EQ_ACTIVE = "#2f5ae0"

    def __init__(self, root):
        self.root = root
        self.expression = ""       # 当前表达式（内部用 * / - 等标准符号）
        self.last_result = None    # 最近一次计算结果（Decimal）
        self.last_expression = None
        self.just_evaluated = False

        root.title("计算器")
        root.configure(bg=self.BG)
        root.resizable(False, False)
        root.geometry("340x520")

        self._build_display()
        self._build_buttons()
        self._bind_keys()
        self.update_display()

    # ---------- 界面搭建 ----------

    def _build_display(self):
        style = ttk.Style(self.root)
        style.configure("Calc.Expr.TLabel", background=self.DISPLAY_BG,
                        foreground=self.TEXT_GRAY, font=("", 14))
        style.configure("Calc.Result.TLabel", background=self.DISPLAY_BG,
                        foreground=self.TEXT_DARK, font=("", 32, "bold"))

        display = tk.Frame(self.root, bg=self.DISPLAY_BG)
        display.pack(fill="x", padx=12, pady=(14, 4))
        self.expression_var = tk.StringVar(value="")
        self.result_var = tk.StringVar(value="0")

        ttk.Label(display, textvariable=self.expression_var, anchor="e",
                  style="Calc.Expr.TLabel").pack(fill="x", padx=14, pady=(14, 0))
        ttk.Label(display, textvariable=self.result_var, anchor="e",
                  style="Calc.Result.TLabel").pack(fill="x", padx=14, pady=(2, 14))

        # 分隔线
        tk.Frame(self.root, bg="#dfe4ec", height=1).pack(fill="x", padx=12)

    def _build_buttons(self):
        # ttk + clam 主题：颜色可控，且兼容性更好
        style = ttk.Style(self.root)
        try:
            style.theme_use("clam")
        except tk.TclError:
            pass
        for name, (bg, fg, active) in {
            "Calc.Num.TButton":   (self.NUM_BG,  self.TEXT_DARK, self.NUM_ACTIVE),
            "Calc.Op.TButton":    (self.OP_BG,   self.TEXT_DARK, self.OP_ACTIVE),
            "Calc.Func.TButton":  (self.FUNC_BG, self.FUNC_FG,   self.FUNC_ACTIVE),
            "Calc.Eq.TButton":    (self.EQ_BG,   "#ffffff",      self.EQ_ACTIVE),
        }.items():
            style.configure(name, background=bg, foreground=fg,
                            font=("", 16, "bold"), padding=(0, 12), borderwidth=0)
            style.map(name, background=[("pressed", active), ("active", active)])

        frame = tk.Frame(self.root, bg=self.BG)
        frame.pack(fill="both", expand=True, padx=8, pady=(6, 12))
        for c in range(4):
            frame.grid_columnconfigure(c, weight=1, uniform="btn")
        for r in range(6):
            frame.grid_rowconfigure(r, weight=1, uniform="btn")

        specs = [
            # (文字, 行, 列, 列跨度, 样式)
            ("C",   0, 0, 1, "func"),
            ("←",   0, 1, 1, "func"),
            ("(",   0, 2, 1, "op"),
            (")",   0, 3, 1, "op"),
            ("7",   1, 0, 1, "num"),
            ("8",   1, 1, 1, "num"),
            ("9",   1, 2, 1, "num"),
            ("÷",   1, 3, 1, "op"),
            ("4",   2, 0, 1, "num"),
            ("5",   2, 1, 1, "num"),
            ("6",   2, 2, 1, "num"),
            ("×",   2, 3, 1, "op"),
            ("1",   3, 0, 1, "num"),
            ("2",   3, 1, 1, "num"),
            ("3",   3, 2, 1, "num"),
            ("−",   3, 3, 1, "op"),
            ("±",   4, 0, 1, "num"),
            ("0",   4, 1, 1, "num"),
            (".",   4, 2, 1, "num"),
            ("+",   4, 3, 1, "op"),
            ("%",   5, 0, 1, "num"),
            ("=",   5, 1, 3, "eq"),
        ]
        style_names = {"num": "Calc.Num.TButton", "op": "Calc.Op.TButton",
                       "func": "Calc.Func.TButton", "eq": "Calc.Eq.TButton"}

        for text, r, c, span, kind in specs:
            btn = ttk.Button(frame, text=text, command=self._command_for(text),
                             style=style_names[kind], takefocus=0)
            btn.grid(row=r, column=c, columnspan=span,
                     sticky="nsew", padx=4, pady=4)
        return frame

    def _command_for(self, text):
        if text == "C":
            return self.clear
        if text == "←":
            return self._backspace
        if text == "=":
            return self.evaluate_current
        if text == "±":
            return self._negate
        return lambda: self.insert(text)

    def _bind_keys(self):
        self.root.bind_all("<Key>", self._on_key)

    def _on_key(self, event):
        ch = event.char
        if ch.isdigit() or ch in "+-*/().%":
            self.insert(ch)
        elif ch == "=" or event.keysym == "Return":
            self.evaluate_current()
        elif event.keysym == "BackSpace":
            self._backspace()
        elif event.keysym == "Escape":
            self.clear()

    # ---------- 输入逻辑 ----------

    @staticmethod
    def pretty(expr):
        """界面显示用的表达式（把 * / 换成 × ÷）"""
        return expr.replace("*", "×").replace("/", "÷")

    def insert(self, text):
        text = {"÷": "/", "×": "*", "−": "-"}.get(text, text)

        if self.just_evaluated:
            if text in "+-*/(.":
                pass                # 基于上次结果继续计算
            elif text == ")":
                return
            else:                   # 数字 / % 开始新的计算
                self.expression = ""
                self.last_result = None
            self.just_evaluated = False

        if text == "(":
            if self.expression and self.expression[-1] in "0123456789.)":
                self.expression += "*"   # 2(3 自动补乘号
            self.expression += "("
        elif text == ")":
            if self.expression.count("(") <= self.expression.count(")"):
                return
            if not self.expression or self.expression[-1] in "+-*/(.":
                return
            self.expression += ")"
        elif text == ".":
            self._insert_dot()
        elif text == "%":
            self._insert_percent()
        elif text in "+-*/":
            self._insert_operator(text)
        elif text.isdigit():
            self._insert_digit(text)

        self.update_display()

    def _insert_digit(self, digit):
        if self.expression == "0":
            self.expression = digit   # 避免前导 0
        else:
            self.expression += digit

    def _insert_dot(self):
        if not self.expression:
            self.expression = "0."
            return
        if self.expression[-1] == ".":
            return
        if self.expression[-1] in "+-*/(":
            self.expression += "0."
            return
        m = re.search(r"(\d+\.?\d*|\.\d+)$", self.expression)
        if m and "." in m.group(1):
            return                  # 当前数字已含小数点
        self.expression += "."

    def _insert_operator(self, op):
        if not self.expression:
            if op == "-":
                self.expression = "-"   # 允许以负数开头
            return
        last = self.expression[-1]
        if last in "+-*/":
            if op == "-" and last in "+*/":
                self.expression += "-"  # 允许 5*-3、5/-3
                return
            if op == last:
                return
            self.expression = self.expression[:-1] + op
        elif last == "(":
            if op == "-":
                self.expression += "-"  # (-5 形式
        else:
            self.expression += op

    def _insert_percent(self):
        if not self.expression:
            return
        if self.expression[-1] in "+-*/(.":
            return
        self.expression += "%"

    def _negate(self):
        """正负号：翻转最后一个数字的符号"""
        if self.just_evaluated and self.last_result is not None:
            self.expression = format_number(-self.last_result)
            self.just_evaluated = False
            self.update_display()
            return
        if not self.expression:
            return
        if re.search(r"[eE][+-]?\d+$", self.expression):
            return                  # 科学计数法结尾，不做翻转
        m = re.search(r"(-)?(\d+\.?\d*|\.\d+)$", self.expression)
        if not m:
            return                  # 末尾不是数字（如右括号）
        if m.group(1):              # 已有负号 → 去掉
            self.expression = self.expression[:m.start()] + m.group(2)
        else:                       # 无负号 → 用括号包裹负号
            self.expression = self.expression[:m.start()] + "(-" + m.group(2) + ")"
        self.update_display()

    def _backspace(self):
        if self.just_evaluated:
            self.expression = ""
            self.last_result = None
            self.just_evaluated = False
        else:
            self.expression = self.expression[:-1]
        self.update_display()

    def clear(self):
        self.expression = ""
        self.last_result = None
        self.last_expression = None
        self.just_evaluated = False
        self.update_display()

    # ---------- 计算与显示 ----------

    def evaluate_current(self):
        if not self.expression:
            return
        original = self.expression
        try:
            value = evaluate(original)
        except EvalError as exc:
            self.just_evaluated = False
            self.expression_var.set(self.pretty(self.expression))
            self.result_var.set(f"错误：{exc}")
            return
        self.last_expression = original
        self.expression = format_number(value)
        self.last_result = value
        self.just_evaluated = True
        self.update_display()

    def update_display(self):
        if self.just_evaluated:
            self.expression_var.set(self.pretty(self.last_expression or "") + " =")
            self.result_var.set(self.expression)
            return
        pretty = self.pretty(self.expression)
        self.expression_var.set(pretty)
        if not self.expression:
            self.result_var.set("0")
            return
        try:
            value = evaluate(self.expression)
            self.result_var.set(format_number(value))
        except EvalError:
            self.result_var.set(pretty)   # 表达式不完整时显示输入本身


# ============================ 自检与入口 ============================

def run_selftest():
    cases = [
        # (表达式, 期望结果；EvalError 表示应报错)
        ("1+2*3", "7"),
        ("(1+2)*3", "9"),
        ("10/4", "2.5"),
        ("0.1+0.2", "0.3"),
        ("-5+3", "-2"),
        ("5*-3", "-15"),
        ("50%", "0.5"),
        ("-50%", "-0.5"),
        ("200*10%", "20"),
        ("12+(-5)", "7"),
        ("(1+2)*(3+4)", "21"),
        ("1/3", "0.333333333333"),
        ("2.5*4", "10"),
        ("1/0", "EvalError"),
        ("(1+2", "EvalError"),
        ("1+2)", "EvalError"),
        ("2+3x", "EvalError"),
        ("", "EvalError"),
    ]
    passed = 0
    for expr, expected in cases:
        try:
            got = format_number(evaluate(expr))
        except EvalError:
            got = "EvalError"
        ok = got == expected
        passed += ok
        print(f"{'通过' if ok else '失败'}  {expr!r:14} => {got:>18} (期望 {expected})")
    total = len(cases)
    print(f"\n自检结果：{passed}/{total} 通过")
    sys.exit(0 if passed == total else 1)


def main():
    if not _TK_AVAILABLE:
        print("未找到 tkinter，请先安装：Windows/macOS 自带；Linux 执行 sudo apt install python3-tk")
        sys.exit(1)

    if sys.platform == "win32":     # 高分屏下让界面更清晰
        try:
            import ctypes
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
        except Exception:
            pass

    root = tk.Tk()
    CalculatorApp(root)
    root.mainloop()


if __name__ == "__main__":
    if "--selftest" in sys.argv:
        run_selftest()
    main()
#（注：内容由AI生成）
