import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
from tkinter.ttk import Progressbar, Combobox
import subprocess
import os
import configparser
import webbrowser
import datetime
import threading
import json  # JSON module for processing analysis data in JSON format

# --- START Global Variables and Constants ---
VERSION = "2.0.1"            # Current version of the program, displayed in the info box and window title.
CONFIG_FILE = "options.ini"  # Name of the configuration file where program settings are stored.
LOG_FILE = "ffmpeg_log.txt"  # Name of the log file for recording FFmpeg normalization processes.
ANALYZE_LOG_FILE = "ffmpeg_analyze.txt" # Name of the log file for recording FFmpeg analysis processes.
STANDARD_LOG_FILE_SIZE_KB = 1024 # Standard size for log files in kilobytes (KB), here 1MB.
log_file_size_kb = STANDARD_LOG_FILE_SIZE_KB # Global variable that stores the current maximum log file size in KB. Initialized with the standard value.
single_log_entry_enabled = True # Global variable that controls whether only one log entry per process is saved (True) or log rolling is used (False). Default is enabled (True).
BUILD_DATE = "2024-02-28" # Build date of the program, manually set, displayed in the info box.

ffmpeg_path = "ffmpeg.exe"  # Default path to ffmpeg.exe. Assumes ffmpeg.exe is in the same directory as the script.
                               # Can be changed in the options.

# --- LUFS Presets Definition ---
LUFS_PRESETS = {
    "Standard (-10 LUFS)": "-10", # Standard LUFS target value.
    "YouTube (-14 LUFS)": "-14",   # LUFS target value for YouTube-compliant normalization.
    "Spotify (-14 LUFS)": "-14",   # LUFS target value for Spotify-compliant normalization.
    "Broadcast EBU R128 (-23 LUFS)": "-23", # LUFS target value for broadcast standards according to EBU R128.
    "Custom": "custom"    # Keyword to indicate that a custom LUFS value is used.
}
# Dictionary that stores predefined LUFS target values.
# Keys are human-readable names for the presets, displayed in the GUI.
# Values are the corresponding LUFS values as strings, passed to FFmpeg.
LUFS_PRESET_NAMES = list(LUFS_PRESETS.keys())
# List of preset names, extracted from the keys of LUFS_PRESETS.
# Used to populate the preset options in the "LUFS Preset" Combobox in the GUI.

# --- True Peak Presets Definition ---
TRUE_PEAK_PRESETS = {
    "Standard (-1 dBTP)": "-1",     # Standard True Peak target value.
    "Broadcast (-2 dBTP)": "-2",    # True Peak target value for broadcast applications.
    "No Limit (0 dBTP)": "0",      # True Peak target value 0 dBTP (no limit). Note: Can lead to clipping and should be used with caution.
    "Custom": "custom"     # Keyword for custom True Peak value.
}
# Dictionary for predefined True Peak target values (dB True Peak).
# Similar to LUFS_PRESETS, but for True Peak values.
TRUE_PEAK_PRESET_NAMES = list(TRUE_PEAK_PRESETS.keys()) # **CORRECTION: Use TRUE_PEAK_PRESETS (plural)**
# List of preset names for True Peak, for the "True Peak Preset" Combobox.
# --- END Global Variables and Constants ---

# --- START Functions for Options and Info Box ---
def load_options():
    """Loads the program settings from the configuration file 'options.ini'."""
    global ffmpeg_path, log_file_size_kb, single_log_entry_enabled # Global variables to be modified in this function.

    config = configparser.ConfigParser() # Creates a ConfigParser object to read the INI file.
    if os.path.exists(CONFIG_FILE): # Checks if the configuration file exists.
        config.read(CONFIG_FILE) # Reads the configuration file.
        if "Settings" in config: # Checks if the "Settings" section exists in the INI file.
            if "ffmpeg_path" in config["Settings"]: # Reads the FFmpeg path from the configuration, if present.
                ffmpeg_path = config["Settings"]["ffmpeg_path"]
            # --- NEW: Load Log File Size ---
            if "log_file_size_kb" in config["Settings"]: # Reads the log file size from the configuration.
                try:
                    log_file_size_kb = int(config["Settings"]["log_file_size_kb"]) # Tries to read the value as an integer.
                except ValueError: # If the value is not a valid number.
                    log_file_size_kb = STANDARD_LOG_FILE_SIZE_KB # Sets the standard value.
            else: # If the option does not exist in the INI file.
                log_file_size_kb = STANDARD_LOG_FILE_SIZE_KB # Sets the standard value.
            # --- NEW: Load Log File Size END ---
            # --- NEW: Load Single Log Entry Option ---
            if "single_log_entry_enabled" in config["Settings"]: # Reads the state of the single log entry option.
                single_log_entry_enabled = config["Settings"]["single_log_entry_enabled"].lower() == "true" # Reads as string and converts to boolean (case-insensitive).
            else: # If the option does not exist in the INI file.
                single_log_entry_enabled = True # Sets the standard value (ON).
            # --- NEW: Load Single Log Entry Option END ---

    else: # If the configuration file does not exist.
        save_options() # Creates a new configuration file with default settings.

    # --- Ensure that log_file_size_kb always has a valid value ---
    if not isinstance(log_file_size_kb, int) or log_file_size_kb <= 0: # Validation: size must be an integer and greater than 0.
        log_file_size_kb = STANDARD_LOG_FILE_SIZE_KB # Sets to standard value if invalid.
    # --- Validation End ---

    print(f"Log file size on loading: {log_file_size_kb} KB") # DEBUG output in the console.
    print(f"Single log entry option on loading: {single_log_entry_enabled}") # DEBUG output in the console.


def save_options():
    """Saves the program settings to the configuration file 'options.ini'."""
    config = configparser.ConfigParser() # Creates a ConfigParser object to write the INI file.
    config["Settings"] = {"ffmpeg_path": ffmpeg_path, # Saves the FFmpeg path.
                              # --- NEW: Save Log File Size ---
                              "log_file_size_kb": log_file_size_kb, # Saves the log file size.
                              # --- NEW: Save Log File Size END ---
                              # --- NEW: Save Single Log Entry Option ---
                              "single_log_entry_enabled": str(single_log_entry_enabled) # Saves the state of the single log entry option as a string ("True" or "False").
                              # --- NEW: Save Single Log Entry Option END ---
                              }
    with open(CONFIG_FILE, "w") as configfile: # Opens the configuration file in write mode ('w').
        config.write(configfile) # Writes the configuration to the file.

