i want to port my application from python 3.12 to 3.13. Here is the official guide by python: 
Porting to Python 3.13

This section lists previously described changes and other bugfixes that may require changes to your code.
Changes in the Python API

    PEP 667 introduces several changes to the semantics of locals() and f_locals:

        Calling locals() in an optimized scope now produces an independent snapshot on each call, and hence no longer implicitly updates previously returned references. Obtaining the legacy CPython behavior now requires explicit calls to update the initially returned dictionary with the results of subsequent calls to locals(). Code execution functions that implicitly target locals() (such as exec and eval) must be passed an explicit namespace to access their results in an optimized scope. (Changed as part of PEP 667.)

        Calling locals() from a comprehension at module or class scope (including via exec or eval) once more behaves as if the comprehension were running as an independent nested function (i.e. the local variables from the containing scope are not included). In Python 3.12, this had changed to include the local variables from the containing scope when implementing PEP 709. (Changed as part of PEP 667.)

        Accessing FrameType.f_locals in an optimized scope now returns a write-through proxy rather than a snapshot that gets updated at ill-specified times. If a snapshot is desired, it must be created explicitly with dict or the proxy’s .copy() method. (Changed as part of PEP 667.)

    functools.partial now emits a FutureWarning when used as a method. The behavior will change in future Python versions. Wrap it in staticmethod() if you want to preserve the old behavior. (Contributed by Serhiy Storchaka in gh-121027.)

    An OSError is now raised by getpass.getuser() for any failure to retrieve a username, instead of ImportError on non-Unix platforms or KeyError on Unix platforms where the password database is empty.

    The value of the mode attribute of gzip.GzipFile is now a string ('rb' or 'wb') instead of an integer (1 or 2). The value of the mode attribute of the readable file-like object returned by zipfile.ZipFile.open() is now 'rb' instead of 'r'. (Contributed by Serhiy Storchaka in gh-115961.)

    mailbox.Maildir now ignores files with a leading dot (.). (Contributed by Zackery Spytz in gh-65559.)

    pathlib.Path.glob() and rglob() now return both files and directories if a pattern that ends with “**” is given, rather than directories only. Add a trailing slash to keep the previous behavior and only match directories.

    The threading module now expects the _thread module to have an _is_main_interpreter() function. This function takes no arguments and returns True if the current interpreter is the main interpreter.

    Any library or application that provides a custom _thread module must provide _is_main_interpreter(), just like the module’s other “private” attributes. (gh-112826.)

Changes in the C API

    Python.h no longer includes the <ieeefp.h> standard header. It was included for the finite() function which is now provided by the <math.h> header. It should now be included explicitly if needed. Remove also the HAVE_IEEEFP_H macro. (Contributed by Victor Stinner in gh-108765.)

    Python.h no longer includes these standard header files: <time.h>, <sys/select.h> and <sys/time.h>. If needed, they should now be included explicitly. For example, <time.h> provides the clock() and gmtime() functions, <sys/select.h> provides the select() function, and <sys/time.h> provides the futimes(), gettimeofday() and setitimer() functions. (Contributed by Victor Stinner in gh-108765.)

    On Windows, Python.h no longer includes the <stddef.h> standard header file. If needed, it should now be included explicitly. For example, it provides offsetof() function, and size_t and ptrdiff_t types. Including <stddef.h> explicitly was already needed by all other platforms, the HAVE_STDDEF_H macro is only defined on Windows. (Contributed by Victor Stinner in gh-108765.)

    If the Py_LIMITED_API macro is defined, Py_BUILD_CORE, Py_BUILD_CORE_BUILTIN and Py_BUILD_CORE_MODULE macros are now undefined by <Python.h>. (Contributed by Victor Stinner in gh-85283.)

    The old trashcan macros Py_TRASHCAN_SAFE_BEGIN and Py_TRASHCAN_SAFE_END were removed. They should be replaced by the new macros Py_TRASHCAN_BEGIN and Py_TRASHCAN_END.

    A tp_dealloc function that has the old macros, such as:

    static void
    mytype_dealloc(mytype *p)
    {
        PyObject_GC_UnTrack(p);
        Py_TRASHCAN_SAFE_BEGIN(p);
        ...
        Py_TRASHCAN_SAFE_END
    }

    should migrate to the new macros as follows:

    static void
    mytype_dealloc(mytype *p)
    {
        PyObject_GC_UnTrack(p);
        Py_TRASHCAN_BEGIN(p, mytype_dealloc)
        ...
        Py_TRASHCAN_END
    }

    Note that Py_TRASHCAN_BEGIN has a second argument which should be the deallocation function it is in. The new macros were added in Python 3.8 and the old macros were deprecated in Python 3.11. (Contributed by Irit Katriel in gh-105111.)

    PEP 667 introduces several changes to frame-related functions:

        The effects of mutating the dictionary returned from PyEval_GetLocals() in an optimized scope have changed. New dict entries added this way will now only be visible to subsequent PyEval_GetLocals() calls in that frame, as PyFrame_GetLocals(), locals(), and FrameType.f_locals no longer access the same underlying cached dictionary. Changes made to entries for actual variable names and names added via the write-through proxy interfaces will be overwritten on subsequent calls to PyEval_GetLocals() in that frame. The recommended code update depends on how the function was being used, so refer to the deprecation notice on the function for details.

        Calling PyFrame_GetLocals() in an optimized scope now returns a write-through proxy rather than a snapshot that gets updated at ill-specified times. If a snapshot is desired, it must be created explicitly (e.g. with PyDict_Copy()), or by calling the new PyEval_GetFrameLocals() API.

        PyFrame_FastToLocals() and PyFrame_FastToLocalsWithError() no longer have any effect. Calling these functions has been redundant since Python 3.11, when PyFrame_GetLocals() was first introduced.

        PyFrame_LocalsToFast() no longer has any effect. Calling this function is redundant now that PyFrame_GetLocals() returns a write-through proxy for optimized scopes.

Regression Test Changes

    Python built with configure --with-pydebug now supports a -X presite=package.module command-line option. If used, it specifies a module that should be imported early in the lifecycle of the interpreter, before site.py is executed. (Contributed by Łukasz Langa in gh-110769.)

Here my python code: import ctypes
import logging
import os
import signal
import shutil
import subprocess
import tempfile
import threading
import time
import tkinter as tk

from collections import defaultdict
from concurrent.futures import Future, ThreadPoolExecutor
from contextlib import contextmanager
from dataclasses import dataclass, field
from pathlib import Path
from queue import Queue
from tkinter import filedialog, ttk
from typing import Any, Callable, Sequence, Generator, cast

import numpy as np
from PIL import Image

# --- Configuration ---


