Speed this up even more import ttkbootstrap as tb
from ttkbootstrap.constants import *
from ttkbootstrap.dialogs import Messagebox
from tkinter import filedialog
import os
import subprocess
import threading
import concurrent.futures
import queue
import psutil

# Constants for the application
APP_NAME = "DeepAscension Video2Image+"
MAX_CONCURRENT_TASKS = 4
DISK_CHECK_INTERVAL = 5000  # in milliseconds

def select_input_folders():
    while True:
        folder = filedialog.askdirectory(title="Select Input Folder (Cancel to finish)")
        if folder:
            if folder not in input_folders:
                input_folders.append(folder)
                tree.insert('', 'end', iid=folder, values=(folder, "Pending"))
            else:
                Messagebox.show_warning("Duplicate Folder", f"The folder '{folder}' is already selected.")
        else:
            break

def remove_selected_folders():
    selected_items = tree.selection()
    for item in selected_items:
        input_folders.remove(item)
        tree.delete(item)
    update_remove_button()

def select_output_folder():
    folder = filedialog.askdirectory(title="Select Output Folder")
    if folder:
        output_folder_var.set(folder)
        output_entry.config(state='normal')
        output_entry.delete(0, tb.END)
        output_entry.insert(0, folder)
        output_entry.config(state='readonly')

def sanitize_folder_name(name):
    """Sanitize the folder name to remove or replace problematic characters."""
    return "".join(c if c.isalnum() or c in (' ', '_', '-') else "_" for c in name).replace(" ", "_")

def process_videos_thread():
    """Run the video processing in a separate thread."""
    threading.Thread(target=process_videos, daemon=True).start()

def process_videos():
    global is_paused
    output_folder = output_folder_var.get()

    if not input_folders:
        Messagebox.show_error("Error", "Please select at least one input folder.")
        return
    if not output_folder:
        Messagebox.show_error("Error", "Please select an output folder.")
        return

    # Ensure the output directory exists
    os.makedirs(output_folder, exist_ok=True)

    # Disable the process button and folder selection buttons to prevent multiple clicks
    set_buttons_state(DISABLED)

    # Initialize overall progress bar
    progress_bar['maximum'] = len(input_folders)
    progress_bar['value'] = 0
    status_label.config(text="Starting processing...")

    # Queue to receive progress updates
    progress_queue = queue.Queue()

    # List to collect errors
    errors = []

    # Create a copy of input_folders to iterate over
    tasks_queue = queue.Queue()
    for folder in input_folders:
        tasks_queue.put(folder)

    def worker(folder):
        # Update Treeview status to "Processing"
        progress_queue.put(("status", folder, "Processing"))
        base_name = os.path.basename(os.path.normpath(folder))
        sanitized_base = sanitize_folder_name(base_name)
        unique_output_dir = os.path.join(output_folder, f"{sanitized_base}_{base_name}")
        os.makedirs(unique_output_dir, exist_ok=True)
        try:
            # Retrieve the selected output format
            selected_format = output_format_var.get()

            # Run the video2image command with the unique output directory and selected format
            subprocess.run(
                ['video2image', '-i', folder, '-o', unique_output_dir, '-f', selected_format],
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE
            )
            progress_queue.put(("success", folder))
        except subprocess.CalledProcessError as e:
            error_msg = f"Failed to process folder: {folder}\nError: {e.stderr.decode().strip()}"
            progress_queue.put(("error", folder, error_msg))
        except Exception as ex:
            error_msg = f"An unexpected error occurred while processing folder: {folder}\nError: {ex}"
            progress_queue.put(("error", folder, error_msg))

    # Initialize ThreadPoolExecutor
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_TASKS)
    futures = []

    def submit_tasks():
        while not tasks_queue.empty() and not is_paused:
            folder = tasks_queue.get()
            future = executor.submit(worker, folder)
            futures.append(future)

    # Initially submit tasks
    submit_tasks()

    def check_queue():
        try:
            while True:
                msg = progress_queue.get_nowait()
                if msg[0] == "status":
                    _, folder, status = msg
                    tree.set(folder, "Status", status)
                elif msg[0] == "success":
                    _, folder = msg
                    tree.set(folder, "Status", "Completed")
                    progress_bar['value'] += 1
                elif msg[0] == "error":
                    _, folder, error_msg = msg
                    tree.set(folder, "Status", "Error")
                    errors.append(error_msg)
                    progress_bar['value'] += 1

                # Check if more tasks need to be submitted
                if not tasks_queue.empty() and not is_paused:
                    submit_tasks()

                # Update overall progress
                overall = progress_bar['value']
                status_label.config(text=f"Processed {overall} of {len(input_folders)} folders.")

                if overall >= len(input_folders):
                    executor.shutdown(wait=False)
                    finalize_processing()
        except queue.Empty:
            pass
        if progress_bar['value'] < len(input_folders):
            root.after(100, check_queue)

    # Start checking the queue
    root.after(100, check_queue)

    def finalize_processing():
        set_buttons_state(NORMAL)
        if errors:
            error_message = "\n\n".join(errors)
            Messagebox.show_error("Processing Completed with Errors", error_message)
        else:
            Messagebox.show_success("Success", "All videos have been successfully processed.")
        progress_bar['value'] = 0
        status_label.config(text="Processing completed.")

    # Start disk space monitoring
    monitor_disk_space()