def show_options_dialog():
    """Displays a dialog window for the program settings (options)."""
    options_window = tk.Toplevel(window) # Creates a new window as a Toplevel window, above the main window.
    options_window.title("Options") # Sets the title of the options window.
    options_window.transient(window) # Sets the options window as dependent on the main window (always appears above it).
    options_window.grab_set() # Focuses the options window and blocks interaction with the main window until it is closed (modal dialog).

    # --- Centering the dialog window ---
    window_width = window.winfo_width() # Width of the main window.
    window_height = window.winfo_height() # Height of the main window.
    window_x = window.winfo_rootx() # X-position of the main window on the screen.
    window_y = window.winfo_rooty() # Y-position of the main window on the screen.
    dialog_width = 450 # Width of the options dialog.
    dialog_height = 220 # Height of the options dialog (slightly increased for LabelFrames).
    x_position = window_x + (window_width - dialog_width) // 2 # Calculates the X-position for centering.
    y_position = window_y + (window_height - dialog_height) // 2 # Calculates the Y-position for centering.
    options_window.geometry(f"+{x_position}+{y_position}") # Sets the position of the options window.
    # --- Centering End ---

    # --- Options Frames (LabelFrames) ---
    ffmpeg_frame = tk.LabelFrame(options_window, text="FFmpeg Path", padx=10, pady=5) # LabelFrame for FFmpeg Path options with border and text.
    ffmpeg_frame.grid(row=0, column=0, columnspan=3, padx=10, pady=10, sticky="ew") # Placement in the grid, spans 3 columns, horizontal expansion, padding.

    log_frame = tk.LabelFrame(options_window, text="Log File Settings", padx=10, pady=5) # LabelFrame for Log options.
    log_frame.grid(row=1, column=0, columnspan=3, padx=10, pady=10, sticky="ew") # Placement in the grid.
    # --- Options Frames End ---

    ffmpeg_path_label = tk.Label(ffmpeg_frame, text="Folder containing ffmpeg.exe:") # Label for the FFmpeg path input field.
    ffmpeg_path_label.grid(row=0, column=0, padx=10, pady=5, sticky="w") # Placement in the grid, left-aligned, padding.
    ffmpeg_path_entry = tk.Entry(ffmpeg_frame, width=50) # Input field for the FFmpeg path.
    ffmpeg_path_entry.grid(row=0, column=1, padx=10, pady=5, sticky="ew") # Placement in the grid, horizontal expansion, padding.

    # --- NEW: Set path in input field (different behavior on first start) ---
    if ffmpeg_path == "ffmpeg.exe": # Checks if the path is still the default value (-> first start).
        program_path = os.path.dirname(os.path.abspath(__file__)) # Determines the path of the current Python script.
        ffmpeg_path_entry.insert(0, program_path) # Sets the program path as the default value in the input field.
    else: # Path is already saved in options.ini -> load saved path.
        ffmpeg_path_entry.insert(0, ffmpeg_path) # Sets the saved FFmpeg path in the input field.
    # --- NEW: Set path in input field END ---

    # --- NEW: Checkbox for Single Log Entry ---
    single_log_check_var = tk.BooleanVar(value=single_log_entry_enabled) # BooleanVar to store the state of the checkbox, initialized with the current value of the global variable.
    single_log_check = tk.Checkbutton(log_frame, text="Save only one log entry per process", variable=single_log_check_var, command=lambda: update_log_size_state(single_log_check_var, log_size_entry)) # Checkbox for the single log option. `command` calls `update_log_size_state` function on click.
    single_log_check.grid(row=1, column=0, columnspan=2, padx=10, pady=(5, 5), sticky="w") # Placement in the grid, spans 2 columns, padding.
    # --- NEW: Checkbox End ---

    log_size_label = tk.Label(log_frame, text="Maximum Log File Size (KB):") # Label for the log file size input field.
    log_size_label.grid(row=0, column=0, padx=10, pady=5, sticky="w") # Placement in the grid, left-aligned, padding.
    log_size_entry = tk.Entry(log_frame, width=10) # Input field for the maximum log file size.
    log_size_entry.grid(row=0, column=1, padx=10, pady=5, sticky="w") # Placement in the grid, left-aligned, padding.
    log_size_entry.insert(0, str(log_file_size_kb)) # Sets the current value of the log file size in the input field.

    # --- NEW: Function to enable/disable the Log Size field ---
    def update_log_size_state(check_var, size_entry_field):
        """Enables or disables the log size input field based on the state of the checkbox."""
        if check_var.get(): # If the checkbox is checked (single log entry active).
            size_entry_field.config(state=tk.DISABLED) # Disables the input field for log file size, as it is not relevant in this mode.
        else: # If the checkbox is unchecked (log rolling active).
            size_entry_field.config(state=tk.NORMAL) # Enables the input field to set the maximum log file size.
    # --- NEW: Function End ---

    # --- Set initial state of the Log Size field ---
    update_log_size_state(single_log_check_var, log_size_entry) # Calls the function on startup to set the initial state of the input field based on the checkbox.
    # --- Initial state End ---


    def browse_ffmpeg_path():
        """Opens a file selection dialog to select the FFmpeg path."""
        path = filedialog.askdirectory(title="Select FFmpeg Path") # Opens a folder selection dialog.
        if path: # If a path was selected.
            ffmpeg_path_entry.delete(0, tk.END) # Clears the input field.
            ffmpeg_path_entry.insert(0, path) # Sets the selected path in the input field.

    browse_button = tk.Button(ffmpeg_frame, text="Browse", command=browse_ffmpeg_path) # Button to open the FFmpeg path selection dialog.
    browse_button.grid(row=0, column=2, padx=10, pady=5) # Placement in the grid, padding.

    def save_and_close_options():
        """Saves the options, validates the FFmpeg path, and closes the options window."""
        global ffmpeg_path, log_file_size_kb, single_log_entry_enabled # Global variables to be modified in this function.
        ffmpeg_path_entry_value = ffmpeg_path_entry.get() # Reads the value from the FFmpeg path input field.
        ffmpeg_exe_path = os.path.join(ffmpeg_path_entry_value, "ffmpeg.exe") # Creates the full path to ffmpeg.exe.

        if not os.path.exists(ffmpeg_exe_path): # Checks if ffmpeg.exe exists at the specified path.
            messagebox.showerror("Error", "Invalid FFmpeg Path!\n'ffmpeg.exe' was not found in the specified folder.", parent=options_window) # Displays an error message if ffmpeg.exe is not found.
            return # Ends the function without saving the options.

        # --- Check if FFmpeg is executable ---
        try:
            subprocess.run([ffmpeg_exe_path, "-version"], capture_output=True, check=True) # Tries to execute FFmpeg and retrieve the version. `check=True` raises an exception if FFmpeg exits with an error code.
        except (FileNotFoundError, subprocess.CalledProcessError): # Catches errors if FFmpeg is not found or not executable.
            messagebox.showerror("Error", "FFmpeg is not executable or corrupted.\nPlease check the path.", parent=options_window) # Displays an error message.
            return # Ends the function.
        # --- Check End ---

        ffmpeg_path = ffmpeg_path_entry_value # Updates the global variable `ffmpeg_path` with the new path.

        # --- NEW: Save Log File Size (only if single log is not active) ---
        if not single_log_check_var.get(): # Only if the single log option is NOT active.
            try:
                log_file_size_kb_input = int(log_size_entry.get()) # Reads the value from the log size input field and converts it to an integer.
                if log_file_size_kb_input <= 0: # Validation: size must be positive.
                    messagebox.showerror("Error", "Invalid Log File Size!\nPlease enter a positive number greater than 0.", parent=options_window) # Displays an error message if the size is invalid.
                    return # Ends the function.
                log_file_size_kb = log_file_size_kb_input # Updates the global variable `log_file_size_kb`.
            except ValueError: # Error if the input is not a number.
                messagebox.showerror("Error", "Invalid Log File Size!\nPlease enter an integer (in KB).", parent=options_window) # Displays an error message if the input is not a number.
                return # Ends the function.
        # --- NEW: Save Log File Size END ---

        # --- NEW: Save Single Log Entry Option ---
        single_log_entry_enabled = single_log_check_var.get() # Saves the state of the checkbox in the global variable `single_log_entry_enabled`.
        # --- NEW: Save Single Log Entry Option END ---

        save_options() # Saves the options to the configuration file.
        options_window.destroy() # Closes the options window.

    save_button = tk.Button(options_window, text="Save", command=save_and_close_options) # Button to save the options and close the dialog.
    save_button.grid(row=2, column=0, columnspan=3, pady=10) # Placement in the grid, below the LabelFrames.

    options_window.columnconfigure(1, weight=1) # Configures column 1 of the options window to expand horizontally (for responsive layout).
    options_window.wait_window(options_window) # Waits until the options window is closed before the main program continues (makes the dialog modal).

