SYSTEM: You are a Python programmer tasked with refactoring code. You are proficient in Python 3.12 and use modern features where appropriate. Your code is clear, easy to read, and free of unnecessary verbosity. You follow PEP 8, 20 and 257 to ensure high quality python code. You remove comments and instead focus on simple and clear one line docstrings to describe every function. You think through your task step by step. USER: import math
import re
from typing import Any, Union, Callable
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QLineEdit, QTextEdit, QScrollBar, QPushButton, QLabel, QFrame,
    QGridLayout
)
from PySide6.QtGui import QFont
from PySide6.QtCore import Qt, QTimer, Signal


# ------------------------ Utility Functions ------------------------
def convert_angle(func: Callable[[float], float], x: float, use_degrees: bool) -> float:
    """Convert angle based on mode (degrees/radians)."""
    return func(math.radians(x)) if use_degrees else func(x)

def kilograms_to_pounds(kg: float) -> float:
    """Convert kilograms to pounds."""
    return kg / 0.45359237

def pounds_to_kilograms(lb: float) -> float:
    """Convert pounds to kilograms."""
    return lb * 0.45359237

def celsius_to_kelvin(c: float) -> float:
    """Convert Celsius to Kelvin."""
    return c + 273.15

def kelvin_to_celsius(k: float) -> float:
    """Convert Kelvin to Celsius."""
    return k - 273.15

def celsius_to_fahrenheit(c: float) -> float:
    """Convert Celsius to Fahrenheit."""
    return c * 9 / 5 + 32

def fahrenheit_to_celsius(f: float) -> float:
    """Convert Fahrenheit to Celsius."""
    return (f - 32) * 5 / 9

def capitalize_sentence(sentence: str) -> str:
    """Capitalize each word in a sentence."""
    return ' '.join(word.capitalize() for word in sentence.split())

def remove_duplicates(text: str) -> str:
    """Remove duplicate words from text."""
    words = ' '.join(line.strip() for line in text.splitlines() if line.strip()).split()
    return ' '.join(dict.fromkeys(words))

def get_safe_globals(use_degrees: bool, variables: dict) -> dict:
    """Define safe global functions and variables for expression evaluation."""
    trig_funcs = {'sin', 'cos', 'tan', 'asin', 'acos', 'atan'}
    safe_globals = {
        "sqrt": math.sqrt,
        "log": math.log10,
        "ln": math.log,
        "pi": math.pi,
        "e": math.e,
        "KilogramToPound": kilograms_to_pounds,
        "PoundToKilogram": pounds_to_kilograms,
        "CelsiusToKelvin": celsius_to_kelvin,
        "KelvinToCelsius": kelvin_to_celsius,
        "CelsiusToFahrenheit": celsius_to_fahrenheit,
        "FahrenheitToCelsius": fahrenheit_to_celsius,
        "capitalize": capitalize_sentence,
        "duplicates": remove_duplicates,
    }
    for func in trig_funcs:
        safe_globals[func] = lambda x, f=func: convert_angle(getattr(math, f), x, use_degrees)
    safe_globals.update(variables)
    return safe_globals

def preprocess_text_functions(expression: str) -> str:
    """Preprocess text functions by ensuring arguments are quoted."""
    def add_quotes(match):
        func, args = match.groups()
        args = args.strip()
        if not (args.startswith('"') and args.endswith('"')) and not (args.startswith("'") and args.endswith("'")):
            args = f'"{args}"'
        return f'{func}({args})'
    return re.sub(r'\b(capitalize|duplicates)\s*\(\s*([^)]+)\s*\)', add_quotes, expression)

def validate_inverse_trig(expression: str, safe_globals: dict) -> Union[str, None]:
    """Validate the domain of inverse trigonometric functions."""
    for func, arg in re.findall(r'\b(asin|acos)\s*\(\s*([^)]+)\s*\)', expression):
        try:
            value = eval(arg, {"builtins": None}, safe_globals)
            if abs(value) > 1:
                return f"Error: {func}({arg}) is undefined for |{arg}| > 1"
        except Exception as e:
            return f"Error: {e}"
    return None

