import os
import shutil
import tkinter as tk
from tkinter import filedialog, messagebox
import re
from collections import defaultdict

# Initialize global variables
folder_path = ""
groups = {}
display_to_group_map = {}

def clean_filename(filename, redundant_words):
    """
    Clean filename by removing leading index numbers and redundant words.

    Args:
        filename (str): The original filename.
        redundant_words (list): List of words to remove from the filename.

    Returns:
        str: The cleaned filename.
    """
    # Split on '：' to remove leading index
    if '：' in filename:
        filename = filename.split('：', 1)[-1]
    filename = os.path.splitext(filename)[0]
    words = filename.replace('_', ' ').lower().split()

    # Remove digits from words and strip
    words = [re.sub(r'\d+', '', word).strip() for word in words]
    words = [word for word in words if word not in redundant_words and len(word) > 2]

    # Remove duplicate words
    seen = set()
    cleaned_words = []
    for word in words:
        if word not in seen:
            seen.add(word)
            cleaned_words.append(word)

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

def find_redundant_words(files):
    """
    Find common redundant words across all filenames.

    Args:
        files (list): List of filenames.

    Returns:
        list: List of redundant words.
    """
    if not files:
        return []
    common_words = set(files[0].replace('_', ' ').lower().split())
    for file in files[1:]:
        common_words.intersection_update(file.replace('_', ' ').lower().split())
    return list(common_words)

def extract_sort_key(filename):
    """
    Extract numerical suffix from the filename for sorting.

    Args:
        filename (str): The filename.

    Returns:
        int: Numerical suffix for sorting.
    """
    # Extract part after 's_' and before file extension
    # Example: 'Ifrete_knit_s_13.jpg' -> 13
    match = re.search(r's_(\d+)', filename.lower())
    if match:
        return int(match.group(1))
    return 0  # Default if pattern not found

def prepare_groups(files):
    """
    Group files based on cleaned filenames.

    Args:
        files (list): List of filenames.

    Returns:
        dict: Dictionary with group names as keys and lists of filenames as values.
    """
    if not files:
        return {}
    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)
    # Sort files within each group based on numerical suffix
    for group in temp_groups:
        temp_groups[group].sort(key=extract_sort_key)
    return temp_groups

