## @file gui.py
#  @brief Graphical User Interface for the Calculatron 3000.
#  @details Contains the main Tkinter/CustomTkinter application class and UI logic.

import tkinter as tk
import mathlib
from tkinter import messagebox
from typing import Optional

import customtkinter as ctk

from evaluator import ExpressionEvaluator

## @class CalculatorApp
#  @brief A graphical calculator application using CustomTkinter.
#  @details Handles the rendering of the calculator keypad, display, user input routing, 
#           and visual formatting of complex mathematical expressions (like subscripts for logarithms).

class CalculatorApp:
    """A graphical calculator application using Tkinter."""

    # UI Constants
    WINDOW_TITLE = "Calculatron 3000"
    DEFAULT_GEOMETRY = "500x700"
    MIN_WIDTH = 500
    MIN_HEIGHT = 700
    
    FONT_DISPLAY = ("Arial", 28)
    FONT_BTN_MAIN = ("Arial", 18)
    FONT_BTN_FUNC = ("Arial", 16)
    
    COLOR_BG_WHITE = "#ffffff"
    COLOR_BTN_DEFAULT = "#d4d4d2"
    COLOR_BTN_HIGHLIGHT = "#90EE90"
    COLOR_OPERATIONS = "#FFA500"
    COLOR_GREY = "#AAAAAA"

    COLOR_RED = "#FF6347"
    COLOR_BLUE = "#1E90FF"

    #History of operations
    FONT_HISTORY = ("Arial", 14)
    COLOR_TEXT_HISTORY = "#777777" 

    ## @brief Initializes the CalculatorApp main window and state.
    #  @param root The root CustomTkinter or Tkinter window/instance.
    def __init__(self, root) -> None:
        # Initialize the main window
        self.root = root
        self.root.title("Calculatron 3000")
        
        # Set fixed window size
        self.root.geometry(self.DEFAULT_GEOMETRY)
        
        # Enable window resizing
        self.root.resizable(True, True)
        # Define min size of the window
        self.root.minsize(self.MIN_WIDTH, self.MIN_HEIGHT)

        # Create display variable to store calculator output
        self.display_var = tk.StringVar()
        self.display_var.set("0")

        self.history_var = tk.StringVar(value="")

        # Initialize state variables
        self.display_var = tk.StringVar(value="0")
        self.current_input: str = ""
        self.tokens: list[str] = []
        self.entering_log_base: bool = False

        # Build the display widget
        self._create_display()
        
        # Create container for buttons to allow dynamic switching if needed
        self.buttons_container = tk.Frame(self.root)
        self.buttons_container.pack(expand=True, fill="both")
        self.current_button_frame = None

        # Show the default keyboard
        self.show_main_buttons()

        # Bind keyboard input to event handler
        self.root.bind("<Key>", self.handle_keypress)
    
    ## @brief Configures the main window properties such as title and geometry.
    def _setup_window(self) -> None:
        """Configures the main window properties."""
        self.root.title(self.WINDOW_TITLE)
        self.root.geometry(self.DEFAULT_GEOMETRY)
        self.root.resizable(True, True)
        self.root.minsize(self.MIN_WIDTH, self.MIN_HEIGHT)

  
    ## @brief Builds the read-only display widgets for calculator history and current output.
    #  @details Creates a transparent frame containing right-aligned labels for the history 
    #           and main display, as well as floating Help and Delete buttons.
    def _create_display(self) -> None:
        """Builds the read-only display widget for calculator output."""

        self.display_frame = ctk.CTkFrame(self.root, fg_color="transparent")
        self.display_frame.pack(fill="x", padx=10, pady=10)

        # Create a label that will show the current input and results, right-aligned
        history_display = ctk.CTkLabel(
            self.display_frame,
            textvariable=self.history_var,
            font=self.FONT_HISTORY,
            text_color=self.COLOR_TEXT_HISTORY,
            fg_color="transparent", 
            anchor="e",
            justify="right",
            wraplength=500
        )
        history_display.pack(fill="x", padx=10, pady=(10, 0))

        # Create the main display label for current input and results, right-aligned
        display = ctk.CTkLabel(
            self.display_frame,
            textvariable=self.display_var,
            font=self.FONT_DISPLAY,
            text_color="black",
            fg_color="transparent", 
            anchor="e",
            justify="right",
            wraplength=450
        )
        display.pack(fill="x", padx=10, pady=(0, 40))
        
        # Add floating Help and Delete buttons in the corners of the display frame
        help_btn = ctk.CTkButton(
            self.display_frame,
            text="?",
            width=28,
            height=28,
            corner_radius=14,
            font=("Arial", 14, "bold"),
            fg_color=self.COLOR_BTN_DEFAULT,
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=self.show_help
        )
        help_btn.place(x=1, y=1, anchor="nw")
        
        # The delete button is placed in the bottom-right corner of the display frame, with a small offset to prevent overlap with the main display text
        del_btn = ctk.CTkButton(
            self.display_frame,
            text="del",
            width=28,
            height=28,
            corner_radius=14,
            font=("Arial", 14, "bold"),
            fg_color="transparent",
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=self._handle_delete
        )
        del_btn.place(relx=1.0, rely=1.0, anchor="se", x=0, y=0)
       
    ## @brief Displays the help dialog as an independent, borderless floating window.
    #  @details Creates a custom `CTkToplevel` window that floats above the main application. 
    #           It removes standard OS window decorations to achieve a "toast" notification appearance. 
    #           The method also binds internal events to allow drag-and-drop functionality across the 
    #           entire screen and a double-click action to close the dialog.
    def show_help(self) -> None:
        """Displays the help dialog as a modern, draggable overlay."""
        # If the help window already exists and is visible, do not open another instance
        if hasattr(self, 'help_window') and self.help_window is not None and self.help_window.winfo_exists():
            return

        # Create a new independent toplevel window
        self.help_window = ctk.CTkToplevel(self.root)
        
        # Remove OS window decorations (title bar, close button) for a clean look
        self.help_window.overrideredirect(True)
        # Keep the help window on top of all other windows
        self.help_window.attributes("-topmost", True)

        # framed border inside the borderless window for better styling
        help_frame = ctk.CTkFrame(
            self.help_window,
            corner_radius=0, 
            fg_color="#f2f2f2",            
            border_width=2,
            border_color=self.COLOR_OPERATIONS 
        )
        help_frame.pack(fill="both", expand=True)

        help_text = (
            "Calculatron 3000\n\n"
            "Podporované operace:\n"
            "• Základní aritmetika: Sčítání (+), odčítání (-), násobení (*), dělení (/)\n"
            "• Umocňování (xʸ / ^): Podporuje celočíselné i desetinné exponenty\n"
            "• Odmocnina (ˣ√y / √): Zadejte stupeň, poté √ a následně základ\n"
            "     - při zadávání v exponenciále se zobrazí jako (odmocnitel)rt(odmocněnec)\n"
            "• Faktoriál (x! / !): Zadejte číslo a stiskněte !\n"
            "• Přirozený logaritmus (ln): Vypočítá logaritmus o základu e\n"
            "• Exponenciála (eˣ / exp): Vypočítá mocninu Eulerova čísla\n"
            "• Logaritmus (logₓy / log): Stiskněte log, zadejte hodnotu x a následně y\n\n"
            "Pro opuštění zadávání exponentu nebo "
            "základu logaritmu a návratu k zadávání hlavního výrazu:\n"
            "  • je nutné stisknout: \n"
            "        červeně zvýrazněné tlačítko. Poté můžete plynule pokračovat v psaní výpočtu.\n\n"
            "Klávesnice: Klávesy 0-9, ., +, -, *, /, ^, !, (, )\n"
            "• Enter nebo = : Provede výpočet,\n"
            "• Backspace nebo (del): Smaže poslední zadaný znak, \n"
            "• Esc nebo C : Smaže výpočet\n\n"
            "Nápověda:\n\n• Lze libovolně přesouvat.\n"
            "• Zavřete dvojklikem"
        )

        label = ctk.CTkLabel(
            help_frame,
            text=help_text,
            font=("Arial", 14),
            text_color="black",
            justify="left"
        )
        label.pack(padx=25, pady=20)

        # Center the help window relative to the main application window
        self.root.update_idletasks()
        x_pos = self.root.winfo_rootx() + (self.root.winfo_width() // 2) - 150
        y_pos = self.root.winfo_rooty() + (self.root.winfo_height() // 2) - 200
        
        # Set absolute screen position
        self.help_window.geometry(f"+{x_pos}+{y_pos}")

        #  Drag and Drop & Close Functions
        def on_drag_start(event):
            # Store the initial click position relative to the help window
            self.help_window.startX = event.x_root - self.help_window.winfo_rootx()
            self.help_window.startY = event.y_root - self.help_window.winfo_rooty()

        def on_drag_motion(event):
            # Calculate the new window position on the screen
            x = event.x_root - self.help_window.startX
            y = event.y_root - self.help_window.startY
            # Move the window to the new physical coordinates
            self.help_window.geometry(f"+{x}+{y}")

        def on_double_click(event):
            # Destroy the help window and reset the reference
            self.help_window.destroy()
            self.help_window = None
    

        # Bind mouse events for dragging and closing across all help UI components
        for widget in (self.help_window, help_frame, label):
            widget.bind("<Button-1>", on_drag_start)
            widget.bind("<B1-Motion>", on_drag_motion)
            widget.bind("<Double-Button-1>", on_double_click)

    ## @brief Displays a temporary warning message (toast notification).
    #  @param message The text to display in the warning.## @brief Displays a temporary warning message covering the calculator display.
    #  @param message The text to display in the warning.
    def show_warning(self, message: str) -> None:
        """
        Displays a warning message overlay.
        Used to alert the user about invalid operations or exceeded limits.
        """
        # Destroy the existing warning window if it is currently displayed to prevent duplicates
        if hasattr(self, 'warning_window') and getattr(self, 'warning_window', None) is not None and self.warning_window.winfo_exists():
            self.warning_window.destroy()

        # Create a new top-level window for the warning
        self.warning_window = ctk.CTkToplevel(self.root)

        self.warning_window.withdraw()
        
        # Remove standard OS window decorations (title bar, borders)
        self.warning_window.overrideredirect(True)
        
        # Ensure the warning window stays on top of all other windows
        self.warning_window.attributes("-topmost", True)

        # Create a styled frame with a red tint and border to visually indicate a warning
        warning_frame = ctk.CTkFrame(
            self.warning_window,
            corner_radius=8,
            fg_color="#ffe6e6",            
            border_width=2,
            border_color=self.COLOR_RED 
        )
        warning_frame.pack(fill="both", expand=True)

        # Create and configure the warning text label
        label = ctk.CTkLabel(
            warning_frame,
            text=message,
            font=("Arial", 18, "bold"),
            text_color="#cc0000",
            justify="center"
        )
        label.pack(expand=True)

        # Process pending UI tasks to get accurate dimensions before positioning
        self.root.update_idletasks()
        
        # Get the exact position and dimensions of the target display frame
        x_pos = self.display_frame.winfo_rootx()
        y_pos = self.display_frame.winfo_rooty()
        width = self.display_frame.winfo_width()
        height = self.display_frame.winfo_height()
        
        # Position and size the warning window to perfectly overlay the display frame
        self.warning_window.geometry(f"{width}x{height}+{x_pos}+{y_pos}")

        self.warning_window.deiconify()

        # Automatically close (destroy) the warning window after 2500 ms (2.5 seconds)
        self.root.after(2500, self.warning_window.destroy)

    ## @brief Maps physical keyboard input to calculator functions.
    #  @details Listens for key events and routes them to the appropriate state handlers.
    #           It supports numeric input, standard operations, execution via Enter, 
    #           deletion via Backspace, and clearing via Escape.
    #  @param event The Tkinter event object containing the pressed key data (character and keysym).
    def handle_keypress(self, event) -> None:
        """Maps keyboard input to calculator functions."""

        char = event.char
        keysym = event.keysym
        
        # Process number and operator keys
        if char in '0123456789.+-*/^!()':
            self.on_button_click(char)
        
        elif char == 'e' or char == 'E':
            self.on_button_click('exp')
        
        elif char == '!':
            self.on_button_click('!')
        
        # Map Enter key to calculate
        elif keysym == 'Return' or char == '=' or  keysym == 'KP_Enter':
            self.handle_equals()
        
        # Map Backspace to delete
        elif keysym == 'BackSpace':
            self.on_button_click('del')


        # Map Escape or 'C' key to clear
        elif keysym == 'Escape' or char.lower() == 'c':
            self.clear()

    ## @brief Renders the standard calculator keypad.
    #  @details Destroys any existing button frame and reconstructs the main grid. 
    #           Consolidates static button generation and binds the dynamic '=' and ')' 
    #           buttons to instance variables so their colors can be updated dynamically.

    def show_main_buttons(self) -> None:
        """Renders the standard calculator keypad."""

        # Destroy current button frame if it exists to switch back to main buttons
        if self.current_button_frame is not None:
            self.current_button_frame.destroy()
            
        self.current_button_frame = tk.Frame(self.buttons_container)
        self.current_button_frame.pack(expand=True, fill="both")

        # Configure layout for main buttons 
        for i in range(5):
            self.current_button_frame.rowconfigure(i, weight=1)
            self.current_button_frame.columnconfigure(i, weight=1)

        # Define button labels and positions
        buttons = [
            ('xʸ', 1, 4, '^'), ('logₓy', 4, 3, 'log'), ('ˣ√y', 1, 3, '√'), ('(', 0, 1, '('),
            ('7', 1, 0, '7'), ('8', 1, 1, '8'), ('9', 1, 2, '9'), ('/', 2, 4, '/'), ('C', 0, 0, 'C'),
            ('4', 2, 0, '4'), ('5', 2, 1, '5'), ('6', 2, 2, '6'), ('*', 2, 3, '*'),
            ('1', 3, 0, '1'), ('2', 3, 1, '2'), ('3', 3, 2, '3'), ('-', 3, 3, '-'),
            ('.', 4, 1, '.'), ('+', 3, 4, '+'),('eˣ', 0, 3, 'exp')
        ]
        
        # Generate buttons based on the defined labels and positions, applying color coding for operations and special functions.
        for (text, row, col, op) in buttons:
            
            cmd = lambda o=op: self.on_button_click(o)
            
            if op in ['+', '-', '*', '/', '^', '√','log', 'exp', 'ln', 'del', 'C']:
                color = self.COLOR_OPERATIONS
            elif op in ['(',')']:
                color = self.COLOR_GREY
            else:
                color = self.COLOR_BTN_DEFAULT

            btn = ctk.CTkButton(
                self.current_button_frame, 
                text=text, 
                font=self.FONT_BTN_MAIN,
                command=cmd,
                corner_radius=8,
                fg_color=color,
                text_color="black",
                hover_color=self.COLOR_BTN_HIGHLIGHT
            )
            btn.grid(row=row, column=col, sticky="nsew", padx=2, pady=2)

        self.btn_eq = ctk.CTkButton(
            self.current_button_frame, 
            text='=',
            font=self.FONT_BTN_MAIN,
            corner_radius=8,
            fg_color=self.COLOR_GREY,
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=self.handle_equals
        )
        self.btn_eq.grid(row=4, column=2, rowspan=1, sticky="nsew", padx=2, pady=2)
        
        btn_mark = ctk.CTkButton(
            self.current_button_frame, 
            text='x!',  
            font=self.FONT_BTN_MAIN,
            corner_radius=8,
            fg_color=self.COLOR_OPERATIONS,
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=lambda: self.on_button_click('!')
        )
        btn_mark.grid(row=0, column=4, sticky="nsew", padx=2, pady=2)
        
        btn_0 = ctk.CTkButton(
            self.current_button_frame, 
            text='0',  
            font=self.FONT_BTN_MAIN,
            corner_radius=8,
            fg_color=self.COLOR_BTN_DEFAULT,
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=lambda: self.on_button_click('0')
        )
        btn_0.grid(row=4, column=0, sticky="nsew", padx=2, pady=2)

        btn_ln = ctk.CTkButton(
            self.current_button_frame, 
            text='ln', 
            font=self.FONT_BTN_MAIN,
            corner_radius=8,
            fg_color=self.COLOR_OPERATIONS,
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=lambda: self.on_button_click('ln')
        )
        btn_ln.grid(row=4, column=4, sticky="nsew", padx=2, pady=2)
        
        self.btn_close_paren = ctk.CTkButton(
            self.current_button_frame, 
            text=')',
            font=self.FONT_BTN_MAIN,
            corner_radius=8,
            fg_color=self.COLOR_GREY,
            text_color="black",
            hover_color=self.COLOR_BTN_HIGHLIGHT,
            command=lambda: self.on_button_click(')')
        )
        self.btn_close_paren.grid(row=0, column=2, sticky="nsew", padx=2, pady=2)

        self.update_display()

    ## @brief Routes standard keypad inputs to their respective state handlers.
    #  @param char The string representation of the button clicked (e.g., '7', '+', 'log').
    def on_button_click(self, char) -> None:
        """Routes standard keypad inputs to their respective handlers."""

        if self.entering_log_base and char in ['+', '-', '*', '/', '^', '√', '!', 'ln', 'exp', 'log']:
            self.show_warning("Operace v základu logaritmu\nnejsou podporovány!")
            return
            

        # Handle number and decimal input
        if char in '0123456789.':
            if len(self.current_input) >= 15:
                self.show_warning("Zadané číslo je\npříliš dlouhé!")
                return

            if self.current_input == "0" and char != ".":
                self.current_input = ""
            self.current_input += char

        elif char in ['(', ')']:
            if char == '(':
                if (self.current_input and self.current_input != "-") or (self.tokens and self.tokens[-1] in [')', '!']):
                    self.show_warning("Implicitní násobení\nnení povoleno!")
                    return
                
            # If we're in the middle of entering a log base, finalize that first before allowing parentheses
            if self.entering_log_base:
                if self.current_input:
                    base = self.current_input
                else:
                    self.show_warning("Závorky nejsou \nv základu logaritmu povoleny!")
                    return
                
                self.tokens.append(base)
                self.tokens.append("log")
                self.entering_log_base = False
                self.current_input = ""


            if self.current_input:
                self.tokens.append(self.current_input)
                self.current_input = ""
            self.tokens.append(char)

        # Handle clear button
        elif char == 'C':
            self.clear()

        # Handle delete button
        elif char == 'del':       
            self._handle_delete()

        # Handle binary operations
        elif char in ['+', '-', '*', '/', '^', '√']:
            self.set_operation(char)
            
        # Handle custom log input flow
        elif char == 'log':
            if (self.current_input and self.current_input != "-") or (self.tokens and self.tokens[-1] in [')', '!']):
                self.show_warning("Implicitní násobení\nnení povoleno!")
                return
            if self.current_input:
                self.tokens.append(self.current_input)
                self.current_input = ""
            self.entering_log_base = True

        # Handle unary operations
        elif char in ['!', 'ln', 'exp']:
            self.apply_unary_operation(char)

        self.update_display()

    ## @brief Safely pops the last entered character or token.
    #  @details Contains logic to unwind complex states, such as removing the log 
    #           identifier but giving the base back to the user to edit, or pulling 
    #           locked numbers out of the token list to backspace them.
    def _handle_delete(self) -> None:
        """Safely pops the last entered character or token."""
        if self.current_input:
            self.current_input = self.current_input[:-1]
            
        elif self.entering_log_base:
            self.entering_log_base = False 
            
        elif self.tokens:
            last_token = self.tokens[-1]
            
            # If it's a standard operator, delete it completely
            if last_token in ['+', '-', '*', '/', '^', '√', '(', ')', '!', 'ln', 'exp']:
                self.tokens.pop()
                
            # If it's a log, remove number then base and then log
            elif last_token == 'log':
                self.tokens.pop()  # Odebere samotný operátor 'log'
                if self.tokens:
                    base_val = self.tokens.pop()  
                    self.current_input = base_val[:-1]  
                self.entering_log_base = True
                    
            # If it's a number that got locked into the token list, pull it out and backspace it
            else:
                self.tokens.pop()
                self.current_input = last_token[:-1]
        
        self.update_display()    
    
    ## @brief Sends the token list to ExpressionEvaluator and processes the result.
    #  @details Balances open parentheses automatically and captures the visually 
    #           formatted display string (including subscripts/superscripts) for the history log.
    def handle_equals(self) -> None:
        """Sends the token list to ExpressionEvaluator and processes the result."""
        # Push  number the user was currently typing into the token list
        if self.entering_log_base:
            if self.current_input :
                base = self.current_input
            else :
                self.show_warning("Vložte prosím základ!")
                return
            
            self.tokens.append(base)
            self.tokens.append("log")
            
            self.entering_log_base = False
            self.current_input = ""
            self.update_display()
            return

        if self.current_input:
            self.tokens.append(self.current_input)
            self.current_input = ""

        # If they hit equals without typing anything, do nothing
        if not self.tokens:
            return

        # Automatically balance left-open parentheses for user convenience
        open_parens = self.tokens.count('(') - self.tokens.count(')')
        if open_parens > 0:
            self.tokens.extend([')'] * open_parens)

        # Update the display to apply all superscript/subscript formatting
        self.update_display()
        
        # Grab the beautifully formatted string for the history
        full_expression = self.display_var.get()

        try:
            # Delegate calculation to new class
            result = ExpressionEvaluator.calculate(self.tokens)
            self._set_result(result, full_expression)
        except Exception as e:
            self._trigger_error(e, full_expression)

    ## @brief Sets the current binary mathematical operation.
    #  @param op The string representation of the binary operation (e.g., '+', '^').
    def set_operation(self, op) -> None:
        """Sets the current binary mathematical operation."""

        if op == '-' and not self.current_input:
            # Pokud jsme na začátku nebo po jiném operátoru/závorce
            if not self.tokens or self.tokens[-1] in ['(', '+', '-', '*', '/', '^', '√', 'log']:
                self.current_input = "-"
                return
        if self.current_input == "-":
            self.show_warning("Dva operátory za sebou\nnejsou povoleny!")
            return

        # Store first operand and current operation
        if self.current_input:
            self.tokens.append(self.current_input)
            self.current_input = ""
            
        if self.tokens:
            if self.tokens[-1] in ['+', '-', '*', '/', '^', '√']:
                self.show_warning("Dva operátory za sebou\nnejsou povoleny!")
                return
            elif self.tokens[-1] == '(':
                self.show_warning("Operátor nelze zadat\nhned za závorkou!")
                return
            else:
                self.tokens.append(op)
                if op == '^':
                    self.tokens.append('(')
                
    ## @brief Resets the calculator to its default empty state.
    def clear(self) -> None:
        """Resets the calculator to its default state."""
        # Reset all calculator state
        self.current_input = ""
        self.tokens = []
        self.display_var.set("0")
        self.history_var.set("")
        self.update_display()
        self.entering_log_base = False

    ## @brief Formats floating point numbers for clean display.
    #  @param num The float number to format.
    #  @return A string representation of the number (removes '.0' for whole numbers).
    @staticmethod
    def format_number(num: float) -> str:
        """Formats floating point numbers for clean display."""

        # Convert integer floats to int format for display
        if isinstance(num, float) and num.is_integer():
            return str(int(num))
        return str(num)
    

    ## @brief Reconstructs the expression string and updates the UI display.
    #  @details Iterates through tokens and translates digits/operators to their subscript 
    #           or superscript forms based on context (e.g., logₓ, n√, eˣ, ^2).
    def update_display(self) -> None:
        """Reconstructs the expression string and updates the UI."""
        # Translate digits and operators to their subscript/superscript forms for display purposes, e.g., logₓ, n√, eˣ, etc.
        SUBSCRIPT = str.maketrans("0123456789.-+()e! ", "₀₁₂₃₄₅₆₇₈₉.₋₊₍₎ₑ﹗ ")
        SUPERSCRIPT = str.maketrans("0123456789.-+()e! ", "⁰¹²³⁴⁵⁶⁷⁸⁹⋅⁻⁺⁽⁾ᵉᵎ ")
        display_str = ""
        i = 0
        exp_depth = 0
        paren_stack = []
        
        # Iterate through tokens and format them beautifully
        while i < len(self.tokens):
            # Watch which token we're on to determine if we should format it as a log base, root degree, or normal token
            token = self.tokens[i]
            # Format log with base as logₓ
            if i + 1 < len(self.tokens) and self.tokens[i+1] == 'log':
                base = self.tokens[i].translate(SUBSCRIPT)
                display_str += f"log{base} "
                i += 2
            # Format n-th root as n√
            elif i + 1 < len(self.tokens) and self.tokens[i+1] == '√':
                degree = self.tokens[i].translate(SUPERSCRIPT)
                if exp_depth > 0:
                    display_str += f"⁽{degree}⁾ʳᵗ "
                else:
                    display_str += f"{degree}√ "
                i += 2 
            # Format exponential as eˣ
            elif token == 'exp':
                if exp_depth > 0:
                    display_str += "ᵉˣᵖ⁽"
                else:
                    display_str += "e"

                if i + 1 < len(self.tokens) and self.tokens[i+1] == '(':
                    paren_stack.append('exp')
                    exp_depth += 1  
                    i += 2
                else:
                    i += 1
            # Format 
            elif token == '^':
           
                if i + 1 < len(self.tokens) and self.tokens[i+1] == '(':
                    paren_stack.append('^') 
                    if exp_depth > 0:
                        display_str += "^"
                        display_str += "⁽"
                    exp_depth += 1  
                    i += 2
                else:
                    display_str += "^"
                    i += 1

                

            elif token == '(':
                paren_stack.append('(')
                if exp_depth > 0:
                    exp_depth += 1
                    display_str += token.translate(SUPERSCRIPT) 
                else:
                    display_str += token + " "
                i += 1

            elif token == ')':

                if paren_stack:
                    paren_stack.pop() 
                
                if exp_depth > 0:
                    exp_depth -= 1
                    if exp_depth > 0:
                        display_str += token.translate(SUPERSCRIPT)
                else:
                    display_str += token + " "
                i += 1

            else:
                # Format normal tokens, applying superscript if we're currently in an exp() function
                if exp_depth > 0:
                    display_str += token.translate(SUPERSCRIPT)
                else:
                    display_str += token + " "
                i += 1

        # Append current input based on our log state
        if self.entering_log_base:
            base = self.current_input.translate(SUBSCRIPT)
            display_str += f"log{base}"
        elif exp_depth > 0:
            exponent = self.current_input.translate(SUPERSCRIPT)
            display_str += exponent
        else:
            display_str += self.current_input

        # Update the screen
        self.display_var.set(display_str.strip() if display_str.strip() else "0")

        # Handle '=' button highlighting
        if hasattr(self, 'btn_eq') and self.btn_eq.winfo_exists():
            if self.entering_log_base:
                self.btn_eq.configure(fg_color=self.COLOR_RED)
                self.btn_eq.configure(hover_color=self.COLOR_BLUE)
            else:
                self.btn_eq.configure(fg_color=self.COLOR_GREY)
                self.btn_eq.configure(hover_color=self.COLOR_BTN_HIGHLIGHT)
        # Handle close_parent highlighting
        if hasattr(self, 'btn_close_paren') and self.btn_close_paren.winfo_exists():
            if paren_stack and paren_stack[-1] in ['exp', '^']:
                self.btn_close_paren.configure(fg_color=self.COLOR_RED, hover_color=self.COLOR_BLUE)
            else:
                self.btn_close_paren.configure(fg_color=self.COLOR_GREY, hover_color=self.COLOR_BTN_HIGHLIGHT)

    ## @brief Executes operations that only require one operand or specific bracket formatting.
    #  @param op The string representation of the unary operation (e.g., 'ln', 'exp', '!').
    def apply_unary_operation(self, op: str) -> None:
        """Executes operations that only require one operand."""
        if self.entering_log_base:
            base = self.current_input if self.current_input else "apply unary operation log base"
            self.tokens.append(base)
            self.tokens.append("log")
            self.entering_log_base = False
            self.current_input = ""
        
        if op in ['ln', 'exp']:
            if (self.current_input and self.current_input != "-") or (self.tokens and self.tokens[-1] in [')', '!']):
                self.show_warning("Implicitní násobení\nnení povoleno!")
                return
            
            if self.current_input == "-":
                self.tokens.append(self.current_input)
                self.current_input = ""

            # These functions go before the number, e.g., ln( 
            self.tokens.append(op)
            self.tokens.append('(')
        elif op == '!':
            # Factorial goes after the number, e.g., 5 !
            if self.current_input:
                self.tokens.append(self.current_input)
                self.current_input = ""
            self.tokens.append('!')

    def calculate(self) -> None:
        """Executes the standard binary operations."""

        if not self.current_operation or not self.current_input:
            return

        full_expression = self.display_var.get()
            
        try:
            second_operand = float(self.current_input)
            
            if self.current_operation == '+':
                result = mathlib.add(self.first_operand, second_operand)
            elif self.current_operation == 'exp':
                result = mathlib.exp(second_operand)

            self._set_result(result, full_expression)

        except Exception as e:
            self._trigger_error(e, full_expression)
    

    ## @brief Helper to round, format, and apply a successful calculation result.
    #  @param result The numerical result of the calculation.
    #  @param expression The formatted string of the expression evaluated (used for history).
    def _set_result(self, result: float, expression: str = "") -> None:
        """Helper to round, format, and apply a successful calculation result."""
        result = round(result, 10)
        
        # Clear tokens and set the result as the new current input 
        self.current_input = self.format_number(result)
        self.tokens = []
        self.update_display()

        if expression:
            self.history_var.set(f"{expression} =")

    ## @brief Safely transitions the calculator into an error state.
    def _trigger_error(self, error: Exception = None, expression: str = "") -> None:     
        """Helper to safely transition the calculator into an error state."""
        # Move the failed expression to the history display
        if expression:
            self.history_var.set(f"{expression} = ")

        # Reset the calculator to its default empty state
        self.display_var.set("0")
        self.current_input = ""
        self.tokens = []
        self.entering_log_base = False
        
        # Default fallback error message
        message = "Neplatný výraz\nnebo neznámá chyba!"

        # Identify the specific error and set a localized message
        if error is not None:
            err_msg = str(error).lower()
            err_type = type(error).__name__

            # Mathematical errors
            if err_type == "ZeroDivisionMathError":
                message = "Nelze dělit nulou!"
            elif err_type == "DomainError":
                if "factorial" in err_msg:
                    message = "Faktoriál ze záporného\nčísla neexistuje!"
                elif "fractional root" in err_msg or "even root" in err_msg:
                    message = "Odmocnina ze záporného\nčísla není reálná!"
                elif "0^0" in err_msg:
                    message = "Výraz 0⁰ není definován!"
                elif "base must be > 0 and not 1" in err_msg:
                    message = "Základ logaritmu musí\nbýt > 0 a různý od 1!"
                elif "log" in err_msg or "ln" in err_msg:
                    message = "Logaritmus existuje\npouze pro kladná čísla!"
                else:
                    message = "Chyba definičního oboru\n(neplatná operace)!"
            elif err_type == "ConvergenceError":
                message = "Výpočet byl příliš složitý."
            elif err_type == "TypeError" and "factorial" in err_msg:
                message = "Faktoriál lze počítat\npouze z celých čísel!"
                
            # Syntax and parsing errors from evaluator.py
            elif err_type == "ValueError":
                if "missing operand" in err_msg:
                    if "exp" in err_msg:
                        message = "Chybí operand\npro operaci  'e^'"
                    elif "ln" in err_msg:
                        message = "Chybí operand\npro operaci  'ln'"
                    else:
                        message = "Chybí operand\npro operaci  'x!'"
                elif "unbalanced" in err_msg:
                    message = "Nevyvážené operátory\na závorky!"
                elif "invalid expression" in err_msg:
                    if "log" in err_msg:
                        message = "Zadejte argument logaritmu"
                    else:
                        message = "Neplatný zápis\nmatematického výrazu!"
            
            # System limitations (e.g., floating-point overflow)
            elif err_type == "OverflowError":
                message = "Výsledek je příliš velký!"

        # Display the custom toast notification overlay
        self.show_warning(message)

if __name__ == "__main__":
    # Create and run the application
    ctk.set_appearance_mode("System")  
    ctk.set_default_color_theme("blue")  
    
    root = ctk.CTk()
    app = CalculatorApp(root)
    root.mainloop()