import os
import shutil
import tkinter as tk
from tkinter import filedialog, messagebox, simpledialog
import re
from collections import defaultdict
import natsort
from PIL import Image, ImageTk
from natsort import natsort_keygen, natsorted, ns

# Initialize global variables
folder_path = ""
groups = {}
display_to_group_map = {}
pending_renames = {}  # Stores pending renames: {original_group_name: new_group_name}
pending_merges = []   # Stores pending merges: list of group_names to merge into new_group_name
merge_history = []    # Stack to keep track of merges for undo functionality
rename_in_progress = False  # Flag to prevent multiple rename operations
img_window = None  # Global reference for the image window

# Define the custom key that ignores leading numbers and colons
strip_prefix_key = natsort_keygen(key=lambda x: re.sub(r'^\d+[:：]\s*', '', x))

def clean_filename(filename, redundant_words):
    """Clean filename according to specified rules (remove numbers, repeated words)."""
    filename = filename.split('：', 1)[-1]
    filename = os.path.splitext(filename)[0]
    words = filename.replace('_', ' ').lower().split()

    if all(part.isdigit() for part in words):
        return ' '.join(words)

    words = [re.sub(r'\d+', ' ', word).strip() for word in words]
    words = ' '.join(words).split()

    unique_words = [w for w in words if w not in redundant_words]
    seen = set()
    cleaned_words = [word for word in unique_words if len(word) > 2 and not (word in seen or seen.add(word))]

    cleaned_filename = ' '.join(cleaned_words)
    return cleaned_filename if cleaned_filename else "misc_files"

def find_redundant_words(files):
    if not files:
        return []

    processed_files = [file.split('：', 1)[-1].replace('_', ' ').lower().split() for file in files]

    common_words = set(processed_files[0])
    for file_words in processed_files[1:]:
        common_words.intersection_update(file_words)

    redundant_words = list(common_words)
    return redundant_words

def extract_sort_key(filename):
    match = re.search(r's_(\d+)', filename.lower())
    return int(match.group(1)) if match else 0

def prepare_groups(files, grouping_enabled):
    if not files:
        return {}

    if grouping_enabled:
        redundant_words = find_redundant_words(files)
        temp_groups = defaultdict(list)

        for file in files:
            cleaned_name = clean_filename(file, redundant_words)
            temp_groups[cleaned_name].append(file)

        for group in temp_groups:
            temp_groups[group].sort(key=extract_sort_key)

        return temp_groups
    else:
        return {"All Files": sorted(files)}

def split_into_subgroups(files, file_num):
    """Splits a list of files into subgroups each containing up to file_num files."""
    subgroups = []
    if file_num <= 0:
        file_num = 30  # default to prevent infinite loop
    total_files = len(files)
    index = 0

    while index < total_files:
        remaining = total_files - index
        if remaining <= 2 * file_num:
            subgroups.append(files[index:])
            break
        else:
            subgroups.append(files[index:index + file_num])
            index += file_num

    return subgroups

def capitalize_folder_name(folder_name):
    short_words = {'of', 'is', 'a', 'an'}
    words = folder_name.split()
    capitalized = [word.capitalize() if word.lower() not in short_words else word for word in words]
    modified_name = ' '.join(capitalized)
    return modified_name