def set_buttons_state(state):
    process_button.config(state=state)
    select_input_button.config(state=state)
    select_output_button.config(state=state)
    remove_button.config(state=NORMAL if input_folders and state == NORMAL else DISABLED)
    pause_button.config(state=state if state == NORMAL else DISABLED)
    resume_button.config(state=DISABLED if state == NORMAL else NORMAL)
    output_format_combobox.config(state=DISABLED if state == DISABLED else NORMAL)

def on_pause():
    global is_paused
    is_paused = True
    status_label.config(text="Processing paused.")
    pause_button.config(state=DISABLED)
    resume_button.config(state=NORMAL)

def on_resume():
    global is_paused
    is_paused = False
    status_label.config(text="Resuming processing...")
    pause_button.config(state=NORMAL)
    resume_button.config(state=DISABLED)
    # Resume task submission
    threading.Thread(target=process_videos, daemon=True).start()

def monitor_disk_space():
    output_path = output_folder_var.get() or '/'
    try:
        total, used, free = psutil.disk_usage(output_path)
        if free < disk_space_threshold_mb * 1024 * 1024:
            if not is_paused:
                on_pause()
                Messagebox.show_warning("Auto-Pause", "Disk space below threshold. Processing has been paused.")
    except Exception as e:
        Messagebox.show_error("Disk Space Error", f"Unable to determine disk space for '{output_path}'.\nError: {e}")
    root.after(DISK_CHECK_INTERVAL, monitor_disk_space)

def set_disk_threshold():
    try:
        value = int(disk_threshold_entry.get())
        if value <= 0:
            raise ValueError
        global disk_space_threshold_mb
        disk_space_threshold_mb = value
        Messagebox.show_info("Threshold Set", f"Auto-pause threshold set to {value} MB.")
    except ValueError:
        Messagebox.show_error("Invalid Input", "Please enter a valid positive integer for the disk space threshold.")

def update_remove_button():
    """Enable or disable the remove button based on folder selection."""
    if input_folders:
        remove_button.config(state=NORMAL)
    else:
        remove_button.config(state=DISABLED)

# Initialize the main window with ttkbootstrap
root = tb.Window(themename="darkly")
root.title(APP_NAME)
root.geometry("1100x800")
root.resizable(True, True)  # Allow resizing for better usability

# Initialize the list to store input folders
input_folders = []

# Variable to store output folder path
output_folder_var = tb.StringVar()

# Variable for disk space threshold
disk_space_threshold_mb = 500  # Default to 500 MB

# Variable for output format
output_format_var = tb.StringVar(value="jpg")  # Default to 'jpg'

# Pause state
is_paused = False

# Configure styles using ttkbootstrap
style = tb.Style()

# Frame for Input Folders
input_frame = tb.LabelFrame(root, text="Input Folders")
input_frame.pack(fill="both", expand=True, padx=20, pady=10)

# Treeview to display selected input folders and their statuses
columns = ("Folder", "Status")
tree = tb.Treeview(input_frame, columns=columns, show='headings', selectmode='extended', height=15)
tree.heading("Folder", text="Folder")
tree.heading("Status", text="Status")
tree.column("Folder", anchor='w', width=800)
tree.column("Status", anchor='center', width=200)
tree.pack(side=tb.LEFT, fill=tb.BOTH, expand=True, padx=(0,5), pady=5)