@dataclass
class SharedState:
    """Represents the shared state across the application."""

    processed_files_lock: threading.Lock = field(default_factory=threading.Lock)
    processed_files_count: int = 0
    total_files_count: int = 0
    log_message_queue: Queue[str] = field(default_factory=Queue)
    tab_messages: dict[str, list[str]] = field(
        default_factory=lambda: {"Log": [], "Black": [], "White": [], "Color": []}
    )
    stop_event: threading.Event = field(default_factory=threading.Event)
    
    active_subprocesses: list[subprocess.Popen] = field(default_factory=list)
    active_subprocesses_lock: threading.Lock = field(default_factory=threading.Lock)


class Config:
    """Configuration settings for the image processing application."""

    SCRIPT_DIR: Path = Path(__file__).parent.resolve()
    INPUT_FOLDER: Path = Path(os.getenv("INPUT_FOLDER", SCRIPT_DIR / "1. Input"))
    OUTPUT_FOLDER: Path = Path(os.getenv("OUTPUT_FOLDER", SCRIPT_DIR / "2. Output"))
    DEFAULT_INPUT_FOLDER: Path = INPUT_FOLDER
    DEFAULT_OUTPUT_FOLDER: Path = OUTPUT_FOLDER
    DEFAULT_RENAME_PATTERN: str = "{Filename}"

    # Executable Paths
    IMAGEMAGICK: Path = Path(
        os.getenv(
            "IMAGEMAGICK", "D:/Bilder/Python/0. Code/Requirements/ImageMagick.exe"
        )
    )
    FFMPEG: Path = Path(
        os.getenv("FFMPEG", "D:/Bilder/Python/0. Code/Requirements/ffmpeg.exe")
    )

    # Supported File Extensions
    AUDIO_EXTENSIONS: set[str] = {".mka", ".mpga", ".mp3", ".aac", ".flac", ".opus", ".ogg"}
    IMAGE_EXTENSIONS: set[str] = {".png", ".jpg", ".jpeg", ".webp"}
    VIDEO_EXTENSIONS: set[str] = {".mkv", ".mp4"}

    # Display and Formatting Constants
    SEPARATOR_LENGTH: int = 310
    PADDING_LENGTH: int = 82
    PROGRESS_BAR_LENGTH: int = 100

    # Default Resize Options
    DEFAULT_RESIZE_MODE: str = "Shortest Side"
    DEFAULT_RESIZE_WIDTH: int = 0
    DEFAULT_RESIZE_HEIGHT: int = 0
    DEFAULT_RESIZE_SIDE: int = 1600
    DEFAULT_RESIZE_FORMAT: str = "png"

    # Color Detection Thresholds
    COLORDIFF_THRESHOLD: int = 5
    FRACTION_COLORED_PIXELS_THRESHOLD: float = 0.03

    @classmethod
    def setup(cls) -> None:
        """Create output directory and configure logging settings."""
        cls.OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
        logging.getLogger("PIL").setLevel(logging.WARNING)

    @classmethod
    def initialize(cls) -> SharedState:
        """Initialize configuration and return shared state."""
        cls.setup()
        return SharedState()


@contextmanager
def create_temp_directory() -> Generator[Path, None, None]:
    """Yield a temporary directory within the output directory."""
    with tempfile.TemporaryDirectory(dir=Config.OUTPUT_FOLDER) as temp_dir:
        yield Path(temp_dir)


# --- Logging ---


def log_error_messages(error_queue: Queue[tuple[Path, list[str]]]) -> None:
    """Log error messages from the error queue."""
    error_dict: defaultdict[Path, list[str]] = defaultdict(list)
    while not error_queue.empty():
        file_path, error_messages = error_queue.get()
        error_dict[file_path].extend(error_messages)

    if error_dict:
        shared_state.log_message_queue.put("UPDATE_LOG")
        for file_path in sorted(error_dict.keys()):
            shared_state.log_message_queue.put(f"{file_path.resolve()}")
            shared_state.log_message_queue.put("")
            for error_message in sorted(error_dict[file_path]):
                log_error_message(error_message)
            shared_state.log_message_queue.put("")


def log_and_execute_in_thread(
    choice: str, label: str, function: Callable[..., Any], *args: Any
) -> None:
    """Log the execution of a function and run it in a separate thread."""

    def run_in_thread() -> None:
        start_time = time.time()
        separator = "-" * Config.SEPARATOR_LENGTH
        shared_state.log_message_queue.put(f"{separator}\n{label}\n{separator}\n")

        try:
            total_files = function(*args)
        except FileNotFoundError as e:
            log_error_message(str(e))
            total_files = 0
        except (OSError, ValueError) as e:
            log_error_message(f"An error occurred during '{label}': {e}")
            total_files = 0

        end_time = time.time()
        execution_time = end_time - start_time

        shared_state.log_message_queue.put("")
        image_processor.collect_processing_results(Config.OUTPUT_FOLDER, choice)

        app.after(0, app.update_final_status, execution_time)
        app.after(0, app.update_text_area)

    thread = threading.Thread(target=run_in_thread)
    thread.start()


# --- Utility Functions ---


def log_error_message(message: str) -> None:
    """Log an error message to the shared state queue."""
    shared_state.log_message_queue.put(f"- {message}")


def validate_and_load_image(path: Path) -> Image.Image | None:
    """Validate and load image, return None if invalid."""
    try:
        with Image.open(path) as img:
            match img.mode:
                case "P":
                    return img.convert("L")
                case "L" | "RGB" | "RGBA":
                    return img.copy()
                case _:
                    return img.convert("RGB")
    except (OSError, ValueError) as e:
        log_error_message(f"Error reading image {path}: {e}")
        return None


def load_image_to_numpy_array(path: Path, mode: str | None = None) -> np.ndarray | None:
    """Load an image and convert it to a NumPy array with error handling."""
    img = validate_and_load_image(path)
    if img is None:
        return None

    img = img.convert(mode) if mode else img

    try:
        return np.array(img)
    except (OSError, ValueError) as e:
        log_error_message(f"Error converting image to array {path}: {e}")
        return None


def convert_png_to_jpg(input_path: Path, output_path: Path) -> None:
    """Convert a PNG image to JPG format."""
    try:
        with Image.open(input_path) as img:
            if img.mode not in ("RGB", "L"):
                img = img.convert("RGB")

            jpg_path = output_path.with_suffix(".jpg")
            img.save(jpg_path, format="JPEG", quality=80, subsampling=0)

        input_path.unlink()
    except Exception as e:
        log_error_message(f"Error converting {input_path} to JPG: {e}")


def verify_executable_exists(executable: Path, name: str) -> None:
    """Check if a given executable is available in the system."""
    if not shutil.which(str(executable)):
        raise FileNotFoundError(f"- {name} executable not found at: {executable}")


def increment_processed_file_count(start_time: float, total_files: int) -> None:
    """Safely increment the processed files count and update the status."""
    with shared_state.processed_files_lock:
        shared_state.processed_files_count += 1
    update_processing_status(start_time, shared_state.processed_files_count, total_files)