def show_info_box():
    """Displays an info window with program version and contact information."""
    info_window = tk.Toplevel(window) # Creates a new Toplevel window for the info box.
    info_window.title("About") # Sets the title of the info window.
    info_window.transient(window) # Sets the info window as dependent on the main window.
    info_window.grab_set() # Focuses the info window and blocks interaction with the main window.

    info_frame = tk.LabelFrame(info_window, text="About melcom's FFmpeg Audio Normalizer", padx=10, pady=10) # LabelFrame for the content of the info box.
    info_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) # Placement in the window, fills in both directions, expands if the window gets larger.

    info_text = (
        f"melcom's FFmpeg Audio Normalizer v{VERSION}\n\n" # Displays the program name and version.
        f"Build Date: {BUILD_DATE}\n\n" # Displays the build date.
        "This tool normalizes audio files using FFmpeg.\n" # Short description of the program.
        "It supports various audio formats and LUFS/True-Peak target values.\n\n" # Further description of the functionality.
        "Author: melcom (Andreas Thomas Urban)\n" # Author of the program.
        "E-Mail: melcom [@] vodafonemail.de\n\n" # Contact email.
        "Websites:\n" # Heading for website links.
    )
    info_label = tk.Label(info_frame, text=info_text, justify=tk.LEFT) # Label for the main info text, left-aligned.
    info_label.pack(padx=10, pady=5, anchor="nw") # Placement in the frame, padding, aligned top left.

    website_label_1 = tk.Label(info_frame, text="melcom-music.de", fg="blue", cursor="hand2") # Label for the first website, blue, hand cursor on hover.
    website_label_1.pack(anchor="w", padx=20, pady=2) # Placement, left-aligned, indented, padding.
    website_label_1.bind("<Button-1>", lambda e: webbrowser.open("https://www.melcom-music.de")) # Binds click event to the label to open the website in the browser.

    website_label_2 = tk.Label(info_frame, text="scenes.at/melcom", fg="blue", cursor="hand2") # Label for the second website.
    website_label_2.pack(anchor="w", padx=20, pady=2) # Placement.
    website_label_2.bind("<Button-1>", lambda e: webbrowser.open("https://scenes.at/melcom")) # Binds click event.

    opensource_label = tk.Label(info_frame, text=f"\nOpen Source Software (free to use)\nCopyright © {datetime.datetime.now().year} Andreas Thomas Urban") # Label for Open Source notice and copyright, current year.
    opensource_label.pack(pady=10, anchor="center") # Placement, centered, padding.

    ok_button = tk.Button(info_window, text="OK", command=info_window.destroy) # OK button to close the info window.
    ok_button.pack(pady=10, anchor="center") # Placement, centered, padding.

    info_window.wait_window(info_window) # Waits until the info window is closed.
# --- END Functions for Options and Info Box ---

# --- START Functions for File Selection, Audio Analysis, and GUI Update after Analysis ---
def browse_file():
    """Opens a file selection dialog to select an audio file."""
    file_path = filedialog.askopenfilename( # Opens an "Open File" dialog.
        defaultextension=".wav", # Sets the default file extension to .wav.
        filetypes=[ # Defines filters for file types in the dialog.
            ("Audio Files", "*.wav *.mp3 *.flac *.aac *.ogg *.m4a"), # Filter for all supported audio formats.
            ("WAV Files", "*.wav"), # Filter for WAV files only.
            ("MP3 Files", "*.mp3"), # Filter for MP3 files only.
            ("FLAC Files", "*.flac"), # Filter for FLAC files only.
            ("AAC Files", "*.aac *.m4a"), # Filter for AAC and M4A files.
            ("OGG Files", "*.ogg"), # Filter for OGG files only.
            ("All Files", "*.*") # Filter for all file types.
        ],
        title="Select Audio File" # Sets the title of the dialog window.
    )
    if file_path: # If a file path was selected.
        file_entry.delete(0, tk.END) # Clears the file path input field.
        file_entry.insert(0, file_path) # Sets the selected file path in the input field.

def analyze_audio():
    """Starts the audio analysis of the selected file in a separate thread."""
    file = file_entry.get() # Reads the file path from the input field.

    if not file: # Checks if a file is selected.
        messagebox.showerror("Error", "Please select an audio file first before analyzing it.", parent=window) # Error message if no file is selected.
        return # Ends the function.

    # --- GUI elements for analysis process state ---
    analyze_button.config(state=tk.DISABLED) # Disables the "Analyze Audio File" button during analysis.
    start_button.config(state=tk.DISABLED) # Disables the "Start Normalization" button during analysis.
    window.config(cursor="wait") # Changes the mouse cursor to "Wait" (hourglass/circle).
    progressbar.grid(row=4, column=0, columnspan=3, sticky="ew", padx=20, pady=(0,10)) # Displays the progress bar in the grid.
    progressbar.start() # Starts the animation of the progress bar (indeterminate progress).
    # --- GUI elements End ---

    process_info_field.config(state=tk.NORMAL) # Enables the process info text field to write text into it.
    process_info_field.delete("1.0", tk.END) # Clears the process info text field.
    process_info_field.insert(tk.END, f"Audio analysis started...\nFile: {os.path.basename(file)}\nPlease wait, analysis is running...") # Adds a start message to the process info text field.
    process_info_field.config(state=tk.DISABLED) # Disables the process info text field again to prevent manual changes.

    log_entry_start = f"======================== {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ========================\n" # Start separator for the log entry with timestamp.
    log_entry_start += f"Audio file analysis started for file: {file}\n" # Log message: Analysis started.
    log_entry_start += "FFmpeg analysis command is being generated...\n" # Log message: Command is being generated.
    append_analyze_log(log_entry_start) # Writes the start message to the analysis log file.

    analyze_thread = threading.Thread(target=audio_analyze_thread_function, args=(file,)) # Creates a new thread for audio analysis. `target` is the function to be executed in the thread, `args` are the arguments for the function.
    analyze_thread.start() # Starts the analysis thread.

