Create a comprehensive python guide for people with no prior knowledge. Create the guide based on the provided code. After reading the guide the user should be able to understand the whole python code. Start with the simple concepts and go to the more complex one in a structured pedagogily structured way. Code 1: 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/Image Processor/0. Code/Requirements/ImageMagick.exe"
        )
    )
    FFMPEG: Path = Path(
        os.getenv("FFMPEG", "D:/Bilder/Python/Image Processor/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()
Code 2: import ctypes
import csv
import json
import tkinter as tk
from pathlib import Path
from tkinter import filedialog, messagebox, simpledialog, ttk
from typing import Any, Optional

from tksheet import Sheet

def set_dpi_awareness():
    """Enable DPI awareness for high DPI settings."""
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(1)
    except Exception as e:
        print(f"Error setting DPI awareness: {e}")

def custom_filter_design(master, style: ttk.Style, headers=None, filter_types=None, filter_font=None, filter_combobox_font=None, colors=None):
    """Creates a custom filter frame with all components."""
    filter_frame = ttk.Frame(master, style="Custom.TFrame")

    # Configure styles
    style.configure("Custom.TFrame", background=colors["background"], relief="flat", borderwidth=0)
    style.configure("TCombobox", relief="flat", borderwidth=0, background=colors["background"], fieldbackground=colors["background"])
    style.map("TCombobox", fieldbackground=[('readonly', colors["background"])], background=[('readonly', colors["background"])])
    style.configure("TEntry", relief="flat", borderwidth=0, background=colors["background"])
    style.configure("Line.TFrame", background=colors["filter_line"])

    # Create widgets
    header_combobox = ttk.Combobox(filter_frame, values=headers, width=15, font=filter_combobox_font, state="readonly")
    filtertype_combobox = ttk.Combobox(filter_frame, values=filter_types, width=15, font=filter_combobox_font, state="readonly")
    filter_entry = ttk.Entry(filter_frame, font=filter_font)
    underline = ttk.Frame(filter_frame, height=1, style="Line.TFrame")

    # Layout widgets
    header_combobox.grid(row=0, column=0, padx=(0, 30), pady=2, sticky='ew')
    filtertype_combobox.grid(row=0, column=1, padx=2, pady=2, sticky='ew')
    filter_entry.grid(row=1, column=0, columnspan=2, padx=2, pady=(2, 0), sticky='ew')
    underline.grid(row=2, column=0, columnspan=2, sticky='ew', pady=(0, 2))

    # Prevent text selection in comboboxes
    for combobox in (header_combobox, filtertype_combobox):
        combobox.bind("<FocusIn>", lambda event: event.widget.selection_clear())

    # Configure column weights
    filter_frame.columnconfigure((0, 1), weight=1)

    # Ensure the underline doesn't resize vertically
    underline.pack_propagate(False)

    return filter_frame, header_combobox, filtertype_combobox, filter_entry

def custom_scrollbar_design(style: ttk.Style, colors=None):
    """Configure custom scrollbar style."""
    style.configure("Custom.Vertical.TScrollbar", troughcolor=colors["scrollbar_trough"], background=colors["scrollbar_background"], borderwidth=0, arrowcolor=colors["scrollbar_arrow"])
    style.map("Custom.Vertical.TScrollbar", background=[('active', colors["scrollbar_active"])])
    style.layout("Custom.Vertical.TScrollbar", [('Vertical.Scrollbar.trough', {'children': [('Vertical.Scrollbar.thumb', {'expand': '1', 'sticky': 'ns'})], 'sticky': 'ns'})])

class TableApp:
    """Table Editor with sidebar and table management."""

    def __init__(self, root: tk.Tk):
        """Initialize the TableApp."""
        self.root = root
        self.config_dimensions()
        self.config_fonts()
        self.config_colors()
        self.initialize_data()
        self.style = ttk.Style()
        self.style.theme_use("default")
        self.setup_ui()
        self.load_tables()

    def config_dimensions(self):
        """Configure app dimensions."""
        self.root.title("Database")
        self.root.geometry("1920x1080")
        self.root.minsize(1435, 910)
        self.sidebar_width = 400
        self.spacer_frame_width = 100
        self.header_height = 190
        self.separator_height = 1

    def config_fonts(self):
        """Configure app fonts."""
        self.fonts = {
            "text": ("Literata", 12),
            "tksheet": ("Literata", 12, "normal"),
            "header_info": ("Literata", 10),
            "filter": ("Literata", 12),
            "filter_combobox": ("Literata", 8),
        }

    def config_colors(self):
        """Configure app colors."""
        self.colors = {
            "text": "#333333",
            "background": "#FFFFFF",
            "sidebar": "#F7F8FC",
            "button": "#F7F8FC",
            "button_selected": "#EAEDEF",
            "separator": "#333333",
            "header_info": "#999999",
            "scrollbar_trough": "#F7F8FC",
            "scrollbar_background": "#c0c1c4",
            "scrollbar_arrow": "#333333",
            "scrollbar_active": "#878789",
            "filter_line": "#00bcf0",
        }

    def setup_ui(self):
        """Set up the user interface with a 3x2 grid layout."""
        self.root.grid_rowconfigure(0, weight=0)
        self.root.grid_rowconfigure(1, weight=1)

        self.root.grid_columnconfigure(0, weight=0)
        self.root.grid_columnconfigure(1, weight=0, minsize=self.spacer_frame_width)
        self.root.grid_columnconfigure(2, weight=3)

        self.create_sidebar()
        self.create_header_frame()
        self.create_spacer_frame()
        self.create_table_frame()

    def create_sidebar(self):
        """Create the sidebar that spans both rows."""
        sidebar = self._create_frame(
            parent=self.root,
            width=self.sidebar_width,
            bg=self.colors["sidebar"]
        )
        sidebar.grid(row=0, column=0, rowspan=2, sticky='ns')
        sidebar.grid_propagate(False)
        sidebar.columnconfigure(0, weight=1)
        sidebar.rowconfigure(0, weight=1)

        functional_sidebar = self._create_frame(
            parent=sidebar,
            width=self.sidebar_width,
            bg=self.colors["sidebar"]
        )
        functional_sidebar.grid(row=0, column=0, sticky='nsew')
        functional_sidebar.columnconfigure(0, weight=1)
        functional_sidebar.rowconfigure(0, weight=0)
        functional_sidebar.rowconfigure(1, weight=1)
        functional_sidebar.rowconfigure(2, weight=0)

        self._add_normal_buttons(functional_sidebar)
        self._add_scrollable_canvas(functional_sidebar)
        self._add_delete_table_section(functional_sidebar)

    def create_header_frame(self) -> None:
        """Create the header divided into upper and lower sub-frames."""
        header_frame = self._create_and_configure_header_frame()
        
        upper_header_frame = self._create_upper_header(header_frame)
        self._create_upper_header_content(upper_header_frame)
        
        lower_header_frame = self._create_lower_header(header_frame)
        self._create_filter_controls(lower_header_frame)
    
    def _create_and_configure_header_frame(self) -> tk.Frame:
        """Create and configure the main header frame."""
        header_frame = self._create_frame(parent=self.root, width=None, bg=self.colors["background"])
        header_frame.grid(row=0, column=1, columnspan=2, sticky='ew')
        header_frame.config(height=self.header_height)
        header_frame.grid_propagate(False)
        
        for i in range(2):
            header_frame.grid_rowconfigure(i, weight=0)
        
        for i in range(5):
            weight = 1 if i == 4 else 0
            header_frame.grid_columnconfigure(i, weight=weight)
        
        return header_frame
    
    def _create_upper_header(self, parent: tk.Frame) -> tk.Frame:
        """Create the upper header frame."""
        upper_header = self._create_frame(parent=parent, width=None, bg=self.colors["background"])
        upper_header.grid(row=0, column=0, columnspan=5, sticky='ew', pady=(0, 10))
        upper_header.grid_columnconfigure(5, weight=1)
        return upper_header
    
    def _create_upper_header_content(self, upper_header: tk.Frame) -> None:
        """Create and add content to the upper header frame."""
        self._create_data_label(upper_header)
        self._create_table_name_label(upper_header)
        self._create_settings_label(upper_header)
    
    def _create_data_label(self, parent: tk.Frame) -> None:
        """Create and grid the 'Data >' label."""
        data_label = tk.Label(
            parent,
            text="Data > ",
            bg=self.colors["background"],
            fg=self.colors["header_info"],
            font=self.fonts["header_info"]
        )
        data_label.grid(row=0, column=0, padx=22, pady=(22, 0), sticky='nw')
    
    def _create_table_name_label(self, parent: tk.Frame) -> None:
        """Create and grid the table name label."""
        self.table_name_label = tk.Label(
            parent,
            text=self.current_table_name if self.current_table_name else "No Table Selected",
            bg=self.colors["background"],
            fg=self.colors["text"],
            font=self.fonts["header_info"]
        )
        self.table_name_label.grid(row=0, column=0, padx=(88, 10), pady=(22, 0), sticky='nw')
    
    def _create_settings_label(self, parent: tk.Frame) -> None:
        """Create and grid the settings label with its associated menu."""
        settings_label = tk.Label(
            parent,
            text="...",
            bg=self.colors["background"],
            fg=self.colors["text"],
            cursor="hand2",
            anchor='e',
            font=self.fonts["text"]
        )
        settings_label.grid(row=0, column=5, padx=40, pady=20, sticky='e')
        
        settings_menu = self._create_settings_menu()
        self._bind_settings_label_events(settings_label, settings_menu)
    
    def _create_settings_menu(self) -> tk.Menu:
        """Create the settings dropdown menu."""
        menu = tk.Menu(self.root, tearoff=0)
        menu.add_command(label="Export Table", command=self.export_table)
        menu.add_command(label="Import Table", command=self.import_file)
        menu.add_command(label="Rename Headers", command=self.rename_headers)
        return menu
    
    def _bind_settings_label_events(self, label: tk.Label, menu: tk.Menu) -> None:
        """Bind events to the settings label."""
        label.bind("<Button-1>", lambda e: menu.post(e.x_root, e.y_root))
        label.bind("<Enter>", lambda _: label.config(bg=self.colors["button_selected"]))
        label.bind("<Leave>", lambda _: label.config(bg=self.colors["background"]))
    
    def _create_lower_header(self, parent: tk.Frame) -> tk.Frame:
        """Create the lower header frame."""
        lower_header = self._create_frame(parent=parent, width=None, bg=self.colors["background"])
        lower_header.grid(row=1, column=0, columnspan=5, sticky='ew')
        return lower_header

    def _create_filter_controls(self, parent: tk.Frame) -> None:
        """Create and add filter controls to the lower header frame."""
        self._create_filter_label(parent)
        filter_frame, self.filter_header_dropdown, self.filter_option_dropdown, self.filter_text_entry = custom_filter_design(parent, self.style, headers=["All"] + self.headers.get(self.current_table_name, []), filter_types=[
            "is", "is not", "contains", "does not contain",
            "starts with", "ends with", "is empty", "is not empty"
        ], filter_font=self.fonts["filter"], filter_combobox_font=self.fonts["filter_combobox"], colors=self.colors)
        filter_frame.grid(row=0, column=1, columnspan=4, padx=0, pady=0, sticky='ew')

        self.filter_header_var.set("All")
        self.filter_option_var.set("contains")
        self.filter_text_var.set("")

        self.filter_header_dropdown['textvariable'] = self.filter_header_var
        self.filter_option_dropdown['textvariable'] = self.filter_option_var
        self.filter_text_entry['textvariable'] = self.filter_text_var

        self.filter_header_dropdown.bind("<<ComboboxSelected>>", self.apply_filter)
        self.filter_option_dropdown.bind("<<ComboboxSelected>>", lambda event: [self.update_filter_text_state(event), self.apply_filter()])
        self.filter_text_entry.bind("<KeyRelease>", self.apply_filter)
        self.filter_text_entry.bind("<Return>", self.apply_filter)
        
    def _create_filter_label(self, parent: tk.Frame) -> None:
        filter_label = tk.Label(parent, text="Filter: ", bg=self.colors["background"], fg=self.colors["text"])
        filter_label.grid(row=0, column=0, padx=(100, 0), pady=20, sticky='e')
    
    def update_filter_text_state(self, event=None):
        """Enable or disable the filter text entry based on filter option."""
        option = self.filter_option_var.get()
        if option in ["is empty", "is not empty"]:
            self.filter_text_var.set("")
            self.filter_text_entry.config(state='disabled')
            self.apply_filter()
        else:
            self.filter_text_entry.config(state='normal')

    def create_spacer_frame(self):
        """Create a spacer frame in column 1, row 1."""
        spacer = self._create_frame(
            parent=self.root,
            width=self.spacer_frame_width,
            bg=self.colors["background"]
        )
        spacer.grid(row=1, column=1, sticky='nsew')
        spacer.grid_propagate(False)

    def create_table_frame(self):
        """Create the table frame in column 2, row 1."""
        self.table_frame = self._create_frame(
            parent=self.root,
            width=None,
            bg=self.colors["background"]
        )
        self.table_frame.grid(row=1, column=2, sticky='nsew')
        self.table_frame.grid_rowconfigure(0, weight=1)
        self.table_frame.grid_columnconfigure(0, weight=1)

    def _create_frame(
        self,
        parent: tk.Widget,
        width: Optional[int],
        bg: str
    ) -> tk.Frame:
        """Create a frame with specified width and background."""
        frame = tk.Frame(parent, width=width, bg=bg, bd=0, relief='flat')
        if width:
            frame.config(width=width)
        return frame

    def _add_normal_buttons(self, sidebar: tk.Frame):
        """Add buttons to the sidebar."""
        normal_buttons_frame = self._create_frame(
            parent=sidebar,
            width=self.sidebar_width,
            bg=self.colors["sidebar"]
        )
        normal_buttons_frame.grid(row=0, column=0, sticky='ew', padx=10, pady=(10, 0))
        normal_buttons_frame.columnconfigure(0, weight=1)

        create_table_label = self._create_clickable_label(
            parent=normal_buttons_frame,
            text="Create New Table",
            command=self.create_new_table,
            anchor='center'
        )
        create_table_label.grid(row=0, column=0, pady=10, sticky='ew')
        self.add_custom_hover(create_table_label)

        separator = tk.Frame(normal_buttons_frame, height=self.separator_height, bg=self.colors["separator"])
        separator.grid(row=1, column=0, sticky='ew', pady=5)

    def _add_scrollable_canvas(self, sidebar: tk.Frame):
        """Add scrollable canvas for table labels within a single frame."""
        scrollable_container = self._create_frame(
            parent=sidebar,
            width=self.sidebar_width,
            bg=self.colors["sidebar"]
        )
        scrollable_container.grid(row=1, column=0, sticky='nsew', padx=10, pady=10)
        scrollable_container.columnconfigure(0, weight=1)
        scrollable_container.rowconfigure(0, weight=1)
    
        self.canvas = tk.Canvas(
            scrollable_container,
            bg=self.colors["sidebar"],
            highlightthickness=0
        )
        self.canvas.grid(row=0, column=0, sticky='nsew')
    
        custom_scrollbar_design(self.style, self.colors) 

        self.scrollbar = ttk.Scrollbar(
            scrollable_container,
            orient="vertical",
            command=self.canvas.yview,
            style="Custom.Vertical.TScrollbar"
        )
        self.scrollbar.grid(row=0, column=1, sticky='ns')

        self.canvas.configure(yscrollcommand=self.scrollbar.set)

        self.table_labels_frame = tk.Frame(self.canvas, bg=self.colors["sidebar"])
        self.canvas.create_window((0, 0), window=self.table_labels_frame, anchor="nw")

        self.canvas.bind("<MouseWheel>", self._on_mouse_wheel)
        self.table_labels_frame.bind("<MouseWheel>", self._on_mouse_wheel)

        self.table_labels_frame.bind(
            "<Configure>",
            lambda event: (
                self.canvas.configure(scrollregion=self.canvas.bbox("all")),
                self._update_scrollbar(scrollable_container, self.canvas, self.scrollbar)
            )
        )
        scrollable_container.bind(
            "<Configure>",
            lambda event: self._update_scrollbar(scrollable_container, self.canvas, self.scrollbar)
        )

    def _on_mouse_wheel(self, event):
        """Handle mouse wheel scrolling only if scrollbar is visible."""
        if self.scrollbar.winfo_viewable():
            self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

    def _update_scrollbar(self, scrollbar_frame, canvas, scrollbar):
        """Update scrollbar visibility based on content size."""
        canvas.update_idletasks()
        canvas_height = scrollbar_frame.winfo_height()
        content_height = self.table_labels_frame.winfo_reqheight()

        if content_height > canvas_height:
            scrollbar.grid()
            canvas.configure(yscrollcommand=scrollbar.set)
            canvas.bind("<MouseWheel>", lambda event: canvas.yview_scroll(int(-1 * (event.delta / 120)), "units"))
        else:
            scrollbar.grid_remove()
            canvas.configure(yscrollcommand=lambda *args: None)
            canvas.unbind("<MouseWheel>")

    def _add_delete_table_section(self, sidebar: tk.Frame):
        """Add delete table option to the sidebar."""
        delete_table_frame = self._create_frame(
            parent=sidebar,
            width=self.sidebar_width,
            bg=self.colors["sidebar"]
        )
        delete_table_frame.grid(row=2, column=0, sticky='ew', padx=10, pady=(0, 10))
        delete_table_frame.columnconfigure(0, weight=1)
        delete_table_frame.rowconfigure(1, weight=1)

        separator = tk.Frame(delete_table_frame, height=self.separator_height, bg=self.colors["separator"])
        separator.grid(row=0, column=0, sticky='ew', pady=5)

        delete_table_label = self._create_clickable_label(
            parent=delete_table_frame,
            text="🗑 Delete Selected Table",
            command=self.delete_table,
            anchor='center'
        )
        delete_table_label.grid(row=1, column=0, pady=10, sticky='ew')
        self.add_custom_hover(delete_table_label)

    def _create_clickable_label(
        self,
        parent: tk.Widget,
        text: str,
        command,
        allow_rename: bool = False,
        anchor: str = 'w'
    ) -> tk.Label:
        """Create a clickable label with optional rename functionality."""
        cursor = "arrow" if allow_rename else "hand2"
        label = tk.Label(
            parent,
            text=text,
            bg=self.colors["button"],
            fg=self.colors["text"],
            cursor=cursor,
            anchor=anchor,
            font=self.fonts["text"]
        )
        label.bind("<Button-1>", lambda _: command())
    
        if allow_rename:
            label.bind("<Double-Button-1>", lambda _: self.begin_rename(text))
    
        label.bind("<MouseWheel>", self._on_mouse_wheel)
    
        return label

    def add_custom_hover(self, widget: tk.Widget):
        """Add custom hover effects to a widget."""
        widget.bind("<Enter>", lambda e: widget.config(bg=self.colors["button_selected"]))
        widget.bind("<Leave>", lambda e: widget.config(bg=self.colors["button"]))


    def initialize_data(self):
        """Initialize data structures."""
        self.save_file_path = Path(__file__).parent / "Data.json"
        self.tables_data: dict[str, dict] = {}
        self.current_sheet: Sheet | None = None
        self.headers: dict[str, list[str]] = {}
        self.current_table_name: str | None = None
        self.rename_entry: tk.Entry | None = None
        self.selected_table_label: tk.Label | None = None
        self.table_labels: dict[str, tk.Label] = {}
        
        self.table_name_label = None
    
        self.filter_header_var = tk.StringVar(value="All")
        self.filter_option_var = tk.StringVar(value="contains")
        self.filter_text_var = tk.StringVar()

    def create_new_table(self):
        """Create a new table."""
        existing_numbers = [int(name.split(" ")[1]) for name in self.tables_data if name.startswith("Table ") and name.split(" ")[1].isdigit()]
        next_number = max(existing_numbers, default=0) + 1
        table_name = f"Table {next_number}"
        self.add_table(table_name)
        self.load_selected_table(table_name)

    def import_file(self):
        """Import table from file."""
        file_path = filedialog.askopenfilename(title="Import File", filetypes=[("All Supported Files", "*.json *.csv"), ("JSON files", "*.json"), ("CSV files", "*.csv")])
        if not file_path:
            return
        try:
            if file_path.endswith('.csv'):
                with open(file_path, newline='', encoding='utf-8') as csvfile:
                    data = list(csv.reader(csvfile))
            elif file_path.endswith('.json'):
                with open(file_path, 'r', encoding='utf-8') as jsonfile:
                    data = json.load(jsonfile)
            else:
                raise ValueError("Unsupported file format.")
            if not data or not isinstance(data, list) or not all(isinstance(row, list) for row in data):
                raise ValueError("Invalid data format in the file.")
            headers, table_data = data[0], data[1:]
            self.add_table_from_data(file_path, headers, table_data)
        except Exception as e:
            messagebox.showerror("File Import Error", str(e))

    def add_table_from_data(self, file_path: str, headers: list[str], table_data: list[list[str]]):
        """Add a table from file data."""
        table_name = Path(file_path).stem
        table_name = self.ensure_unique_table_name(table_name)
        self.headers[table_name] = headers
        self.tables_data[table_name] = {"headers": headers, "data": table_data}
        self.add_table(table_name, table_data)
        self.load_selected_table(table_name)
        self.save_data()
    
    def ensure_unique_table_name(self, table_name: str) -> str:
        """Ensure table name is unique."""
        original_name = table_name
        counter = 1
        while table_name in self.tables_data:
            table_name = f"{original_name} (New {counter})"
            counter += 1
        return table_name

    def save_table_data(self, file_path: str, data: list[list[str]], format: str):
        """Save table data in specified format."""
        match format:
            case 'csv':
                with open(file_path, 'w', newline='', encoding='utf-8') as file:
                    csv.writer(file).writerows(data)
            case 'json':
                with open(file_path, 'w', encoding='utf-8') as file:
                    json.dump(data, file, indent=4)
            case _:
                raise ValueError("Unsupported file format.")

    def export_table(self):
        """Export current table to file."""
        if not self.current_table_name or self.current_table_name not in self.tables_data:
            messagebox.showwarning("No Table Selected", "Please select a table to export.")
            return
        file_path = filedialog.asksaveasfilename(title="Export File", defaultextension=".json", filetypes=[("JSON files", "*.json"), ("CSV files", "*.csv")], initialfile=self.current_table_name)
        if not file_path:
            return
        try:
            data = [self.headers[self.current_table_name]] + self.tables_data[self.current_table_name]["data"]
            if file_path.endswith('.csv'):
                self.save_table_data(file_path, data, 'csv')
            elif file_path.endswith('.json'):
                with open(file_path, 'w', encoding='utf-8') as file:
                    json.dump(data, file, indent=4)
            else:
                raise ValueError("Unsupported file format.")
            self.save_data()
            messagebox.showinfo("Export Successful", f"Table '{self.current_table_name}' has been exported successfully.")
        except Exception as e:
            messagebox.showerror("Export Failed", f"An error occurred during export: {e}")

    def add_table(self, table_name: str, data: list[list[str]] | None = None):
        """Add a new table."""
        default_headers = ["Column 1", "Column 2", "Column 3", "Column 4", "Column 5"]
        self.headers[table_name] = default_headers
        self.tables_data[table_name] = {"headers": default_headers, "data": data or [["" for _ in range(5)] for _ in range(10)]}
        self.create_table_label(table_name)
        self.save_data()

    def create_table_label(self, table_name: str):
        """Create label for table."""
        label = self._create_clickable_label(self.table_labels_frame, table_name, lambda: self.load_selected_table(table_name), True)
        label.pack(fill='x', padx=20, pady=5, expand=True)
        self.table_labels[table_name] = label
        self.add_custom_hover(label)

    def delete_table(self):
        """Delete selected table."""
        if not self.current_table_name or self.current_table_name not in self.tables_data:
            messagebox.showwarning("No Table Selected", "Please select a valid table to delete.")
            return
        if messagebox.askyesno("Delete Table", f"Are you sure you want to delete the table '{self.current_table_name}'?"):
            self.remove_table(self.current_table_name)

    def remove_table(self, table_name: str):
        """Remove a table."""
        if self.current_table_name == table_name and self.current_sheet:
            self.current_sheet.destroy()
            self.current_sheet = None
        label = self.table_labels.get(table_name)
        if label:
            label.destroy()
        if table_name in self.tables_data:
            del self.tables_data[table_name]
        if table_name in self.headers:
            del self.headers[table_name]
        if table_name in self.table_labels:
            del self.table_labels[table_name]
        self.current_table_name = None
        self.selected_table_label = None
        self.save_data()

    def load_tables(self):
        """Load tables from file."""
        if not self.save_file_path.exists():
            return
        try:
            with self.save_file_path.open('r', encoding='utf-8') as file:
                all_data = json.load(file)
        except Exception as e:
            messagebox.showerror("Load Error", f"Failed to load tables: {e}")
            return
        for table_name, data in all_data.items():
            if not isinstance(data, list) or not data:
                continue
            headers, table_data = data[0], data[1:]
            self.headers[table_name] = headers
            self.tables_data[table_name] = {"headers": headers, "data": table_data}
            self.create_table_label(table_name)

    def load_selected_table(self, table_name: str):
        """Display selected table."""
        if self.current_sheet:
            self.current_sheet.pack_forget()
            self.current_sheet.destroy()
            self.current_sheet = None
        if self.selected_table_label:
            try:
                self.selected_table_label.config(bg=self.colors["button"])
            except tk.TclError:
                self.selected_table_label = None
    
        if table_name not in self.tables_data:
            messagebox.showerror("Error", f"Table '{table_name}' does not exist.")
            self.current_table_name = None
            self.selected_table_label = None
            return
    
        table_info = self.tables_data[table_name]
        headers = table_info["headers"]
        data = table_info["data"]
        try:
            sheet = Sheet(
                self.table_frame,
                data=data,
                show_table=True,
                headers=headers,
                font=self.fonts["tksheet"],
                header_font=self.fonts["tksheet"],
                auto_resize_columns=True
            )
            sheet.set_options(table_fg=self.colors["text"], header_fg=self.colors["text"])
            sheet.enable_bindings()
            sheet.extra_bindings([("end_edit_cell", self.on_cell_edit)])
            sheet.pack(expand=True, fill='both')
            self.current_sheet = sheet
            self.current_table_name = table_name
    
            if self.table_name_label:
                self.table_name_label.config(text=self.current_table_name)
            else:
                self.table_name_label = tk.Label(
                    self.root,
                    text=self.current_table_name,
                    bg=self.colors["background"],
                    fg=self.colors["text"],
                    font=self.fonts["header_info"]
                )
                self.table_name_label.grid(row=0, column=0, padx=(60, 10), pady=(5, 0), sticky='nw')
    
            label = self.table_labels.get(table_name)
            if label:
                self.selected_table_label = label
                try:
                    self.selected_table_label.config(bg=self.colors["button_selected"])
                except tk.TclError:
                    self.selected_table_label = None
                    messagebox.showerror("Error", f"Failed to update the label for '{table_name}'.")
            else:
                self.selected_table_label = None
                messagebox.showerror("Error", f"Label for '{table_name}' not found.")
            
            self.filter_header_var.set("All")
            self.filter_header_dropdown['values'] = ["All"] + headers
            
            self.filter_option_var.set("contains")
            self.update_filter_text_state()
            
            self.apply_filter()
        except Exception as e:
            messagebox.showerror("Sheet Error", f"Failed to load table '{table_name}': {e}")
            self.current_table_name = None
            self.selected_table_label = None

    def on_cell_edit(self, event=None):
        """Handle cell edits."""
        if self.current_table_name and self.current_sheet:
            self.tables_data[self.current_table_name]["data"] = self.current_sheet.get_sheet_data()
            self.save_data()

    def save_data(self):
        """Save all tables data."""
        all_data = {}
        for table_name, table_info in self.tables_data.items():
            all_data[table_name] = [table_info["headers"]] + table_info["data"]
        try:
            with self.save_file_path.open('w', encoding='utf-8') as file:
                json.dump(all_data, file, indent=4)
        except Exception as e:
            print(f"Error saving data: {e}")

    def apply_filter(self, event=None):
        """Apply filter to table data based on selected criteria."""
        if not self.current_table_name or not self.current_sheet:
            return
        header = self.filter_header_var.get()
        option = self.filter_option_var.get()
        text = self.filter_text_var.get().lower()
        original_data = self.tables_data[self.current_table_name]["data"]
        
        def row_matches(row):
            if header == "All":
                cells = row
                target_indices = range(len(cells))
            else:
                try:
                    target_indices = [self.headers[self.current_table_name].index(header)]
                except ValueError:
                    return False
            
            for idx in target_indices:
                cell = str(row[idx]).lower()
                if option == "is":
                    if cell == text:
                        return True
                elif option == "is not":
                    if cell != text:
                        return True
                elif option == "contains":
                    if text in cell:
                        return True
                elif option == "does not contain":
                    if text not in cell:
                        return True
                elif option == "starts with":
                    if cell.startswith(text):
                        return True
                elif option == "ends with":
                    if cell.endswith(text):
                        return True
                elif option == "is empty":
                    if cell == "":
                        return True
                elif option == "is not empty":
                    if cell != "":
                        return True
            return False
        
        if option in ["is empty", "is not empty"]:
            filtered_data = [row for row in original_data if row_matches(row)]
        else:
            filtered_data = [row for row in original_data if row_matches(row)]
        
        try:
            self.current_sheet.set_sheet_data(filtered_data)
        except tk.TclError as e:
            print(f"Error applying filter: {e}")

    def rename_headers(self):
        """Rename table headers."""
        if not self.current_table_name:
            messagebox.showwarning("No Table Selected", "Please select a table to rename headers.")
            return
        current_headers = self.headers[self.current_table_name]
        new_headers = simpledialog.askstring(
            "Rename Headers", 
            "Enter new headers (comma-separated):", 
            initialvalue=", ".join(current_headers)
        )
        if new_headers:
            header_list = [header.strip() for header in new_headers.split(',')]
            if not header_list:
                messagebox.showerror("Invalid Headers", "Header list cannot be empty.")
                return
            self.headers[self.current_table_name] = header_list
            self.tables_data[self.current_table_name]["headers"] = header_list
            if self.current_sheet:
                try:
                    self.current_sheet.headers(header_list)
                except tk.TclError as e:
                    messagebox.showerror("Error", f"Failed to rename headers: {e}")
            
            self.filter_header_var.set("All")
            self.filter_header_dropdown['values'] = ["All"] + header_list
            self.apply_filter()
            
            self.save_data()

    def begin_rename(self, table_name: str):
        """Start renaming a table."""
        if table_name not in self.table_labels:
            messagebox.showerror("Error", f"Table '{table_name}' not found.")
            return
        label = self.table_labels[table_name]
        self.cancel_rename()
        self.rename_entry = tk.Entry(self.table_labels_frame, bg=label.cget("bg"), fg=self.colors["text"], relief='flat')
        self.rename_entry.insert(0, table_name)
        self.rename_entry.place(x=label.winfo_x(), y=label.winfo_y(), width=label.winfo_width(), height=label.winfo_height())
        self.rename_entry.focus()
        self.rename_entry.select_range(0, tk.END)
        self.rename_entry.bind('<Return>', lambda event: self.end_rename(table_name))
        self.rename_entry.bind('<FocusOut>', lambda event: self.cancel_rename())

    def end_rename(self, old_name: str):
        """Finalize table renaming."""
        new_name = self.rename_entry.get().strip()
        if not new_name:
            messagebox.showerror("Invalid Name", "Table name cannot be empty.")
            self.rename_entry.focus_set()
            return
        if new_name in self.tables_data and new_name != old_name:
            messagebox.showerror("Duplicate Name", f"A table named '{new_name}' already exists.")
            self.rename_entry.focus_set()
            return
        if new_name != old_name:
            self.update_table_name(old_name, new_name)
            self.load_selected_table(new_name)
        self.cancel_rename()

    def update_table_name(self, old_name: str, new_name: str):
        """Update table name and resources."""
        self.tables_data[new_name] = self.tables_data.pop(old_name)
        self.headers[new_name] = self.headers.pop(old_name)
        label = self.table_labels.pop(old_name)
        label.config(text=new_name)
        label.bind("<Button-1>", lambda event: self.load_selected_table(new_name))
        label.bind("<Double-1>", lambda event: self.begin_rename(new_name))
        self.table_labels[new_name] = label
        if self.selected_table_label == label:
            try:
                label.config(bg=self.colors["button_selected"])
            except tk.TclError:
                pass
        self.save_data()

    def cancel_rename(self):
        """Cancel rename operation."""
        if self.rename_entry:
            self.rename_entry.destroy()
            self.rename_entry = None

    def add_custom_hover(self, label: tk.Label):
        """Add hover effects to label."""
        def on_enter(event):
            if label != self.selected_table_label:
                try:
                    label.config(bg=self.colors["button_selected"])
                except tk.TclError:
                    pass

        def on_leave(event):
            if label != self.selected_table_label:
                try:
                    label.config(bg=self.colors["button"])
                except tk.TclError:
                    pass

        label.bind("<Enter>", on_enter)
        label.bind("<Leave>", on_leave)

if __name__ == "__main__":
    set_dpi_awareness()
    root = tk.Tk()
    app = TableApp(root)
    root.mainloop()
Code 3: import json
import math
import os
import re
from pathlib import Path
from typing import Any, Callable, Union

from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
    QApplication, QFileDialog, QFrame, QGridLayout, QHBoxLayout, QLabel,
    QLineEdit, QMainWindow, QMessageBox, QPushButton, QScrollBar, QTextEdit,
    QVBoxLayout, QWidget
)

#--- Calculations ---

def convert_angle(func: Callable[[float], float], x: float, use_degrees: bool) -> float:
    """Convert angle based on mode (degrees/radians)."""
    return func(math.radians(x)) if use_degrees else func(x)

def kilograms_to_pounds(kg: float) -> float:
    """Convert kilograms to pounds."""
    return kg / 0.45359237

def pounds_to_kilograms(lb: float) -> float:
    """Convert pounds to kilograms."""
    return lb * 0.45359237

def celsius_to_kelvin(c: float) -> float:
    """Convert Celsius to Kelvin."""
    return c + 273.15

def kelvin_to_celsius(k: float) -> float:
    """Convert Kelvin to Celsius."""
    return k - 273.15

def celsius_to_fahrenheit(c: float) -> float:
    """Convert Celsius to Fahrenheit."""
    return c * 9 / 5 + 32

def fahrenheit_to_celsius(f: float) -> float:
    """Convert Fahrenheit to Celsius."""
    return (f - 32) * 5 / 9

def capitalize_sentence(sentence: str) -> str:
    """Capitalize each word in a sentence."""
    return ' '.join(word.capitalize() for word in sentence.split())

def remove_duplicates(text: str) -> str:
    """Remove duplicate words from text."""
    words = ' '.join(line.strip() for line in text.splitlines() if line.strip()).split()
    return ' '.join(dict.fromkeys(words))

#--- Processing ---

def get_safe_globals(use_degrees: bool, variables: dict) -> dict:
    """Define safe global functions and variables for expression evaluation."""
    trig_funcs = {'sin', 'cos', 'tan', 'asin', 'acos', 'atan'}
    safe_globals = {
        "sqrt": math.sqrt,
        "log": math.log10,
        "ln": math.log,
        "pi": math.pi,
        "e": math.e,
        "KilogramToPound": kilograms_to_pounds,
        "PoundToKilogram": pounds_to_kilograms,
        "CelsiusToKelvin": celsius_to_kelvin,
        "KelvinToCelsius": kelvin_to_celsius,
        "CelsiusToFahrenheit": celsius_to_fahrenheit,
        "FahrenheitToCelsius": fahrenheit_to_celsius,
        "capitalize": capitalize_sentence,
        "duplicates": remove_duplicates,
    }
    safe_globals.update({func: lambda x, f=func: convert_angle(getattr(math, f), x, use_degrees) for func in trig_funcs})
    return safe_globals | variables

def preprocess_text_functions(expression: str) -> str:
    """Preprocess text functions by ensuring arguments are quoted."""
    return re.sub(r'\b(capitalize|duplicates)\s*\(\s*([^)]+)\s*\)', 
                  lambda m: f'{m.group(1)}("{m.group(2)}")' if not (m.group(2).startswith('"') and m.group(2).endswith('"')) else m.group(0), 
                  expression)

def validate_inverse_trig(expression: str, safe_globals: dict) -> Union[str, None]:
    """Validate the domain of inverse trig functions."""
    for func, arg in re.findall(r'\b(asin|acos)\s*\(\s*([^)]+)\s*\)', expression):
        try:
            if abs(eval(arg, {"builtins": None}, safe_globals)) > 1:
                return f"Error: {func}({arg}) is undefined for |{arg}| > 1"
        except Exception as e:
            return f"Error: {e}"
    return None

def safe_evaluate(expression: str, safe_globals: dict) -> Union[str, Any]:
    """Safely evaluate the mathematical expression."""
    try:
        result = eval(expression, {"builtins": None}, safe_globals)
        if isinstance(result, (int, float)):
            return format_number(result)
        return result
    except Exception as e:
        return f"Error: {e}"

def format_number(num: Union[int, float]) -> str:
    """Format a number for display."""
    if isinstance(num, int):
        return str(num) if len(str(abs(num))) <= 9 else f"{num:.3e}"
    epsilon = 1e-10
    if math.isclose(num, 0.0, abs_tol=epsilon):
        return "0"
    if math.isclose(num, round(num), abs_tol=epsilon):
        return str(int(round(num)))
    abs_num = abs(num)
    if abs_num >= 1e9 or (abs_num < 1e-6 and abs_num != 0):
        return f"{num:.3e}"
    return f"{num:.3f}".rstrip('0').rstrip('.')

def evaluate_expression(expression: str, variables: dict, use_degrees: bool = False) -> Union[float, str]:
    """Evaluate the user's expression, handling variable assignments."""
    expression = preprocess_text_functions(expression)
    base_safe_globals = get_safe_globals(use_degrees, {})
    assignment_match = re.match(r'^\s*([a-zA-Z_]\w*)\s*=\s*(.+)$', expression)

    if assignment_match:
        var_name, var_expr = assignment_match.groups()
        if var_name in base_safe_globals:
            return f"Error: '{var_name}' is a reserved name and cannot be used as a variable."
        safe_globals = get_safe_globals(use_degrees, variables)
        var_value = safe_evaluate(var_expr, safe_globals)
        if not isinstance(var_value, str) or not var_value.startswith("Error"):
            try:
                var_value_converted = float(var_value) if '.' in var_value else int(var_value)
                variables[var_name] = var_value_converted
                return f"{var_name} = {var_value_converted}"
            except ValueError:
                return f"Error: Cannot assign non-numeric value to variable '{var_name}'"
        return var_value
    else:
        safe_globals = get_safe_globals(use_degrees, variables)
        error = validate_inverse_trig(expression, safe_globals)
        return error or safe_evaluate(expression, safe_globals)

#--- GUI ---

class LongPressButton(QPushButton):
    """A QPushButton that emits a signal when pressed and held for a specified duration."""
    
    longPressed = Signal()

    def __init__(self, text: str = "", parent=None, hold_duration: int = 3000):
        """Initialize the long press button."""
        super().__init__(text, parent)
        self.hold_duration = hold_duration
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self._on_long_press)
        self.pressed.connect(self._on_press)
        self.released.connect(self._on_release)

    def _on_press(self):
        """Start the timer when the button is pressed."""
        self.timer.start(self.hold_duration)

    def _on_release(self):
        """Stop the timer if it's active when the button is released."""
        self.timer.stop()

    def _on_long_press(self):
        """Emit the longPressed signal."""
        self.longPressed.emit()


class CalculatorGUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.script_dir = Path(__file__).parent
        self.use_degrees = True
        self.variables = {}
        self.history_entries = []
        self.text_functions = ['capitalize', 'duplicates']
        self.setup_window()
        self.create_widgets()
        self.layout_widgets()
        self.connect_signals()
        self.load_saved_data()

    def setup_window(self) -> None:
        """Set up the main window properties."""
        self.setWindowTitle("Calculator")
        self.setGeometry(100, 100, 1280, 720)

    def create_widgets(self) -> None:
        """Create all widgets used in the application."""
        font = QFont()
        font.setPointSize(12)
        font.setFamily('Literata')

        self.entry = QLineEdit()
        self.entry.setFont(font)
        self.entry.setPlaceholderText('Enter Expression')
        self.entry.setClearButtonEnabled(True)
        self.entry.returnPressed.connect(self.calculate)

        self.history_text = QTextEdit()
        self.history_text.setFont(font)
        self.history_text.setReadOnly(True)
        self.history_text.setStyleSheet("position: relative;")

        self.scrollbar = QScrollBar(Qt.Vertical)
        self.history_text.setVerticalScrollBar(self.scrollbar)

        # --- Existing Buttons ---

        self.search_button = QPushButton("🔍", self.history_text)
        self.search_button.setFixedSize(24, 24)
        self.search_button.setToolTip("Search History")
        self.search_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.search_button.clicked.connect(self.toggle_search)
        self.search_button.raise_()

        self.delete_button = LongPressButton("🗑️", self.history_text, hold_duration=3000)
        self.delete_button.setFixedSize(24, 24)
        self.delete_button.setToolTip("Clear History (Hold for 3 seconds)")
        self.delete_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.delete_button.longPressed.connect(self.clear_history)
        self.delete_button.raise_()

        self.vars_button = QPushButton("🧮", self.history_text)
        self.vars_button.setFixedSize(24, 24)
        self.vars_button.setToolTip("Show Variables")
        self.vars_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.vars_button.clicked.connect(self.toggle_vars_overlay)
        self.vars_button.raise_()

        self.vars_overlay = QLineEdit(self.history_text)
        self.vars_overlay.setReadOnly(True)
        self.vars_overlay.setPlaceholderText("a = 1, b = 85, ...")
        self.vars_overlay.setFixedHeight(24)
        self.vars_overlay.setFixedWidth(300)
        self.vars_overlay.setStyleSheet("""
            QLineEdit {
                border: 1px solid gray;
                border-radius: 4px;
                padding: 2px 4px;
                background-color: white;
            }
        """)
        self.vars_overlay.setVisible(False)

        self.vars_delete_button = LongPressButton("🗑️", self.vars_overlay, hold_duration=3000)
        self.vars_delete_button.setFixedSize(16, 16)
        self.vars_delete_button.setToolTip("Clear Variables (Hold for 3 seconds)")
        self.vars_delete_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 10px;
                border-radius: 8px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.vars_delete_button.longPressed.connect(self.clear_variables)
        self.vars_delete_button.setVisible(False)

        self.search_bar = QLineEdit(self.history_text)
        self.search_bar.setPlaceholderText("Search...")
        self.search_bar.setFixedHeight(24)
        self.search_bar.setFixedWidth(300)
        self.search_bar.setVisible(False)
        self.search_bar.setStyleSheet("""
            QLineEdit {
                border: 1px solid gray;
                border-radius: 4px;
                padding: 2px 4px;
                background-color: white;
            }
        """)
        self.search_bar.textChanged.connect(self.filter_history)
        self.search_bar.setClearButtonEnabled(True)

        self.save_button = QPushButton("💾", self.history_text)
        self.save_button.setFixedSize(24, 24)
        self.save_button.setToolTip("Export History and Variables")
        self.save_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.save_button.clicked.connect(self.save_data)
        self.save_button.raise_()

        self.load_button = QPushButton("📂", self.history_text)
        self.load_button.setFixedSize(24, 24)
        self.load_button.setToolTip("Import History and Variables")
        self.load_button.setStyleSheet("""
            QPushButton {
                border: none;
                background-color: rgba(255, 255, 255, 200);
                font-size: 14px;
                border-radius: 12px;
            }
            QPushButton:hover {
                background-color: rgba(234, 235, 239, 255);
                color: white;
            }
        """)
        self.load_button.clicked.connect(self.load_data)
        self.load_button.raise_()

        self.function_buttons = {
            "Functions": ["sqrt", "sin", "cos", "tan", "asin", "acos", "atan", "log", "ln"],
            "Conversions": ["lb", "kg", "K", "C", "F", "CF"],
            "Text": ["capitalize", "duplicates"],
            "Constants": ["pi", "e"],
            "Settings": []
        }
        self.function_frame = QFrame()
        self.create_function_buttons(font)

    def create_function_buttons(self, font: QFont) -> None:
        """Create and arrange function buttons."""
        layout = QGridLayout()
        categories = [("Functions", 3), ("Conversions", 2), ("Text", 2), ("Constants", 2), ("Settings", 1)]
        starting_col = 0

        for category, col_span in categories:
            label = QLabel(category)
            label.setFont(font)
            layout.addWidget(label, 0, starting_col, 1, col_span, Qt.AlignCenter)

            buttons = self.function_buttons.get(category, [])
            if category == "Functions":
                for i, func in enumerate(buttons):
                    button = QPushButton(func)
                    button.setFont(font)
                    button.clicked.connect(
                        lambda _, f=func: self.insert_function(f, is_text_function=(f in self.text_functions))
                    )
                    layout.addWidget(button, (i // 3) + 1, starting_col + (i % 3))

            elif category == "Settings":
                self.mode_button = QPushButton("Degrees")
                self.mode_button.setFont(font)
                self.mode_button.clicked.connect(self.toggle_mode)
                layout.addWidget(self.mode_button, 1, starting_col, 1, 1)

            elif category == "Conversions":
                conversion_mapping = {
                    "lb": "KilogramToPound",
                    "kg": "PoundToKilogram",
                    "K": "CelsiusToKelvin",
                    "C": "KelvinToCelsius",
                    "F": "CelsiusToFahrenheit",
                    "CF": "FahrenheitToCelsius"
                }

                for idx, func in enumerate(buttons):
                    display_text = {
                        "lb": "lb(Kg)",
                        "kg": "kg(lb)",
                        "K": "K(°C)",
                        "C": "°C(K)",
                        "F": "F(°C)",
                        "CF": "°C(F)"
                    }.get(func, func)

                    button = QPushButton(display_text)
                    button.setFont(font)

                    function_to_insert = conversion_mapping.get(func, func)

                    button.clicked.connect(
                        lambda _, f=function_to_insert: self.insert_function(f, is_text_function=False)
                    )
                    row, col = 1 + idx // 2, starting_col + idx % 2
                    layout.addWidget(button, row, col)

            elif category == "Text":
                for idx, func in enumerate(buttons):
                    button = QPushButton(func)
                    button.setFont(font)
                    button.clicked.connect(
                        lambda _, f=func: self.insert_function(f, is_text_function=True)
                    )
                    row, col = 1 + idx // col_span, starting_col + idx % col_span
                    layout.addWidget(button, row, col)

            elif category == "Constants":
                for idx, func in enumerate(buttons):
                    button = QPushButton(func)
                    button.setFont(font)
                    button.clicked.connect(
                        lambda _, f=func: self.insert_function(f, is_constant=True)
                    )
                    row, col = 1 + idx // col_span, starting_col + idx % col_span
                    layout.addWidget(button, row, col)

            total_rows = math.ceil(len(buttons) / col_span)
            
            for extra in range(col_span * total_rows - len(buttons)):
                layout.addWidget(QLabel(""), 1 + extra // col_span, starting_col + extra % col_span)

            starting_col += col_span

        self.function_frame.setLayout(layout)

    def layout_widgets(self) -> None:
        """Arrange all widgets in the main window."""
        central_widget = QWidget()
        main_layout = QVBoxLayout()

        main_layout.addWidget(self.entry)

        history_layout = QHBoxLayout()
        history_layout.addWidget(self.history_text)
        history_layout.addWidget(self.scrollbar)
        main_layout.addLayout(history_layout)

        main_layout.addWidget(self.function_frame)

        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

    def connect_signals(self) -> None:
        """Connect global signals."""
        QApplication.instance().focusChanged.connect(self.on_focus_changed)

    def toggle_mode(self) -> None:
        """Toggle between degrees and radians for trigonometric functions."""
        self.use_degrees = not self.use_degrees
        self.mode_button.setText("Degrees" if self.use_degrees else "Radians")

    def insert_function(self, function: str, is_constant: bool = False, is_text_function: bool = False) -> None:
        """Insert the selected function into the entry field."""
        current_text = self.entry.text()
        cursor_position = self.entry.cursorPosition()
        if is_constant:
            new_text = f"{current_text[:cursor_position]}{function} "
            new_cursor_position = cursor_position + len(function) + 1
        elif is_text_function and function in self.text_functions:
            new_text = f'{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}'
            new_cursor_position = cursor_position + len(function) + 1
        else:
            new_text = f"{current_text[:cursor_position]}{function}() {current_text[cursor_position:]}"
            new_cursor_position = cursor_position + len(function) + 1
        self.entry.setText(new_text)
        self.entry.setCursorPosition(new_cursor_position)
        self.entry.setFocus()

    def calculate(self) -> None:
        """Evaluate the expression entered by the user."""
        expression = self.entry.text().strip()
        if expression:
            result = evaluate_expression(
                expression, self.variables, use_degrees=self.use_degrees
            )
            self.display_result(expression, result)
            self.entry.clear()

    def display_result(self, expression: str, result: Union[float, str]) -> None:
        """Display the result in the history."""
        mode = "D" if self.use_degrees else "R"
        if re.match(r'^\s*[a-zA-Z_]\w*\s*=\s*.+$', expression):
            history_entry = f"[{mode}] {result}"
        else:
            history_entry = f"[{mode}] {expression} = {result}"
        self.history_entries.append(history_entry)
        self.refresh_history()
        self.history_text.verticalScrollBar().setValue(
            self.history_text.verticalScrollBar().maximum()
        )

    def toggle_search(self) -> None:
        """Toggle the visibility of the search bar."""
        if self.search_bar.isVisible():
            self.search_bar.setVisible(False)
        else:
            self.search_bar.setVisible(True)
            self.search_bar.setFocus()
        self.update_overlay_buttons_position()

    def filter_history(self, text: str) -> None:
        """Filter the history based on search text."""
        self.refresh_history(text)

    def refresh_history(self, filter_text: str = "") -> None:
        """Refresh the history display, optionally filtering results."""
        self.history_text.clear()
        for entry in self.history_entries:
            if filter_text.lower() in entry.lower():
                self.history_text.append(entry)

    def resizeEvent(self, event) -> None:
        """Handle window resize events."""
        super().resizeEvent(event)
        self.update_overlay_buttons_position()

    def update_overlay_buttons_position(self) -> None:
        """Update the position of overlay buttons based on current window size."""
        size = self.history_text.viewport().size()

        margin_right = 5
        margin_top = 5
        spacing = 5

        available_width = size.width()

        search_offset = self.search_bar.width() + spacing if self.search_bar.isVisible() else 0
        load_offset = self.load_button.width() + spacing if self.load_button.isVisible() else 0
        save_offset = self.save_button.width() + spacing if self.save_button.isVisible() else 0

        self.search_button.move(
            available_width - search_offset - self.search_button.width() - margin_right,
            margin_top
        )

        vars_offset = self.vars_overlay.width() + spacing if self.vars_overlay.isVisible() else 0
        vars_button_y = self.search_button.y() + self.search_button.height() + spacing
        self.vars_button.move(
            available_width - vars_offset - self.vars_button.width() - margin_right,
            vars_button_y
        )

        save_button_y = self.vars_button.y() + self.vars_button.height() + spacing
        self.save_button.move(
            available_width - self.save_button.width() - margin_right,
            save_button_y
        )

        load_button_y = self.save_button.y() + self.save_button.height() + spacing
        self.load_button.move(
            available_width - self.load_button.width() - margin_right,
            load_button_y
        )

        delete_button_y = self.load_button.y() + self.load_button.height() + spacing
        self.delete_button.move(
            available_width - self.delete_button.width() - margin_right,
            delete_button_y
        )

        if self.vars_overlay.isVisible():
            self.vars_overlay.move(
                available_width - self.vars_overlay.width() - margin_right,
                self.vars_button.y()
            )
            self.vars_delete_button.move(
                self.vars_overlay.width() - self.vars_delete_button.width() - margin_right,
                (self.vars_overlay.height() - self.vars_delete_button.height()) // 2
            )
            self.vars_delete_button.setVisible(True)

        if self.search_bar.isVisible():
            self.search_bar.move(
                available_width - self.search_bar.width() - margin_right,
                margin_top
            )

    def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
        """Handle focus changes to hide overlays when necessary."""
        if self.vars_overlay.isVisible() and new not in {self.vars_overlay, self.vars_delete_button, self.vars_button}:
            self.vars_overlay.setVisible(False)
            self.vars_delete_button.setVisible(False)
            self.update_overlay_buttons_position()
        if self.search_bar.isVisible() and not self.search_bar.text().strip() and new != self.search_bar:
            self.toggle_search()

    def showEvent(self, event) -> None:
        """Handle the show event to position overlays correctly."""
        super().showEvent(event)
        self.update_overlay_buttons_position()

    def clear_history(self) -> None:
        """Clear the calculation history."""
        self.history_entries.clear()
        self.refresh_history()

    def toggle_vars_overlay(self) -> None:
        """Toggle the visibility of the variables overlay."""
        if self.vars_overlay.isVisible():
            self.vars_overlay.setVisible(False)
            self.vars_delete_button.setVisible(False)
        else:
            self.update_vars_overlay()
            self.vars_overlay.setVisible(True)
            self.vars_delete_button.setVisible(True)
        self.update_overlay_buttons_position()

    def update_vars_overlay(self) -> None:
        """Update the text displayed in the variables overlay."""
        if not self.variables:
            self.vars_overlay.setText("No variables assigned.")
        else:
            vars_text = ", ".join(f"{var} = {value}" for var, value in self.variables.items())
            self.vars_overlay.setText(vars_text)

    def clear_variables(self) -> None:
        """Clear all assigned variables."""
        self.variables.clear()
        self.update_vars_overlay()
        self.vars_overlay.setVisible(True)
        self.vars_delete_button.setVisible(True)

    def save_data(self) -> None:
        """Save history and variables to a file."""
        try:
            data = {
                'history': self.history_entries,
                'variables': self.variables
            }

            options = QFileDialog.Options()
            options |= QFileDialog.DontUseNativeDialog
            file_path, _ = QFileDialog.getSaveFileName(
                self,
                "Save Calculator Data",
                "",
                "JSON Files (*.json);;All Files (*)",
                options=options
            )
            if file_path:
                with open(file_path, 'w') as f:
                    json.dump(data, f, indent=4)
                QMessageBox.information(self, "Save Successful", f"Data saved to {file_path}")
        except Exception as e:
            QMessageBox.critical(self, "Save Failed", f"An error occurred while saving: {e}")

    def load_data(self) -> None:
        """Load history and variables from a file."""
        try:
            options = QFileDialog.Options()
            options |= QFileDialog.DontUseNativeDialog
            file_path, _ = QFileDialog.getOpenFileName(
                self,
                "Load Calculator Data",
                "",
                "JSON Files (*.json);;All Files (*)",
                options=options
            )
            if file_path:
                with open(file_path, 'r') as f:
                    data = json.load(f)
                if 'history' in data and 'variables' in data:
                    self.history_entries = data['history']
                    self.variables = data['variables']
                    self.refresh_history()
                    self.update_vars_overlay()
                    QMessageBox.information(self, "Load Successful", f"Data loaded from {file_path}")
                else:
                    QMessageBox.warning(self, "Invalid Data", "The selected file does not contain valid calculator data.")
        except Exception as e:
            QMessageBox.critical(self, "Load Failed", f"An error occurred while loading: {e}")

    def load_saved_data(self) -> None:
        """Load history and variables from a default save file if it exists."""
        default_save = self.script_dir / 'calculator_save.json'
        if default_save.exists():
            try:
                with default_save.open('r') as f:
                    data = json.load(f)
                self.history_entries = data.get('history', [])
                self.variables = data.get('variables', {})
                self.refresh_history()
                self.update_vars_overlay()
            except Exception as e:
                QMessageBox.warning(self, "Load Failed", f"Failed to load saved data: {e}")

    def closeEvent(self, event) -> None:
        """Save data before closing the application."""
        try:
            data = {
                'history': self.history_entries,
                'variables': self.variables
            }
            default_save = self.script_dir / 'calculator_save.json'
            with default_save.open('w') as f:
                json.dump(data, f, indent=4)
        except Exception as e:
            QMessageBox.warning(self, "Auto-Save Failed", f"Failed to auto-save data: {e}")
        event.accept()

def main() -> None:
    """Main function to run the calculator application."""
    app = QApplication([])
    calculator = CalculatorGUI()
    calculator.show()
    app.exec()

if __name__ == "__main__":
    main()