def safe_evaluate(expression: str, safe_globals: dict) -> Union[str, Any]:
    """Safely evaluate the mathematical expression."""
    try:
        result = eval(expression, {"builtins": None}, safe_globals)
        if isinstance(result, (int, float)):
            epsilon = 1e-10
            if isinstance(result, float):
                if math.isclose(result, 0.0, abs_tol=epsilon):
                    result = 0.0
                abs_result = abs(result)
                if (abs_result >= 1e9) or (abs_result != 0 and abs_result < 1e-6):
                    result = f"{result:.3e}"
                else:
                    result = f"{result:.3f}"
            else:
                if len(str(abs(result))) > 9:
                    result = f"{result:.3e}"
                else:
                    result = str(result)
            return result
        else:
            return result
    except Exception as e:
        return f"Error: {e}"

def evaluate_expression(expression: str, variables: dict, use_degrees: bool = False) -> Union[float, str]:
    """Evaluate the user's expression, handling variable assignments."""
    assignment_match = re.match(r'^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$', expression)
    expression = preprocess_text_functions(expression)
    # Get safe_globals without user variables to derive reserved names
    base_safe_globals = get_safe_globals(use_degrees, {})
    reserved_names = set(base_safe_globals.keys())

    if assignment_match:
        var_name, var_expr = assignment_match.groups()

        # Check if the variable name is reserved
        if var_name in reserved_names:
            return f"Error: '{var_name}' is a reserved name and cannot be used as a variable."

        safe_globals = get_safe_globals(use_degrees, variables)
        var_value = safe_evaluate(var_expr, safe_globals)
        try:
            if isinstance(var_value, str) and not var_value.startswith("Error"):
                if '.' in var_value:
                    var_value_converted = float(var_value)
                else:
                    var_value_converted = int(var_value)
                variables[var_name] = var_value_converted
                return f"{var_name} = {var_value_converted}"
            else:
                return var_value
        except ValueError:
            return f"Error: Cannot assign non-numeric value to variable '{var_name}'"
    else:
        safe_globals = get_safe_globals(use_degrees, variables)
        error = validate_inverse_trig(expression, safe_globals)
        if error:
            return error
        return safe_evaluate(expression, safe_globals)

#--- GUI ---

class LongPressButton(QPushButton):
    """A QPushButton that emits a signal only if pressed and held for a specified duration."""
    longPressed = Signal()

    def __init__(self, text="", parent=None, hold_duration=3000):
        """Initialize the button."""
        super().__init__(text, parent)
        self.hold_duration = hold_duration
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.on_long_press)
        self.pressed.connect(self.on_press)
        self.released.connect(self.on_release)
        self.is_long_press = False

    def on_press(self):
        """Start the timer when the button is pressed."""
        self.is_long_press = False
        self.timer.start(self.hold_duration)

    def on_release(self):
        """Stop the timer when the button is released."""
        if self.timer.isActive():
            self.timer.stop()

    def on_long_press(self):
        """Handle the long press event."""
        self.is_long_press = True
        self.longPressed.emit()