def get_preview_groups():
    preview = groups.copy()

    # Apply pending renames
    for original_group, new_group in pending_renames.items():
        if original_group in preview:
            preview[new_group] = preview.pop(original_group)

    # Apply pending merges
    for merge_op in pending_merges:
        groups_to_merge = merge_op['groups']
        new_group_name = merge_op['new_group']
        merged_files = []

        for group in groups_to_merge:
            merged_files.extend(preview.get(group, []))
            if group in preview:
                del preview[group]

        if new_group_name in preview:
            preview[new_group_name].extend(merged_files)
        else:
            preview[new_group_name] = merged_files

    # Handle Group by File Number
    grouping_enabled_flag = enable_grouping_var.get()
    group_files_flag = group_files_var.get()
    file_num = int(file_number_var.get()) if file_number_var.get().isdigit() and int(file_number_var.get()) > 0 else 30

    if grouping_enabled_flag and group_files_flag:
        # When both Folder Grouping and Group by File Number are enabled
        temp_preview = {}
        for group_name, files in preview.items():
            subgroups = split_into_subgroups(files, file_num)
            for i, subgroup in enumerate(subgroups, 1):
                # Append subgroup index to group name for uniqueness
                subgroup_name = f"{group_name} - {i}"
                temp_preview[subgroup_name] = subgroup
        preview = temp_preview
    elif not grouping_enabled_flag and group_files_flag:
        # When Folder Grouping is disabled and Group by File Number is enabled
        all_files = sum(preview.values(), [])
        subgroups = split_into_subgroups(all_files, file_num)
        parent_prefix = parent_name_var.get().strip()
        preview = {f"{parent_prefix} - {i+1}": subgroup for i, subgroup in enumerate(subgroups)}

    return preview

def update_folder_info(preview_groups):
    total_files_label.config(text=f"Total Files: {sum(len(files) for files in preview_groups.values())}")

def show_folder_contents(event=None):
    try:
        selection = folder_listbox.curselection()
        if not selection:
            content_listbox.delete(0, tk.END)
            file_number_info.config(text="")
            return

        folder_display_name = folder_listbox.get(selection[0])
        original_group_name = display_to_group_map.get(folder_display_name, folder_display_name)

        preview_groups = get_preview_groups()

        update_folder_info(preview_groups)

        # Save the current scroll position
        current_yview = content_listbox.yview()

        content_listbox.delete(0, tk.END)

        files_in_group = preview_groups.get(original_group_name, [])

        if not files_in_group:
            content_listbox.insert(tk.END, "No files in this group.")
            file_number_info.config(text=f"Files in this Group: {len(files_in_group)}")
            return

        # Apply natural sorting ignoring prefixes
        sorted_files = natsort.natsorted(files_in_group, key=strip_prefix_key)

        for file in sorted_files:
            content_listbox.insert(tk.END, file)

        file_number_info.config(text=f"Files in this Group: {len(files_in_group)}")

        # Restore the previous scroll position
        content_listbox.yview_moveto(current_yview[0])

    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while displaying folder contents:\n{str(e)}")

def load_directory():
    global folder_path, groups, pending_renames, pending_merges, merge_history
    folder_path = filedialog.askdirectory(title="Select the folder containing your files")
    if not folder_path:
        return
    parent_folder_name = os.path.basename(os.path.normpath(folder_path))
    parent_name_var.set(parent_folder_name)
    try:
        files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
        groups = prepare_groups(files, enable_grouping_var.get())
        pending_renames.clear()
        pending_merges.clear()
        merge_history.clear()
        update_displayed_folders()
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while loading the directory:\n{str(e)}")

def apply_options(*args):
    if not folder_path:
        return
    try:
        prefix_check.config(state=tk.NORMAL)
        capitalize_check.config(state=tk.NORMAL)

        if not enable_grouping_var.get() and group_files_var.get():
            prefix_check.config(state=tk.DISABLED)
            capitalize_check.config(state=tk.DISABLED)
        
        update_displayed_folders()
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while applying options:\n{str(e)}")

def toggle_grouping(*args):
    if not folder_path:
        return
    try:
        files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
        groups.clear()
        groups.update(prepare_groups(files, enable_grouping_var.get()))
        pending_renames.clear()
        pending_merges.clear()
        merge_history.clear()
        update_displayed_folders()
        show_folder_contents()
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while toggling grouping:\n{str(e)}")