def audio_analyze_thread_function(file):
    """Executes the audio analysis with FFmpeg in a separate thread."""
    ffmpeg_analyze_command = [
        os.path.join(ffmpeg_path, "ffmpeg.exe"), # Assemble path to ffmpeg.exe.
        "-i", file, # Specify input file.
        "-af", "loudnorm=print_format=json", # Activate audio filter 'loudnorm' for analysis, set output format to JSON.
        "-f", "null", "-" # Set output format to 'null' (do not create output file), set output destination to '-' (standard output/standard error).
    ]
    log_entry_command = "FFmpeg Analysis Command:\n" + " ".join(ffmpeg_analyze_command) + "\n\n" # Formats the FFmpeg command for the log entry.
    append_analyze_log(log_entry_command) # Writes the generated FFmpeg command to the analysis log file.

    try:
        analysis_result = subprocess.run(ffmpeg_analyze_command, check=True, capture_output=True, text=True) # Executes the FFmpeg analysis command. `check=True` raises an exception if FFmpeg exits with an error code. `capture_output=True` captures standard output and standard error. `text=True` decodes the output as text.
        log_output = analysis_result.stderr # FFmpeg writes the analysis data to standard error (stderr).
        # --- DEBUGGING: OUTPUT RAW FFmpeg OUTPUT (for developers) ---
        print("--- RAW FFmpeg OUTPUT (stderr) ---") # Marker in the console for debug output.
        print(log_output) # Outputs the raw FFmpeg output to the console (useful for troubleshooting).
        print("--- RAW FFmpeg OUTPUT END ---") # Marker in the console for debug output end.
        # --- DEBUGGING END ---
        append_analyze_log("FFmpeg Analysis Output:\n" + log_output + "\n") # Writes the FFmpeg output to the analysis log file.

        # --- Parse JSON output and extract values ---
        try:
            start_index = log_output.find("{") # Searches for the beginning of the JSON block in the FFmpeg output.
            end_index = log_output.rfind("}") + 1 # Searches for the end of the JSON block.
            json_string = log_output[start_index:end_index] # Extracts the JSON string from the FFmpeg output.
            json_output = json.loads(json_string) # Parses the JSON string into a Python dictionary.

            input_i = json_output.get("input_i") # Extracts the value for "input_i" (Integrated Loudness) from the JSON.
            input_tp = json_output.get("input_tp") # Extracts the value for "input_tp" (True Peak).
            lra = json_output.get("input_lra") # Extracts the value for "input_lra" (Loudness Range).
            analysis_results = { # Stores the extracted analysis results in a dictionary.
                "input_i": input_i,
                "input_tp": input_tp,
                "lra": lra
            }
            window.after(0, update_gui_on_analyze_completion, "Success", file, analysis_results) # Calls the GUI update function in the main thread to display the results. `window.after(0, ...)` ensures that the function is executed in the Tkinter main thread (important for GUI operations from a thread).
                                                                                                    # Passes the status "Success", the filename, and the analysis results.

        except json.JSONDecodeError: # Error when parsing JSON.
            error_message_json = "Error evaluating FFmpeg analysis data (JSON format).\nPlease check the FFmpeg output in the analysis log file." # Error message for JSON parsing errors.
            append_analyze_log("ERROR: " + error_message_json + "\n") # Writes the error message to the analysis log file.
            window.after(0, update_gui_on_analyze_completion, "Error", file, error_message_json) # Calls the GUI update function with the status "Error" and the error message.

        # ... (Error handling within audio_analyze_thread_function) ...
    except subprocess.CalledProcessError as e: # Error when executing FFmpeg.
        error_message_ffmpeg = f"FFmpeg error during analysis:\nReturn Code: {e.returncode}\n{e.stderr}" # Formats an error message with the return code and error output from FFmpeg.
        append_analyze_log("ERROR: " + error_message_ffmpeg + "\n") # Writes the FFmpeg error message to the analysis log file.
        window.after(0, update_gui_on_analyze_completion, "Error", file, error_message_ffmpeg) # Calls the GUI update function with the status "Error" and the FFmpeg error message.
    except FileNotFoundError: # ffmpeg.exe not found.
        error_message_ffmpeg_path = f"ffmpeg.exe not found!\nPlease check the FFmpeg path in the options (File -> Options)." # Error message for the case where ffmpeg.exe was not found.
        append_analyze_log("ERROR: " + error_message_ffmpeg_path + "\n") # Writes the 'File not found' error message to the analysis log file.
        window.after(0, update_gui_on_analyze_completion, "FileNotFound", file, error_message_ffmpeg_path) # Calls the GUI update function with the status "FileNotFound" and the error message.
    except Exception as e: # Unexpected error.
        error_message_unknown = f"Unexpected error during audio analysis: {e}" # Error message for unexpected errors.
        append_analyze_log("ERROR: " + error_message_unknown + "\n") # Writes the unknown error message to the analysis log file.
        window.after(0, update_gui_on_analyze_completion, "UnknownError", file, error_message_unknown) # Calls the GUI update function with the status "UnknownError" and the error message.
    finally: # Is always executed, regardless of whether an error occurred or not.
        window.after(0, update_gui_on_analyze_completion) # Calls the GUI update function without status and results to reset GUI elements (e.g., reactivate buttons, hide progress bar).

def update_gui_on_analyze_completion(status=None, file=None, result=None):
    """Updates the GUI after audio analysis is complete (executed in the main thread)."""
    analyze_button.config(state=tk.NORMAL) # Reactivates the "Analyze Audio File" button.
    start_button.config(state=tk.NORMAL) # Reactivates the "Start Normalization" button.
    window.config(cursor="") # Sets the mouse cursor back to default.
    progressbar.stop() # Stops the animation of the progress bar.
    progressbar.grid_forget() # Hides the progress bar from the grid.
    progress_label.grid_forget() # Hides the progress label (currently not visible in the code, could be removed if not used).

    process_info_field.config(state=tk.NORMAL) # Enables the process info text field.
    process_info_field.delete("1.0", tk.END) # Clears the process info text field.
    if status == "Success": # Analysis completed successfully.
        analysis_results = result # Gets the analysis results from the `result` parameter.
        process_info_field.insert(tk.END, f"Analysis results for: {os.path.basename(file)}\n\n") # Adds a heading with the filename to the process info text field.
        process_info_field.insert(tk.END, f"Integrated Loudness (LUFS):  {analysis_results['input_i']} LUFS\n") # Displays the Integrated Loudness (LUFS) in the text field.
        process_info_field.insert(tk.END, f"True Peak:                  {analysis_results['input_tp']} dBTP\n") # Displays the True Peak value in the text field.
        process_info_field.insert(tk.END, f"Loudness Range (Input LRA): {analysis_results['lra']} LU\n") # Displays the Loudness Range (LRA) in the text field.

        # --- NEW: Hint text for analysis log file ---
        hint_text_analyze = "Note:\nYou can find the audio analysis process in the file: ffmpeg_analyze.txt" # Hint text referring to the analysis log file.
        # --- NEW: Hint text End ---

        analyze_message = ( # Formats a message for the MessageBox with the analysis results.
            f"Analysis completed for:\n{os.path.basename(file)}\n\n" # Heading with filename.
            f"Integrated Loudness (LUFS):  {analysis_results['input_i']} LUFS\n" # LUFS value.
            f"True Peak:                  {analysis_results['input_tp']} dBTP\n" # True Peak value.
            f"Loudness Range (Input LRA): {analysis_results['lra']} LU\n\n" # LRA value.
            f"{hint_text_analyze}" # Adds the hint text for the analysis log file.
        )
        messagebox.showinfo("Analysis completed", analyze_message, parent=window) # Displays a MessageBox with the analysis results.

    elif status == "Error": # FFmpeg error during analysis.
        process_info_field.insert(tk.END, result) # Displays the error message in the process info text field.
        messagebox.showerror("Error", result, parent=window) # Displays a MessageBox with the error message.
    elif status == "FileNotFound": # ffmpeg.exe not found.
        process_info_field.insert(tk.END, error_message) # Displays the error message in the process info text field.
        messagebox.showerror("Error", error_message, parent=window) # Displays a MessageBox with the error message.
    elif status == "UnknownError": # Unexpected error during analysis.
        process_info_field.insert(tk.END, result) # Displays the error message in the process info text field.
        messagebox.showerror("Error", "Unexpected error during analysis!", parent=window) # Displays a MessageBox with a generic error message.
    process_info_field.config(state=tk.DISABLED) # Disables the process info text field again.

    window.after(100, lambda: window.focus_force()) # Sets the focus back to the main window (after 100ms delay).
    window.after(100, lambda: window.update()) # Updates the GUI (after 100ms delay). `window.update()` forces the GUI to redraw, but is probably not strictly necessary here.