def generate_new_filename(pattern: str, original_filename: str) -> str:
    """Rename the file based on the provided pattern."""
    original_path = Path(original_filename)
    new_base = pattern.replace("{Filename}", original_path.stem).strip()

    # Ensure the new base name is not empty
    if not new_base:
        new_base = original_path.stem

    # Replace or remove any illegal characters if necessary
    illegal_chars = r'\/:*?"<>|'
    new_base = ''.join('_' if char in illegal_chars else char for char in new_base)

    return f"{new_base}{original_path.suffix}"


def run_subprocess(command: list[str], check: bool = False, **kwargs) -> subprocess.CompletedProcess:
    """Run a subprocess while tracking it in active_subprocesses."""
    with shared_state.active_subprocesses_lock:
        proc = subprocess.Popen(
            command,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            **kwargs
        )
        shared_state.active_subprocesses.append(proc)
    
    try:
        stdout, stderr = proc.communicate()
        if check and proc.returncode != 0:
            raise subprocess.CalledProcessError(proc.returncode, proc.args, output=stdout, stderr=stderr)
        return subprocess.CompletedProcess(proc.args, proc.returncode, stdout, stderr)
    finally:
        with shared_state.active_subprocesses_lock:
            if proc in shared_state.active_subprocesses:
                shared_state.active_subprocesses.remove(proc)


# --- Process Bar and Multithreading ---


def update_processing_status(start_time: float, processed_count: int, total_files: int) -> None:
    """Update the application's status text with processing details."""
    elapsed_time = time.time() - start_time
    minutes, seconds = divmod(int(elapsed_time), 60)
    elapsed_time_str = f"{minutes:02d}:{seconds:02d}"

    start_time_str = time.strftime("%H:%M:%S", time.localtime(start_time))
    status_text = (
        f"Start Time: {start_time_str} - "
        f"Processed Files: {processed_count}/{total_files} - "
        f"Elapsed Time: {elapsed_time_str} Minutes"
    )

    padded_status_text = status_text.ljust(Config.PADDING_LENGTH)
    app.update_status_text(padded_status_text)


def update_elapsed_time(
    start_time: float, total_files: int, stop_event: threading.Event
) -> None:
    """Continuously update the elapsed time until the stop event is set."""
    while not stop_event.is_set():
        update_processing_status(start_time, shared_state.processed_files_count, total_files)
        time.sleep(1)


def start_elapsed_time_thread(
    start_time: float, total_files: int, stop_event: threading.Event
) -> threading.Thread:
    """Start a thread to update the elapsed time."""
    thread = threading.Thread(
        target=update_elapsed_time,
        args=(start_time, total_files, stop_event),
        daemon=True,
    )
    thread.start()
    return thread


def execute_tasks_with_thread_pool(
    tasks: Sequence[Path], process_func: Callable[..., Any], *args: Any
) -> int:
    """Execute tasks using a thread pool, updating progress and handling errors."""
    shared_state.processed_files_count = 0
    shared_state.total_files_count = total = len(tasks)
    start_time = time.time()

    app.update_progress(0)

    stop_event = threading.Event()
    elapsed_time_thread = start_elapsed_time_thread(start_time, total, stop_event)

    try:
        with ThreadPoolExecutor() as executor:
            futures: list[Future] = [
                executor.submit(process_func, task, *args) for task in tasks
            ]

            for index, future in enumerate(futures, start=1):
                if shared_state.stop_event.is_set():
                    for f in futures:
                        f.cancel()
                    break
                try:
                    future.result()
                except Exception as e:
                    log_error_message(f"Error during processing: {e}")
                finally:
                    progress = (index / total) * 100
                    app.update_progress(progress)
    finally:
        stop_event.set()
        elapsed_time_thread.join()

    return total


# --- Color Detection And Adjustment ---


def is_color_image(
    img: np.ndarray,
    diff_threshold: int = Config.COLORDIFF_THRESHOLD,
    fraction_threshold: float = Config.FRACTION_COLORED_PIXELS_THRESHOLD,
) -> bool:
    """Determine if an image is color based on RGB channel differences."""
    # Check if the image has three channels (RGB)
    if img.ndim != 3 or img.shape[2] != 3:
        return False

    # Calculate the peak-to-peak (max - min) difference across the RGB channels for each pixel
    max_diff = np.ptp(img, axis=2)

    # Compute the fraction of pixels where the max difference exceeds the threshold
    fraction_colored = np.mean(max_diff > diff_threshold)

    # Determine if the image meets or exceeds the required fraction of colored pixels
    return fraction_colored >= fraction_threshold


def adjust_grayscale_values(img: np.ndarray) -> tuple[np.ndarray, int, int]:
    """Adjust the values of a grayscale image using a LUT."""
    try:
        # Calculate modal white value in the range [250, 255]
        hist_white, bins_white = np.histogram(img[img >= 250], bins=6, range=(250, 256))
        modal_white = int(bins_white[np.argmax(hist_white)])

        # Calculate modal black value in the range [0, 55]
        hist_black, bins_black = np.histogram(img[img < 56], bins=56, range=(0, 56))
        modal_black = int(bins_black[np.argmax(hist_black)])

        # Create LUT for the given modal values
        lut = create_lut(modal_black, modal_white)

        # Apply LUT to the original image
        adjusted = lut[img]

        return adjusted, modal_black, modal_white

    except ValueError as e:
        log_error_message(f"Error adjusting values for image: {e}")
        return img, 0, 255


def create_lut(modal_black: int, modal_white: int) -> np.ndarray:
    """Create a lookup table (LUT) for adjusting grayscale values."""
    # Initialize LUT with direct conversion
    lut = np.arange(256, dtype=np.float64)

    # Scale the LUT based on the modal white value if it's not 255
    if modal_white != 255:
        lut *= 255.0 / modal_white
        lut = np.clip(lut, 0, 255)

    # Adjust LUT based on the modal black value if it's not 0
    if modal_black:
        lut -= modal_black
        lut = np.clip(lut, 0, 255)
        lut *= 255.0 / (255.0 - modal_black)

        gamma = 1.0 - (modal_black / 255.0)
        lut = np.power(lut / 255.0, gamma) * 255

    # Convert LUT to uint8 for direct lookup and round values
    lut = np.rint(lut).astype(np.uint8)

    return lut


# --- Image Analysis ---


def analyze_image_color_properties(image_processor: "ImageProcessor") -> None:
    """Analyze images in the input folder for color properties."""
    image_processor.reset_results()
    image_files: list[Path] = [
        path
        for path in Config.INPUT_FOLDER.rglob("*")
        if path.is_file() and path.suffix.lower() in Config.IMAGE_EXTENSIONS
    ]

    if not image_files:
        log_error_message(f"No image files found in '{Config.INPUT_FOLDER}'")
        return

    with create_temp_directory() as temp_folder:
        start_time = time.time()
        execute_tasks_with_thread_pool(image_files, analyze_image_file, start_time, len(image_files))