# ---------------------------- CalculatorGUI Class ----------------------------
class CalculatorGUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.use_degrees = True
        self.variables = {}
        self.history_entries = []
        self.text_functions = ['capitalize', 'duplicates']
        self.setup_window()
        self.create_widgets()
        self.layout_widgets()
        self.connect_signals()

    def setup_window(self) -> None:
        """Set up the main window properties."""
        self.setWindowTitle("Calculator")
        self.setGeometry(100, 100, 1280, 720)

    def create_widgets(self) -> None:
        """Create all widgets used in the application."""
        font = QFont()
        font.setPointSize(12)
        font.setFamily('Literata')

        self.entry = QLineEdit()
        self.entry.setFont(font)
        self.entry.setPlaceholderText('Enter expression')
        self.entry.setClearButtonEnabled(True)
        self.entry.returnPressed.connect(self.calculate)

        self.history_text = QTextEdit()
        self.history_text.setFont(font)
        self.history_text.setReadOnly(True)
        self.history_text.setStyleSheet("position: relative;")

        self.scrollbar = QScrollBar(Qt.Vertical)
        self.history_text.setVerticalScrollBar(self.scrollbar)

        # Search Button
        self.search_button = QPushButton("🔍", self.history_text)
        self.search_button.setFixedSize(24, 24)
        self.search_button.setToolTip("Search History")
        self.search_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.search_button.clicked.connect(self.toggle_search)
        self.search_button.raise_()

        # Replace delete_button with LongPressButton for clearing history
        self.delete_button = LongPressButton("🗑️", self.history_text, hold_duration=3000)
        self.delete_button.setFixedSize(24, 24)
        self.delete_button.setToolTip("Clear History (Hold for 3 seconds)")
        self.delete_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.delete_button.longPressed.connect(self.clear_history)
        self.delete_button.raise_()

        # Variables Button
        self.vars_button = QPushButton("🧮", self.history_text)
        self.vars_button.setFixedSize(24, 24)
        self.vars_button.setToolTip("Show Variables")
        self.vars_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.vars_button.clicked.connect(self.toggle_vars_overlay)
        self.vars_button.raise_()

        # Variables Overlay
        self.vars_overlay = QLineEdit(self.history_text)
        self.vars_overlay.setReadOnly(True)
        self.vars_overlay.setPlaceholderText("a = 1, b = 85, ...")
        self.vars_overlay.setFixedHeight(24)
        self.vars_overlay.setFixedWidth(300)
        self.vars_overlay.setStyleSheet("""
            QLineEdit {
                border: 1px solid gray;
                border-radius: 4px;
                padding: 2px 4px;
                background-color: white;
            }
        """)
        self.vars_overlay.setVisible(False)

        # Replace vars_delete_button with LongPressButton
        self.vars_delete_button = LongPressButton("🗑️", self.vars_overlay, hold_duration=3000)
        self.vars_delete_button.setFixedSize(16, 16)
        self.vars_delete_button.setToolTip("Clear Variables (Hold for 3 seconds)")
        self.vars_delete_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 10px;
                border-radius: 8px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.vars_delete_button.longPressed.connect(self.clear_variables)
        self.vars_delete_button.setVisible(False)

        # Search Bar
        self.search_bar = QLineEdit(self.history_text)
        self.search_bar.setPlaceholderText("Search...")
        self.search_bar.setFixedHeight(24)
        self.search_bar.setFixedWidth(300)
        self.search_bar.setVisible(False)
        self.search_bar.setStyleSheet("""
            QLineEdit {
                border: 1px solid gray;
                border-radius: 4px;
                padding: 2px 4px;
                background-color: white;
            }
        """)
        self.search_bar.textChanged.connect(self.filter_history)
        self.search_bar.setClearButtonEnabled(True)

        # Function Buttons
        self.function_buttons = {
            "Functions": ["sqrt", "sin", "cos", "tan", "asin", "acos", "atan", "log", "ln"],
            "Conversions": ["lb", "kg", "K", "C", "F", "CF"],
            "Text": ["capitalize", "duplicates"],
            "Constants": ["pi", "e"],
            "Settings": []
        }
        self.function_frame = QFrame()
        self.create_function_buttons(font)

    def create_function_buttons(self, font: QFont) -> None:
        """Create and arrange function buttons."""
        layout = QGridLayout()
        categories = [("Functions", 3), ("Conversions", 2), ("Text", 2), ("Constants", 2), ("Settings", 1)]
        starting_col = 0

        for category, col_span in categories:
            label = QLabel(category)
            label.setFont(font)
            layout.addWidget(label, 0, starting_col, 1, col_span, Qt.AlignCenter)

            buttons = self.function_buttons.get(category, [])
            if category == "Functions":
                for i, func in enumerate(buttons):
                    button = QPushButton(func)
                    button.setFont(font)
                    button.clicked.connect(
                        lambda _, f=func: self.insert_function(f, is_text_function=(f in self.text_functions))
                    )
                    layout.addWidget(button, (i // 3) + 1, starting_col + (i % 3))

            elif category == "Settings":
                self.mode_button = QPushButton("Degrees")
                self.mode_button.setFont(font)
                self.mode_button.clicked.connect(self.toggle_mode)
                layout.addWidget(self.mode_button, 1, starting_col, 1, 1)

            elif category == "Conversions":
                conversion_mapping = {
                    "lb": "KilogramToPound",
                    "kg": "PoundToKilogram",
                    "K": "CelsiusToKelvin",
                    "C": "KelvinToCelsius",
                    "F": "CelsiusToFahrenheit",
                    "CF": "FahrenheitToCelsius"
                }

                for idx, func in enumerate(buttons):
                    display_text = {
                        "lb": "lb(Kg)",
                        "kg": "kg(lb)",
                        "K": "K(°C)",
                        "C": "°C(K)",
                        "F": "F(°C)",
                        "CF": "°C(F)"
                    }.get(func, func)

                    button = QPushButton(display_text)
                    button.setFont(font)

                    function_to_insert = conversion_mapping.get(func, func)

                    button.clicked.connect(
                        lambda _, f=function_to_insert: self.insert_function(f, is_text_function=False)
                    )
                    row, col = 1 + idx // 2, starting_col + idx % 2
                    layout.addWidget(button, row, col)

            elif category == "Text":
                for idx, func in enumerate(buttons):
                    button = QPushButton(func)
                    button.setFont(font)
                    button.clicked.connect(
                        lambda _, f=func: self.insert_function(f, is_text_function=True)
                    )
                    row, col = 1 + idx // col_span, starting_col + idx % col_span
                    layout.addWidget(button, row, col)

            elif category == "Constants":
                for idx, func in enumerate(buttons):
                    button = QPushButton(func)
                    button.setFont(font)
                    button.clicked.connect(
                        lambda _, f=func: self.insert_function(f, is_constant=True)
                    )
                    row, col = 1 + idx // col_span, starting_col + idx % col_span
                    layout.addWidget(button, row, col)

            total_rows = math.ceil(len(buttons) / col_span)
            # Fill empty grid spaces with empty labels
            for extra in range(col_span * total_rows - len(buttons)):
                layout.addWidget(QLabel(""), 1 + extra // col_span, starting_col + extra % col_span)

            starting_col += col_span

        self.function_frame.setLayout(layout)

    def layout_widgets(self) -> None:
        """Arrange all widgets in the main window."""
        central_widget = QWidget()
        main_layout = QVBoxLayout()

        main_layout.addWidget(self.entry)

        history_layout = QHBoxLayout()
        history_layout.addWidget(self.history_text)
        history_layout.addWidget(self.scrollbar)
        main_layout.addLayout(history_layout)

        main_layout.addWidget(self.function_frame)

        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

    def connect_signals(self) -> None:
        """Connect global signals."""
        QApplication.instance().focusChanged.connect(self.on_focus_changed)

    def toggle_mode(self) -> None:
        """Toggle between degrees and radians for trigonometric functions."""
        self.use_degrees = not self.use_degrees
        self.mode_button.setText("Degrees" if self.use_degrees else "Radians")

    def insert_function(self, function: str, is_constant: bool = False, is_text_function: bool = False) -> None:
        """Insert the selected function into the entry field."""
        current_text = self.entry.text()
        cursor_position = self.entry.cursorPosition()
        if is_constant:
            new_text = f"{current_text[:cursor_position]}{function} "
            new_cursor_position = cursor_position + len(function) + 1
        elif is_text_function and function in self.text_functions:
            new_text = f'{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}'
            new_cursor_position = cursor_position + len(function) + 1
        else:
            new_text = f"{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}"
            new_cursor_position = cursor_position + len(function) + 1
        self.entry.setText(new_text)
        self.entry.setCursorPosition(new_cursor_position)
        self.entry.setFocus()

    def calculate(self) -> None:
        """Evaluate the expression entered by the user."""
        expression = self.entry.text().strip()
        if expression:
            result = evaluate_expression(
                expression, self.variables, use_degrees=self.use_degrees
            )
            self.display_result(expression, result)
            self.entry.clear()

    def display_result(self, expression: str, result: Union[float, str]) -> None:
        """Display the result in the history."""
        mode = "D" if self.use_degrees else "R"
        if re.match(r'^\s*[a-zA-Z_]\w*\s*=\s*.+$', expression):
            history_entry = f"[{mode}] {result}"
        else:
            history_entry = f"[{mode}] {expression} = {result}"
        self.history_entries.append(history_entry)
        self.refresh_history()
        self.history_text.verticalScrollBar().setValue(
            self.history_text.verticalScrollBar().maximum()
        )

    def toggle_search(self) -> None:
        """Toggle the visibility of the search bar."""
        if self.search_bar.isVisible():
            self.search_bar.setVisible(False)
        else:
            self.search_bar.setVisible(True)
            self.search_bar.setFocus()
        self.update_overlay_buttons_position()

    def filter_history(self, text: str) -> None:
        """Filter the history based on search text."""
        self.refresh_history(text)

    def refresh_history(self, filter_text: str = "") -> None:
        """Refresh the history display, optionally filtering results."""
        self.history_text.clear()
        for entry in self.history_entries:
            if filter_text.lower() in entry.lower():
                self.history_text.append(entry)

    def resizeEvent(self, event) -> None:
        """Handle window resize events."""
        super().resizeEvent(event)
        self.update_overlay_buttons_position()

    def update_overlay_buttons_position(self) -> None:
        """Update the position of overlay buttons based on current window size."""
        size = self.history_text.viewport().size()

        margin_right = 5
        margin_top = 5
        spacing = 5

        available_width = size.width()

        search_offset = self.search_bar.width() + spacing if self.search_bar.isVisible() else 0
        self.search_button.move(
            available_width - search_offset - self.search_button.width() - margin_right,
            margin_top
        )

        vars_offset = self.vars_overlay.width() + spacing if self.vars_overlay.isVisible() else 0
        vars_button_y = self.search_button.y() + self.search_button.height() + spacing
        self.vars_button.move(
            available_width - vars_offset - self.vars_button.width() - margin_right,
            vars_button_y
        )

        self.delete_button.move(
            available_width - self.delete_button.width() - margin_right,
            self.vars_button.y() + self.vars_button.height() + spacing
        )

        if self.vars_overlay.isVisible():
            self.vars_overlay.move(
                available_width - self.vars_overlay.width() - margin_right,
                self.vars_button.y()
            )
            self.vars_delete_button.move(
                self.vars_overlay.width() - self.vars_delete_button.width() - margin_right,
                (self.vars_overlay.height() - self.vars_delete_button.height()) // 2
            )
            self.vars_delete_button.setVisible(True)

        if self.search_bar.isVisible():
            self.search_bar.move(
                available_width - self.search_bar.width() - margin_right,
                margin_top
            )

    def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
        """Handle focus changes to hide overlays when necessary."""
        if self.vars_overlay.isVisible() and new not in {self.vars_overlay, self.vars_delete_button, self.vars_button}:
            self.vars_overlay.setVisible(False)
            self.vars_delete_button.setVisible(False)
            self.update_overlay_buttons_position()
        if self.search_bar.isVisible() and not self.search_bar.text().strip() and new != self.search_bar:
            self.toggle_search()

    def showEvent(self, event) -> None:
        """Handle the show event to position overlays correctly."""
        super().showEvent(event)
        self.update_overlay_buttons_position()

    def clear_history(self) -> None:
        """Clear the calculation history."""
        self.history_entries.clear()
        self.refresh_history()

    def toggle_vars_overlay(self) -> None:
        """Toggle the visibility of the variables overlay."""
        if self.vars_overlay.isVisible():
            self.vars_overlay.setVisible(False)
            self.vars_delete_button.setVisible(False)
        else:
            self.update_vars_overlay()
            self.vars_overlay.setVisible(True)
            self.vars_delete_button.setVisible(True)
        self.update_overlay_buttons_position()

    def update_vars_overlay(self) -> None:
        """Update the text displayed in the variables overlay."""
        if not self.variables:
            self.vars_overlay.setText("No variables assigned.")
        else:
            vars_text = ", ".join(f"{var} = {value}" for var, value in self.variables.items())
            self.vars_overlay.setText(vars_text)

    def clear_variables(self) -> None:
        """Clear all assigned variables."""
        self.variables.clear()
        self.update_vars_overlay()
        self.vars_overlay.setVisible(True)
        self.vars_delete_button.setVisible(True)

# ---------------------------- Main Function ----------------------------
def main() -> None:
    """Main function to run the calculator application."""
    app = QApplication([])
    calculator = CalculatorGUI()
    calculator.show()
    app.exec()

if __name__ == "__main__":
    main()