def split_into_subgroups(files, file_num):
    """
    Split files into subgroups based on file_num and remaining files rule.

    Args:
        files (list): List of filenames.
        file_num (int): Number of files per subgroup.

    Returns:
        list: List of subgroups (each subgroup is a list of filenames).
    """
    subgroups = []
    total_files = len(files)
    index = 0

    while index < total_files:
        remaining = total_files - index
        # If remaining files are less than twice the file_num and more than file_num, group all remaining
        if remaining <= 2 * file_num and remaining > 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):
    """
    Capitalize folder name excluding certain short words.

    Args:
        folder_name (str): The original folder name.

    Returns:
        str: The capitalized 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]
    return ' '.join(capitalized)

def show_folder_contents(event=None):
    """
    Display files or subfolders in the selected group (Preview).

    Args:
        event: Event triggered by selecting an item in folder_listbox.
    """
    try:
        selection = folder_listbox.curselection()
        if not selection:
            content_listbox.delete(0, tk.END)
            return

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

        # Clear previous contents
        content_listbox.delete(0, tk.END)

        # Get the list of files in the selected group
        files_in_group = groups.get(original_group_name, [])

        if not files_in_group:
            content_listbox.insert(tk.END, "No files in this group.")
            return

        if group_files_var.get():
            try:
                file_num = int(file_number_var.get())
                if file_num <= 0:
                    raise ValueError
            except ValueError:
                messagebox.showerror("Invalid Input", "Please enter a valid positive integer for file grouping.")
                content_listbox.delete(0, tk.END)  # Clear contents if invalid input
                return

            # Calculate subgroups based on the intelligent grouping rule
            subgroups = split_into_subgroups(files_in_group, file_num)

            for idx, subgroup in enumerate(subgroups, 1):
                # Update subfolder naming to include a hyphen separator
                subfolder_display_name = f"{folder_display_name} - {idx}"
                content_listbox.insert(tk.END, f"{subfolder_display_name}:")
                for file in subgroup:
                    content_listbox.insert(tk.END, f"  - {file}")
        else:
            for file in files_in_group:
                content_listbox.insert(tk.END, file)
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while displaying folder contents:\n{str(e)}")
        print(f"Error in show_folder_contents: {str(e)}")

def load_directory():
    """
    Load the selected directory, prepare groups, and update the displayed folders.
    """
    global folder_path, groups
    folder_path = filedialog.askdirectory(title="Select the folder containing your files")
    if not folder_path:
        return
    parent_folder_name = os.path.basename(folder_path)
    parent_name_var.set(parent_folder_name)
    try:
        # Only consider files, ignore subdirectories
        files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
        groups = prepare_groups(files)
        update_displayed_folders()
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred while loading the directory:\n{str(e)}")
        print(f"Error in load_directory: {str(e)}")

def apply_options(*args):
    """
    Apply the selected options and update the preview.
    """
    update_displayed_folders()
    show_folder_contents()

def update_displayed_folders():
    """
    Update the folder_listbox based on current groups and selected options.
    """
    global display_to_group_map
    parent_prefix = parent_name_var.get().strip()
    add_prefix = prefix_var.get()
    capitalize_names = capitalize_var.get()

    folder_listbox.delete(0, tk.END)
    display_to_group_map.clear()
    for folder_name in sorted(groups.keys()):
        # Step 1: Capitalize the group folder name if the option is selected
        if capitalize_names:
            modified_folder_name = capitalize_folder_name(folder_name)
        else:
            modified_folder_name = folder_name

        # Step 2: Add the parent prefix after capitalization if the option is selected
        if add_prefix:
            if modified_folder_name:
                # Prefix is added after capitalization
                modified_folder_name = f"{parent_prefix} - {modified_folder_name}"
            else:
                # If group folder name is empty, only prefix
                modified_folder_name = parent_prefix

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

    # Automatically select the first folder and show its contents
    if folder_listbox.size() > 0:
        folder_listbox.select_set(0)
        show_folder_contents()

def execute_sorting():
    """
    Execute the sorting: move files into organized folders and subfolders based on selected options.
    """
    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()
        file_number = int(file_number_var.get()) if group_files else None

        if group_files and (file_number is None or file_number <= 0):
            messagebox.showerror("Invalid Input", "Please enter a valid positive integer for file grouping.")
            return

        for folder_display_name, group_name in display_to_group_map.items():
            files = groups.get(group_name, [])
            modified_folder_name = folder_display_name  # Already includes prefix and capitalization
            new_folder_path = os.path.join(folder_path, modified_folder_name)
            os.makedirs(new_folder_path, exist_ok=True)

            if group_files and file_number:
                subgroups = split_into_subgroups(files, file_number)
                for idx, subgroup in enumerate(subgroups, 1):
                    # Name subfolders using the display folder name with a hyphen separator and serialized number
                    subfolder_name = f"{modified_folder_name} - {idx}"
                    subfolder_path = os.path.join(new_folder_path, subfolder_name)
                    os.makedirs(subfolder_path, exist_ok=True)
                    for file in subgroup:
                        src = os.path.join(folder_path, file)
                        dest = os.path.join(subfolder_path, file)
                        shutil.move(src, dest)
            else:
                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!")
        load_directory()  # Reload to reflect changes
    except ValueError:
        messagebox.showerror("Invalid Input", "Please enter a valid positive integer for file grouping.")
    except Exception as e:
        messagebox.showerror("Error", f"An error occurred during sorting:\n{str(e)}")
        print(f"Error in execute_sorting: {str(e)}")

# GUI Setup
root = tk.Tk()
root.title("File Sorter")

frame = tk.Frame(root)
frame.pack(pady=20, padx=20)

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

parent_name_var = tk.StringVar()
parent_name_entry = tk.Entry(frame, width=30, textvariable=parent_name_var)
parent_name_entry.grid(row=0, column=1, padx=10, sticky='w')
parent_name_var.trace_add('write', lambda *args: apply_options())

# Add Prefix Checkbox
prefix_var = tk.IntVar()
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)

# Group Files Checkbox and Entry
group_files_var = tk.IntVar()
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, sticky='e')

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

file_number_var = tk.StringVar(value="5")  # Default for file grouping
file_number_entry = tk.Entry(frame, width=5, textvariable=file_number_var)
file_number_entry.grid(row=2, column=3, sticky='w', padx=(0,10))
file_number_var.trace_add('write', lambda *args: apply_options())

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

# Folder Listbox (Preview)
folder_listbox_label = tk.Label(frame, text="Grouped Folders (Preview):")
folder_listbox_label.grid(row=4, column=0, columnspan=2, pady=(10,0))

folder_listbox = tk.Listbox(frame, width=40, height=15)
folder_listbox.grid(row=5, column=0, columnspan=2, pady=5)
folder_listbox.bind("<<ListboxSelect>>", show_folder_contents)

# Content Listbox (Preview)
content_listbox_label = tk.Label(frame, text="Folder Contents (Preview):")
content_listbox_label.grid(row=4, column=2, columnspan=2, pady=(10,0))

content_listbox = tk.Listbox(frame, width=40, height=15)
content_listbox.grid(row=5, column=2, columnspan=2, pady=5)

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

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

root.mainloop()

Why does the content of the filename Listbox disappears whenever I select the Listbox?