def analyze_image_file(path: Path, start_time: float, total: int) -> None:
    """Analyze an image to classify it as color or grayscale."""
    if shared_state.stop_event.is_set():
        return

    img_array = load_image_to_numpy_array(path)
    if img_array is None:
        increment_processed_file_count(start_time, total)
        return

    subfolder = path.parent.relative_to(Config.INPUT_FOLDER).as_posix()
    filename = path.name

    if is_color_image(img_array):
        image_processor.color_results[subfolder].append(filename)
    else:
        # Load the image as grayscale before adjusting values
        img_gray = load_image_to_numpy_array(path, mode='L')
        if img_gray is None:
            increment_processed_file_count(start_time, total)
            return
        adjusted, modal_black, modal_white = adjust_grayscale_values(img_gray)
        if modal_black:
            image_processor.black_results[subfolder].append((filename, modal_black))
        if modal_white != 255:
            image_processor.white_results[subfolder].append((filename, modal_white))
    increment_processed_file_count(start_time, total)


def identify_and_move_color_images(image_processor: "ImageProcessor") -> None:
    """Find and move color images to the output folder."""
    image_processor.reset_results()
    image_extensions: set[str] = {ext.lower() for ext in Config.IMAGE_EXTENSIONS}

    tasks: list[Path]
    if not (
        tasks := [
            path
            for path in Config.INPUT_FOLDER.rglob("*")
            if path.is_file() and path.suffix.lower() in image_extensions
        ]
    ):
        log_error_message(f"No image files found in '{Config.INPUT_FOLDER}'")
        return

    with create_temp_directory():
        execute_tasks_with_thread_pool(
            tasks, move_color_image, time.time(), len(tasks), image_processor
        )


def move_color_image(
    path: Path, start_time: float, total_files: int, image_processor: "ImageProcessor"
) -> None:
    """Move a color image file to the output folder with renaming and update processing status."""
    if shared_state.stop_event.is_set():
        return

    img_array = load_image_to_numpy_array(path)
    if img_array is None:
        increment_processed_file_count(start_time, total_files)
        return

    if is_color_image(img_array):
        rel_path = path.relative_to(Config.INPUT_FOLDER)
        
        # Extract the original filename
        original_filename = rel_path.name
        
        # Apply renaming pattern to the original filename
        new_filename = generate_new_filename(app.RENAME_PATTERN.get(), original_filename)
        
        # Determine the new relative path with the renamed file
        new_rel_path = rel_path.with_name(new_filename)
        
        # Construct the full output path
        out_path = Config.OUTPUT_FOLDER / new_rel_path
        
        # Ensure that the parent directories exist
        out_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            shutil.move(str(path), str(out_path))
        except Exception as e:
            log_error_message(f"Failed to move {path} to {out_path}: {e}")
            increment_processed_file_count(start_time, total_files)
            return

        subfolder = str(rel_path.parent)
        
        # **Store the original filename instead of the new filename**
        image_processor.color_results[subfolder].append(original_filename)

    increment_processed_file_count(start_time, total_files)


# --- Corrupted Files ---


def check_for_corrupted_files() -> None:
    """Check for corrupted files in the input folder using FFmpeg."""
    image_processor.reset_results()

    try:
        verify_executable_exists(Config.FFMPEG, "FFmpeg")
    except FileNotFoundError as error:
        log_error_message(str(error))
        return

    valid_extensions: set[str] = (
        Config.VIDEO_EXTENSIONS | Config.AUDIO_EXTENSIONS | Config.IMAGE_EXTENSIONS
    )
    tasks: list[Path] = [
        file
        for file in Config.INPUT_FOLDER.rglob("*")
        if file.is_file() and file.suffix.lower() in valid_extensions
    ]

    if not tasks:
        log_error_message(f"No valid media files found in '{Config.INPUT_FOLDER}'")
        return

    error_queue: Queue[tuple[Path, list[str]]] = Queue()
    start_time: float = time.time()
    total_files: int = len(tasks)

    def check_file(file_path: Path) -> None:
        """Function to check a single file for corruption using FFmpeg."""
        if shared_state.stop_event.is_set():
            return
        try:
            base_command: list[str] = [
                str(Config.FFMPEG),
                "-v",
                "error",
                "-i",
                str(file_path),
            ]
            if file_path.suffix.lower() in Config.VIDEO_EXTENSIONS:
                base_command += ["-map", "0:a:0"]
            base_command += ["-f", "null", "-"]

            result: subprocess.CompletedProcess = run_subprocess(
                base_command,
                check=False,
                encoding="utf-8",
                errors="ignore",
            )
            combined_output: str = f"{result.stdout}\n{result.stderr}"
            error_messages: list[str] = [
                line.strip()
                for line in combined_output.splitlines()
                if "error" in line.lower()
            ]

            if error_messages:
                error_queue.put((file_path.resolve(), error_messages))
        except Exception as e:
            error_queue.put(
                (file_path.resolve(), [f"Error processing file {file_path}: {e}"])
            )
        finally:
            increment_processed_file_count(start_time, total_files)

    execute_tasks_with_thread_pool(tasks, check_file)
    log_error_messages(error_queue)


# --- Resize Images ---


def resize_images(
    input_folder: Path, output_folder: Path, image_processor: "ImageProcessor"
) -> None:
    """Resize images in the input folder and save them to the output folder."""
    try:
        verify_executable_exists(Config.IMAGEMAGICK, "ImageMagick")
    except FileNotFoundError as e:
        log_error_message(str(e))
        return

    tasks: list[Path] = [
        input_path
        for input_path in input_folder.rglob("*")
        if input_path.is_file() and input_path.suffix.lower() in Config.IMAGE_EXTENSIONS
    ]

    if not tasks:
        log_error_message(f"No image files found in '{input_folder}'")
        return

    with create_temp_directory() as temp_folder:
        execute_tasks_with_thread_pool(
            tasks,
            process_file,
            time.time(),
            len(tasks),
            image_processor,
            temp_folder,
            True,
        )