# --- END Functions for File Selection, Audio Analysis, and GUI Update after Analysis ---

# --- START Functions for Normalization, GUI Update after Normalization, and Exit ---
def start_normalization():
    """Starts the normalization process of the selected audio file in a separate thread."""
    file = file_entry.get() # Reads the file path from the input field.
    output_format = output_format_var.get() # Reads the selected output format from the combobox.
    lufs_preset_name = lufs_preset_var.get() # Reads the name of the selected LUFS preset from the combobox.
    target_lufs = "" # Initializes the variable for the target LUFS value.
    true_peak_preset_name = true_peak_preset_var.get() # Reads the name of the selected True Peak preset from the combobox.
    true_peak_value_str = true_peak_entry.get() # Reads the entered True Peak value as a string from the input field.
    target_true_peak = "" # Initializes the variable for the target True Peak value.

    if not file: # Checks if a file is selected.
        messagebox.showerror("Error", "Please select an audio file first.") # Error message if no file is selected.
        return # Ends the function.

    # --- Determine Target LUFS Value ---
    if lufs_preset_name == "Custom": # Custom preset selected.
        target_lufs = lufs_entry.get() # Uses the value from the LUFS input field.
        if not target_lufs: # If the field is empty.
            target_lufs = "-10" # Sets the default value -10 LUFS if the input field is empty.
    else: # Predefined preset selected.
        target_lufs = LUFS_PRESETS[lufs_preset_name] # Gets the LUFS value from the LUFS_PRESETS dictionary based on the selected preset name.
    # --- Target LUFS Value End ---

    # --- Determine and Validate Target True Peak Value ---
    if true_peak_preset_name == "Custom": # Custom preset selected.
        target_true_peak = true_peak_entry.get() # Uses the value from the True Peak input field.
    else: # Predefined preset selected.
        target_true_peak = TRUE_PEAK_PRESETS[true_peak_preset_name] # Gets the True Peak value from the TRUE_PEAK_PRESETS dictionary.

    if true_peak_preset_name == "Custom": # Validation only necessary for custom value.
        try:
            target_true_peak = float(target_true_peak) # Tries to convert the entered value to a floating-point number.
        except ValueError: # Error if the entered value is not a valid number.
            messagebox.showerror("Error", "Invalid True Peak Value!\nPlease enter a number (e.g., -1 or -2.5).", parent=window) # Error message if the True Peak value is invalid.
            return # Ends the function.
    # --- Target True Peak Value End ---

    # --- Output Format Options ---
    format_options = { # Dictionary that stores format options for different output formats.
        "WAV": {"extension": ".wav", "codec": "pcm_f32le"}, # WAV format options: file extension and codec.
        "MP3": {"extension": ".mp3", "codec": "libmp3lame"}, # MP3 format options.
        "FLAC": {"extension": ".flac", "codec": "flac"},   # FLAC format options.
        "AAC": {"extension": ".m4a", "codec": "aac"},     # AAC format options.
        "OGG": {"extension": ".ogg", "codec": "libvorbis"}  # OGG format options.
    }
    selected_format = format_options[output_format] # Gets the format options for the selected output format from the dictionary.
    output_file_name_without_extension = os.path.splitext(file)[0] + "-Normalized" # Creates the output filename by removing the file extension of the input file and appending "-Normalized".
    output_file = output_file_name_without_extension + selected_format["extension"] # Adds the file extension of the selected format to the output filename.
    # --- Output Format Options End ---

    # --- File Overwrite Dialog ---
    if os.path.exists(output_file): # Checks if the output file already exists.
        answer = messagebox.askyesnocancel( # Displays a dialog to confirm overwriting (Yes/No/Cancel).
            "File exists", # Title of the dialog.
            f"The file '{os.path.basename(output_file)}' already exists.\nDo you want to overwrite it?", # Message in the dialog.
            default=messagebox.NO, # Sets "No" as the default selection.
            icon=messagebox.WARNING, # Displays a warning icon in the dialog.
            parent=window # Sets the main window as the parent window for the dialog.
        )
        if answer is True: # User selected "Yes" (overwrite).
            pass # Continue with overwriting.
        elif answer is False: # User selected "No" (do not overwrite).
            return # Abort normalization process.
        elif answer is None: # User selected "Cancel".
            return # Abort normalization process.
    # --- File Overwrite Dialog End ---

    # --- FFmpeg Command for Normalization ---
    ffmpeg_command = [
        os.path.join(ffmpeg_path, "ffmpeg.exe"), # Assemble path to ffmpeg.exe.
        "-y", # Option "-y": Overwrites output files without asking.
        "-i", file, # Specify input file.
        "-af", f"loudnorm=I={target_lufs}:TP={target_true_peak}", # Activate audio filter 'loudnorm' and set target LUFS and True Peak values.
        "-ar", "48000", # Set sample rate to 48kHz.
        "-ac", "2", # Set audio channels to 2 (Stereo).
        "-c:a", selected_format["codec"], # Set audio codec for the output format, retrieved from the format options.
        output_file # Specify output file.
    ]
    # --- FFmpeg Command End ---

    # --- GUI Elements for Normalization Process State ---
    start_button.config(state=tk.DISABLED) # Disables the "Start Normalization" button during normalization.
    analyze_button.config(state=tk.DISABLED) # Disables the "Analyze Audio File" button during normalization.
    window.config(cursor="wait") # Changes the mouse cursor to "Wait".
    progressbar.grid(row=4, column=0, columnspan=3, sticky="ew", padx=20, pady=(0,10)) # Displays the progress bar in the grid.
    progressbar.start() # Starts the animation of the progress bar.
    # --- GUI Elements End ---

    process_info_field.config(state=tk.NORMAL) # Enables the process info text field.
    process_info_field.delete("1.0", tk.END) # Clears the process info text field.
    process_info_field.insert(tk.END, f"Normalization started...\nFile: {os.path.basename(file_entry.get())}\nTarget LUFS: {target_lufs} LUFS\nTrue Peak: {target_true_peak} dBTP\nOutput format: {output_format}\nLUFS Preset: {lufs_preset_name}\nTP Preset: {true_peak_preset_name}\n Please wait, the process is running...") # Sets a start message with process information in the process info text field.
    process_info_field.config(state=tk.DISABLED) # Disables the process info text field again.
    process_info_field.grid() # Ensures that the text field is displayed in the grid layout (redundant, as it was already placed in the grid, but doesn't hurt).

    log_entry_start = f"======================== {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ========================\n" # Start separator for the log entry with timestamp.
    log_entry_start += f"Normalization started for file: {file}\n" # Log message: Normalization started.
    log_entry_start += f"Target LUFS: {target_lufs} LUFS\n" # Log message: Target LUFS value.
    log_entry_start += f"True Peak: {target_true_peak} dBTP\n" # Log message: Target True Peak value.
    log_entry_start += f"Output format: {output_format}\n" # Log message: Selected output format.
    log_entry_start += f"LUFS Preset: {lufs_preset_name}\n" # Log message: Selected LUFS preset.
    log_entry_start += f"TP Preset: {true_peak_preset_name}\n" # Log message: Selected True Peak preset.
    log_entry_start += "FFmpeg Command:\n" + " ".join(ffmpeg_command) + "\n\n" # Log message: FFmpeg command.
    append_log(log_entry_start) # Writes the start message and the FFmpeg command to the normalization log file.

    normalize_thread = threading.Thread(target=normalize_audio_thread_function, args=(ffmpeg_command, output_file)) # Creates a new thread for the normalization function.
    normalize_thread.start() # Starts the normalization thread.