def update_displayed_folders():
    global display_to_group_map

    parent_prefix = parent_name_var.get().strip()
    add_prefix = prefix_var.get()
    capitalize_names = capitalize_var.get()
    group_files = group_files_var.get()
    grouping_enabled_flag = enable_grouping_var.get()

    folder_listbox.delete(0, tk.END)
    display_to_group_map.clear()

    # Get preview_groups which includes pending renames and merges
    preview_groups = get_preview_groups()

    update_folder_info(preview_groups)

    sorted_folder_names = natsort.natsorted(preview_groups.keys())
    for folder_name in sorted_folder_names:
        display_name = folder_name
        if grouping_enabled_flag:
            if " - " in folder_name:
                # This is a subgroup, e.g., "GroupName - 1"
                base_group_name = " - ".join(folder_name.split(" - ")[:-1])
                subgroup_suffix = " - " + folder_name.split(" - ")[-1]
                if base_group_name != "All Files":
                    base_display_name = capitalize_folder_name(base_group_name) if capitalize_names else base_group_name
                    if add_prefix:
                        base_display_name = f"{parent_prefix} - {base_display_name}"
                    display_name = f"{base_display_name}{subgroup_suffix}"
                else:
                    display_name = f"{parent_prefix}{subgroup_suffix}" if add_prefix else subgroup_suffix.strip()
            else:
                if folder_name != "All Files":
                    modified_folder_name = capitalize_folder_name(display_name) if capitalize_names else display_name
                    modified_folder_name = f"{parent_prefix} - {modified_folder_name}" if add_prefix else modified_folder_name
                    display_name = modified_folder_name
                else:
                    display_name = parent_prefix if parent_prefix else "Organized Files"
        else:
            if group_files:
                modified_folder_name = folder_name  # Subgroup names already include parent_prefix
                display_name = modified_folder_name
            else:
                modified_folder_name = parent_prefix if parent_prefix else "Organized Files"
                display_name = modified_folder_name

        folder_listbox.insert(tk.END, display_name)
        display_to_group_map[display_name] = folder_name

    if folder_listbox.size() > 0:
        folder_listbox.select_set(0)
        show_folder_contents()

def reset_program():
    global folder_path, groups, display_to_group_map, pending_renames, pending_merges, merge_history, rename_in_progress, img_window

    folder_path = ""
    groups.clear()
    display_to_group_map.clear()
    pending_renames.clear()
    pending_merges.clear()
    merge_history.clear()
    rename_in_progress = False

    if img_window is not None and img_window.winfo_exists():
        img_window.destroy()
        img_window = None

    folder_listbox.delete(0, tk.END)
    content_listbox.delete(0, tk.END)
    parent_name_var.set("")
    total_files_label.config(text="Total Files: 0")
    file_number_info.config(text="")