def resize_image(input_path: Path, output_path: Path) -> Path:
    """Resize an image using ImageMagick."""
    output_path = output_path.with_suffix(".png")

    identify_command = [
        str(Config.IMAGEMAGICK),
        str(input_path),
        "-format",
        "%wx%h",
        "info:",
    ]
    try:
        result = run_subprocess(
            identify_command,
            check=True
        )
        current_width, current_height = map(int, result.stdout.strip().split("x"))
    except subprocess.CalledProcessError as e:
        log_error_message(f"ImageMagick identify failed for {input_path}: {e.stderr.strip()}")
        return output_path

    resize_mode = app.RESIZE_MODE.get()
    maintain_aspect = app.maintain_aspect_ratio.get()

    if resize_mode == "Fit":
        target_width = int(app.RESIZE_WIDTH.get() or 0)
        target_height = int(app.RESIZE_HEIGHT.get() or 0)

        if maintain_aspect:
            if target_width == 0 and target_height > 0:
                target_width = int(target_height * current_width / current_height)
            elif target_height == 0 and target_width > 0:
                target_height = int(target_width * current_height / current_width)

        resize_arg = f"{target_width}x{target_height}!"
    else:  # Shortest Side
        target_size = int(app.RESIZE_SIDE.get())
        resize_arg = f"{target_size}x{target_size}^"

    is_upscaling = (
        resize_mode == "Fit"
        and (target_width > current_width or target_height > current_height)
    ) or (
        resize_mode == "Shortest Side"
        and target_size > min(current_width, current_height)
    )

    command = [
        str(Config.IMAGEMAGICK),
        str(input_path),
    ]

    if is_upscaling:
        command += [
            "-filter",
            "LanczosSharp",
            "-distort",
            "Resize",
            resize_arg,
        ]
    else:
        command += [
            "-colorspace",
            "RGB",
            "-filter",
            "Lanczos2Sharp",
            "-resize",
            resize_arg,
            "-colorspace",
            "sRGB",
        ]

    command += ["-define", "png:compression-level=5", str(output_path)]

    try:
        result = run_subprocess(
            command,
            check=True
        )
        if result.stderr:
            log_error_message(f"ImageMagick: {result.stderr.strip()}")
    except subprocess.CalledProcessError as e:
        log_error_message(f"ImageMagick resize failed for {input_path}: {e.stderr.strip()}")

    return output_path


# --- Processing ---


def process_file(
    path: Path,
    start_time: float,
    total_files: int,
    image_processor: "ImageProcessor",
    temp_folder: Path,
    resize: bool = False,
    auto_correct_and_resize: bool = False,
) -> None:
    """Process a single image file."""
    if shared_state.stop_event.is_set():
        return

    img_array = load_image_to_numpy_array(path)
    if img_array is None:
        increment_processed_file_count(start_time, total_files)
        return

    relative_path = path.relative_to(Config.INPUT_FOLDER)
    temp_output_path = temp_folder / relative_path.with_suffix(".png")
    temp_output_path.parent.mkdir(parents=True, exist_ok=True)

    if auto_correct_and_resize:
        # Adjust and save image, then resize
        intermediate_path, _, _ = adjust_and_save_image(
            img_array, path, image_processor, temp_folder
        )
        resized_path = resize_image(intermediate_path, temp_output_path)
    elif resize:
        # Only resize the original image
        resized_path = resize_image(path, temp_output_path)
    else:
        # Adjust and save image without resizing
        resized_path, _, _ = adjust_and_save_image(
            img_array, path, image_processor, temp_folder
        )

    # Determine the final output format
    output_format = app.RESIZE_FORMAT.get().lower()
    if output_format not in ["jpg", "png"]:
        output_format = "png"  # Fallback to PNG if an unsupported format is selected

    # Apply renaming pattern
    new_filename = generate_new_filename(app.RENAME_PATTERN.get(), relative_path.name)
    new_filename = os.path.splitext(new_filename)[0] + f".{output_format}"
    final_output_path = Config.OUTPUT_FOLDER / relative_path.parent / new_filename
    final_output_path.parent.mkdir(parents=True, exist_ok=True)

    # Save the final processed file
    if output_format == "jpg":
        convert_png_to_jpg(resized_path, final_output_path)
    else:
        shutil.move(str(resized_path), str(final_output_path))

    increment_processed_file_count(start_time, total_files)


def process_folder(
    folder: Path,
    image_processor: "ImageProcessor",
    auto_correct_and_resize: bool = False,
) -> None:
    """Process all images in a folder."""
    image_processor.reset_results()

    if auto_correct_and_resize:
        try:
            verify_executable_exists(Config.IMAGEMAGICK, "ImageMagick")
        except FileNotFoundError as e:
            log_error_message(str(e))
            return

    tasks: list[Path] = [
        path
        for path in folder.rglob("*")
        if path.is_file() and path.suffix.lower() in Config.IMAGE_EXTENSIONS
    ]

    if not tasks:
        log_error_message(f"No image files found in '{folder}'")
        return

    with create_temp_directory() as temp_folder:
        execute_tasks_with_thread_pool(
            tasks,
            process_file,
            time.time(),
            len(tasks),
            image_processor,
            temp_folder,
            False,
            auto_correct_and_resize,
        )


def adjust_and_save_image(
    img: np.ndarray,
    path: Path,
    image_processor: "ImageProcessor",
    temp_folder: Path,
    diff_threshold: int = Config.COLORDIFF_THRESHOLD,
    fraction_threshold: float = Config.FRACTION_COLORED_PIXELS_THRESHOLD,
) -> tuple[Path, int | None, int | None]:
    """Process an image, adjusting and saving depending on whether it is color or grayscale."""
    try:
        is_color = is_color_image(img, diff_threshold, fraction_threshold)
        relative_parent = path.parent.relative_to(Config.INPUT_FOLDER)
        out_path = temp_folder / relative_parent / f"{path.stem}.png"
        out_path.parent.mkdir(parents=True, exist_ok=True)
        subfolder = relative_parent.as_posix()
        filename = path.name

        if is_color:
            # Save color image as PNG
            Image.fromarray(img).save(out_path, format="PNG", compress_level=5)
            image_processor.color_results[subfolder].append(filename)
            return out_path, None, None

        # Load the image as grayscale before adjusting values
        img_gray = load_image_to_numpy_array(path, mode='L')
        if img_gray is None:
            log_error_message(f"Error loading image {path} as grayscale.")
            return out_path, None, None

        adjusted_img, modal_black, modal_white = adjust_grayscale_values(img_gray)
        Image.fromarray(adjusted_img).convert("L").save(
            out_path, format="PNG", compress_level=5
        )

        if modal_black:
            image_processor.black_results[subfolder].append((filename, modal_black))
        if modal_white != 255:
            image_processor.white_results[subfolder].append((filename, modal_white))

        return out_path, modal_black, modal_white

    except (OSError, ValueError) as e:
        log_error_message(f"Error processing image {path}: {e}")
        return path, None, None


# --- ImageProcessor Class ---