def normalize_audio_thread_function(ffmpeg_command, output_file):
    """Executes the audio normalization with FFmpeg in a separate thread."""
    try:
        result = subprocess.run(ffmpeg_command, check=True, capture_output=True, text=True) # Executes the FFmpeg normalization command.
        log_output = result.stdout + "\n" + result.stderr # Combines standard output and standard error from FFmpeg for the log file.
        append_log(log_output) # Writes the FFmpeg output to the normalization log file.
        window.after(0, update_gui_on_normalization_completion, "Success", output_file) # Calls the GUI update function in the main thread to report success.
    except subprocess.CalledProcessError as e: # Error when executing FFmpeg.
        error_message = f"FFmpeg Error:\nReturn Code: {e.returncode}\n{e.stderr}" # Formats an error message with the return code and error output from FFmpeg.
        append_log("ERROR: " + error_message + "\n") # Writes the FFmpeg error message to the normalization log file.
        window.after(0, update_gui_on_normalization_completion, "Error", output_file, error_message) # Calls the GUI update function with the status "Error" and the error message.
    except FileNotFoundError: # ffmpeg.exe not found.
        error_message = f"ffmpeg.exe not found!\nPlease check the FFmpeg path in the options (File -> Options)." # Error message for the case where ffmpeg.exe was not found.
        append_log("ERROR: " + error_message + "\n") # Writes the 'File not found' error message to the normalization log file.
        window.after(0, update_gui_on_normalization_completion, "FileNotFound", output_file, error_message) # Calls the GUI update function with the status "FileNotFound" and the error message.
    except Exception as e: # Unexpected error.
        error_message = f"Unexpected error during normalization: {e}" # Error message for unexpected errors.
        append_log("ERROR: " + error_message + "\n") # Writes the unknown error message to the normalization log file.
        window.after(0, update_gui_on_normalization_completion, "UnknownError", output_file, error_message) # Calls the GUI update function with the status "UnknownError" and the error message.
    finally: # Is always executed, regardless of whether an error occurred or not.
        window.after(0, update_gui_on_normalization_completion) # Calls the GUI update function without status to reset GUI elements.

def update_gui_on_normalization_completion(status=None, output_file=None, error_message=None):
    """Updates the GUI after normalization is complete (executed in the main thread)."""
    start_button.config(state=tk.NORMAL) # Reactivates the "Start Normalization" button.
    analyze_button.config(state=tk.NORMAL) # Reactivates the "Analyze Audio File" button.
    window.config(cursor="") # Sets the mouse cursor back to default.
    progressbar.stop() # Stops the animation of the progress bar.
    progressbar.grid_forget() # Hides the progress bar from the grid.
    progress_label.grid_forget() # Hides the progress label.

    process_info_field.config(state=tk.NORMAL) # Enables the process info text field.
    process_info_field.delete("1.0", tk.END) # Clears the process info text field.
    if status == "Success": # Normalization completed successfully.
        process_info_field.insert(tk.END, f"Normalization completed!\nFile: {os.path.basename(file_entry.get())}\nOutput file created:\n{output_file}") # Success message in the process info text field.
        output_file_name_only_file = os.path.basename(output_file)  # Extracts only the filename of the output file.
        hint_text = "Note:\nYou can find the normalization process in the file: ffmpeg_log.txt" # Hint text about the log file.
        messagebox.showinfo( # Displays a success MessageBox.
            "Success", # Title of the MessageBox.
            f"Normalization completed!\n\nOutput file:\n{output_file_name_only_file}\n\n{hint_text}", # Message in the MessageBox.
            parent=window # Sets the main window as the parent window for the MessageBox.
        )
    elif status == "Error": # FFmpeg error during normalization.
        process_info_field.insert(tk.END, error_message) # Displays the error message in the process info text field.
        messagebox.showerror("Error", error_message, parent=window) # Displays a MessageBox with the error message.
    elif status == "FileNotFound": # ffmpeg.exe not found.
        process_info_field.insert(tk.END, error_message) # Displays the error message in the process info text field.
        messagebox.showerror("Error", error_message, parent=window) # Displays a MessageBox with the error message.
    elif status == "UnknownError": # Unexpected error during normalization.
        process_info_field.insert(tk.END, error_message) # Displays the error message in the process info text field.
        messagebox.showerror("Error", "Unexpected error!", parent=window) # Displays a MessageBox with a generic error message.
    process_info_field.config(state=tk.DISABLED) # Disables the process info text field again.

    window.after(100, lambda: window.focus_force()) # Sets the focus back to the main window.
    window.after(100, lambda: window.update()) # Updates the GUI.

def exit_program():
    """Exits the program."""
    window.destroy() # Destroys the main window and ends the Tkinter mainloop.
# --- END Functions for Normalization, GUI Update after Normalization, and Exit ---

# --- START Functions for Log File Handling ---
def append_log(text):
    """Writes text to the log file 'ffmpeg_log.txt' (with size limit and rolling)."""
    _append_log_with_rolling(LOG_FILE, text) # Calls the helper function for writing to the log file, passing the filename and the text to be written.

def append_analyze_log(text):
    """Writes text to the log file for analysis 'ffmpeg_analyze.txt' (with size limit and rolling)."""
    _append_log_with_rolling(ANALYZE_LOG_FILE, text) # Calls the helper function for the analysis log file.