def execute_sorting():
    if not folder_path or not groups:
        messagebox.showerror("Error", "No directory loaded or no files grouped. Please load a directory first.")
        return

    try:
        parent_prefix = parent_name_var.get().strip()
        add_prefix = prefix_var.get()
        group_files = group_files_var.get()
        capitalize_names = capitalize_var.get()
        grouping_enabled_flag = enable_grouping_var.get()
        file_num_str = file_number_var.get().strip()
        file_num = int(file_num_str) if file_num_str.isdigit() and int(file_num_str) > 0 else 30

        # Get the latest groupings, including pending renames and merges
        preview_groups = get_preview_groups()

        if grouping_enabled_flag:
            for group_name, files in preview_groups.items():
                # Determine the actual folder name based on whether it's a subgroup
                if " - " in group_name:
                    # It's a subgroup
                    base_group_name = " - ".join(group_name.split(" - ")[:-1])
                    subgroup_suffix = " - " + group_name.split(" - ")[-1]
                    if base_group_name != "All Files":
                        base_folder_name = capitalize_folder_name(base_group_name) if capitalize_names else base_group_name
                        modified_folder_name = f"{parent_prefix} - {base_folder_name}" if add_prefix else base_folder_name
                        subfolder_name = f"{modified_folder_name}{subgroup_suffix}"
                    else:
                        subfolder_name = f"{parent_prefix}{subgroup_suffix}" if add_prefix else subgroup_suffix.strip()
                else:
                    if group_name == "All Files":
                        subfolder_name = parent_prefix if add_prefix else "Organized Files"
                    else:
                        base_folder_name = capitalize_folder_name(group_name) if capitalize_names else group_name
                        subfolder_name = f"{parent_prefix} - {base_folder_name}" if add_prefix else base_folder_name

                new_folder_path = os.path.join(folder_path, subfolder_name)

                if os.path.abspath(new_folder_path) == os.path.abspath(folder_path):
                    messagebox.showerror("Error", f"Cannot create a folder with the same name as the parent folder: {subfolder_name}")
                    continue

                os.makedirs(new_folder_path, exist_ok=True)
                for file in files:
                    src = os.path.join(folder_path, file)
                    dest = os.path.join(new_folder_path, file)
                    shutil.move(src, dest)
        elif group_files and file_num:
            # When Folder Grouping is Disabled and Group by File Number is Enabled
            for subfolder_name, files in preview_groups.items():
                new_folder_path = os.path.join(folder_path, subfolder_name)
                os.makedirs(new_folder_path, exist_ok=True)
                for file in files:
                    src = os.path.join(folder_path, file)
                    dest = os.path.join(new_folder_path, file)
                    shutil.move(src, dest)
        else:
            # When Folder Grouping is Disabled and Group by File Number is Disabled
            # Move all files into a single folder named after the parent prefix
            new_folder_path = os.path.join(folder_path, parent_prefix if parent_prefix else "Organized Files")
            os.makedirs(new_folder_path, exist_ok=True)
            for files in preview_groups.values():
                for file in files:
                    src = os.path.join(folder_path, file)
                    dest = os.path.join(new_folder_path, file)
                    shutil.move(src, dest)

        messagebox.showinfo("Success", "Files sorted and organized successfully!")
        reset_program()
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred during sorting:\n{str(e)}")

def rename_selected_folder(event=None):
    global rename_in_progress
    selected_indices = folder_listbox.curselection()
    if len(selected_indices) != 1:
        messagebox.showinfo("Rename Folder", "Please select exactly one folder to rename.")
        return

    # Check if the selected folder is auto-generated
    selected_display_name = folder_listbox.get(selected_indices[0])
    if re.search(r'\s-\s\d+$', selected_display_name):
        messagebox.showinfo("Rename Not Allowed", "Cannot rename auto-generated folders.")
        return

    if rename_in_progress:
        messagebox.showinfo("Rename in Progress", "Please complete the current rename operation before starting a new one.")
        return

    rename_in_progress = True

    index = selected_indices[0]
    old_display_name = folder_listbox.get(index)
    original_group_name = display_to_group_map.get(old_display_name, old_display_name)

    entry = tk.Entry(frame, width=80)  # Increased width
    entry.insert(0, old_display_name)
    entry.select_range(0, tk.END)
    entry.focus()

    bbox = folder_listbox.bbox(index)
    if bbox:
        entry.place(
            x=folder_listbox.winfo_x() + bbox[0],
            y=folder_listbox.winfo_y() + bbox[1],
            width=bbox[2] * 2,  # Adjusted width
            height=bbox[3]
        )

    def on_enter(event=None):
        global rename_in_progress
        new_display_name = entry.get().strip()
        if not new_display_name:
            messagebox.showerror("Invalid Name", "Folder name cannot be empty.")
            rename_in_progress = False
            entry.destroy()
            root.unbind("<Button-1>")
            return
        if new_display_name in display_to_group_map and new_display_name != old_display_name:
            messagebox.showerror("Duplicate Name", "A folder with this name already exists.")
            rename_in_progress = False
            entry.destroy()
            root.unbind("<Button-1>")
            return

        pending_renames[original_group_name] = new_display_name

        folder_listbox.delete(index)
        folder_listbox.insert(index, new_display_name)
        display_to_group_map[new_display_name] = new_display_name
        del display_to_group_map[old_display_name]

        entry.destroy()
        rename_in_progress = False
        root.unbind("<Button-1>")
        update_displayed_folders()

    entry.bind("<Return>", on_enter)

    def on_click_anywhere(event):
        if event.widget != entry:
            on_enter()

    root.bind("<Button-1>", on_click_anywhere)
    entry.bind("<FocusOut>", on_enter)