class ImageProcessor:
    """Class to handle image processing results."""

    def __init__(self) -> None:
        self.white_results: dict[str, list[tuple[str, int]]] = defaultdict(list)
        self.black_results: dict[str, list[tuple[str, int]]] = defaultdict(list)
        self.color_results: dict[str, list[str]] = defaultdict(list)

    def collect_processing_results(self, output_folder: Path, choice: str) -> None:
        """Collect processing results and prepare them for display or logging."""
        results_map: dict[str, tuple[str, str, str] | tuple[str, str]] = {
            "2": ("2. Analyze Images", "Modal Value Black", "Modal Value White"),
            "3": ("3. Find Color Images", "Filename"),
            "5": (
                "5. Automatic Color Correction",
                "Modal Value Black",
                "Modal Value White",
            ),
            "6": (
                "6. Automatic Color Correction + Resize",
                "Modal Value Black",
                "Modal Value White",
            ),
        }
        headers = results_map.get(choice)
        if headers:
            if choice == "3":
                self._write_results("Color", self.color_results, headers[0], headers[1])
            else:
                self._write_results("Black", self.black_results, headers[0], headers[1])
                self._write_results("White", self.white_results, headers[0], headers[2])
                self._write_results("Color", self.color_results, headers[0], "Filename")

    def _write_results(
        self,
        file_path: str,
        results: dict[str, list[tuple[str, int]] | list[str]],
        header: str,
        value_name: str,
    ) -> None:
        """Write results to a specific file."""
        separator = "-" * Config.SEPARATOR_LENGTH
        content = f"{separator}\n{header}\n{separator}\n"
        if results:
            for subfolder, subfolder_results in sorted(results.items()):
                full_subfolder_path = Config.INPUT_FOLDER / subfolder
                content += f"\n{full_subfolder_path}\n\n"
                if value_name == "Filename":
                    subfolder_results = cast(list[str], subfolder_results)
                    subfolder_results.sort()
                    content += "\n".join(
                        f" - {filename}" for filename in subfolder_results
                    )
                else:
                    subfolder_results = cast(list[tuple[str, int]], subfolder_results)
                    subfolder_results.sort(key=lambda x: x[1], reverse=True)
                    content += "\n".join(
                        f" - {filename}, {value_name}: {value}"
                        for filename, value in subfolder_results
                    )
                content += "\n"
        shared_state.tab_messages[file_path].append(content)

    def reset_results(self) -> None:
        """Reset all results."""
        self.white_results = defaultdict(list)
        self.black_results = defaultdict(list)
        self.color_results = defaultdict(list)


# --- Tkinter App ---


