SYSTEM: You are a Python 3.12 programmer, capable of analyzing problems in depth and explaining solutions in a clear and concise manner. Your responses prioritize code efficiency, readability, and simplicity. You leverage the latest Python features. When addressing tasks, you break them down into clear steps and present only essential information, ensuring users receive focused, expert guidance tailored to their needs. You only print the parts of the code you changed in a way that makes them easy to copy paste in the code base. USER:   I get the following error: Failed to load table "1. Movies": "<" not supported between instances of "float" and "str" Python 3.12 Code: import ctypes
import csv
import json
import re
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, simpledialog, ttk
from typing import Any, Optional

from tksheet import Sheet

def set_dpi_awareness():
    """Enable DPI awareness for high DPI settings."""
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(1)
    except Exception as e:
        print(f"Error setting DPI awareness: {e}")

def custom_filter_design(master, style: ttk.Style, headers=None, filter_types=None, filter_font=None, filter_combobox_font=None, colors=None):
    """Creates a custom filter frame with all components."""
    filter_frame = ttk.Frame(master, style="Custom.TFrame")

    # Configure styles
    style.configure("Custom.TFrame", background=colors["background"], relief="flat", borderwidth=0)
    style.configure("TCombobox", relief="flat", borderwidth=0, background=colors["background"], fieldbackground=colors["background"])
    style.map("TCombobox", fieldbackground=[('readonly', colors["background"])], background=[('readonly', colors["background"])])
    style.configure("TEntry", relief="flat", borderwidth=0, background=colors["background"])
    style.configure("Line.TFrame", background=colors["filter_line"])

    # Create widgets
    header_combobox = ttk.Combobox(filter_frame, values=headers, width=15, font=filter_combobox_font, state="readonly")
    filtertype_combobox = ttk.Combobox(filter_frame, values=filter_types, width=15, font=filter_combobox_font, state="readonly")
    filter_entry = ttk.Entry(filter_frame, font=filter_font)
    underline = ttk.Frame(filter_frame, height=1, style="Line.TFrame")

    # Layout widgets
    header_combobox.grid(row=0, column=0, padx=(0, 30), pady=2, sticky='ew')
    filtertype_combobox.grid(row=0, column=1, padx=2, pady=2, sticky='ew')
    filter_entry.grid(row=1, column=0, columnspan=2, padx=2, pady=(2, 0), sticky='ew')
    underline.grid(row=2, column=0, columnspan=2, sticky='ew', pady=(0, 2))

    # Prevent text selection in comboboxes
    for combobox in (header_combobox, filtertype_combobox):
        combobox.bind("<FocusIn>", lambda event: event.widget.selection_clear())

    # Configure column weights
    filter_frame.columnconfigure((0, 1), weight=1)

    # Ensure the underline doesn't resize vertically
    underline.pack_propagate(False)

    return filter_frame, header_combobox, filtertype_combobox, filter_entry

def custom_sort_design(master, style: ttk.Style, headers=None, sort_orders=None, sort_font=None, sort_combobox_font=None, colors=None):
    """Creates a custom sort frame with comboboxes for primary and secondary sorts."""
    sort_frame = ttk.Frame(master, style="Custom.TFrame")

    # Configure styles
    style.configure("Custom.TFrame", background=colors["background"], relief="flat", borderwidth=0)
    style.configure("TCombobox", relief="flat", borderwidth=0, background=colors["background"], fieldbackground=colors["background"])
    style.map("TCombobox", fieldbackground=[('readonly', colors["background"])], background=[('readonly', colors["background"])])
    style.configure("Line.TFrame", background=colors["filter_line"])

    # Create widgets for primary sort
    primary_column_combobox = ttk.Combobox(sort_frame, values=headers, width=15, font=sort_combobox_font, state="readonly")
    primary_order_combobox = ttk.Combobox(sort_frame, values=sort_orders, width=15, font=sort_combobox_font, state="readonly")

    # Create widgets for secondary sort
    secondary_column_combobox = ttk.Combobox(sort_frame, values=headers, width=15, font=sort_combobox_font, state="readonly")
    secondary_order_combobox = ttk.Combobox(sort_frame, values=sort_orders, width=15, font=sort_combobox_font, state="readonly")

    # Layout widgets
    primary_column_combobox.grid(row=0, column=0, padx=5, pady=2, sticky='ew')
    primary_order_combobox.grid(row=0, column=1, padx=5, pady=2, sticky='ew')

    secondary_column_combobox.grid(row=1, column=0, padx=5, pady=2, sticky='ew')
    secondary_order_combobox.grid(row=1, column=1, padx=5, pady=2, sticky='ew')

    # Prevent text selection in comboboxes
    for combobox in (primary_column_combobox, primary_order_combobox, secondary_column_combobox, secondary_order_combobox):
        combobox.bind("<FocusIn>", lambda event: event.widget.selection_clear())

    # Configure column weights
    sort_frame.columnconfigure((0, 1), weight=1)

    return (
        sort_frame,
        primary_column_combobox,
        primary_order_combobox,
        secondary_column_combobox,
        secondary_order_combobox
    )

def custom_scrollbar_design(style: ttk.Style, colors=None):
    """Configure custom scrollbar style."""
    style.configure("Custom.Vertical.TScrollbar", troughcolor=colors["scrollbar_trough"], background=colors["scrollbar_background"], borderwidth=0, arrowcolor=colors["scrollbar_arrow"])
    style.map("Custom.Vertical.TScrollbar", background=[('active', colors["scrollbar_active"])])
    style.layout("Custom.Vertical.TScrollbar", [('Vertical.Scrollbar.trough', {'children': [('Vertical.Scrollbar.thumb', {'expand': '1', 'sticky': 'ns'})], 'sticky': 'ns'})])