def merge_selected_folders():
    selected_indices = folder_listbox.curselection()
    if len(selected_indices) < 2:
        messagebox.showinfo("Merge Folders", "Please select two or more folders to merge.")
        return

    selected_display_names = [folder_listbox.get(i) for i in selected_indices]
    selected_group_names = [display_to_group_map.get(name, name) for name in selected_display_names]

    # Check if any selected folders are auto-generated
    auto_generated = any(re.search(r'\s-\s\d+$', name) for name in selected_display_names)
    if auto_generated:
        messagebox.showinfo("Merge Not Allowed", "Cannot merge auto-generated folders.")
        return

    default_new_folder_name = selected_display_names[0]
    new_folder_name = simpledialog.askstring("Merge Folders", "Enter the name for the merged folder:", initialvalue=default_new_folder_name)
    if not new_folder_name:
        messagebox.showerror("Invalid Name", "Folder name cannot be empty.")
        return

    # Check for duplicate names
    if new_folder_name in display_to_group_map and new_folder_name not in selected_display_names:
        messagebox.showerror("Duplicate Name", "A folder with this name already exists.")
        return

    for merge_op in pending_merges:
        if new_folder_name == merge_op['new_group']:
            messagebox.showerror("Duplicate Name", "A pending merge to this name already exists.")
            return

    pending_merges.append({'groups': selected_group_names, 'new_group': new_folder_name})
    merge_history.append({'groups': selected_group_names, 'new_group': new_folder_name})

    for i in reversed(selected_indices):
        display_name = folder_listbox.get(i)
        folder_listbox.delete(i)
        del display_to_group_map[display_name]

    folder_listbox.insert(tk.END, new_folder_name)
    display_to_group_map[new_folder_name] = new_folder_name
    update_displayed_folders()

def undo_merge():
    if not merge_history:
        messagebox.showinfo("Undo Merge", "No merge operations to undo.")
        return

    last_merge = merge_history.pop()
    groups_to_split = last_merge['groups']
    new_group_name = last_merge['new_group']

    pending_merges[:] = [merge for merge in pending_merges if merge['new_group'] != new_group_name]

    folder_listbox.delete(tk.END)
    for group in groups_to_split:
        folder_listbox.insert(tk.END, group)
        display_to_group_map[group] = group

    if new_group_name in display_to_group_map:
        del display_to_group_map[new_group_name]

    update_displayed_folders()

def create_context_menu(event):
    try:
        # Get the clicked item's index
        clicked_index = folder_listbox.nearest(event.y)
        if clicked_index == -1:
            return

        # Determine if the clicked item is already selected
        if clicked_index not in folder_listbox.curselection():
            # If not, clear selection and select the clicked item
            folder_listbox.selection_clear(0, tk.END)
            folder_listbox.selection_set(clicked_index)

        # Get all currently selected items
        selected_indices = folder_listbox.curselection()
        selected_display_names = [folder_listbox.get(i) for i in selected_indices]

        # Determine if all selected items are not auto-generated
        auto_generated = any(re.search(r'\s-\s\d+$', name) for name in selected_display_names)
        if auto_generated:
            # If any selected folder is auto-generated, do not show context menu
            return

        context_menu = tk.Menu(root, tearoff=0)
        context_menu.add_command(label="Rename", command=rename_selected_folder)

        # Check if multiple non-auto-generated folders are selected
        if len(selected_indices) > 1:
            context_menu.add_command(label="Merge Selected Folders", command=merge_selected_folders)

        if merge_history:
            context_menu.add_separator()
            context_menu.add_command(label="Undo Last Merge", command=undo_merge)

        # Only show the context menu if there are menu items
        if context_menu.index("end") is not None:
            context_menu.post(event.x_root, event.y_root)
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while creating the context menu:\n{str(e)}")