def _append_log_with_rolling(log_file_name, text):
    """Helper function for writing to a log file with size limit and rolling."""
    try:
        log_file_path = log_file_name # Sets the log file path to the passed filename.
        log_size_limit_bytes = log_file_size_kb * 1024 # Calculates the size limit for the log file in bytes (KB * 1024).

        # --- NEW: Check Single Log Entry Option ---
        if single_log_entry_enabled: # If the single log entry option is enabled.
            if os.path.exists(log_file_path): # Checks if the log file already exists.
                with open(log_file_path, "w") as logfile_truncate: # Opens the log file in write mode "w" (truncate mode), which deletes the file content.
                    pass # The file is emptied by opening it in "w" mode.
        else: # If the single log entry option is NOT active -> Normal Log Rolling Logic.
            # --- Check Log File Size before writing ---
            if os.path.exists(log_file_path) and os.path.getsize(log_file_path) >= log_size_limit_bytes: # Checks if the log file exists and its size exceeds the limit.
                with open(log_file_path, "r") as logfile_r: # Opens the log file in read mode "r".
                    lines = logfile_r.readlines() # Reads all lines of the log file into a list.

                lines_to_write = lines[len(lines)//2:] # Takes the second half of the lines (from the middle) for the new log file. `len(lines)//2` calculates the middle index.

                with open(log_file_path, "w") as logfile_w: # Opens the log file in write mode "w" to overwrite it.
                    logfile_w.writelines(lines_to_write) # Writes the second half of the old lines to the new (truncated) log file.
            # --- Rolling Logic End ---
        # --- NEW: Single Log Entry Option End ---

        with open(log_file_path, "a") as logfile: # Opens the log file in append mode "a".
            logfile.write(text) # Writes the passed text at the end of the log file.

    except Exception as e: # Catches all possible errors when writing to the log file.
        print(f"Error writing to log file {log_file_name}: {e}") # Outputs an error message to the console if an error occurs during writing.
# --- END Functions for Log File Handling ---

# --- START GUI Code ---
# --- Main Window ---
window = tk.Tk() # Creates the main window of the Tkinter application.
window.title(f"melcom's FFmpeg Audio Normalizer v{VERSION}") # Sets the window title with the program name and version.
window.geometry("700x610") # Sets the initial window size (width x height in pixels).

# --- Menu Bar ---
menubar = tk.Menu(window) # Creates the menu bar.
filemenu = tk.Menu(menubar, tearoff=0) # Creates the "File" menu, `tearoff=0` prevents it from being detached.
filemenu.add_command(label="Options", command=show_options_dialog) # Adds the "Options" menu item and links it to the `show_options_dialog` function.
filemenu.add_separator() # Adds a horizontal separator line in the menu.
filemenu.add_command(label="Exit", command=exit_program) # Adds the "Exit" menu item and links it to the `exit_program` function.
menubar.add_cascade(label="File", menu=filemenu) # Adds the "File" menu to the menu bar.

infomenu = tk.Menu(menubar, tearoff=0) # Creates the "Info" menu.
infomenu.add_command(label="About", command=show_info_box) # Adds the "About" menu item and links it to the `show_info_box` function.
menubar.add_cascade(label="Info", menu=infomenu) # Adds the "Info" menu to the menu bar.
window.config(menu=menubar) # Configures the main window to display the menu bar.
# --- Menu Bar End ---

# --- Main GUI Elements (Frame for Content) ---
content_frame = tk.Frame(window, padx=20, pady=20) # Creates a frame for the main content, with inner padding.
content_frame.grid(row=0, column=0, sticky="nsew") # Places the content frame in the grid layout in the main window, `sticky="nsew"` ensures that it expands in all directions when the window is resized.
# --- Main GUI Elements End ---
# --- END GUI Code ---

# --- START GUI Code - Content of the Content Frame ---
# --- File Selection Area ---
file_frame = tk.Frame(content_frame) # Frame for the file selection area.
file_frame.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 10)) # Placed in the grid, spans 3 columns, horizontal expansion, bottom padding.

file_label = tk.Label(file_frame, text="Select Audio File:", anchor="w") # Label for file selection, left-aligned (`anchor="w"`).
file_label.grid(row=0, column=0, sticky="w", padx=(0, 5)) # Placed in the grid, left-aligned, right padding.
file_entry = tk.Entry(file_frame, width=70) # Input field for the file path, width 70 characters.
file_entry.grid(row=0, column=1, sticky="ew") # Placed in the grid, horizontal expansion.
browse_button = tk.Button(file_frame, text="Browse", command=browse_file) # Button to open the file selection dialog, linked to the `browse_file` function.
browse_button.grid(row=0, column=2, padx=(5, 0)) # Placed in the grid, left padding.
# --- File Selection Area End ---

# --- LUFS Options Area (LabelFrame) ---
lufs_options_frame = tk.LabelFrame(content_frame, text="LUFS Options", padx=10, pady=10) # LabelFrame for LUFS options, with border and label "LUFS Options", inner padding.
lufs_options_frame.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(10, 10)) # Placed in the grid, spans 3 columns, horizontal expansion, top and bottom padding.

lufs_preset_label = tk.Label(lufs_options_frame, text="LUFS Preset:", anchor="w") # Label for LUFS preset selection.
lufs_preset_label.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=(0, 5)) # Placed in the grid, left-aligned, right and bottom padding.
lufs_preset_var = tk.StringVar() # StringVar to store the selected LUFS preset (for the combobox).
lufs_preset_combobox = Combobox( # Combobox for selecting LUFS presets.
    lufs_options_frame, # Parent widget.
    textvariable=lufs_preset_var, # Linked to the StringVar.
    values=LUFS_PRESET_NAMES, # Sets the displayed values to the list of preset names (from LUFS_PRESET_NAMES list).
    state="readonly", # Prevents manual input in the combobox, only selection from the list is possible.
    width=30 # Sets the width of the combobox.
)
lufs_preset_combobox.set(LUFS_PRESET_NAMES[0]) # Sets the default selected preset to the first in the list.
lufs_preset_combobox.grid(row=0, column=1, sticky="ew", padx=(0, 20), pady=(0, 5)) # Placed in the grid, horizontal expansion, right and bottom padding.

lufs_label = tk.Label(lufs_options_frame, text="Target LUFS Value (Custom):", anchor="w") # Label for the custom LUFS input field.
lufs_label.grid(row=1, column=0, sticky="w", padx=(0, 5), pady=(5, 10)) # Placed in the grid, left-aligned, right, top, and bottom padding.
lufs_entry = tk.Entry(lufs_options_frame, width=10) # Input field for the custom LUFS value.
lufs_entry.insert(0, "-10") # Sets the default value in the input field to -10.
lufs_entry.grid(row=1, column=1, sticky="w", padx=(0, 20), pady=(5, 10)) # Placed in the grid, left-aligned, right, top, and bottom padding.

true_peak_preset_label = tk.Label(lufs_options_frame, text="True Peak Preset:", anchor="w") # Label for True Peak preset selection.
true_peak_preset_label.grid(row=2, column=0, sticky="w", padx=(0, 5), pady=(0, 5)) # Placed in the grid, left-aligned, right and bottom padding.
true_peak_preset_var = tk.StringVar() # StringVar for True Peak preset combobox.
true_peak_preset_combobox = Combobox( # Combobox for True Peak preset selection.
    lufs_options_frame, # Parent widget.
    textvariable=true_peak_preset_var, # Linked to StringVar.
    values=TRUE_PEAK_PRESET_NAMES, # Values from TRUE_PEAK_PRESET_NAMES list.
    state="readonly", # Readonly combobox.
    width=20 # Width of the combobox.
)
true_peak_preset_combobox.set(TRUE_PEAK_PRESET_NAMES[0]) # Default selection.
true_peak_preset_combobox.grid(row=2, column=1, sticky="ew", padx=(0, 20), pady=(0, 5)) # Placement in the grid.