class TableApp:
    """Table Editor with sidebar and table management."""

    def __init__(self, root: tk.Tk):
        """Initialize the TableApp."""
        self.root = root
        self.config_dimensions()
        self.config_fonts()
        self.config_colors()
        self.initialize_data()
        self.style = ttk.Style()
        self.style.theme_use("default")
        self.setup_ui()
        self.load_tables()

    def config_dimensions(self):
        """Configure app dimensions."""
        self.root.title("Database")
        self.root.geometry("1920x1080")
        self.root.minsize(1435, 910)
        self.dimensions = {
            "sidebar_width": 400,
            "spacer_frame_width": 100,
            "header_height": 190,
            "separator_height": 1,
        }

    def config_fonts(self):
        """Configure app fonts."""
        self.fonts = {
            "text": ("Literata", 12),
            "tksheet": ("Literata", 12, "normal"),
            "header_info": ("Literata", 10),
            "filter": ("Literata", 12),
            "filter_combobox": ("Literata", 8),
            "sort_combobox": ("Literata", 8),  # Added for sorting
        }

    def config_colors(self):
        """Configure app colors."""
        self.colors = {
            "text": "#333333",
            "background": "#FFFFFF",
            "sidebar": "#F7F8FC",
            "button": "#F7F8FC",
            "button_selected": "#EAEDEF",
            "separator": "#333333",
            "header_info": "#999999",
            "scrollbar_trough": "#F7F8FC",
            "scrollbar_background": "#c0c1c4",
            "scrollbar_arrow": "#333333",
            "scrollbar_active": "#878789",
            "filter_line": "#00bcf0",
        }

    def setup_ui(self):
        """Set up the user interface with a 3x2 grid layout."""
        self.root.grid_rowconfigure(0, weight=0)
        self.root.grid_rowconfigure(1, weight=1)

        self.root.grid_columnconfigure(0, weight=0)
        self.root.grid_columnconfigure(1, weight=0, minsize=self.dimensions["spacer_frame_width"])
        self.root.grid_columnconfigure(2, weight=3)

        self.create_sidebar()
        self.create_header_frame()
        self.create_spacer_frame()
        self.create_table_frame()

    def create_sidebar(self):
        """Create the sidebar that spans both rows."""
        sidebar = self._create_frame(
            parent=self.root,
            width=self.dimensions["sidebar_width"],
            bg=self.colors["sidebar"]
        )
        sidebar.grid(row=0, column=0, rowspan=2, sticky='ns')
        sidebar.grid_propagate(False)
        sidebar.columnconfigure(0, weight=1)
        sidebar.rowconfigure(0, weight=1)

        functional_sidebar = self._create_frame(
            parent=sidebar,
            width=self.dimensions["sidebar_width"],
            bg=self.colors["sidebar"]
        )
        functional_sidebar.grid(row=0, column=0, sticky='nsew')
        functional_sidebar.columnconfigure(0, weight=1)
        functional_sidebar.rowconfigure(0, weight=0)
        functional_sidebar.rowconfigure(1, weight=1)
        functional_sidebar.rowconfigure(2, weight=0)

        self._add_normal_buttons(functional_sidebar)
        self._add_scrollable_canvas(functional_sidebar)
        self._add_delete_table_section(functional_sidebar)

    def create_header_frame(self) -> None:
        """Create the header divided into upper and lower sub-frames."""
        header_frame = self._create_and_configure_header_frame()
        
        upper_header_frame = self._create_upper_header(header_frame)
        self._create_upper_header_content(upper_header_frame)
        
        lower_header_frame = self._create_lower_header(header_frame)
        self._create_filter_controls(lower_header_frame)
        self._create_sort_controls(lower_header_frame)  # Added: Create sort controls

    def _create_and_configure_header_frame(self) -> tk.Frame:
        """Create and configure the main header frame."""
        header_frame = self._create_frame(parent=self.root, width=None, bg=self.colors["background"])
        header_frame.grid(row=0, column=1, columnspan=2, sticky='ew')
        header_frame.config(height=self.dimensions["header_height"])
        header_frame.grid_propagate(False)
        
        for i in range(2):
            header_frame.grid_rowconfigure(i, weight=0)
        
        for i in range(5):
            weight = 1 if i == 4 else 0
            header_frame.grid_columnconfigure(i, weight=weight)
        
        return header_frame

    def _create_upper_header(self, parent: tk.Frame) -> tk.Frame:
        """Create the upper header frame."""
        upper_header = self._create_frame(parent=parent, width=None, bg=self.colors["background"])
        upper_header.grid(row=0, column=0, columnspan=5, sticky='ew', pady=(0, 10))
        upper_header.grid_columnconfigure(5, weight=1)
        return upper_header

    def _create_upper_header_content(self, upper_header: tk.Frame) -> None:
        """Create and add content to the upper header frame."""
        self._create_data_label(upper_header)
        self._create_table_name_label(upper_header)
        self._create_settings_label(upper_header)

    def _create_data_label(self, parent: tk.Frame) -> None:
        """Create and grid the 'Data >' label."""
        data_label = tk.Label(
            parent,
            text="Data > ",
            bg=self.colors["background"],
            fg=self.colors["header_info"],
            font=self.fonts["header_info"]
        )
        data_label.grid(row=0, column=0, padx=22, pady=(22, 0), sticky='nw')

    def _create_table_name_label(self, parent: tk.Frame) -> None:
        """Create and grid the table name label."""
        self.table_name_label = tk.Label(
            parent,
            text=self.current_table_name if self.current_table_name else "No Table Selected",
            bg=self.colors["background"],
            fg=self.colors["text"],
            font=self.fonts["header_info"]
        )
        self.table_name_label.grid(row=0, column=0, padx=(88, 10), pady=(22, 0), sticky='nw')

    def _create_settings_label(self, parent: tk.Frame) -> None:
        """Create and grid the settings label with its associated menu."""
        settings_label = tk.Label(
            parent,
            text="...",
            bg=self.colors["background"],
            fg=self.colors["text"],
            cursor="hand2",
            anchor='e',
            font=self.fonts["text"]
        )
        settings_label.grid(row=0, column=5, padx=40, pady=20, sticky='e')
        
        settings_menu = self._create_settings_menu()
        self._bind_settings_label_events(settings_label, settings_menu)

    def _create_settings_menu(self) -> tk.Menu:
        """Create the settings dropdown menu."""
        menu = tk.Menu(self.root, tearoff=0)
        menu.add_command(label="Export Table", command=self.export_table)
        menu.add_command(label="Import Table", command=self.import_file)
        menu.add_command(label="Rename Headers", command=self.rename_headers)
        return menu

    def _bind_settings_label_events(self, label: tk.Label, menu: tk.Menu) -> None:
        """Bind events to the settings label."""
        label.bind("<Button-1>", lambda e: menu.post(e.x_root, e.y_root))
        label.bind("<Enter>", lambda _: label.config(bg=self.colors["button_selected"]))
        label.bind("<Leave>", lambda _: label.config(bg=self.colors["background"]))

    def _create_lower_header(self, parent: tk.Frame) -> tk.Frame:
        """Create the lower header frame."""
        lower_header = self._create_frame(parent=parent, width=None, bg=self.colors["background"])
        lower_header.grid(row=1, column=0, columnspan=5, sticky='ew')
        return lower_header

    def _create_filter_controls(self, parent: tk.Frame) -> None:
        """Create and add filter controls to the lower header frame."""
        filter_frame, self.filter_header_dropdown, self.filter_option_dropdown, self.filter_text_entry = custom_filter_design(
            parent, 
            self.style, 
            headers=["All"] + self.headers.get(self.current_table_name, []), 
            filter_types=[
                "is", "is not", "contains", "does not contain",
                "starts with", "ends with", "is empty", "is not empty"
            ], 
            filter_font=self.fonts["filter"], 
            filter_combobox_font=self.fonts["filter_combobox"], 
            colors=self.colors
        )
        filter_frame.grid(row=0, column=0, padx=50, pady=0, sticky='ew')

        self.filter_header_var = tk.StringVar(value="All")
        self.filter_option_var = tk.StringVar(value="contains")
        self.filter_text_var = tk.StringVar()

        self.filter_header_dropdown['textvariable'] = self.filter_header_var
        self.filter_option_dropdown['textvariable'] = self.filter_option_var
        self.filter_text_entry['textvariable'] = self.filter_text_var

        # Bindings updated to correct method references and signature
        self.filter_header_dropdown.bind("<<ComboboxSelected>>", self.update_filter_sort_headers)
        self.filter_option_dropdown.bind("<<ComboboxSelected>>", self.update_display)
        self.filter_text_entry.bind("<KeyRelease>", self.update_display)
        self.filter_text_entry.bind("<Return>", self.update_display)

    def _create_sort_controls(self, parent: tk.Frame) -> None:
        """Create and add sort controls to the lower header frame."""
        sort_frame, self.sort_primary_column_dropdown, self.sort_primary_order_dropdown, self.sort_secondary_column_dropdown, self.sort_secondary_order_dropdown = custom_sort_design(
            parent, 
            self.style, 
            headers=self.headers.get(self.current_table_name, []), 
            sort_orders=["Ascending", "Descending"], 
            sort_font=self.fonts["filter"], 
            sort_combobox_font=self.fonts["sort_combobox"], 
            colors=self.colors
        )
        sort_frame.grid(row=0, column=1, padx=50, pady=0, sticky='ew')

        # Initialize sort variables
        self.sort_primary_column_var = tk.StringVar()
        self.sort_primary_order_var = tk.StringVar()
        self.sort_secondary_column_var = tk.StringVar()
        self.sort_secondary_order_var = tk.StringVar()

        # Set defaults if headers are available
        current_headers = self.headers.get(self.current_table_name, [])
        if current_headers:
            self.sort_primary_column_var.set(current_headers[0])
            self.sort_primary_order_var.set("Ascending")
            self.sort_secondary_column_var.set(current_headers[0])
            self.sort_secondary_order_var.set("Ascending")
        else:
            self.sort_primary_column_var.set("")
            self.sort_primary_order_var.set("Ascending")
            self.sort_secondary_column_var.set("")
            self.sort_secondary_order_var.set("Ascending")

        self.sort_primary_column_dropdown['textvariable'] = self.sort_primary_column_var
        self.sort_primary_order_dropdown['textvariable'] = self.sort_primary_order_var
        self.sort_secondary_column_dropdown['textvariable'] = self.sort_secondary_column_var
        self.sort_secondary_order_dropdown['textvariable'] = self.sort_secondary_order_var

        # Bind sorting comboboxes to sort function
        self.sort_primary_column_dropdown.bind("<<ComboboxSelected>>", self.update_display)
        self.sort_primary_order_dropdown.bind("<<ComboboxSelected>>", self.update_display)
        self.sort_secondary_column_dropdown.bind("<<ComboboxSelected>>", self.update_display)
        self.sort_secondary_order_dropdown.bind("<<ComboboxSelected>>", self.update_display)

    def update_filter_sort_headers(self, event=None):
        """Update filter and sort comboboxes when a new table is selected or headers are changed."""
        if not self.current_table_name:
            headers = []
        else:
            headers = self.headers.get(self.current_table_name, [])

        # Update filter headers
        self.filter_header_dropdown['values'] = ["All"] + headers
        self.filter_header_var.set("All")

        # Update sort headers
        self.sort_primary_column_dropdown['values'] = headers
        self.sort_secondary_column_dropdown['values'] = headers

        # Reset sort selections
        if headers:
            self.sort_primary_column_var.set(headers[0])
            self.sort_primary_order_var.set("Ascending")
            self.sort_secondary_column_var.set(headers[0])
            self.sort_secondary_order_var.set("Ascending")
        else:
            self.sort_primary_column_var.set("")
            self.sort_primary_order_var.set("Ascending")
            self.sort_secondary_column_var.set("")
            self.sort_secondary_order_var.set("Ascending")

        self.update_display()

    def update_display(self, event=None):
        """Apply filter and sort, then update the sheet."""
        self.apply_filter()
        self.apply_sort()

    def apply_filter(self):
        """Apply filter to table data based on selected criteria."""
        if not self.current_table_name or not self.current_sheet:
            return
        header = self.filter_header_var.get()
        option = self.filter_option_var.get()
        text = self.filter_text_var.get().lower()
        original_data = self.tables_data[self.current_table_name]["data"]

        def row_matches(row):
            if header == "All":
                cells = row
                target_indices = range(len(cells))
            else:
                try:
                    target_indices = [self.headers[self.current_table_name].index(header)]
                except ValueError:
                    return False

            conditions = {
                "is": lambda cell, text: cell == text,
                "is not": lambda cell, text: cell != text,
                "contains": lambda cell, text: text in cell,
                "does not contain": lambda cell, text: text not in cell,
                "starts with": lambda cell, text: cell.startswith(text),
                "ends with": lambda cell, text: cell.endswith(text),
                "is empty": lambda cell, _: cell == "",
                "is not empty": lambda cell, _: cell != ""
            }

            for idx in target_indices:
                cell = str(row[idx]).lower()
                if conditions.get(option, lambda _, __: False)(cell, text):
                    return True
            return False

        # Collect both filtered data and their original indices
        filtered_data = []
        self.displayed_row_indices = []

        for i, row in enumerate(original_data):
            if row_matches(row):
                filtered_data.append(row)
                self.displayed_row_indices.append(i)

        try:
            self.current_sheet.set_sheet_data(filtered_data)
        except tk.TclError as e:
            print(f"Error applying filter: {e}")

    def apply_sort(self):
        """Sort the filtered data based on selected sort options."""
        if not self.current_table_name or not self.current_sheet:
            return
        headers = self.headers.get(self.current_table_name, [])
        if not headers:
            return

        primary_column = self.sort_primary_column_var.get()
        primary_order = self.sort_primary_order_var.get()
        secondary_column = self.sort_secondary_column_var.get()
        secondary_order = self.sort_secondary_order_var.get()

        if not primary_column:
            return  # No sorting applied

        try:
            primary_index = headers.index(primary_column)
        except ValueError:
            primary_index = -1

        try:
            secondary_index = headers.index(secondary_column)
        except ValueError:
            secondary_index = -1

        # Extract the filtered data based on displayed_row_indices
        filtered_data = [self.tables_data[self.current_table_name]["data"][i] for i in self.displayed_row_indices]

        # Define sort key with type indicators
        def sort_key(row):
            primary_value = self.get_sortable_value(row[primary_index]) if primary_index != -1 else ""
            secondary_value = self.get_sortable_value(row[secondary_index]) if secondary_index != -1 else ""
            return (primary_value, secondary_value)

        # Determine sort order
        reverse_primary = True if primary_order == "Descending" else False
        reverse_secondary = True if secondary_order == "Descending" else False

        # Sort the data
        # First sort by secondary to ensure stability
        if secondary_column != primary_column and secondary_index != -1:
            filtered_data.sort(
                key=lambda row: self.get_sortable_value(row[secondary_index]),
                reverse=reverse_secondary
            )
        # Then sort by primary
        if primary_index != -1:
            filtered_data.sort(
                key=lambda row: self.get_sortable_value(row[primary_index]),
                reverse=reverse_primary
            )

        # Update the sheet with sorted data
        try:
            self.current_sheet.set_sheet_data(filtered_data)
        except tk.TclError as e:
            print(f"Error applying sort: {e}")

    def get_sortable_value(self, value):
        """Convert value to a sortable format."""
        if value is None:
            return ""  # Treat None as empty string
        try:
            return float(value)
        except (ValueError, TypeError):
            return str(value).lower()

    def create_spacer_frame(self):
        """Create a spacer frame in column 1, row 1."""
        spacer = self._create_frame(
            parent=self.root,
            width=self.dimensions["spacer_frame_width"],
            bg=self.colors["background"]
        )
        spacer.grid(row=1, column=1, sticky='nsew')
        spacer.grid_propagate(False)

    def create_table_frame(self):
        """Create the table frame in column 2, row 1."""
        self.table_frame = self._create_frame(
            parent=self.root,
            width=None,
            bg=self.colors["background"]
        )
        self.table_frame.grid(row=1, column=2, sticky='nsew')
        self.table_frame.grid_rowconfigure(0, weight=1)
        self.table_frame.grid_columnconfigure(0, weight=1)

    def _create_frame(
        self,
        parent: tk.Widget,
        width: Optional[int],
        bg: str
    ) -> tk.Frame:
        """Create a frame with specified width and background."""
        frame = tk.Frame(parent, width=width, bg=bg, bd=0, relief='flat')
        if width:
            frame.config(width=width)
        return frame

    def _add_normal_buttons(self, sidebar: tk.Frame):
        """Add buttons to the sidebar."""
        normal_buttons_frame = self._create_frame(
            parent=sidebar,
            width=self.dimensions["sidebar_width"],
            bg=self.colors["sidebar"]
        )
        normal_buttons_frame.grid(row=0, column=0, sticky='ew', padx=10, pady=(10, 0))
        normal_buttons_frame.columnconfigure(0, weight=1)

        create_table_label = self._create_clickable_label(
            parent=normal_buttons_frame,
            text="Create New Table",
            command=self.create_new_table,
            anchor='center'
        )
        create_table_label.grid(row=0, column=0, pady=10, sticky='ew')
        self.add_custom_hover(create_table_label)

        separator = tk.Frame(normal_buttons_frame, height=self.dimensions["separator_height"], bg=self.colors["separator"])
        separator.grid(row=1, column=0, sticky='ew', pady=5)

    def _on_canvas_configure(self, event):
        """Update the table_labels_frame width to match the canvas width."""
        self.canvas.itemconfig(self.table_labels_window, width=event.width)

    def _add_scrollable_canvas(self, sidebar: tk.Frame):
        """Add scrollable canvas for table labels within a single frame."""
        scrollable_container = self._create_frame(
            parent=sidebar,
            width=self.dimensions["sidebar_width"],
            bg=self.colors["sidebar"]
        )
        scrollable_container.grid(row=1, column=0, sticky='nsew', padx=10, pady=10)
        scrollable_container.columnconfigure(0, weight=1)
        scrollable_container.rowconfigure(0, weight=1)
    
        self.canvas = tk.Canvas(
            scrollable_container,
            bg=self.colors["sidebar"],
            highlightthickness=0
        )
        self.canvas.grid(row=0, column=0, sticky='nsew')
    
        custom_scrollbar_design(self.style, self.colors) 
    
        self.scrollbar = ttk.Scrollbar(
            scrollable_container,
            orient="vertical",
            command=self.canvas.yview,
            style="Custom.Vertical.TScrollbar"
        )
        self.scrollbar.grid(row=0, column=1, sticky='ns')
    
        self.canvas.configure(yscrollcommand=self.scrollbar.set)
    
        self.table_labels_frame = tk.Frame(self.canvas, bg=self.colors["sidebar"])
        
        # Capture the window item ID
        self.table_labels_window = self.canvas.create_window((0, 0), window=self.table_labels_frame, anchor="nw", width=self.dimensions["sidebar_width"])
    
        self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
        self.table_labels_frame.bind("<MouseWheel>", self._on_mouse_wheel)
    
        self.table_labels_frame.bind(
            "<Configure>",
            lambda event: (
                self.canvas.configure(scrollregion=self.canvas.bbox("all")),
                self._update_scrollbar(scrollable_container, self.canvas, self.scrollbar)
            )
        )
        scrollable_container.bind(
            "<Configure>",
            lambda event: self._update_scrollbar(scrollable_container, self.canvas, self.scrollbar)
        )
    
        # Bind the canvas resize to update the table_labels_frame width
        self.canvas.bind('<Configure>', self._on_canvas_configure)

    def _on_mouse_wheel(self, event):
        """Handle mouse wheel scrolling only if scrollbar is visible."""
        if self.scrollbar.winfo_viewable():
            if event.delta:
                self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
            elif event.num == 5:
                self.canvas.yview_scroll(1, "units")
            elif event.num == 4:
                self.canvas.yview_scroll(-1, "units")

    def _update_scrollbar(self, scrollbar_frame, canvas, scrollbar):
        """Update scrollbar visibility based on content size."""
        canvas.update_idletasks()
        canvas_height = scrollbar_frame.winfo_height()
        content_height = self.table_labels_frame.winfo_reqheight()

        if content_height > canvas_height:
            scrollbar.grid()
            canvas.configure(yscrollcommand=scrollbar.set)
            canvas.bind("<MouseWheel>", self._on_mouse_wheel)
        else:
            scrollbar.grid_remove()
            canvas.configure(yscrollcommand=lambda *args: None)
            canvas.unbind("<MouseWheel>")

    def _add_delete_table_section(self, sidebar: tk.Frame):
        """Add delete table option to the sidebar."""
        delete_table_frame = self._create_frame(
            parent=sidebar,
            width=self.dimensions["sidebar_width"],
            bg=self.colors["sidebar"]
        )
        delete_table_frame.grid(row=2, column=0, sticky='ew', padx=10, pady=(0, 10))
        delete_table_frame.columnconfigure(0, weight=1)
        delete_table_frame.rowconfigure(1, weight=1)

        separator = tk.Frame(delete_table_frame, height=self.dimensions["separator_height"], bg=self.colors["separator"])
        separator.grid(row=0, column=0, sticky='ew', pady=5)

        delete_table_label = self._create_clickable_label(
            parent=delete_table_frame,
            text="🗑 Delete Selected Table",
            command=self.delete_table,
            anchor='center'
        )
        delete_table_label.grid(row=1, column=0, pady=10, sticky='ew')
        self.add_custom_hover(delete_table_label)

    def _create_clickable_label(
        self,
        parent: tk.Widget,
        text: str,
        command,
        allow_rename: bool = False,
        anchor: str = 'w'
    ) -> tk.Label:
        """Create a clickable label with optional rename functionality."""
        cursor = "arrow" if allow_rename else "hand2"
        label = tk.Label(
            parent,
            text=text,
            bg=self.colors["button"],
            fg=self.colors["text"],
            cursor=cursor,
            anchor=anchor,
            font=self.fonts["text"]
        )
        label.bind("<Button-1>", lambda _: command())

        if allow_rename:
            label.bind("<Double-Button-1>", lambda _: self.begin_rename(text))

        label.bind("<MouseWheel>", self._on_mouse_wheel)

        return label

    def add_custom_hover(self, widget: tk.Widget):
        """Add custom hover effects to a widget."""
        widget.bind("<Enter>", lambda e: widget.config(bg=self.colors["button_selected"]))
        widget.bind("<Leave>", lambda e: widget.config(bg=self.colors["button"]))

    def initialize_data(self):
        """Initialize data structures."""
        self.save_file_path = Path(__file__).parent / "Data.json"
        self.tables_data: dict[str, dict] = {}
        self.current_sheet: Sheet | None = None
        self.headers: dict[str, list[str]] = {}
        self.current_table_name: str | None = None
        self.rename_entry: tk.Entry | None = None
        self.selected_table_label: tk.Label | None = None
        self.table_labels: dict[str, tk.Label] = {}
            
        self.table_name_label = None
    
        self.filter_header_var = tk.StringVar(value="All")
        self.filter_option_var = tk.StringVar(value="contains")
        self.filter_text_var = tk.StringVar()

        # Sorting variables
        self.sort_primary_column_var = tk.StringVar()
        self.sort_primary_order_var = tk.StringVar()
        self.sort_secondary_column_var = tk.StringVar()
        self.sort_secondary_order_var = tk.StringVar()
    
        # Initialize displayed_row_indices
        self.displayed_row_indices: list[int] = []

    def sort_table_labels(self):
        """Sort table labels with numeric names first, then alphabetic, then alphanumeric."""
        def sort_key(name):
            # Category 1: Starts with digits (e.g., "1. Movies")
            m = re.match(r'^(\d+)', name)
            if m:
                return (1, int(m.group(1)))
            
            # Category 3: Starts with letters followed by digits (e.g., "T1", "T2")
            m = re.match(r'^([A-Za-z]+)(\d+)', name)
            if m:
                return (3, m.group(1).lower(), int(m.group(2)))
            
            # Category 2: Starts with letters only (e.g., "a", "b", "c")
            m = re.match(r'^([A-Za-z]+)', name)
            if m:
                return (2, m.group(1).lower())
            
            # Category 4: Any other names
            return (4, name.lower())
        
        # Sort the table names using the defined sort key
        sorted_tables = sorted(self.tables_data.keys(), key=sort_key)
        
        # Re-pack the labels in the sorted order
        for table_name in sorted_tables:
            label = self.table_labels.get(table_name)
            if label:
                label.pack_forget()
                label.pack(fill='x', padx=0, pady=5, expand=True)

    def create_new_table(self):
        """Create a new table."""
        existing_numbers = [int(name.split(" ")[1]) for name in self.tables_data if name.startswith("Table ") and name.split(" ")[1].isdigit()]
        next_number = max(existing_numbers, default=0) + 1
        table_name = f"Table {next_number}"
        self.add_table(table_name)
        self.load_selected_table(table_name)

    def import_file(self):
        """Import table from file."""
        file_path = filedialog.askopenfilename(title="Import File", filetypes=[("All Supported Files", "*.json *.csv"), ("JSON files", "*.json"), ("CSV files", "*.csv")])
        if not file_path:
            return
        try:
            if file_path.endswith('.csv'):
                with open(file_path, newline='', encoding='utf-8') as csvfile:
                    data = list(csv.reader(csvfile))
            elif file_path.endswith('.json'):
                with open(file_path, 'r', encoding='utf-8') as jsonfile:
                    data = json.load(jsonfile)
            else:
                raise ValueError("Unsupported file format.")
            if not data or not isinstance(data, list) or not all(isinstance(row, list) for row in data):
                raise ValueError("Invalid data format in the file.")
            headers, table_data = data[0], data[1:]
            self.add_table_from_data(file_path, headers, table_data)
        except Exception as e:
            messagebox.showerror("File Import Error", str(e))

    def add_table_from_data(self, file_path: str, headers: list[str], table_data: list[list[str]]):
        """Add a table from file data."""
        table_name = Path(file_path).stem
        table_name = self.ensure_unique_table_name(table_name)
        self.add_table(table_name, data=table_data, headers=headers)
        self.load_selected_table(table_name)
        self.save_data()

    def ensure_unique_table_name(self, table_name: str) -> str:
        """Ensure table name is unique."""
        original_name = table_name
        counter = 1
        while table_name in self.tables_data:
            table_name = f"{original_name} (New {counter})"
            counter += 1
        return table_name

    def save_table_data(self, file_path: str, data: list[list[str]], format: str):
        """Save table data in specified format."""
        match format:
            case 'csv':
                with open(file_path, 'w', newline='', encoding='utf-8') as file:
                    csv.writer(file).writerows(data)
            case 'json':
                with open(file_path, 'w', encoding='utf-8') as file:
                    json.dump(data, file, indent=4)
            case _:
                raise ValueError("Unsupported file format.")

    def export_table(self):
        """Export current table to file."""
        if not self.current_table_name or self.current_table_name not in self.tables_data:
            messagebox.showwarning("No Table Selected", "Please select a table to export.")
            return
        file_path = filedialog.asksaveasfilename(title="Export File", defaultextension=".json", filetypes=[("JSON files", "*.json"), ("CSV files", "*.csv")], initialfile=self.current_table_name)
        if not file_path:
            return
        try:
            data = [self.headers[self.current_table_name]] + self.tables_data[self.current_table_name]["data"]
            if file_path.endswith('.csv'):
                self.save_table_data(file_path, data, 'csv')
            elif file_path.endswith('.json'):
                with open(file_path, 'w', encoding='utf-8') as file:
                    json.dump(data, file, indent=4)
            else:
                raise ValueError("Unsupported file format.")
            self.save_data()
            messagebox.showinfo("Export Successful", f"Table '{self.current_table_name}' has been exported successfully.")
        except Exception as e:
            messagebox.showerror("Export Failed", f"An error occurred during export: {e}")

    def add_table(self, table_name: str, data: list[list[str]] | None = None, headers: Optional[list[str]] = None):
        """Add a new table."""
        if headers:
            self.headers[table_name] = headers
        else:
            default_headers = ["Column 1", "Column 2", "Column 3", "Column 4", "Column 5"]
            self.headers[table_name] = default_headers
        self.tables_data[table_name] = {"headers": self.headers[table_name], "data": data or [["" for _ in range(len(self.headers[table_name]))] for _ in range(10)]}
        self.create_table_label(table_name)
        self.save_data()

    def create_table_label(self, table_name: str):
        """Create label for table."""
        label = self._create_clickable_label(self.table_labels_frame, table_name, lambda: self.load_selected_table(table_name), True)
        label.pack(fill='x', padx=0, pady=5, expand=True)
        self.table_labels[table_name] = label
        self.add_custom_hover(label)
        self.sort_table_labels()

    def delete_table(self):
        """Delete selected table."""
        if not self.current_table_name or self.current_table_name not in self.tables_data:
            messagebox.showwarning("No Table Selected", "Please select a valid table to delete.")
            return
        if messagebox.askyesno("Delete Table", f"Are you sure you want to delete the table '{self.current_table_name}'?"):
            self.remove_table(self.current_table_name)

    def on_columns_deleted(self, event):
        """Handle column deletion and update filter headers."""
        if not self.current_table_name or not self.current_sheet:
            return
        new_headers = self.current_sheet.headers()
        self.headers[self.current_table_name] = new_headers
        self.update_filter_sort_headers()
        self.update_display()

    def remove_table(self, table_name: str):
        """Remove a table."""
        if self.current_table_name == table_name and self.current_sheet:
            self.current_sheet.pack_forget()
            self.current_sheet.destroy()
            self.current_sheet = None
        label = self.table_labels.get(table_name)
        if label:
            label.destroy()
        if table_name in self.tables_data:
            del self.tables_data[table_name]
        if table_name in self.headers:
            del self.headers[table_name]
        if table_name in self.table_labels:
            del self.table_labels[table_name]
        self.current_table_name = None
        self.selected_table_label = None
        self.save_data()
        self.sort_table_labels()

    def load_tables(self):
        """Load tables from file."""
        if not self.save_file_path.exists():
            return
        try:
            with self.save_file_path.open('r', encoding='utf-8') as file:
                all_data = json.load(file)
        except Exception as e:
            messagebox.showerror("Load Error", f"Failed to load tables: {e}")
            return
        for table_name, data in all_data.items():
            if not isinstance(data, list) or not data:
                continue
            headers, table_data = data[0], data[1:]
            self.headers[table_name] = headers
            self.tables_data[table_name] = {"headers": headers, "data": table_data}
            self.create_table_label(table_name)
        self.sort_table_labels()

    def load_selected_table(self, table_name: str):
        """Display selected table."""
        if self.current_sheet:
            self.current_sheet.pack_forget()
            self.current_sheet.destroy()
            self.current_sheet = None
        if self.selected_table_label:
            try:
                self.selected_table_label.config(bg=self.colors["button"])
            except tk.TclError:
                self.selected_table_label = None

        if table_name not in self.tables_data:
            messagebox.showerror("Error", f"Table '{table_name}' does not exist.")
            self.current_table_name = None
            self.selected_table_label = None
            return

        table_info = self.tables_data[table_name]
        headers = table_info["headers"]
        data = table_info["data"]
        try:
            sheet = Sheet(
                self.table_frame,
                data=data,
                show_table=True,
                headers=headers,
                font=self.fonts["tksheet"],
                header_font=self.fonts["tksheet"],
                auto_resize_columns=True
            )
            sheet.set_options(table_fg=self.colors["text"], header_fg=self.colors["text"])
            sheet.enable_bindings()
            sheet.extra_bindings([
                ("end_edit_cell", self.on_cell_edit),
                ("end_delete_columns", self.on_columns_deleted)  # Added binding for column deletion
            ])
            sheet.pack(expand=True, fill='both')
            self.current_sheet = sheet
            self.current_table_name = table_name

            if self.table_name_label:
                self.table_name_label.config(text=self.current_table_name)
            else:
                self.table_name_label = tk.Label(
                    self.root,
                    text=self.current_table_name,
                    bg=self.colors["background"],
                    fg=self.colors["text"],
                    font=self.fonts["header_info"]
                )
                self.table_name_label.grid(row=0, column=0, padx=(60, 10), pady=(5, 0), sticky='nw')

            label = self.table_labels.get(table_name)
            if label:
                self.selected_table_label = label
                try:
                    self.selected_table_label.config(bg=self.colors["button_selected"])
                except tk.TclError:
                    self.selected_table_label = None
                    messagebox.showerror("Error", f"Failed to update the label for '{table_name}'.")
            else:
                self.selected_table_label = None
                messagebox.showerror("Error", f"Label for '{table_name}' not found.")
            
            self.update_filter_sort_headers()
        except Exception as e:
            messagebox.showerror("Sheet Error", f"Failed to load table '{table_name}': {e}")
            self.current_table_name = None
            self.selected_table_label = None

    def on_cell_edit(self, event=None):
        """Handle cell edits."""
        if self.current_table_name and self.current_sheet:
            sheet_data = self.current_sheet.get_sheet_data()
            for idx, row in enumerate(sheet_data):
                if idx < len(self.displayed_row_indices):
                    data_idx = self.displayed_row_indices[idx]
                    self.tables_data[self.current_table_name]["data"][data_idx] = row
                else:
                    # Handle new rows by appending to the underlying data
                    self.tables_data[self.current_table_name]["data"].append(row)
            self.save_data()

    def save_data(self):
        """Save all tables data."""
        all_data = {}
        for table_name, table_info in self.tables_data.items():
            all_data[table_name] = [table_info["headers"]] + table_info["data"]
        try:
            with self.save_file_path.open('w', encoding='utf-8') as file:
                json.dump(all_data, file, indent=4)
        except Exception as e:
            print(f"Error saving data: {e}")

    def apply_filter(self):
        """Apply filter to table data based on selected criteria."""
        if not self.current_table_name or not self.current_sheet:
            return
        header = self.filter_header_var.get()
        option = self.filter_option_var.get()
        text = self.filter_text_var.get().lower()
        original_data = self.tables_data[self.current_table_name]["data"]

        def row_matches(row):
            if header == "All":
                cells = row
                target_indices = range(len(cells))
            else:
                try:
                    target_indices = [self.headers[self.current_table_name].index(header)]
                except ValueError:
                    return False

            conditions = {
                "is": lambda cell, text: cell == text,
                "is not": lambda cell, text: cell != text,
                "contains": lambda cell, text: text in cell,
                "does not contain": lambda cell, text: text not in cell,
                "starts with": lambda cell, text: cell.startswith(text),
                "ends with": lambda cell, text: cell.endswith(text),
                "is empty": lambda cell, _: cell == "",
                "is not empty": lambda cell, _: cell != ""
            }

            for idx in target_indices:
                cell = str(row[idx]).lower()
                if conditions.get(option, lambda _, __: False)(cell, text):
                    return True
            return False

        # Collect both filtered data and their original indices
        filtered_data = []
        self.displayed_row_indices = []

        for i, row in enumerate(original_data):
            if row_matches(row):
                filtered_data.append(row)
                self.displayed_row_indices.append(i)

        try:
            self.current_sheet.set_sheet_data(filtered_data)
        except tk.TclError as e:
            print(f"Error applying filter: {e}")

    def apply_sort(self):
        """Sort the filtered data based on selected sort options."""
        if not self.current_table_name or not self.current_sheet:
            return
        headers = self.headers.get(self.current_table_name, [])
        if not headers:
            return
    
        primary_column = self.sort_primary_column_var.get()
        primary_order = self.sort_primary_order_var.get()
        secondary_column = self.sort_secondary_column_var.get()
        secondary_order = self.sort_secondary_order_var.get()
    
        if not primary_column:
            return  # No sorting applied
    
        try:
            primary_index = headers.index(primary_column)
        except ValueError:
            primary_index = -1
    
        try:
            secondary_index = headers.index(secondary_column)
        except ValueError:
            secondary_index = -1
    
        # Extract the filtered data based on displayed_row_indices
        filtered_data = [self.tables_data[self.current_table_name]["data"][i] for i in self.displayed_row_indices]
    
        # Define sort key with type indicators
        def sort_key(row):
            primary_value = self.get_sortable_value(row[primary_index]) if primary_index != -1 else ""
            secondary_value = self.get_sortable_value(row[secondary_index]) if secondary_index != -1 else ""
            return (primary_value, secondary_value)
    
        # Determine sort order
        reverse_primary = True if primary_order == "Descending" else False
        reverse_secondary = True if secondary_order == "Descending" else False
    
        # Sort the data
        # First sort by secondary to ensure stability
        if secondary_column != primary_column and secondary_index != -1:
            filtered_data.sort(
                key=lambda row: self.get_sortable_value(row[secondary_index]),
                reverse=reverse_secondary
            )
        # Then sort by primary
        if primary_index != -1:
            filtered_data.sort(
                key=lambda row: self.get_sortable_value(row[primary_index]),
                reverse=reverse_primary
            )
    
        # Update the sheet with sorted data
        try:
            self.current_sheet.set_sheet_data(filtered_data)
        except tk.TclError as e:
            print(f"Error applying sort: {e}")

    def get_sortable_value(self, value):
        """Convert value to a sortable format."""
        if value is None:
            return ""  # Treat None as empty string
        try:
            return float(value)
        except (ValueError, TypeError):
            return str(value).lower()

    def rename_headers(self):
        """Rename table headers."""
        if not self.current_table_name:
            messagebox.showwarning("No Table Selected", "Please select a table to rename headers.")
            return
        current_headers = self.headers[self.current_table_name]
        new_headers = simpledialog.askstring(
            "Rename Headers", 
            "Enter new headers (comma-separated):", 
            initialvalue=", ".join(current_headers)
        )
        if new_headers:
            header_list = [header.strip() for header in new_headers.split(',')]
            if not header_list:
                messagebox.showerror("Invalid Headers", "Header list cannot be empty.")
                return
            self.headers[self.current_table_name] = header_list
            self.tables_data[self.current_table_name]["headers"] = header_list
            if self.current_sheet:
                try:
                    self.current_sheet.headers(header_list)
                except tk.TclError as e:
                    self.headers[self.current_table_name] = current_headers  # Revert on error
                    messagebox.showerror("Error", f"Failed to rename headers: {e}")
                    return
            
            self.update_filter_sort_headers()
            
            self.save_data()

    def begin_rename(self, table_name: str):
        """Start renaming a table."""
        if table_name not in self.table_labels:
            messagebox.showerror("Error", f"Table '{table_name}' not found.")
            return
        label = self.table_labels[table_name]
        self.cancel_rename()
        self.rename_entry = tk.Entry(self.table_labels_frame, bg=label.cget("bg"), fg=self.colors["text"], relief='flat')
        self.rename_entry.insert(0, table_name)
        self.rename_entry.place(x=label.winfo_x(), y=label.winfo_y(), width=label.winfo_width(), height=label.winfo_height())
        self.rename_entry.focus()
        self.rename_entry.select_range(0, tk.END)
        self.rename_entry.bind('<Return>', lambda event: self.end_rename(table_name))
        self.rename_entry.bind('<FocusOut>', lambda event: self.cancel_rename())

    def end_rename(self, old_name: str):
        """Finalize table renaming."""
        new_name = self.rename_entry.get().strip()
        if not new_name:
            messagebox.showerror("Invalid Name", "Table name cannot be empty.")
            self.rename_entry.focus_set()
            return
        if new_name in self.tables_data and new_name != old_name:
            messagebox.showerror("Duplicate Name", f"A table named '{new_name}' already exists.")
            self.rename_entry.focus_set()
            return
        if new_name != old_name:
            self.update_table_name(old_name, new_name)
            self.load_selected_table(new_name)
        self.cancel_rename()

    def update_table_name(self, old_name: str, new_name: str):
        """Update table name and resources."""
        self.tables_data[new_name] = self.tables_data.pop(old_name)
        self.headers[new_name] = self.headers.pop(old_name)
        label = self.table_labels.pop(old_name)
        label.config(text=new_name)
        label.bind("<Button-1>", lambda event: self.load_selected_table(new_name))
        label.bind("<Double-Button-1>", lambda event: self.begin_rename(new_name))
        self.table_labels[new_name] = label
        if self.selected_table_label == label:
            try:
                label.config(bg=self.colors["button_selected"])
            except tk.TclError:
                pass
        self.save_data()
        self.sort_table_labels()

    def cancel_rename(self):
        """Cancel rename operation."""
        if self.rename_entry:
            self.rename_entry.destroy()
            self.rename_entry = None

    def add_custom_hover(self, label: tk.Label):
        """Add hover effects to label."""
        def on_enter(event):
            if label != self.selected_table_label:
                try:
                    label.config(bg=self.colors["button_selected"])
                except tk.TclError:
                    pass

        def on_leave(event):
            if label != self.selected_table_label:
                try:
                    label.config(bg=self.colors["button"])
                except tk.TclError:
                    pass

        label.bind("<Enter>", on_enter)
        label.bind("<Leave>", on_leave)

if __name__ == "__main__":
    set_dpi_awareness()
    root = tk.Tk()
    app = TableApp(root)
    root.mainloop()