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 according to specified rules (remove numbers, repeated words)."""
    # 1. Ignore text before and including ':'.
    filename = filename.split('：', 1)[-1]
    
    # 2. Remove file extension.
    filename = os.path.splitext(filename)[0]
    
    # 3. Replace delimiters with spaces, and transform to lowercase.
    words = filename.replace('_', ' ').lower().split()
    
    # Check if the entire filename is numeric
    if all(part.isdigit() for part in words):
        return ' '.join(words)
    
    # 4. Replace numeric groups with spaces
    words = [re.sub(r'\d+', ' ', word).strip() for word in words]

    # Flatten the words split further on spaces
    words = ' '.join(words).split()

    # 5. Remove redundant words/groups of characters.
    unique_words = [w for w in words if w not in redundant_words]
    
    # 6. Remove words that are less than 3 characters and ensure uniqueness across filename
    seen = set()
    cleaned_words = [word for word in unique_words if len(word) > 2 and not (word in seen or seen.add(word))]

    # Join cleaned words back into a string
    cleaned_filename = ' '.join(cleaned_words)
    
    # Use a placeholder for files that end up without significant content
    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=4, 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="30")  # 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, exportselection=False)
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, exportselection=False)
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()


Please help me  add a checkbox (default checked) in front of the Folder Listbox header that allows you to toggle the folder grouping process. When this checkbox is unchecked, the program will bypass the folder grouping based on filenames and only organize files into subfolders using the parent folder name and the specified group file number.