true_peak_label = tk.Label(lufs_options_frame, text="True Peak (dBTP):", anchor="w") # Label for True Peak input field.
true_peak_label.grid(row=3, column=0, sticky="w", padx=(0, 5), pady=(5, 0)) # Placement in the grid.
true_peak_entry = tk.Entry(lufs_options_frame, width=10) # Input field for True Peak value.
true_peak_entry.insert(0, "-1") # Default value -1.
true_peak_entry.grid(row=3, column=1, sticky="w", padx=(0, 20), pady=(5, 0)) # Placement in the grid.

def update_lufs_entry_state(*args):
    """Enables or disables the LUFS input field based on the preset selection."""
    if lufs_preset_var.get() == "Custom": # If "Custom" is selected in the LUFS preset combobox.
        lufs_entry.config(state=tk.NORMAL) # Enable the LUFS input field.
    else: # If a predefined preset is selected.
        lufs_entry.config(state=tk.DISABLED) # Disable the LUFS input field.
        lufs_entry.delete(0, tk.END) # Clear the LUFS input field.
        lufs_entry.insert(0, LUFS_PRESETS[lufs_preset_var.get()]) # Set the value of the input field to the value of the selected preset from LUFS_PRESETS.

lufs_preset_var.trace("w", update_lufs_entry_state) # Monitors changes in the LUFS preset combobox. `update_lufs_entry_state` is called whenever there is a change.
update_lufs_entry_state() # Initial call to set the state of the LUFS input field at program startup.

def update_true_peak_entry_state(*args):
    """Enables or disables the True Peak input field based on the preset selection."""
    if true_peak_preset_var.get() == "Custom": # If "Custom" is selected in the True Peak preset.
        true_peak_entry.config(state=tk.NORMAL) # Enable True Peak input field.
    else: # Predefined preset.
        true_peak_entry.config(state=tk.DISABLED) # Disable True Peak input field.
        true_peak_entry.delete(0, tk.END) # Clear True Peak input field.
        true_peak_entry.insert(0, TRUE_PEAK_PRESETS[true_peak_preset_var.get()]) # Set value to preset value.

true_peak_preset_var.trace("w", update_true_peak_entry_state) # Monitors changes in the True Peak preset combobox and calls `update_true_peak_entry_state`.
update_true_peak_entry_state() # Initial call.
# --- LUFS Options Area End ---

# --- Output Format Options Area (LabelFrame) ---
output_format_frame = tk.LabelFrame(content_frame, text="Output Format", padx=10, pady=10) # LabelFrame for output format options.
output_format_frame.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(0, 10)) # Placement in the grid.

output_format_label = tk.Label(output_format_frame, text="Format:", anchor="w") # Label for output format selection.
output_format_label.grid(row=0, column=0, sticky="w", padx=(0, 5), pady=(0, 5)) # Placement in the grid.
output_format_var = tk.StringVar() # StringVar for output format combobox.
output_format_combobox = Combobox( # Combobox for output format selection.
    output_format_frame, # Parent widget.
    textvariable=output_format_var, # Linked to StringVar.
    values=["WAV", "MP3", "FLAC", "AAC", "OGG"], # List of available output formats.
    state="readonly" # Readonly combobox.
)
output_format_combobox.set("WAV") # Default selection "WAV".
output_format_combobox.grid(row=0, column=1, sticky="w", padx=(0, 20), pady=(0, 5)) # Placement in the grid.
# --- Output Format Options Area End ---

# --- Buttons: Normalize & Analyze (side by side in a Frame) ---
buttons_frame = tk.Frame(content_frame) # Frame for the "Start Normalization" and "Analyze Audio File" buttons.
buttons_frame.grid(row=3, column=0, columnspan=3, pady=(20, 10), sticky="ew") # Placement in the grid, top and bottom padding, horizontal expansion.

start_button = tk.Button(buttons_frame, text="Start Normalization", command=start_normalization, padx=20, pady=10) # "Start Normalization" button, linked to the `start_normalization` function.
start_button.grid(row=0, column=0, sticky="ew") # Placement in the grid, horizontal expansion.
analyze_button = tk.Button(buttons_frame, text="Analyze Audio File", command=analyze_audio, padx=20, pady=10) # "Analyze Audio File" button, linked to `analyze_audio`.
analyze_button.grid(row=0, column=1, sticky="ew", padx=(10, 0)) # Placement in the grid, horizontal expansion, left padding.

buttons_frame.columnconfigure(0, weight=1) # Configures column 0 of the buttons frame to expand horizontally and distribute the available space evenly.
buttons_frame.columnconfigure(1, weight=1) # Also configures column 1 of the buttons frame for horizontal expansion.
# --- Buttons Frame End ---
# --- END GUI Code - Content of the Content Frame ---

# --- START GUI Code - Progress Bar, Process Information, and Mainloop ---
# --- Progress Bar ---
progressbar = Progressbar(content_frame, mode='indeterminate') # Progress bar in 'indeterminate' mode (no progress display, only animation), parent widget is `content_frame`.

# --- Process Information Area ---
process_info_frame = tk.Frame(content_frame) # Frame for the area to display process information.
process_info_frame.grid(row=5, column=0, columnspan=3, sticky="nsew") # Placement in the grid, spans 3 columns, expands in all directions.

progress_label = tk.Label(process_info_frame, text="Process Information:", anchor="w") # Label for the process information area.
progress_label.grid(row=0, column=0, sticky="ew", padx=5, pady=(0, 5)) # Placement in the grid, horizontal expansion, horizontal and bottom padding.

process_info_field = tk.Text(process_info_frame, height=6, width=60, wrap=tk.WORD, state=tk.DISABLED, bg="lightgrey") # Text field to display process information.
                                                                                                      # `height=6`: Height in text lines.
                                                                                                      # `width=60`: Width in characters.
                                                                                                      # `wrap=tk.WORD`: Line wrapping at word boundaries.
                                                                                                      # `state=tk.DISABLED`: Text field is initially disabled (not editable).
                                                                                                      # `bg="lightgrey"`: Light grey background color.
process_info_field.grid(row=1, column=0, sticky="nsew", padx=5, pady=(0, 5)) # Placement in the grid, expands in all directions, horizontal and bottom padding.

process_info_frame.columnconfigure(0, weight=1) # Configures column 0 of the process info frame to expand horizontally.
process_info_frame.rowconfigure(1, weight=1) # Configures row 1 of the process info frame (the text field) to expand vertically.
# --- Process Information Area End ---

# --- Configuration of the Grid in the Main Window and Content Frame ---
content_frame.columnconfigure(0, weight=1) # Configures column 0 of the content frame to expand horizontally.
content_frame.rowconfigure(5, weight=1) # Configures row 5 of the content frame (process info area) to expand vertically.
window.columnconfigure(0, weight=1) # Configures column 0 of the main window to expand horizontally.
window.rowconfigure(0, weight=1) # Configures row 0 of the main window (content frame) to expand vertically.

load_options() # Loads the program settings at program startup (calls the `load_options` function).

window.mainloop() # Starts the Tkinter event loop. The program remains in this loop and waits for GUI events (e.g., button clicks, menu selections) until the window is closed.
# --- END GUI Code - Progress Bar, Process Information, and Mainloop ---