def open_image(event=None):
    global img_window

    try:
        selected_indices = content_listbox.curselection()
        if not selected_indices:
            messagebox.showinfo("No Selection", "Please select a file to open.")
            return

        selected_file = content_listbox.get(selected_indices[0])
        file_path = os.path.join(folder_path, selected_file)

        if not os.path.exists(file_path):
            messagebox.showerror("File Not Found", f"The file does not exist:\n{file_path}")
            return

        if not re.search(r'\.(jpg|jpeg|png|gif|bmp|tiff)$', selected_file, re.IGNORECASE):
            messagebox.showinfo("Not an Image", "The selected file is not a supported image format.")
            return

        img = Image.open(file_path)

        # Check if the image window already exists
        if img_window is None or not img_window.winfo_exists():
            img_window = tk.Toplevel(root)
            img_window.title("Image Viewer")

        # Resize image while maintaining the aspect ratio
        original_width, original_height = img.size
        aspect_ratio = original_width / original_height

        # Set a max height for the image to match the main window's height
        max_height = root.winfo_height()
        new_height = min(max_height, original_height)
        new_width = int(new_height * aspect_ratio)

        img = img.resize((new_width, new_height), Image.LANCZOS)

        # Adjust the window's dimensions
        img_window.geometry(f"{new_width}x{new_height}")

        photo = ImageTk.PhotoImage(img)

        # Remove any existing label widgets in img_window to prevent overlaying the images
        for widget in img_window.winfo_children():
            widget.destroy()

        img_label = tk.Label(img_window, image=photo)
        img_label.image = photo  # Keep reference to avoid garbage collection
        img_label.pack()

        # Update the position of the popup window
        update_popup_position()

    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while opening the image:\n{str(e)}")

# Function to update popup window position
def update_popup_position(event=None):
    """Update the position of the popup window to lock it to the edge of the main window."""
    global img_window
    if img_window is not None and img_window.winfo_exists():
        main_window_x = root.winfo_x()
        main_window_y = root.winfo_y()
        main_window_width = root.winfo_width()

        # Calculate new position for the popup window
        popup_x = main_window_x + main_window_width
        popup_y = main_window_y
        img_window.geometry(f"+{popup_x}+{popup_y}")

# Initialize the main application window
root = tk.Tk()
root.title("File Sorter")
root.geometry("900x600")
root.resizable(False, False)

# Create and layout the main frame
frame = tk.Frame(root)
frame.pack(pady=20, padx=20, fill=tk.BOTH, expand=True)

folder_listbox_width = 40
content_listbox_width = 70

# Parent Folder Name Entry
parent_name_label = tk.Label(frame, text="Parent Folder Name:")
parent_name_label.grid(row=0, column=0, padx=5, sticky='w')

parent_name_var = tk.StringVar()
parent_name_entry = tk.Entry(frame, width=50, textvariable=parent_name_var)
parent_name_entry.grid(row=0, column=1, columnspan=2, padx=5, sticky='ew')
parent_name_var.trace_add('write', lambda *args: apply_options())

# Total Files Label
total_files_label = tk.Label(frame, text="Total Files: 0")
total_files_label.grid(row=0, column=3, padx=5, sticky='w')

# Add Parent Name Prefix Checkbox
prefix_var = tk.IntVar(value=0)
prefix_check = tk.Checkbutton(frame, text="Add Parent Name Prefix", variable=prefix_var, command=apply_options)
prefix_check.grid(row=1, column=0, columnspan=2, pady=5, sticky='w')