class App(tk.Tk):
    def __init__(self) -> None:
        super().__init__()
        self.setup_window()
        self.create_variables()
        self.create_widgets()
        self.configure_logging()
        self.process_start_time: float = 0.0

    def setup_window(self) -> None:
        self.configure_dpi_awareness()
        self.title("Image Processor")
        self.geometry("2100x1180")
        self.default_font = ("Roboto Flex", 16)
        self.apply_default_font()

    def configure_dpi_awareness(self) -> None:
        try:
            ctypes.windll.shcore.SetProcessDpiAwareness(1)
        except Exception as e:
            print(f"Could not set DPI awareness: {e}")

    def apply_default_font(self) -> None:
        self.option_add("*Font", self.default_font)
        self.style = ttk.Style()
        for widget_type in [
            "TButton",
            "TLabel",
            "TEntry",
            "TRadiobutton",
            "TNotebook.Tab",
        ]:
            self.style.configure(widget_type, font=self.default_font)
        self.style.configure("TFrame", background="#fdfdfd")
        self.style.configure("TLabel", background="#fdfdfd")
        self.style.configure("TNotebook", background="#f8f8f8")
        self.style.configure("TRadiobutton", background="#fdfdfd")

    def create_variables(self) -> None:
        self.RESIZE_MODE = tk.StringVar(value=Config.DEFAULT_RESIZE_MODE)
        self.RESIZE_WIDTH = tk.StringVar(value=str(Config.DEFAULT_RESIZE_WIDTH))
        self.RESIZE_HEIGHT = tk.StringVar(value=str(Config.DEFAULT_RESIZE_HEIGHT))
        self.RESIZE_SIDE = tk.StringVar(value=str(Config.DEFAULT_RESIZE_SIDE))
        self.RESIZE_FORMAT = tk.StringVar(value=Config.DEFAULT_RESIZE_FORMAT)
        self.RENAME_PATTERN = tk.StringVar(value=Config.DEFAULT_RENAME_PATTERN)
        self.option_var = tk.StringVar(value="1")
        self.status_text = tk.StringVar(value="")
        self.maintain_aspect_ratio = tk.BooleanVar(value=False)

    def create_widgets(self) -> None:
        main_frame = ttk.Frame(self, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)

        self.create_folder_selection_frames(main_frame)
        self.create_resize_options_frame(main_frame)
        self.create_options_and_start_frame(main_frame)
        self.create_text_frame(main_frame)
        self.create_progress_bar(main_frame)
        self.create_status_label(main_frame)

    def create_folder_selection_frames(self, parent: ttk.Frame) -> None:
        self.create_folder_frame(
            parent,
            "Input",
            Config.INPUT_FOLDER,
            self.set_default_input,
            self.select_input_folder,
        )
        self.create_folder_frame(
            parent,
            "Output",
            Config.OUTPUT_FOLDER,
            self.set_default_output,
            self.select_output_folder,
        )

    def create_folder_frame(
        self,
        parent: ttk.Frame,
        label: str,
        default_folder: Path,
        default_command: Callable[[], None],
        select_command: Callable[[], None],
    ) -> None:
        frame = ttk.Frame(parent)
        frame.pack(fill=tk.X, pady=(0, 10))

        ttk.Button(frame, text="Default", command=default_command, width=10).pack(
            side=tk.LEFT, padx=(0, 10)
        )

        entry = ttk.Entry(frame)
        entry.insert(0, str(default_folder))
        entry.pack(side=tk.LEFT, expand=True, fill=tk.X)

        ttk.Button(
            frame, text=f"Select {label} Folder", command=select_command, width=20
        ).pack(side=tk.LEFT, padx=(10, 0))

        setattr(self, f"{label.lower()}_entry", entry)

    def create_resize_options_frame(self, parent: ttk.Frame) -> None:
        resize_frame = ttk.Frame(parent)
        resize_frame.pack(fill=tk.X, pady=(0, 10))

        ttk.Button(
            resize_frame, text="Default", command=self.reset_resize_options, width=10
        ).pack(side=tk.LEFT, padx=(0, 10))

        self.create_resize_options(resize_frame)

        self.start_button = ttk.Button(
            resize_frame, text="Start", command=self.start_processing, width=20
        )
        self.start_button.pack(side=tk.RIGHT, padx=(10, 0))

    def create_resize_options(self, frame: ttk.Frame) -> None:
        # Format Label and Combobox
        ttk.Label(frame, text="Format:").pack(side=tk.LEFT, padx=(0, 5))
        ttk.Combobox(
            frame,
            textvariable=self.RESIZE_FORMAT,
            values=["png", "jpg"],
            state="readonly",
            width=4,
        ).pack(side=tk.LEFT, padx=(0, 10))

        # Resize Label and Combobox
        ttk.Label(frame, text="Resize:").pack(side=tk.LEFT, padx=(0, 5))
        mode_combo = ttk.Combobox(
            frame,
            textvariable=self.RESIZE_MODE,
            values=["Shortest Side", "Fit"],
            state="readonly",
            width=12,
        )
        mode_combo.pack(side=tk.LEFT, padx=(0, 10))
        mode_combo.bind("<<ComboboxSelected>>", self.update_resize_options)

        # Frame for Resize Option Fields (Width, Height, Side)
        self.resize_options_frame = ttk.Frame(frame)
        self.resize_options_frame.pack(side=tk.LEFT, fill=tk.Y)

        # Width and Height Widgets
        self.width_label = ttk.Label(self.resize_options_frame, text="Width:")
        self.width_entry = ttk.Entry(
            self.resize_options_frame, textvariable=self.RESIZE_WIDTH, width=5
        )
        self.height_label = ttk.Label(self.resize_options_frame, text="Height:")
        self.height_entry = ttk.Entry(
            self.resize_options_frame, textvariable=self.RESIZE_HEIGHT, width=5
        )
        self.maintain_aspect_ratio_label = ttk.Label(
            self.resize_options_frame, text="Maintain Aspect Ratio", font=self.default_font
        )
        self.maintain_aspect_ratio_checkbox = ttk.Checkbutton(
            self.resize_options_frame, variable=self.maintain_aspect_ratio
        )

        # Side Widgets
        self.side_label = ttk.Label(self.resize_options_frame, text="Side:")
        self.side_entry = ttk.Entry(
            self.resize_options_frame, textvariable=self.RESIZE_SIDE, width=5
        )

        # Initialize Resize Options Based on Default Mode
        self.update_resize_options()

        # Rename Frame (Packed Last to Occupy Remaining Space)
        rename_frame = ttk.Frame(frame)
        rename_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

        rename_label = ttk.Label(rename_frame, text="Rename:")
        rename_label.pack(side=tk.LEFT, padx=(0, 5))

        rename_entry = ttk.Entry(
            rename_frame,
            textvariable=self.RENAME_PATTERN,
        )
        rename_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)

    def create_options_and_start_frame(self, parent: ttk.Frame) -> None:
        frame = ttk.Frame(parent)
        frame.pack(fill=tk.X, pady=(0, 10))

        self.create_options_frame(frame)
        self.create_stop_button(frame)

    def create_options_frame(self, parent: ttk.Frame) -> None:
        options_frame = ttk.Frame(parent)
        options_frame.pack(side=tk.LEFT, fill=tk.X, expand=True)

        options = [
            ("Check For Corrupted Files", "1"),
            ("Analyze Images", "2"),
            ("Find Color Images", "3"),
            ("Resize Images", "4"),
            ("Automatic Color Correction", "5"),
            ("Automatic Color Correction + Resize", "6"),
        ]

        for col, (text, value) in enumerate(options):
            ttk.Radiobutton(
                options_frame, text=text, variable=self.option_var, value=value
            ).grid(row=0, column=col, sticky="ew")
            options_frame.columnconfigure(col, weight=1)

    def create_stop_button(self, parent: ttk.Frame) -> None:
        self.stop_button = ttk.Button(
            parent,
            text="Stop",
            command=self.stop_processing,
            width=20,
            state=tk.DISABLED,
        )
        self.stop_button.pack(side=tk.RIGHT, padx=(10, 0))

    def create_text_frame(self, parent: ttk.Frame) -> None:
        text_frame = ttk.Frame(parent)
        text_frame.pack(fill=tk.BOTH, expand=True)

        self.notebook = ttk.Notebook(text_frame)
        self.notebook.pack(fill=tk.BOTH, expand=True)

        self.text_areas: dict[str, tk.Text] = {}
        for tab in ["Log", "Black", "White", "Color"]:
            frame = ttk.Frame(self.notebook)
            self.notebook.add(frame, text=tab)
            self.text_areas[tab] = self.create_text_area(frame)

    def create_text_area(self, parent: ttk.Frame) -> tk.Text:
        frame = ttk.Frame(parent)
        frame.pack(fill=tk.BOTH, expand=True)

        text_area = tk.Text(
            frame,
            wrap=tk.NONE,
            bg="#ffffff",
            state="disabled",
            padx=7,
            pady=7,
            fg="#000000",
            font=self.default_font,
        )
        scrollbar_y = ttk.Scrollbar(frame, command=text_area.yview)
        scrollbar_x = ttk.Scrollbar(frame, command=text_area.xview, orient="horizontal")

        scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
        text_area.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
        scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)

        text_area.config(yscrollcommand=scrollbar_y.set, xscrollcommand=scrollbar_x.set)
        return text_area

    def create_progress_bar(self, parent: ttk.Frame) -> None:
        self.progress = ttk.Progressbar(
            parent, mode="determinate", maximum=Config.PROGRESS_BAR_LENGTH
        )
        self.progress.pack(pady=(30, 0), padx=20, fill="x")
        self.progress.config(value=0)

    def create_status_label(self, parent: ttk.Frame) -> None:
        ttk.Label(parent, textvariable=self.status_text).pack(pady=(10, 15))

    def configure_logging(self) -> None:
        self.queue_thread = threading.Thread(
            target=self.update_text_area_from_queue, daemon=True
        )
        self.queue_thread.start()

    def set_default_folder(self, entry: ttk.Entry, default_folder: Path) -> Path:
        entry.delete(0, tk.END)
        entry.insert(0, str(default_folder))
        return default_folder

    def set_default_input(self) -> None:
        Config.INPUT_FOLDER = self.set_default_folder(
            self.input_entry, Config.DEFAULT_INPUT_FOLDER
        )

    def set_default_output(self) -> None:
        Config.OUTPUT_FOLDER = self.set_default_folder(
            self.output_entry, Config.DEFAULT_OUTPUT_FOLDER
        )

    def select_folder(self, entry: ttk.Entry, config_attr: str) -> None:
        selected_folder = filedialog.askdirectory()
        if selected_folder:
            path = Path(selected_folder).resolve()
            entry.delete(0, tk.END)
            entry.insert(0, str(path))
            setattr(Config, config_attr, path)

    def select_input_folder(self) -> None:
        self.select_folder(self.input_entry, "INPUT_FOLDER")

    def select_output_folder(self) -> None:
        self.select_folder(self.output_entry, "OUTPUT_FOLDER")

    def update_status_text(self, text: str) -> None:
        self.status_text.set(text)
        self.update_idletasks()

    def update_progress(self, value: float) -> None:
        self.progress["value"] = min(value, Config.PROGRESS_BAR_LENGTH)
        self.update_idletasks()

    def update_text_area(self, content_key: str | None = None) -> None:
        if content_key is not None:
            tab_keys = [content_key]
        else:
            tab_keys = self.text_areas.keys()

        for tab_key in tab_keys:
            text_area = self.text_areas[tab_key]
            text_area.configure(state="normal")
            text_area.delete("1.0", tk.END)
            content = "\n".join(shared_state.tab_messages[tab_key])
            text_area.insert(tk.END, content)
            text_area.configure(state="disabled")

    def start_processing(self) -> None:
        shared_state.stop_event.clear()
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)
        self.process_start_time = time.time()
        if hasattr(self, "stopped_by_user"):
            delattr(self, "stopped_by_user")
        choice = self.option_var.get()

        actions = {
            "1": ("1. Check For Corrupted Files", check_for_corrupted_files),
            "2": ("2. Analyze Images", analyze_image_color_properties, image_processor),
            "3": ("3. Find Color Images", identify_and_move_color_images, image_processor),
            "4": (
                "4. Resize Images",
                resize_images,
                Config.INPUT_FOLDER,
                Config.OUTPUT_FOLDER,
                image_processor,
            ),
            "5": (
                "5. Automatic Color Correction",
                process_folder,
                Config.INPUT_FOLDER,
                image_processor,
            ),
            "6": (
                "6. Automatic Color Correction + Resize",
                process_folder,
                Config.INPUT_FOLDER,
                image_processor,
                True,
            ),
        }

        label, func, *args = actions.get(choice, ("Unknown Action", lambda: None))
        log_and_execute_in_thread(choice, label, func, *args)

    def stop_processing(self) -> None:
        """Handle the Stop button press by setting the stop event and terminating subprocesses."""
        # Signal all threads to stop
        shared_state.stop_event.set()

        # Terminate all active subprocesses
        with shared_state.active_subprocesses_lock:
            for proc in shared_state.active_subprocesses.copy():
                try:
                    # Send SIGINT instead of SIGTERM for a cleaner shutdown, if supported
                    if os.name == 'nt':
                        proc.terminate()  # On Windows, terminate is equivalent to kill
                    else:
                        proc.send_signal(signal.SIGINT)
                except Exception as e:
                    log_error_message(f"Error terminating subprocess: {e}")

        # Optionally, wait for subprocesses to terminate gracefully
        with shared_state.active_subprocesses_lock:
            for proc in shared_state.active_subprocesses.copy():
                try:
                    proc.wait(timeout=5)  # Wait up to 5 seconds for termination
                except subprocess.TimeoutExpired:
                    try:
                        proc.kill()  # Force kill if not terminated
                    except Exception as e:
                        log_error_message(f"Error killing subprocess: {e}")
                except Exception as e:
                    log_error_message(f"Error waiting for subprocess termination: {e}")

        # Update the UI to reflect that processing has been stopped
        self.stopped_by_user = True
        self.stop_button.config(state=tk.DISABLED)
        self.start_button.config(state=tk.NORMAL)

    def update_resize_options(self, event: tk.Event | None = None) -> None:
        mode = self.RESIZE_MODE.get()
        
        # Hide all resize option widgets initially
        self.width_label.pack_forget()
        self.width_entry.pack_forget()
        self.height_label.pack_forget()
        self.height_entry.pack_forget()
        self.maintain_aspect_ratio_label.pack_forget()
        self.maintain_aspect_ratio_checkbox.pack_forget()
        self.side_label.pack_forget()
        self.side_entry.pack_forget()
    
        if mode == "Fit":
            # Show Width and Height Widgets
            self.width_label.pack(side=tk.LEFT, padx=(0, 5), pady=2)
            self.width_entry.pack(side=tk.LEFT, padx=(0, 10), pady=2)
            self.height_label.pack(side=tk.LEFT, padx=(0, 5), pady=2)
            self.height_entry.pack(side=tk.LEFT, padx=(0, 10), pady=2)
            self.maintain_aspect_ratio_label.pack(side=tk.LEFT, padx=(10, 5), pady=2)
            self.maintain_aspect_ratio_checkbox.pack(side=tk.LEFT, padx=(0, 10), pady=2)
        elif mode == "Shortest Side":
            # Show Side Widget
            self.side_label.pack(side=tk.LEFT, padx=(0, 5), pady=2)
            self.side_entry.pack(side=tk.LEFT, padx=(0, 10), pady=2)

    def reset_resize_options(self) -> None:
        self.RESIZE_MODE.set(Config.DEFAULT_RESIZE_MODE)
        self.RESIZE_WIDTH.set(str(Config.DEFAULT_RESIZE_WIDTH))
        self.RESIZE_HEIGHT.set(str(Config.DEFAULT_RESIZE_HEIGHT))
        self.RESIZE_SIDE.set(str(Config.DEFAULT_RESIZE_SIDE))
        self.RESIZE_FORMAT.set(Config.DEFAULT_RESIZE_FORMAT)
        
        # Reset the renaming pattern to its default value
        self.RENAME_PATTERN.set(Config.DEFAULT_RENAME_PATTERN)
        
        self.update_resize_options()

    def update_text_area_from_queue(self) -> None:
        while True:
            if not shared_state.log_message_queue.empty():
                try:
                    item = shared_state.log_message_queue.get(block=True, timeout=0.1)
                    self.process_log_message(item)
                except:
                    pass
            else:
                time.sleep(0.1)

    def process_log_message(self, item: str) -> None:
        update_tabs = {
            "UPDATE_LOG": "Log",
            "UPDATE_BLACK": "Black",
            "UPDATE_WHITE": "White",
            "UPDATE_COLOR": "Color",
        }
        if item in update_tabs:
            self.after(0, self.update_text_area, update_tabs[item])
        else:
            self.update_tab_messages(item)

    def update_tab_messages(self, item: str) -> None:
        if "Modal Value Black" in item:
            shared_state.tab_messages["Black"].append(item)
        if "Modal Value White" in item:
            shared_state.tab_messages["White"].append(item)
        if "Filename" in item:
            shared_state.tab_messages["Color"].append(item)
        shared_state.tab_messages["Log"].append(item)

        for tab in ["Log", "Black", "White", "Color"]:
            self.after(0, self.update_text_area, tab)

    def update_final_status(self, execution_time: float) -> None:
        self.update_progress(100)
        end_time = time.time()
        start_time = self.process_start_time
        processed_count = shared_state.processed_files_count
        total_files = shared_state.total_files_count

        start_time_str = time.strftime("%H:%M:%S", time.localtime(start_time))
        elapsed_minutes = int(execution_time // 60)
        elapsed_seconds = int(execution_time % 60)
        elapsed_time_str = f"{elapsed_minutes:02d}:{elapsed_seconds:02d}"

        status_text = (
            f"Start Time: {start_time_str} - "
            f"Processed Files: {processed_count}/{total_files} - "
            f"Elapsed Time: {elapsed_time_str} Minutes"
        )

        if hasattr(self, "stopped_by_user") and self.stopped_by_user:
            status_text += " - Processing stopped by user"
            delattr(self, "stopped_by_user")

        padded_status_text = status_text.ljust(Config.PADDING_LENGTH)
        self.update_status_text(padded_status_text)
        self.reset_gui()

    def reset_gui(self) -> None:
        self.start_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)


# --- Main Execution ---

if __name__ == "__main__":
    shared_state = Config.initialize()
    image_processor = ImageProcessor()
    app = App()
    app.mainloop()