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 on line docstrings. USER: import math
import re
from typing import Any, Union, Callable
from math import ceil

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, QSize

# -- Utility Functions --

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

# -- Conversions Category --

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

# -- Text Category --

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

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

# -- Processing --

def get_safe_globals(use_degrees: bool, variables: dict) -> dict:
    """Return a dictionary of safe globals for expression evaluation, including user-defined variables."""
    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,
        "lb": kilograms_to_pounds,
        "kg": pounds_to_kilograms,
        "C": kelvin_to_celsius,
        "K": celsius_to_kelvin,
        "F": celsius_to_fahrenheit,
        "CF": fahrenheit_to_celsius,
        "capitalize": capitalize_sentence,
        "duplicates": remove_duplicates,
    }
    # Add trigonometric functions with angle conversion
    for func in trig_funcs:
        safe_globals[func] = (lambda f: (lambda x: convert_angle(math.__dict__[f], x, use_degrees)))(func)
    # Include user-defined variables
    safe_globals.update(variables)
    return safe_globals

def preprocess_text_functions(expression: str) -> str:
    """Preprocess text functions to ensure proper quoting of arguments."""
    def add_quotes(match):
        func, args = match.groups()
        return f'{func}("{args}")' if func in ['capitalize', 'duplicates'] else match.group(0)
    # Corrected regex pattern with only two capturing groups
    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 inputs for inverse trigonometric functions."""
    for func, arg in re.findall(r"(\b\w+\b)\s*\(\s*([^)]+)\s*\)", expression):
        if func in ["asin", "acos"]:
            try:
                value = eval(arg, {"__builtins__": None}, safe_globals)
                if abs(value) > 1:
                    return f"Error: {func}(x) is undefined for |x| > 1"
            except Exception as e:
                return f"Error: {e}"
    return None

def safe_evaluate(expression: str, safe_globals: dict, use_e_notation: bool) -> Union[str, Any]:
    """Safely evaluate the expression and format the result."""
    try:
        result = eval(expression, {"__builtins__": None}, safe_globals)
        if isinstance(result, float) and abs(result) < 1e-10:
            result = 0.0
        if isinstance(result, (int, float)):
            if use_e_notation:
                # Format using scientific notation with up to 2 decimal places
                result = f"{result:.2e}"
            else:
                # Standard formatting with rounding
                result = round(result, 2)
        return result
    except Exception as e:
        return f"Error: {e}"

def evaluate_expression(expression: str, variables: dict, use_degrees: bool = False, use_e_notation: bool = False) -> Union[float, str]:
    """Safely evaluate a mathematical expression considering degree mode and handle variable assignments."""
    # Check for variable assignment
    assignment_match = re.match(r'^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$', expression)
    if assignment_match:
        var_name, var_expr = assignment_match.groups()
        # Evaluate the expression for the variable
        safe_globals = get_safe_globals(use_degrees, variables)
        var_value = safe_evaluate(var_expr, safe_globals, use_e_notation)
        if isinstance(var_value, (int, float)):
            variables[var_name] = var_value
            return f"{var_name} = {var_value}"
        elif isinstance(var_value, str) and var_value.startswith("Error"):
            return var_value
        else:
            return f"{var_name} = {var_value}"
    else:
        # Regular expression evaluation
        safe_globals = get_safe_globals(use_degrees, variables)
        expression = expression.replace("^", "**")
        expression = preprocess_text_functions(expression)
        error = validate_inverse_trig(expression, safe_globals)
        if error:
            return error
        return safe_evaluate(expression, safe_globals, use_e_notation)

# -- GUI --

class CalculatorGUI(QMainWindow):
    """PySide6-based Calculator with Degrees/Radians toggle, Variables, History, and Search."""

    def __init__(self):
        super().__init__()
        self.use_degrees = False
        self.use_e_notation = False
        self.variables = {}  # Dictionary to store variables
        self.history_entries = []  # List to store history entries
        self.setup_window()
        self.create_widgets()
        self.layout_widgets()
        self.connect_signals()

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

    def create_widgets(self) -> None:
        """Initialize all GUI components."""
        font = QFont()
        font.setPointSize(12)
        font.setFamily('Literata')

        # Entry field
        self.entry = QLineEdit()
        self.entry.setFont(font)
        self.entry.setPlaceholderText('Enter expression')
        self.entry.setClearButtonEnabled(True)
        self.entry.returnPressed.connect(self.calculate)  # Ensure this line is present

        # History text area
        self.history_text = QTextEdit()
        self.history_text.setFont(font)
        self.history_text.setReadOnly(True)
        self.history_text.setStyleSheet("")  # Removed padding-right to eliminate vertical bar

        # Scrollbar for history
        self.scrollbar = QScrollBar(Qt.Vertical)
        self.history_text.setVerticalScrollBar(self.scrollbar)

        # Create search button as overlay
        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); /* Semi-transparent background */
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(0, 120, 215, 200); /* Blue on hover */
                color: white;
            }
        """)
        self.search_button.clicked.connect(self.toggle_search)

        # Search bar (hidden initially)
        self.search_bar = QLineEdit(self.history_text)
        self.search_bar.setPlaceholderText("Search...")
        self.search_bar.setFixedHeight(24)
        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)  # Enable the clear (delete) button

        # 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 buttons for inserting functions into the entry."""
        layout = QGridLayout()

        # Define categories and their column spans
        categories = [("Functions", 3), ("Conversions", 2), ("Text", 2), ("Constants", 2), ("Settings", 1)]
        starting_col = 0

        for category, col_span in categories:
            # Add category label
            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, False))
                    layout.addWidget(button, (i // 3) + 1, starting_col + (i % 3))

            elif category == "Settings":
                # Arrange Settings buttons vertically within a single column
                self.mode_button = QPushButton("Radians")
                self.mode_button.setFont(font)
                self.mode_button.clicked.connect(self.toggle_mode)
                layout.addWidget(self.mode_button, 1, starting_col, 1, 1)

                self.e_notation_button = QPushButton("E Notation: Off")
                self.e_notation_button.setFont(font)
                self.e_notation_button.clicked.connect(self.toggle_e_notation)
                layout.addWidget(self.e_notation_button, 2, starting_col, 1, 1)

            elif category == "Conversions":
                for idx, func in enumerate(buttons):
                    if func == "lb":
                        display_text = "lb(kg)"
                    elif func == "kg":
                        display_text = "kg(lb)"
                    elif func == "K":
                        display_text = "K(°C)"
                    elif func == "C":
                        display_text = "°C(K)"
                    elif func == "F":
                        display_text = "F(°C)"
                    elif func == "CF":
                        display_text = "°C(F)"
                    else:
                        display_text = func
                    button = QPushButton(display_text)
                    button.setFont(font)
                    button.clicked.connect(lambda _, f=func: self.insert_function(f, False))
                    row = 1 + (idx // 2)
                    col = starting_col + (idx % 2)
                    layout.addWidget(button, row, col)

            else:
                num_buttons = len(buttons)
                num_rows = ceil(num_buttons / col_span)

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

                for extra in range(col_span * num_rows - num_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 widgets within the window."""
        central_widget = QWidget()
        main_layout = QVBoxLayout()

        # Add entry field
        main_layout.addWidget(self.entry)

        # Add history and scrollbar in a horizontal layout
        history_layout = QHBoxLayout()
        history_layout.addWidget(self.history_text)
        history_layout.addWidget(self.scrollbar)
        main_layout.addLayout(history_layout)

        # Add function buttons frame
        main_layout.addWidget(self.function_frame)

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

    def connect_signals(self) -> None:
        """Connect additional signals."""
        # Connect focusChanged signal to handle automatic hiding of search bar
        QApplication.instance().focusChanged.connect(self.on_focus_changed)

    def toggle_mode(self) -> None:
        """Switch between Degrees and Radians modes."""
        self.use_degrees = not self.use_degrees
        self.mode_button.setText("Degrees" if self.use_degrees else "Radians")

    def toggle_e_notation(self) -> None:
        """Toggle the E notation setting."""
        self.use_e_notation = not self.use_e_notation
        state = "On" if self.use_e_notation else "Off"
        self.e_notation_button.setText(f"E Notation: {state}")

    def insert_function(self, function: str, is_constant: bool = False) -> None:
        """Insert function or constant text into the expression entry."""
        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)
        else:
            new_text = f"{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}"
            new_cursor_position = cursor_position + len(function) + 2  # Adjusted for parentheses and space
        self.entry.setText(new_text)
        self.entry.setCursorPosition(new_cursor_position)
        self.entry.setFocus()

    def calculate(self) -> None:
        """Evaluate the expression and update history."""
        expression = self.entry.text().strip()
        if expression:
            result = evaluate_expression(
                expression, self.variables, use_degrees=self.use_degrees, use_e_notation=self.use_e_notation
            )
            self.display_result(expression, result)
            self.entry.clear()

    def display_result(self, expression: str, result: Union[float, str]) -> None:
        """Append the calculation to the history log."""
        mode = "D" if self.use_degrees else "R"
        # Check if the expression is a variable assignment
        if re.match(r'^\s*[a-zA-Z_]\w*\s*=\s*.+$', expression):
            # For variable assignments, display only the expression
            history_entry = f"[{mode}] {result}"
        else:
            # For regular expressions, display expression and result
            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()
        )

    # ------------------- Search Functionality -------------------

    def toggle_search(self) -> None:
        """Show or hide the search bar."""
        is_visible = self.search_bar.isVisible()
        self.search_bar.setVisible(not is_visible)
        if not is_visible:
            self.search_bar.setFocus()
            self.search_bar.setText("")  # Clear previous search
        else:
            self.search_bar.clear()
        self.update_search_bar_position()
        self.refresh_history()

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

    def refresh_history(self, filter_text: str = "") -> None:
        """Refresh the history text field based on the filter."""
        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 to reposition search button and search bar."""
        super().resizeEvent(event)
        self.update_search_bar_position()

    def update_search_bar_position(self) -> None:
        """Update the position of the search button and search bar relative to the history_text."""
        margin_right = 5
        margin_top = 5
        spacing = 5

        history_width = self.history_text.viewport().width()
        history_height = self.history_text.viewport().height()

        # Position the search button at the top-right corner
        self.search_button.move(
            history_width - self.search_button.width() - margin_right,
            margin_top
        )

        if self.search_bar.isVisible():
            # Position the search bar to the left of the search button
            # Ensure it does not go beyond the history_text's viewport
            search_bar_width = int(history_width * 0.25)
            if search_bar_width + self.search_button.width() + spacing > history_width:
                search_bar_width = history_width - self.search_button.width() - spacing - margin_right
            self.search_bar.setFixedWidth(search_bar_width)
            self.search_bar.move(
                self.search_button.x() - self.search_bar.width() - spacing,
                self.search_button.y()
            )

    def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
        """
        Handle focus changes to automatically hide the search bar
        if it is empty and loses focus.
        """
        # If the search bar is visible, empty, and not focused, hide it
        if self.search_bar.isVisible() and self.search_bar.text().strip() == "" and new != self.search_bar:
            self.toggle_search()

    def showEvent(self, event) -> None:
        """Handle the event when the window is shown to correctly position the search button."""
        super().showEvent(event)
        self.update_search_bar_position()

    # --------------------------------------------------------------

def main() -> None:
    """Run the Calculator application."""
    app = QApplication([])
    calculator = CalculatorGUI()
    calculator.show()
    app.exec()

if __name__ == "__main__":
    main()