# Group by File Number Checkbox and Entry
group_files_var = tk.IntVar(value=0)
group_files_check = tk.Checkbutton(frame, text="Group by file number", variable=group_files_var, command=apply_options)
group_files_check.grid(row=2, column=0, pady=5, sticky='w')

file_number_label = tk.Label(frame, text="Files per subfolder:")
file_number_label.grid(row=2, column=1, padx=0, sticky='w')

file_number_var = tk.StringVar(value="30")
file_number_entry = tk.Entry(frame, width=5, textvariable=file_number_var)
file_number_entry.grid(row=2, column=1, sticky='w', padx=(110, 0))
file_number_var.trace_add('write', lambda *args: apply_options())

# Capitalize Folder Names Checkbox
capitalize_var = tk.IntVar(value=1)
capitalize_check = tk.Checkbutton(frame, text="Capitalize Folder Names", variable=capitalize_var, command=apply_options)
capitalize_check.grid(row=3, column=0, columnspan=2, pady=5, sticky='w')

# Enable Folder Grouping Checkbox
enable_grouping_var = tk.IntVar(value=1)
enable_grouping_check = tk.Checkbutton(frame, text="Enable Folder Grouping", variable=enable_grouping_var, command=apply_options)
enable_grouping_check.grid(row=4, column=0, columnspan=2, pady=5, sticky='w')
enable_grouping_var.trace_add('write', toggle_grouping)

# Grouped Folders Listbox
folder_listbox_label = tk.Label(frame, text="Grouped Folders (Preview):", font=('Century Gothic', 10, 'bold'))
folder_listbox_label.grid(row=5, column=0, sticky='w')

folder_listbox = tk.Listbox(frame, width=folder_listbox_width, height=15, exportselection=False, selectmode=tk.EXTENDED)
folder_listbox.grid(row=6, column=0, columnspan=2, pady=5, sticky='nsew')
folder_listbox.bind("<<ListboxSelect>>", show_folder_contents)
folder_listbox.bind("<Double-Button-1>", rename_selected_folder)

# Bind right-click for context menu
folder_listbox.bind("<Button-3>", create_context_menu)
folder_listbox.bind("<Button-2>", create_context_menu)  # For macOS right-click

# Folder Contents Listbox
content_listbox_label = tk.Label(frame, text="Folder Contents (Preview):", font=('Century Gothic', 10, 'bold'))
content_listbox_label.grid(row=5, column=2, sticky='w')

content_listbox = tk.Listbox(frame, width=content_listbox_width, height=15, exportselection=False)
content_listbox.grid(row=6, column=2, columnspan=2, pady=5, sticky='nsew')
# Removed the unnecessary binding
# content_listbox.bind("<<ListboxSelect>>", show_folder_contents)
content_listbox.bind("<Double-Button-1>", open_image)  # Double-click to open image

# View Image Button
view_image_button = tk.Button(frame, text="View Image", command=open_image)
view_image_button.grid(row=7, column=2, pady=(5, 0), sticky='w')

# Configure grid weights for responsive resizing
frame.grid_rowconfigure(6, weight=1)
frame.grid_columnconfigure(1, weight=1)

# File Number Info Label
file_number_info = tk.Label(frame, text="", font=('Century Gothic', 8))
file_number_info.grid(row=7, column=3, pady=(0,20), sticky='ne')

# Browse Directory and Execute Sorting Buttons
browse_button = tk.Button(frame, text="Browse Directory", command=load_directory)
browse_button.grid(row=8, column=0, pady=(10, 0), sticky='w')

execute_button = tk.Button(frame, text="Execute Sorting", command=execute_sorting)
execute_button.grid(row=8, column=3, pady=(10, 0), sticky='e')

# Bind the main window's "<Configure>" event to update the popup position
root.bind("<Configure>", update_popup_position)

# Start the main event loop
root.mainloop()

Do not add index number  - 1 at the end of the folder number if there is only one subfolder.  Please fix this.