# Scrollbar for the Treeview
tree_scroll = tb.Scrollbar(input_frame, orient=tb.VERTICAL, command=tree.yview)
tree_scroll.pack(side=tb.RIGHT, fill=tb.Y)
tree.config(yscrollcommand=tree_scroll.set)

# Frame for Input Buttons
input_button_frame = tb.Frame(root)
input_button_frame.pack(fill='x', padx=20, pady=(0,10))

# Button to select input folders
select_input_button = tb.Button(input_button_frame, text="Select Input Folders", bootstyle=PRIMARY, command=select_input_folders)
select_input_button.pack(side=tb.LEFT, padx=5)

# Button to remove selected input folders
remove_button = tb.Button(input_button_frame, text="Remove Selected", bootstyle=DANGER, command=remove_selected_folders, state=DISABLED)
remove_button.pack(side=tb.LEFT, padx=5)

# Frame for Output Folder
output_frame = tb.LabelFrame(root, text="Output Folder")
output_frame.pack(fill="both", expand=True, padx=20, pady=10)

# Entry widget to display selected output folder
output_entry = tb.Entry(output_frame, textvariable=output_folder_var, width=80, state='readonly')
output_entry.pack(side=tb.LEFT, padx=(0, 5), expand=True, fill=tb.X)

# Button to select output folder
select_output_button = tb.Button(output_frame, text="Select Output Folder", bootstyle=PRIMARY, command=select_output_folder)
select_output_button.pack(side=tb.LEFT, padx=5)

# Frame for Output Format Selection
format_frame = tb.LabelFrame(root, text="Output Format")
format_frame.pack(fill="x", expand=True, padx=20, pady=10)

format_label = tb.Label(format_frame, text="Select Output Format:")
format_label.pack(side=tb.LEFT, padx=(0,5))

output_format_combobox = tb.Combobox(format_frame, textvariable=output_format_var, values=["jpg", "png"], state="readonly")
output_format_combobox.pack(side=tb.LEFT, padx=5)
output_format_combobox.current(0)  # Set default to 'jpg'

# Frame for Disk Space Threshold
disk_frame = tb.LabelFrame(root, text="Auto-Pause Disk Space Threshold (MB)")
disk_frame.pack(fill="x", expand=True, padx=20, pady=10)

disk_threshold_entry = tb.Entry(disk_frame, width=10, justify='center')
disk_threshold_entry.insert(0, str(disk_space_threshold_mb))
disk_threshold_entry.pack(side=tb.LEFT, padx=(0,5))

set_threshold_button = tb.Button(disk_frame, text="Set Threshold", bootstyle=SUCCESS, command=set_disk_threshold)
set_threshold_button.pack(side=tb.LEFT, padx=5)

# Progress Frame with Overall Progress Bar
progress_frame = tb.Frame(root)
progress_frame.pack(fill='x', padx=20, pady=10)

progress_label = tb.Label(progress_frame, text="Overall Progress:")
progress_label.pack(anchor='w')

progress_bar = tb.Progressbar(progress_frame, orient='horizontal', length=1000, mode='determinate', bootstyle="info")
progress_bar.pack(pady=5, fill=tb.X)

# Status Label
status_label = tb.Label(root, text="Ready.", relief=RAISED, anchor='w', bootstyle="secondary")
status_label.pack(fill='x', padx=20, pady=(0,10))

# Frame for Control Buttons
control_button_frame = tb.Frame(root)
control_button_frame.pack(fill='x', padx=20, pady=10)

# Process Button
process_button = tb.Button(control_button_frame, text="Process Videos", bootstyle=SUCCESS, command=process_videos_thread)
process_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)

# Pause Button
pause_button = tb.Button(control_button_frame, text="Pause", bootstyle=WARNING, command=on_pause, state=DISABLED)
pause_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)

# Resume Button
resume_button = tb.Button(control_button_frame, text="Resume", bootstyle=INFO, command=on_resume, state=DISABLED)
resume_button.pack(side=tb.LEFT, padx=5, ipadx=10, ipady=5)

# Monitor changes in the input_folders list to update the remove button state
def monitor_input_folders():
    """Periodically check if input_folders is empty to enable/disable the remove button."""
    update_remove_button()
    root.after(500, monitor_input_folders)

monitor_input_folders()

# Run the GUI loop
root.mainloop()