#!/usr/bin/env bash
# shellcheck disable=SC2001

# This file contains common functions that the scripts will source.

set -u

# -----------------------------------------------------------------------------
# CONSTANTS
# -----------------------------------------------------------------------------

ACCESSED_RECENTLY_DIR="$ROOT_DIR/Accessed recently"
ACCESSED_RECENTLY_LINKS_TO_KEEP=10
FIELD_SEPARATOR=$'\r'          # The main field separator.
GUI_BOX_HEIGHT=550             # Height of the GUI dialog boxes.
GUI_BOX_WIDTH=900              # Width of the GUI dialog boxes.
GUI_INFO_WIDTH=400             # Width of the GUI small dialog boxes.
IGNORE_FIND_PATH="*.git/*"     # Path to ignore in the 'find' command.
PREFIX_ERROR_LOG_FILE="Errors" # Name of 'error' directory.
PREFIX_OUTPUT_DIR="Output"     # Name of 'output' directory.
TEMP_DIR=$(mktemp --directory) # Temp directories for use in scripts.
TEMP_DIR_ITEMS_TO_REMOVE="$TEMP_DIR/items_to_remove"
TEMP_DIR_LOGS="$TEMP_DIR/logs"
TEMP_DIR_STORAGE_TEXT="$TEMP_DIR/storage_text"
TEMP_DIR_TASK="$TEMP_DIR/task"
WAIT_BOX_CONTROL_KDE="$TEMP_DIR/wait_box_control_kde"
WAIT_BOX_CONTROL="$TEMP_DIR/wait_box_control"
WAIT_BOX_FIFO="$TEMP_DIR/wait_box_fifo"

readonly \
    ACCESSED_RECENTLY_DIR \
    ACCESSED_RECENTLY_LINKS_TO_KEEP \
    FIELD_SEPARATOR \
    GUI_BOX_HEIGHT \
    GUI_BOX_WIDTH \
    GUI_INFO_WIDTH \
    IGNORE_FIND_PATH \
    PREFIX_ERROR_LOG_FILE \
    PREFIX_OUTPUT_DIR \
    TEMP_DIR \
    TEMP_DIR_LOGS \
    TEMP_DIR_STORAGE_TEXT \
    TEMP_DIR_TASK \
    WAIT_BOX_CONTROL \
    WAIT_BOX_CONTROL_KDE \
    WAIT_BOX_FIFO

# -----------------------------------------------------------------------------
# GLOBAL VARIABLES
# -----------------------------------------------------------------------------

IFS=$FIELD_SEPARATOR
INPUT_FILES=$*
TEMP_DATA_TASK=""

# -----------------------------------------------------------------------------
# BUILD THE STRUCTURE OF THE 'TEMP_DIR'
# -----------------------------------------------------------------------------

# Store the path of temporary items to be removed after exit.
mkdir -p "$TEMP_DIR_ITEMS_TO_REMOVE"
mkdir -p "$TEMP_DIR_LOGS"         # Store 'error logs'.
mkdir -p "$TEMP_DIR_STORAGE_TEXT" # Store the output from parallel processes.
mkdir -p "$TEMP_DIR_TASK"         # Store temporary files of scripts.

# -----------------------------------------------------------------------------
# FUNCTIONS
# -----------------------------------------------------------------------------

_cleanup_on_exit() {
    # This function performs cleanup tasks when the script exits. It is
    # designed to safely and efficiently remove temporary directories or files
    # that were created during the script's execution.

    # Remove local temporary dirs or files.
    local items_to_remove=""
    items_to_remove=$(cat -- "$TEMP_DIR_ITEMS_TO_REMOVE/"* 2>/dev/null)

    # Escape single quotes in filenames to handle them correctly in 'xargs'
    # with 'bash -c'.
    items_to_remove=$(sed -z "s|'|'\\\''|g" <<<"$items_to_remove")

    printf "%s" "$items_to_remove" | xargs \
        --no-run-if-empty \
        --delimiter="$FIELD_SEPARATOR" \
        --max-procs="$(_get_max_procs)" \
        --replace="{}" \
        bash -c "{ chmod -R u+rw -- '{}' && rm -rf -- '{}'; } &>/dev/null"

    # Remove the main temporary dir.
    rm -rf -- "$TEMP_DIR" &>/dev/null

    if ! _is_gui_session; then
        printf "End of the script.\n" >&2
    fi
}
trap _cleanup_on_exit EXIT

_check_dependencies() {
    # This function ensures that all required dependencies are available for
    # the scripts to run. It verifies the presence of specified commands or
    # packages and prompts the user to install missing ones.
    #
    # Parameters:
    #   - $1 (dependencies): A list of dependencies to check, formatted as a
    #     "|" delimited string. Each dependency can specify:
    #     - "command": The name of a command to check for in the shell.
    #     - "package": The package associated with the command (if different).
    #     - "pkg_manager": Optional. The specific package manager.
    #
    # Example:
    #   - _check_dependencies "
    #       command=ffmpeg; pkg_manager=apt; package=ffmpeg |
    #       command=ffmpeg; pkg_manager=dnf; package=ffmpeg-free |
    #       command=ffmpeg; pkg_manager=pacman; package=ffmpeg |
    #       command=ffmpeg; pkg_manager=zypper; package=ffmpeg"

    local dependencies=$1
    local packages_to_install=""
    local pkg_manager_installed=""

    [[ -z "$dependencies" ]] && return

    # Skip duplicated dependencies in the input list.
    dependencies=$(tr "|" "\n" <<<"$dependencies")
    dependencies=$(tr -d " " <<<"$dependencies")
    dependencies=$(sort --unique <<<"$dependencies")
    dependencies=$(_text_remove_empty_lines "$dependencies")

    [[ -z "$dependencies" ]] && return

    # Get the name of the installed package manager.
    pkg_manager_installed=$(_pkg_get_package_manager)
    if [[ -z "$pkg_manager_installed" ]]; then
        _display_error_box "Could not find a package manager!"
        _exit_script
    fi

    # Check all dependencies.
    dependencies=$(tr "\n" "$FIELD_SEPARATOR" <<<"$dependencies")
    local dependency=""
    for dependency in $dependencies; do
        local command=""
        local package=""
        local pkg_manager=""

        # Evaluate the values parameters from the 'dependency' variable.
        eval "$dependency"

        # Ignore installing the dependency if there is a command in the shell.
        if [[ -n "$command" ]] && _command_exists "$command"; then
            continue
        fi

        # Ignore installing the dependency if the installed
        # package managers differ.
        if [[ -n "$pkg_manager" ]] &&
            [[ "$pkg_manager_installed" != "$pkg_manager" ]]; then
            continue
        fi

        # Ignore installing the dependency if the package is already installed
        # (packages that do not have a command).
        if [[ -n "$package" ]] && [[ -z "$command" ]] &&
            _pkg_is_package_installed "$pkg_manager_installed" "$package"; then
            continue
        fi

        # If the package is not specified, use the command name as
        # the package name.
        if [[ -z "$package" ]] && [[ -n "$command" ]]; then
            package=$command
        fi

        # Add the package to the list to install.
        if [[ -n "$package" ]]; then
            packages_to_install+=" $package"
        fi
    done

    # Ask the user to install the packages.
    if [[ -n "$packages_to_install" ]]; then
        local message="These packages were not found:"
        message+=$(sed "s| |\n- |g" <<<"$packages_to_install")
        message+=$'\n'$'\n'
        message+="Would you like to install them?"
        if _display_question_box "$message"; then
            _pkg_install_packages \
                "$pkg_manager_installed" "${packages_to_install/ /}"
        else
            _exit_script
        fi
    fi
}

_check_output() {
    # This function validates the success of a command or process based on its
    # exit code and output. It logs errors if the command fails or if an
    # expected output file is missing.
    #
    # Parameters:
    #   - $1 (exit_code): The exit code returned by the command or process.
    #   - $2 (std_output): The standard output or error from the command.
    #   - $3 (input_file): The input file associated (if applicable).
    #   - $4 (output_file): The expected output file to verify its existence.
    #
    # Returns:
    #   - "0" (true): If the command was successful and the output file exists.
    #   - "1" (false): If the command failed or the output file does not exist.

    local exit_code=$1
    local std_output=$2
    local input_file=$3
    local output_file=$4

    # Check the 'exit_code' and log the error.
    if ((exit_code != 0)); then
        _log_error "Command failed with a non-zero exit code." \
            "$input_file" "$std_output" "$output_file"
        return 1
    fi

    # Check if the output file exists.
    if [[ -n "$output_file" ]] && [[ ! -e "$output_file" ]]; then
        _log_error "The output file does not exist." \
            "$input_file" "$std_output" "$output_file"
        return 1
    fi

    return 0
}

_command_exists() {
    # This function checks whether a given command is available on the system.
    #
    # Parameters:
    #   - $1 (command_check): The name of the command to verify.
    #
    # Returns:
    #   - "0" (true): If the command is available.
    #   - "1" (false): If the command is not available.

    local command_check=$1

    if command -v "$command_check" &>/dev/null; then
        return 0
    fi
    return 1
}

_convert_delimited_string_to_text() {
    # This function converts a delimited string of items into
    # newline-separated text.
    #
    # Parameters:
    #   - $1 (input_items): A string containing items separated by the
    #   'FIELD_SEPARATOR' variable.
    #
    # Returns:
    #   - A string containing the items separated by newlines.

    local input_items=$1
    local new_line="'\$'\\\n''"

    input_items=$(sed -z "s|\n|$new_line|g; s|$new_line$||g" <<<"$input_items")
    input_items=$(tr "$FIELD_SEPARATOR" "\n" <<<"$input_items")

    printf "%s" "$input_items"
}

_directory_pop() {
    # This function pops the top directory off the directory stack and changes
    # to the previous directory.
    #
    # Returns:
    #   - "0" (true): If the directory was successfully popped and changed.
    #   - "1" (false): If there was an error popping the directory.

    popd &>/dev/null || {
        _log_error "Could not pop a directory." "" "" ""
        return 1
    }
    return 0
}

_directory_push() {
    # This function pushes the specified directory onto the directory stack and
    # changes to it.
    #
    # Parameters:
    #   - $1 (directory): The target directory to push onto the directory stack
    #     and navigate to.
    #
    # Returns:
    #   - "0" (true): If the directory was successfully pushed and changed.
    #   - "1" (false): If there was an error pushing the directory.

    local directory=$1

    pushd "$directory" &>/dev/null || {
        _log_error "Could not push the directory '$directory'." "" "" ""
        return 1
    }
    return 0
}

_convert_text_to_delimited_string() {
    # This function converts newline-separated text into a delimited string of
    # items.
    #
    # Parameters:
    #   - $1 (input_items): A string containing items separated by newlines.
    #
    # Returns:
    #   - A string containing the items separated by the 'FIELD_SEPARATOR'
    #   variable.

    local input_items=$1
    local new_line="'\$'\\\n''"

    input_items=$(tr "\n" "$FIELD_SEPARATOR" <<<"$input_items")
    input_items=$(sed -z "s|$new_line|\n|g" <<<"$input_items")

    input_items=$(_str_remove_empty_tokens "$input_items")
    printf "%s" "$input_items"
}

_display_dir_selection_box() {
    # This function presents a graphical interface to allow the user to select
    # one or more directories.

    local input_files=""

    if _command_exists "zenity"; then
        input_files=$(zenity --title "$(_get_script_name)" \
            --file-selection --multiple --directory \
            --separator="$FIELD_SEPARATOR" 2>/dev/null) || _exit_script
    elif _command_exists "kdialog"; then
        input_files=$(kdialog --title "$(_get_script_name)" \
            --getexistingdirectory 2>/dev/null) || _exit_script
        # Use parameter expansion to remove the last space.
        input_files=${input_files% }
        input_files=${input_files// \//$FIELD_SEPARATOR/}
    fi

    input_files=$(_str_remove_empty_tokens "$input_files")
    printf "%s" "$input_files"
}

# shellcheck disable=SC2120
_display_file_selection_box() {
    # This function presents a graphical interface to allow the user to select
    # a file.
    #
    # Parameters:
    #   - $1 (file_filter): Optional. File filter pattern to restrict the types
    #     of files shown.
    #   - $2 (title): Optional. Title of the window.

    local file_filter=${1:-""}
    local title=${2:-"$(_get_script_name)"}
    local input_files=""

    if _command_exists "zenity"; then
        input_files=$(zenity --title "$title" \
            --file-selection \
            ${file_filter:+--file-filter="$file_filter"} \
            --separator="$FIELD_SEPARATOR" 2>/dev/null) || _exit_script
    elif _command_exists "kdialog"; then
        input_files=$(kdialog --title "$title" \
            --getopenfilename 2>/dev/null) || _exit_script
        # Use parameter expansion to remove the last space.
        input_files=${input_files% }
        input_files=${input_files// \//$FIELD_SEPARATOR/}
    fi

    input_files=$(_str_remove_empty_tokens "$input_files")
    printf "%s" "$input_files"
}

_display_error_box() {
    # This function displays an error message to the user, adapting to the
    # available environment.
    #
    # Parameters:
    #   - $1 (message): The error message to display.

    local message=$1

    if ! _is_gui_session; then
        printf "Error: %s\n" "$message" >&2
    elif [[ -n "$DBUS_SESSION_BUS_ADDRESS" ]]; then
        _gdbus_notify "dialog-error" "$(_get_script_name)" "$message"
    elif _command_exists "zenity"; then
        zenity --title "$(_get_script_name)" --error \
            --width="$GUI_INFO_WIDTH" --text "$message" &>/dev/null
    elif _command_exists "kdialog"; then
        kdialog --title "$(_get_script_name)" --error "$message" &>/dev/null
    elif _command_exists "xmessage"; then
        xmessage -title "$(_get_script_name)" "Error: $message" &>/dev/null
    fi
}

_display_info_box() {
    # This function displays an information message to the user, adapting to
    # the available environment.
    #
    # Parameters:
    #   - $1 (message): The information message to display.

    local message=$1

    if ! _is_gui_session; then
        printf "Info: %s\n" "$message" >&2
    elif [[ -n "$DBUS_SESSION_BUS_ADDRESS" ]]; then
        _gdbus_notify "dialog-information" "$(_get_script_name)" "$message"
    elif _command_exists "zenity"; then
        zenity --title "$(_get_script_name)" --info \
            --width="$GUI_INFO_WIDTH" --text "$message" &>/dev/null
    elif _command_exists "kdialog"; then
        kdialog --title "$(_get_script_name)" --msgbox "$message" &>/dev/null
    elif _command_exists "xmessage"; then
        xmessage -title "$(_get_script_name)" "Info: $message" &>/dev/null
    fi
}

_display_list_box() {
    # This function displays a list box with selectable items, adapting to the
    # available environment.
    #
    # Parameters:
    #   - $1 (message): A string containing the items to display in the list.
    #   - $2 (columns): Column definitions for the list, typically in the
    #   format "--column=<name>;--column=<name>".
    #   - $3 (item_name): A string representing the name of the items in the
    #   list. If not provided, the default value is "items".
    #   - $4 (resolve_links): A boolean-like string ("true" or "false")
    #   indicating whether symbolic links in item paths should be resolved to
    #   their target locations when opening the item's location. Defaults to
    #   "true".

    local message=$1
    local columns=$2
    local item_name=${3:-"items"}
    local resolve_links=${4:-"true"}
    local columns_count=0
    local items_count=0
    local selected_item=""
    local message_select=""
    _close_wait_box
    _logs_consolidate ""

    if [[ -n "$message" ]]; then
        items_count=$(tr -cd "\n" <<<"$message" | wc -c)
        message_select=" Select an item to open its location:"
    fi

    # Count the number of columns.
    columns_count=$(grep --only-matching "column=" <<<"$columns" | wc -l)

    if ! _is_gui_session; then
        if [[ -z "$message" ]]; then
            message="(Empty result)"
            printf "%s\n" "$message" >&2
        else
            message=$(tr "$FIELD_SEPARATOR" " " <<<"$message")
            printf "%s\n" "$message"
        fi
    elif _command_exists "zenity"; then
        if [[ -z "$message" ]]; then
            # NOTE: Some versions of Zenity crash if the
            # message is empty (Segmentation fault).
            message=" "
        fi
        columns=$(tr ";" "$FIELD_SEPARATOR" <<<"$columns")
        message=$(tr "\n" "$FIELD_SEPARATOR" <<<"$message")
        # shellcheck disable=SC2086
        selected_item=$(zenity --title "$(_get_script_name)" --list \
            --editable --multiple --separator="$FIELD_SEPARATOR" \
            --width="$GUI_BOX_WIDTH" --height="$GUI_BOX_HEIGHT" \
            --print-column "$columns_count" \
            --text "Total of $items_count $item_name.$message_select" \
            $columns $message 2>/dev/null) || _exit_script

        if ((items_count != 0)) && [[ -n "$selected_item" ]]; then
            # Open the directory of the clicked item in the list.
            _open_items_locations "$selected_item" "$resolve_links"
        fi
    elif _command_exists "kdialog"; then
        columns=$(sed "s|--column=||g" <<<"$columns")
        columns=$(tr ";" "\t" <<<"$columns")
        message=$(tr "$FIELD_SEPARATOR" "\t" <<<"$message")
        message="$columns"$'\n'$'\n'"$message"
        kdialog --title "$(_get_script_name)" \
            --geometry "${GUI_BOX_WIDTH}x${GUI_BOX_HEIGHT}" \
            --textinputbox "" "$message" &>/dev/null || _exit_script
    elif _command_exists "xmessage"; then
        columns=$(sed "s|--column=||g" <<<"$columns")
        columns=$(tr ";" "\t" <<<"$columns")
        message=$(tr "$FIELD_SEPARATOR" "\t" <<<"$message")
        message="$columns"$'\n'$'\n'"$message"
        xmessage -title "$(_get_script_name)" \
            "$message" &>/dev/null || _exit_script
    fi
}

_display_password_box() {
    # This function prompts the user to enter a password, either via the
    # terminal or a graphical dialog box.
    #
    # Parameters:
    #   - $1 (message): A message to display as a prompt for the password.

    local message=$1
    local password=""

    # Ask the user for the 'password'.
    if ! _is_gui_session; then
        read -r -p "$message " password >&2
    elif _command_exists "zenity"; then
        sleep 0.2 # Avoid 'wait_box' open before.
        password=$(zenity \
            --title="Password" --entry --hide-text --width="$GUI_INFO_WIDTH" \
            --text "$message" 2>/dev/null) || return 1
    elif _command_exists "kdialog"; then
        sleep 0.2 # Avoid 'wait_box' open before.
        password=$(kdialog --title "Password" \
            --password "$message" 2>/dev/null) || return 1
    fi

    printf "%s" "$password"
}

_display_password_box_define() {
    # This function prompts the user to enter a password and ensures the
    # password is not empty.

    local message="Type your password:"
    local password=""

    password=$(_display_password_box "$message") || return 1

    # Check if the 'password' is not empty.
    if [[ -z "$password" ]]; then
        _display_error_box "The password can not be empty!"
        return 1
    fi

    printf "%s" "$password"
}

_display_question_box() {
    # This function prompts the user with a yes/no question and returns the
    # user's response.
    #
    # Parameters:
    #   - $1 (message): The question message to display to the user.

    local message=$1
    local response=""

    if ! _is_gui_session; then
        read -r -p "$message [Y/n] " response
        [[ ${response,,} == *"n"* ]] && return 1
    elif _command_exists "zenity"; then
        zenity --title "$(_get_script_name)" --question \
            --width="$GUI_INFO_WIDTH" --text="$message" &>/dev/null || return 1
    elif _command_exists "kdialog"; then
        kdialog --title "$(_get_script_name)" \
            --yesno "$message" &>/dev/null || return 1
    elif _command_exists "xmessage"; then
        xmessage -title "$(_get_script_name)" \
            -buttons "Yes:0,No:1" "$message" &>/dev/null || return 1
    fi
    return 0
}

_display_text_box() {
    # This function displays a message to the user in a text box, either in the
    # terminal or using a GUI dialog.
    #
    # Parameters:
    #   - $1 (message): The message to display. If empty, a default message
    #     "(Empty result)" is shown.

    local message=$1
    _close_wait_box
    _logs_consolidate ""

    if [[ -z "$message" ]]; then
        message="(Empty result)"
    fi

    if ! _is_gui_session; then
        printf "%s\n" "$message"
    elif _command_exists "zenity"; then
        zenity --title "$(_get_script_name)" --text-info --no-wrap \
            --width="$GUI_BOX_WIDTH" --height="$GUI_BOX_HEIGHT" \
            <<<"$message" &>/dev/null || _exit_script
    elif _command_exists "kdialog"; then
        kdialog --title "$(_get_script_name)" \
            --geometry "${GUI_BOX_WIDTH}x${GUI_BOX_HEIGHT}" \
            --textinputbox "" \
            "$message" &>/dev/null || _exit_script
    elif _command_exists "xmessage"; then
        xmessage -title "$(_get_script_name)" \
            "$message" &>/dev/null || _exit_script
    fi
}

_display_result_box() {
    # This function displays a result summary at the end of a process,
    # including error checking and output directory information.
    #
    # Parameters:
    #   - $1 (output_dir): The directory where output files are stored or
    #     expected to be.

    local output_dir=$1
    _close_wait_box
    _logs_consolidate "$output_dir"

    # If 'output_dir' parameter is defined.
    if [[ -n "$output_dir" ]]; then
        # Try to remove the output directory (if it is empty).
        if [[ "$output_dir" == *"/$PREFIX_OUTPUT_DIR"* ]]; then
            rmdir "$output_dir" &>/dev/null
        fi

        # Check if the output directory still exists.
        if [[ -d "$output_dir" ]]; then
            local dir_label=""
            dir_label=$(_str_human_readable_path "$output_dir")
            _display_info_box \
                "Finished! The output files are in the $dir_label directory."
        else
            _display_info_box "Finished, but there is nothing to do."
        fi
    else
        _display_info_box "Finished!"
    fi
}

_display_wait_box() {
    # This function displays a wait box to inform the user that a task is
    # running and they need to wait.
    #
    # Parameters:
    #   - $1 (open_delay): Optional. The delay (in seconds) before the wait box
    #     is shown. Defaults to 2 seconds if not provided.

    local open_delay=${1:-"2"}
    local message="Running the task. Please, wait..."

    _display_wait_box_message "$message" "$open_delay"
}

_display_wait_box_message() {
    # This function displays a wait box (progress indicator) to inform the user
    # that a task is in progress.
    #
    # Parameters:
    #   - $1 (message): The message to display inside the wait box (e.g.,
    #     "Running the task. Please, wait...").
    #   - $2 (open_delay): Optional. The delay (in seconds) before the wait box
    #     is shown. Defaults to 2 seconds if not provided.

    local message=$1
    local open_delay=${2:-"2"}

    if ! _is_gui_session; then
        # For non-GUI sessions, simply print the message to the console.
        printf "%s\n" "$message" >&2

    # Check if the Zenity is available.
    elif _command_exists "zenity"; then
        # Control flag to inform that a 'wait_box' will open
        # (if the task takes over 2 seconds).
        touch "$WAIT_BOX_CONTROL"

        # Create the FIFO for communication with Zenity 'wait_box'.
        if [[ ! -p "$WAIT_BOX_FIFO" ]]; then
            mkfifo "$WAIT_BOX_FIFO"
        fi

        # Launch a background thread for Zenity 'wait_box':
        #   - Waits for the specified delay.
        #   - Opens the Zenity 'wait_box' if the control flag still exists.
        #   - If Zenity 'wait_box' fails or is cancelled, exit the script.
        # shellcheck disable=SC2002
        sleep "$open_delay" && [[ -f "$WAIT_BOX_CONTROL" ]] &&
            tail -f -- "$WAIT_BOX_FIFO" | (zenity \
                --title="$(_get_script_name)" --progress \
                --width="$GUI_INFO_WIDTH" \
                --pulsate --auto-close --text="$message" || _exit_script) &

    # Check if the KDialog is available.
    elif _command_exists "kdialog"; then
        _get_qdbus_command || return 0
        # Control flag to inform that a 'wait_box' will open
        # (if the task takes over 2 seconds).
        touch "$WAIT_BOX_CONTROL"

        # Launch a background thread for KDialog 'wait_box':
        #   - Waits for the specified delay.
        #   - Opens the KDialog 'wait_box' if the control flag still exists.
        sleep "$open_delay" && [[ -f "$WAIT_BOX_CONTROL" ]] &&
            kdialog --title="$(_get_script_name)" \
                --progressbar "$message" 0 >"$WAIT_BOX_CONTROL_KDE" &

        # Launch another background thread to monitor the KDialog 'wait_box':
        #   - Periodically checks if the dialog has been closed or cancelled.
        #   - If KDialog 'wait_box' is cancelled, exit the script.
        (
            while [[ -f "$WAIT_BOX_CONTROL" ]] ||
                [[ -f "$WAIT_BOX_CONTROL_KDE" ]]; do
                if [[ -f "$WAIT_BOX_CONTROL_KDE" ]]; then
                    # Extract the D-Bus reference for the KDialog instance.
                    local dbus_ref=""
                    dbus_ref=$(cut -d " " -f 1 <"$WAIT_BOX_CONTROL_KDE")
                    if [[ -n "$dbus_ref" ]]; then
                        # Check if the user has cancelled the wait box.
                        $(_get_qdbus_command) "$dbus_ref" "/ProgressDialog" \
                            "wasCancelled" 2>/dev/null || _exit_script
                    fi
                fi
                sleep 0.2
            done
        ) &
    fi
}

_close_wait_box() {
    # This function is responsible for closing any open "wait boxes" (progress
    # indicators) that were displayed during the execution of a task. It checks
    # for both Zenity and KDialog wait boxes and handles their closure.

    # Check if 'wait_box' will open.
    if [[ -f "$WAIT_BOX_CONTROL" ]]; then
        rm -f -- "$WAIT_BOX_CONTROL" # Cancel the future open.
    fi

    # Check if Zenity 'wait_box' is open (waiting for an input in the FIFO).
    if pgrep -fl "$WAIT_BOX_FIFO" &>/dev/null; then
        # Close the Zenity using the FIFO.
        printf "100\n" >"$WAIT_BOX_FIFO"
    fi

    # Check if KDialog 'wait_box' is open.
    while [[ -f "$WAIT_BOX_CONTROL_KDE" ]]; do
        # Extract the D-Bus reference for the KDialog instance.
        local dbus_ref=""
        dbus_ref=$(cut -d " " -f 1 <"$WAIT_BOX_CONTROL_KDE")
        if [[ -n "$dbus_ref" ]]; then
            # Close the KDialog 'wait_box'.
            $(_get_qdbus_command) "$dbus_ref" "/ProgressDialog" \
                "close" 2>/dev/null
            rm -f -- "$WAIT_BOX_CONTROL_KDE"
        fi
        sleep 0.2
    done
}

_exit_script() {
    # This function is responsible for safely exiting the script by terminating
    # all child processes associated with the current script and printing an
    # exit message to the terminal.

    _close_wait_box

    local child_pids=""
    local script_pid=$$

    if ! _is_gui_session; then
        printf "Exiting the script...\n" >&2
    fi

    # Get the process ID (PID) of all child processes.
    child_pids=$(pstree -p "$script_pid" |
        grep --only-matching --perl-regexp "\(+\K[^)]+")

    # NOTE: Use 'xargs' and kill to send the SIGTERM signal to all child
    # processes, including the current script.
    # See the: https://www.baeldung.com/linux/safely-exit-scripts
    xargs kill <<<"$child_pids" &>/dev/null
}

_find_filtered_files() {
    # This function filters a list of files or directories based on various
    # user-specified criteria, such as file type, extensions, and recursion.
    #
    # Parameters:
    #   - $1 (input_files): A space-separated string containing file or
    #     directory paths to filter. These paths are passed to the "find"
    #     command.
    #   - $2 (par_type): A string specifying the type of file to search for. It
    #     can be:
    #     - "file": To search for files and symbolic links.
    #     - "directory": To search for directories and symbolic links.
    #   - $3 (par_skip_extension): A string of file extensions to exclude from
    #     the search. Only files with extensions not matching this list will be
    #     included.
    #   - $4 (par_select_extension): A string of file extensions to include in
    #     the search. Only files with matching extensions will be included.
    #   - $5 (par_find_parameters): Optional. Additional parameters to be
    #     passed directly to the "find" command.
    #
    # Example:
    #   - Input: "dir1 dir2", "file", "", "txt|pdf", "true"
    #   - Output: A list of files with extensions ".txt" or ".pdf" from the
    #     directories "dir1" and "dir2", searched recursively.

    local input_files=$1
    local par_type=$2
    local par_skip_extension=$3
    local par_select_extension=$4
    local par_find_parameters=$5
    local filtered_files=""
    local find_command=""

    input_files=$(sed "s|'|'\"'\"'|g" <<<"$input_files")
    input_files=$(sed "s|$FIELD_SEPARATOR|' '|g" <<<"$input_files")

    # Build a 'find' command.
    find_command="find '$input_files'"

    if [[ -n "$par_find_parameters" ]]; then
        find_command+=" $par_find_parameters"
    fi

    # Expand the directories with the 'find' command.
    case "$par_type" in
    "file") find_command+=" \( -type l -o -type f \)" ;;
    "directory") find_command+=" \( -type l -o -type d \)" ;;
    esac

    if [[ -n "$par_select_extension" ]]; then
        find_command+=" -regextype posix-extended "
        find_command+=" -regex \".*\.($par_select_extension)$\""
    fi

    if [[ -n "$par_skip_extension" ]]; then
        find_command+=" -regextype posix-extended "
        find_command+=" ! -regex \".*\.($par_skip_extension)$\""
    fi

    find_command+=" ! -path \"$IGNORE_FIND_PATH\""
    # shellcheck disable=SC2089
    find_command+=" -print0"

    # shellcheck disable=SC2086
    filtered_files=$(eval $find_command 2>/dev/null |
        tr "\0" "$FIELD_SEPARATOR")
    filtered_files=$(_str_remove_empty_tokens "$filtered_files")

    # Return the filtered files.
    printf "%s" "$filtered_files"
}

_gdbus_notify() {
    # This function sends a desktop notification using the "gdbus" tool, which
    # interfaces with the D-Bus notification system (specifically the
    # "org.freedesktop.Notifications" service).
    #
    # Parameters:
    #   - $1 (icon): The icon to display with the notification.
    #   - $2 (title): The title of the notification.
    #   - $3 (body): The main message to be displayed in the notification.

    local icon=$1
    local title=$2
    local body=$3
    local app_name=$title
    local method="Notify"
    local interface="org.freedesktop.Notifications"
    local object_path="/org/freedesktop/Notifications"

    # Use 'gdbus' to send the notification.
    gdbus call --session --dest "$interface" --object-path "$object_path" \
        --method "$interface.$method" "$app_name" 0 "$icon" "$title" "$body" \
        "[]" '{"urgency": <1>}' 5000 &>/dev/null
}

_get_filename_dir() {
    # This function extracts the directory path from a given file path.
    #
    # Parameters:
    #   - $1 (input_filename): The full path or relative path to the file.

    local input_filename=$1
    local dir=""

    dir=$(cd -- "$(dirname -- "$input_filename")" &>/dev/null && pwd)

    printf "%s" "$dir"
}

_get_filename_extension() {
    # This function extracts the file extension from a given filename.
    #
    # Parameters:
    #   - $1 (filename): The input filename (can be absolute or relative).

    local filename=$1
    filename=$(sed -E "s|.*/(\.)*||g" <<<"$filename")
    filename=$(sed -E "s|^(\.)*||g" <<<"$filename")

    printf "%s" "$filename" |
        grep --ignore-case --only-matching --perl-regexp \
            "(\.tar)?\.[a-z0-9_~-]{0,15}$" || true
}

_get_filename_full_path() {
    # This function returns the full absolute path of a given filename.
    #
    # Parameters:
    #   - $1 (input_filename): The input filename or relative path.

    local input_filename=$1
    local full_path=$input_filename
    local dir=""

    if [[ $input_filename != "/"* ]]; then
        dir=$(_get_filename_dir "$input_filename")
        full_path=$dir/$(basename -- "$input_filename")
    fi

    printf "%s" "$full_path"
}

_get_filename_next_suffix() {
    # This function generates a unique filename by adding a numeric suffix to
    # the base filename if a file with the same name already exists. It ensures
    # that the new filename does not overwrite an existing file.
    #
    # Parameters:
    #   - $1 (filename): The input filename or path. This can be an absolute or
    #   relative filename. If the input file has an extension, it will be
    #   stripped for the purpose of generating the new filename.

    local filename=$1
    local filename_result=$filename
    local filename_base=""
    local filename_extension=""

    # Directories do not have an extension.
    if [[ -d "$filename" ]]; then
        filename_base=$filename
    else
        filename_base=$(_strip_filename_extension "$filename")
        filename_extension=$(_get_filename_extension "$filename")
    fi

    # Avoid overwriting a file. If there is a file with the same name,
    # try to add a suffix, as 'file (1)', 'file (2)', ...
    local count=1
    while [[ -e "$filename_result" ]]; do
        filename_result="$filename_base ($count)$filename_extension"
        ((count++))
    done

    printf "%s" "$filename_result"
}

_get_filenames_filemanager() {
    # This function retrieves a list of selected filenames or URIs from a file
    # manager (such as Caja, Nemo, or Nautilus) and processes the input
    # accordingly. If no selection is detected, it falls back to using a
    # standard input file list.

    local input_files=""

    # Try to use the information provided by the file manager.
    if [[ -v "CAJA_SCRIPT_SELECTED_URIS" ]]; then
        input_files=$CAJA_SCRIPT_SELECTED_URIS
    elif [[ -v "NEMO_SCRIPT_SELECTED_URIS" ]]; then
        input_files=$NEMO_SCRIPT_SELECTED_URIS
    elif [[ -v "NAUTILUS_SCRIPT_SELECTED_URIS" ]]; then
        input_files=$NAUTILUS_SCRIPT_SELECTED_URIS
    fi

    if [[ -n "$input_files" ]]; then
        # Replace '\n' with 'FIELD_SEPARATOR'.
        input_files=$(_convert_text_to_delimited_string "$input_files")

        # Decode the URI list.
        input_files=$(_text_uri_decode "$input_files")
    else
        input_files=$INPUT_FILES # Standard input.
        input_files=$(_str_remove_empty_tokens "$input_files")
    fi

    printf "%s" "$input_files"
}

_get_files() {
    # This function retrieves a list of files or directories based on the
    # provided parameters and performs various filtering, validation, and
    # sorting operations. The input files can be filtered by type, extension,
    # mime type, etc. It also supports recursive directory expansion and
    # validation of file conflicts.
    #
    # Parameters:
    #   - $1 (parameters): A string containing key-value pairs that configure
    #   the function's behavior. Example: 'par_type=file; par_min_items=2'.
    #
    # Parameters options:
    #   - "par_type": Specifies the type of items to filter:
    #       - "file" (default): Filters files.
    #       - "directory": Filters directories.
    #       - "all": Includes both files and directories.
    #   - "par_recursive": Specifies whether to expand directories recursively:
    #       - "false" (default): Does not expand directories.
    #       - "true": Expands directories recursively.
    #   - "par_get_pwd": If "true", returns the current working directory if no
    #     files are selected.
    #   - "par_max_items", "par_min_items": Limits the number of files.
    #   - "par_select_extension": Filters by file extension.
    #   - "par_select_mime": Filters by MIME type.
    #   - "par_skip_extension": Skips files with specific extensions.
    #   - "par_skip_encoding": Skips files with specific encodings.
    #   - "par_sort_list": If "true", sorts the list of files.
    #   - "par_validate_conflict": If "true", validates for filenames with the
    #     same base name.

    local parameters=$1
    local input_files=""
    input_files=$(_get_filenames_filemanager)

    # Default values for input parameters.
    local par_get_pwd="false"
    local par_max_items=""
    local par_min_items=""
    local par_recursive="false"
    local par_select_extension=""
    local par_select_mime=""
    local par_skip_encoding=""
    local par_skip_extension=""
    local par_sort_list="false"
    local par_type="file"
    local par_validate_conflict=""

    # Evaluate the values from the 'parameters' variable.
    eval "$parameters"

    # Check if there are input files.
    if (($(_get_items_count "$input_files") == 0)); then
        if [[ "$par_get_pwd" == "true" ]]; then
            # Return the current working directory if no files have been
            # selected.
            input_files=$(_get_working_directory)
        else
            # Try selecting the files by opening a file selection box.
            if [[ "$par_type" == "directory" ]]; then
                input_files=$(_display_dir_selection_box)
            else
                input_files=$(_display_file_selection_box)
            fi
        fi
    fi

    # If the items are in a remote server, translate the addresses to 'gvfs'.
    if [[ "$input_files" == *"://"* ]]; then
        local working_directory=""
        working_directory=$(_get_working_directory)
        input_files=$(sed \
            "s|[a-z0-9\+_-]*://[^$FIELD_SEPARATOR]*/|$working_directory/|g" \
            <<<"$input_files")
    fi

    local initial_items_count=0
    initial_items_count=$(_get_items_count "$input_files")

    local find_parameters=""
    if ((initial_items_count == 1)) && [[ -d "$input_files" ]] &&
        [[ "$par_recursive" == "false" ]] &&
        printf "%s" "$(basename -- "$input_files")" |
        grep --quiet --ignore-case --word-regexp "batch"; then
        # This workaround allows the scripts to handle cases with a large input
        # list of files. In this case, just select a single directory  with a
        # name that includes the word 'batch'. Then, the scripts operate on the
        # files within the selected directory. This addresses the GNOME error:
        # "Could not start application: Failed to execute child process
        # "/bin/sh" (Argument list too long)".
        find_parameters="-mindepth 1 -maxdepth 1"
    elif [[ "$par_recursive" == "false" ]]; then
        find_parameters="-maxdepth 0"
    fi

    # Pre-select the input files.
    input_files=$(_find_filtered_files \
        "$input_files" \
        "$par_type" \
        "$par_skip_extension" \
        "$par_select_extension" \
        "$find_parameters")

    # Return the current working directory if no directories have been
    # selected.
    if (($(_get_items_count "$input_files") == 0)); then
        if [[ "$par_get_pwd" == "true" ]] &&
            [[ "$par_type" == "directory" ]]; then
            input_files=$(_get_working_directory)
        fi
    fi

    # Validates the mime or encoding of the file.
    if [[ -n "$par_select_mime" ]] || [[ -n "$par_skip_encoding" ]]; then
        input_files=$(_validate_file_mime_parallel \
            "$input_files" \
            "$par_select_mime" \
            "$par_skip_encoding")
    fi

    # Validates the number of valid files.
    _validate_files_count \
        "$input_files" \
        "$par_type" \
        "$par_select_extension" \
        "$par_select_mime" \
        "$par_min_items" \
        "$par_max_items" \
        "$par_recursive"

    # Sort the list by filename.
    if [[ "$par_sort_list" == "true" ]]; then
        input_files=$(printf "%s" "$input_files" | tr "$FIELD_SEPARATOR" "\0" |
            sort --zero-terminated --version-sort | tr "\0" "$FIELD_SEPARATOR")
        input_files=$(_str_remove_empty_tokens "$input_files")
    fi

    # Validates filenames with the same base name.
    if [[ "$par_validate_conflict" == "true" ]]; then
        _validate_conflict_filenames "$input_files"
    fi

    printf "%s" "$input_files"
}

_get_file_encoding() {
    # This function retrieves the MIME encoding of a specified file.
    #
    # Parameters:
    #   - $1 (filename): The path to the file whose encoding is to be
    #     determined.

    local filename=$1
    local std_output=""

    std_output=$(file --dereference --brief --mime-encoding \
        -- "$filename" 2>/dev/null)

    if [[ "$std_output" == "cannot"* ]]; then
        return
    fi

    printf "%s" "$std_output"
}

_get_file_mime() {
    # This function retrieves the MIME type of a specified file.
    #
    # Parameters:
    #   - $1 (filename): The path to the file whose MIME is to be determined.

    local filename=$1
    local std_output=""

    std_output=$(file --dereference --brief --mime-type \
        -- "$filename" 2>/dev/null)

    if [[ "$std_output" == "cannot"* ]]; then
        return
    fi

    printf "%s" "$std_output"
}

_get_items_count() {
    # This function counts the number of items in a string, where items are
    # separated by a specific field separator. It assumes that the input string
    # contains a list of items separated by the value of the variable
    # $FIELD_SEPARATOR.
    #
    # Parameters:
    # - $1 (input_files): A string containing a list of items separated by
    #   the defined $FIELD_SEPARATOR.

    local input_files=$1
    local files_count=0

    if [[ -n "$input_files" ]]; then
        files_count=$(tr -cd "$FIELD_SEPARATOR" <<<"$input_files" | wc -c)
        ((files_count++))
    fi

    printf "%s" "$files_count"
}

_get_max_procs() {
    # This function returns the maximum number of processing units (CPU cores)
    # available on the system.

    nproc --all 2>/dev/null
}

_get_output_dir() {
    # This function determines and returns the appropriate output directory
    # path based on the provided parameters and the system's available
    # directories.
    #
    # Parameters:
    #   - $1 (parameters): A string containing key-value pairs that configure
    #   the function's behavior. Example: 'par_use_same_dir=true'.
    #
    # Parameters options:
    #   - "par_use_same_dir": If set to "true", the function uses the base
    #   directory (e.g., current working directory or an alternative with write
    #   permissions) as the output directory. If "false" or not set, a new
    #   subdirectory is created for the output.

    local parameters=$1
    local base_dir=""
    local output_dir=""

    # Default values for input parameters.
    local par_use_same_dir=""

    # Evaluate the values from the 'parameters' variable.
    eval "$parameters"

    # Check directories available to put the 'output' dir.
    base_dir=$(_get_working_directory)
    [[ ! -w "$base_dir" ]] && base_dir=$HOME
    [[ ! -w "$base_dir" ]] && base_dir="/tmp"
    if [[ ! -w "$base_dir" ]]; then
        _display_error_box "Could not find a directory with write permissions!"
        _exit_script
    fi

    if [[ "$par_use_same_dir" == "true" ]]; then
        printf "%s" "$base_dir"
        return
    fi

    output_dir="$base_dir/$PREFIX_OUTPUT_DIR"

    # If the file already exists, add a suffix.
    output_dir=$(_get_filename_next_suffix "$output_dir")

    mkdir --parents "$output_dir"
    printf "%s" "$output_dir"
}

_get_output_filename() {
    # This function generates the output filename based on the given input
    # file, output directory, and a set of optional parameters. It allows for
    # customization of the output filename by adding prefixes, suffixes, and
    # selecting how the file extension should be handled.
    #
    # Parameters:
    #   - $1 (input_file): The input file for which the output filename will be
    #     generated.
    #   - $2 (output_dir): The directory where the output file will be placed.
    #   - $3 (parameters): A string containing optional parameters that define
    #     how the output filename should be constructed. It supports the
    #     following options:
    #     - par_extension_opt: Specifies how to handle the file extension.
    #       Options are:
    #       - "append": Append a new extension "par_extension" to the existing
    #         file extension.
    #       - "preserve": Keep the original file extension.
    #       - "replace": Replace the current extension with a new one
    #         "par_extension".
    #       - "strip": Remove the file extension entirely.
    #     - par_extension: The extension to use when "par_extension_opt" is set
    #       to "append" or "replace". This value is ignored for the "preserve"
    #       and "strip" options.
    #     - par_prefix: A string to be added as prefix to the output filename.
    #     - par_suffix: A string to be added as suffix to the output
    #       filename, placed before the extension.

    local input_file=$1
    local output_dir=$2
    local parameters=$3
    local output_file=""
    local filename=""

    # Default values for input parameters.
    local par_extension_opt="preserve"
    local par_extension=""
    local par_prefix=""
    local par_suffix=""

    # Evaluate the values from the 'parameters' variable.
    eval "$parameters"

    filename=$(basename -- "$input_file")

    # Start constructing the output file path with the output directory.
    output_file="$output_dir/"
    [[ -n "$par_prefix" ]] && output_file+="$par_prefix "

    if [[ -d "$input_file" ]]; then
        # Handle case where input_file is a directory.
        case "$par_extension_opt" in
        "append" | "replace")
            output_file+=$filename
            output_file+=".$par_extension"
            ;;
        "preserve" | "strip")
            output_file+=$filename
            ;;
        esac
    else
        # Handle case where input_file is a regular file.
        case "$par_extension_opt" in
        "append")
            # Append the new extension to the existing one.
            output_file+=$(_strip_filename_extension "$filename")
            [[ -n "$par_suffix" ]] && output_file+=" $par_suffix"
            output_file+=$(_get_filename_extension "$filename")
            output_file+=".$par_extension"
            ;;
        "preserve")
            # Preserve the original extension.
            output_file+=$(_strip_filename_extension "$filename")
            [[ -n "$par_suffix" ]] && output_file+=" $par_suffix"
            output_file+=$(_get_filename_extension "$filename")
            ;;
        "replace")
            # Replace the existing extension with the new one.
            output_file+=$(_strip_filename_extension "$filename")
            [[ -n "$par_suffix" ]] && output_file+=" $par_suffix"
            output_file+=".$par_extension"
            ;;
        "strip")
            # Remove the extension.
            output_file+=$(_strip_filename_extension "$filename")
            [[ -n "$par_suffix" ]] && output_file+=" $par_suffix"
            ;;
        esac
    fi

    # If the file already exists, add a suffix.
    output_file=$(_get_filename_next_suffix "$output_file")

    printf "%s" "$output_file"
}

_get_qdbus_command() {
    # This function retrieves the first command matching the pattern "qdbus".
    # The command may vary depending on the Linux distribution and could be
    # "qdbus", "qdbus-qt6", "qdbus6", or similar variations.
    #
    # Returns:
    #   - "0" (true): If a command matching the pattern "qdbus" is found.
    #   - "1" (false): If no command matching the pattern "qdbus" is found.

    compgen -c | grep --perl-regexp -m1 "^qdbus" || return 1
    return 0
}

_get_script_name() {
    # This function returns the name of the currently executing script. It uses
    # the "basename" command to extract the script's filename from the full
    # path provided by "$0".

    basename -- "$0"
}

_get_temp_dir_local() {
    # This function creates a temporary directory in a specified location and
    # returns its path. The directory is created using "mktemp", with a custom
    # prefix ("basename"). It also generates a temporary file to track the
    # directory to be removed later.
    #
    # Parameters:
    #   - $1 (output_dir): The directory where the temporary directory will be
    #     created.
    #   - $2 (basename): The prefix for the temporary directory name.
    #
    # Output:
    #   - The full path to the newly created temporary directory.

    local output_dir=$1
    local basename=$2
    local temp_dir=""
    temp_dir=$(mktemp --directory \
        --tmpdir="$output_dir" "$basename.XXXXXXXX.tmp")

    # Remember to remove this directory after exit.
    item_to_remove=$(mktemp --tmpdir="$TEMP_DIR_ITEMS_TO_REMOVE")
    printf "%s$FIELD_SEPARATOR" "$temp_dir" >"$item_to_remove"

    printf "%s" "$temp_dir"
}

_get_temp_file() {
    # This function creates a temporary file in a specified temporary directory
    # and returns its path. The file is created using "mktemp", and the
    # directory for the temporary file is specified by the $TEMP_DIR_TASK
    # variable.
    #
    # Output:
    #   - The full path to the newly created temporary file.

    local temp_file=""
    temp_file=$(mktemp --tmpdir="$TEMP_DIR_TASK")

    printf "%s" "$temp_file"
}

_get_temp_file_dry() {
    # This function simulates the creation of a temporary file in a specified
    # directory without actually creating the file. It uses the "--dry-run"
    # option with "mktemp", which allows checking what the file path would be
    # if it were to be created.
    #
    # Output:
    #   - The path of the temporary file, without actually creating the file.

    local temp_file=""
    temp_file=$(mktemp --dry-run --tmpdir="$TEMP_DIR_TASK")

    printf "%s" "$temp_file"
}

_get_working_directory() {
    # This function attempts to determine the current working directory in a
    # variety of ways, based on the available environment variables or input
    # files. It first checks if the file manager (e.g., Caja, Nemo, or
    # Nautilus) provides the current URI (directory) of the script. If not, it
    # falls back to other methods. If the working directory cannot be obtained
    # from the file manager or input files, it defaults to the current
    # directory.
    #
    # Output:
    #   - The determined working directory.

    local working_directory=""

    # Try to use the information provided by the file manager.
    if [[ -v "CAJA_SCRIPT_CURRENT_URI" ]]; then
        working_directory=$CAJA_SCRIPT_CURRENT_URI
    elif [[ -v "NEMO_SCRIPT_CURRENT_URI" ]]; then
        working_directory=$NEMO_SCRIPT_CURRENT_URI
    elif [[ -v "NAUTILUS_SCRIPT_CURRENT_URI" ]]; then
        working_directory=$NAUTILUS_SCRIPT_CURRENT_URI
    fi

    if [[ -n "$working_directory" ]] &&
        [[ "$working_directory" == "file://"* ]]; then
        working_directory=$(_text_uri_decode "$working_directory")
    else
        # Files selected in the search screen (or other possible cases).
        working_directory=""
    fi

    if [[ -z "$working_directory" ]]; then
        # NOTE: The working directory can be detected by using the directory
        # name of the first input file. Some file managers do not send the
        # working directory for the scripts, so it is not precise to use the
        # 'pwd' command.
        local item_1=""
        item_1=$(cut -d "$FIELD_SEPARATOR" -f 1 <<<"$INPUT_FILES")

        if [[ -n "$item_1" ]]; then
            working_directory=$(_get_filename_dir "$item_1")
        else
            working_directory=$(pwd)
        fi
    fi

    printf "%s" "$working_directory"
}

_is_directory_empty() {
    # This function checks if a given directory is empty.
    #
    # Parameters:
    #   - $1 (directory): The path of the directory to check.
    #
    # Returns:
    #   - "0" (true): If the directory is empty.
    #   - "1" (false): If the directory contains any files or subdirectories.

    local directory=$1

    if ! find "$directory" -mindepth 1 -maxdepth 1 -print -quit |
        grep --quiet .; then
        return 0
    fi
    return 1
}

_is_gui_session() {
    # This function checks whether the script is running in a graphical user
    # interface (GUI) session. It does so by checking if the DISPLAY
    # environment variable is set, which is typically present in GUI sessions
    # (e.g., X11 or Wayland).
    #
    # Returns:
    #   - "0" (true): If is a GUI session.
    #   - "1" (false): If is not a GUI session.

    if env | grep --quiet "^DISPLAY"; then
        return 0
    fi
    return 1
}

_log_error() {
    # This function writes an error log entry with a specified message,
    # including details such as the input file, output file, and terminal
    # output. The entry is saved to a temporary log file.
    #
    # Parameters:
    #   - $1 (message): The error message to be logged.
    #   - $2 (input_file): The path of the input file associated with the
    #     operation.
    #   - $3 (std_output): The standard output or result from the operation
    #     that will be logged.
    #   - $4 (output_file): The path of the output file associated with the
    #     operation.

    local message=$1
    local input_file=$2
    local std_output=$3
    local output_file=$4

    local log_temp_file=""
    log_temp_file=$(mktemp --tmpdir="$TEMP_DIR_LOGS")

    {
        printf "[%s]\n" "$(date "+%Y-%m-%d %H:%M:%S")"
        if [[ -n "$input_file" ]]; then
            printf " > Input file: %s\n" "$input_file"
        fi
        if [[ -n "$output_file" ]]; then
            printf " > Output file: %s\n" "$output_file"
        fi
        printf " > %s\n" "Error: $message"
        if [[ -n "$std_output" ]]; then
            printf " > Standard output:\n"
            printf "%s\n" "$std_output"
        fi
        printf "\n"
    } >"$log_temp_file"
}

_logs_consolidate() {
    # This function gathers all error logs from a temporary directory and
    # compiles them into a single consolidated log file. If any error logs are
    # found, it displays an error message indicating the location of the
    # consolidated log file and terminates the script.
    #
    # Parameters:
    #   $1 (output_dir): Optional. The directory where the consolidated log
    #   file will be saved. If not specified, a default directory is used.

    local output_dir=$1
    local log_file_output="$output_dir/$PREFIX_ERROR_LOG_FILE.log"
    local log_files_count=""

    # Do nothing if there are no error log files.
    log_files_count="$(find "$TEMP_DIR_LOGS" -type f 2>/dev/null | wc -l)"
    if ((log_files_count == 0)); then
        return 0
    fi

    if [[ -z "$output_dir" ]]; then
        output_dir=$(_get_output_dir "par_use_same_dir=true")
    fi
    log_file_output="$output_dir/$PREFIX_ERROR_LOG_FILE.log"

    # If the file already exists, add a suffix.
    log_file_output=$(_get_filename_next_suffix "$log_file_output")

    # Compile log errors in a single file.
    {
        printf "Script: '%s'.\n" "$(_get_script_name)"
        printf "Total errors: %s.\n\n" "$log_files_count"
        cat -- "$TEMP_DIR_LOGS/"* 2>/dev/null
    } >"$log_file_output"

    local log_file=""
    log_file=$(_str_human_readable_path "$log_file_output")
    _display_error_box "Finished with errors! See the $log_file for details."

    _exit_script
}

_move_file() {
    # This function moves a file from the source location to the destination,
    # with options to handle conflicts when the destination file already
    # exists.
    #
    # Parameters:
    #   - $1 (par_when_conflict): Optional, default: "skip". Defines the
    #     behavior when the destination file already exists.
    #     - "overwrite": Overwrite the destination file.
    #     - "rename": Rename the source file to avoid conflicts by adding a
    #       suffix to the destination filename.
    #     - "skip": Skip moving the file if the destination file exists (logs a
    #       error).
    #   - $2 (file_src): The path to the source file to be moved.
    #   - $3 (file_dst): The destination path where the file should be moved.
    #
    # Returns:
    #   - "0" (true): If the operation is successful or if the source and
    #     destination are the same file.
    #   - "1" (false): If any required parameters are missing, if the move
    #     fails, or if an invalid conflict parameter is provided.

    local par_when_conflict=${1:-"skip"}
    local file_src=$2
    local file_dst=$3

    # Check for empty parameters.
    if [[ -z "$file_src" ]] || [[ -z "$file_dst" ]]; then
        return 1
    fi

    # Add the './' prefix in the path.
    if [[ ! "$file_src" == "/"* ]] &&
        [[ ! "$file_src" == "./"* ]] && [[ ! "$file_src" == "." ]]; then
        file_src="./$file_src"
    fi
    if [[ ! "$file_dst" == "/"* ]] &&
        [[ ! "$file_dst" == "./"* ]] && [[ ! "$file_dst" == "." ]]; then
        file_dst="./$file_dst"
    fi

    # Ignore moving to the same file.
    if [[ "$file_src" == "$file_dst" ]]; then
        return 0
    fi

    # Process the parameter "when_conflict": what to do when the 'file_dst'
    # already exists.
    case "$par_when_conflict" in
    "overwrite")
        mv -f -- "$file_src" "$file_dst" 2>/dev/null
        ;;
    "rename")
        # Rename the file (add a suffix).
        file_dst=$(_get_filename_next_suffix "$file_dst")
        mv -n -- "$file_src" "$file_dst" 2>/dev/null
        ;;
    "skip")
        # Do not move the file if the destination file already exists.
        mv -n -- "$file_src" "$file_dst" 2>/dev/null
        ;;
    esac

    if [[ -e "$file_src" ]]; then
        if [[ -e "$file_dst" ]]; then
            _log_error "The destination file already exists." \
                "$file_src" "" "$file_dst"
        else
            _log_error "Unable to move." \
                "$file_src" "" "$file_dst"
        fi
        return 1
    fi

    return 0
}

_move_temp_file_to_output() {
    # This function moves a temporary file to a specified output location,
    # handling various conditions to ensure the move is safe and proper.
    #
    # Parameters:
    #   - $1 (input_file): The path to the original input file, which may be
    #     backed up during the process.
    #   - $2 (temp_file): The temporary file to be moved to the output
    #     location.
    #   - $3 (output_file): The target path where the temp file should be
    #     moved.

    local input_file=$1
    local temp_file=$2
    local output_file=$3
    local std_output=""

    # Skip empty files.
    if [[ ! -s "$temp_file" ]]; then
        return 1
    fi

    # Skip files if the content of 'temp_file' is equal to the 'input_file'.
    if cmp --silent -- "$temp_file" "$input_file"; then
        return 1
    fi

    # If 'input_file' equals 'output_file', create a backup of 'input_file'.
    if [[ "$input_file" == "$output_file" ]]; then
        _move_file "rename" "$input_file" "$input_file.bak" || return 1
    fi

    # Move the 'temp_file' to 'output_file'.
    _move_file "rename" "$temp_file" "$output_file" || return 1

    # Preserve the same permissions of 'input_file'.
    std_output=$(chmod --reference="$input_file" -- "$output_file" 2>&1)
    _check_output "$?" "$std_output" "$input_file" "$output_file" || return 1

    return 0
}

_open_items_locations() {
    # This function opens the locations of selected items in the appropriate
    # file manager.
    #
    # Parameters:
    #   - $1 (items): A space-separated list of file or directory paths whose
    #   locations will be opened. Paths can be relative or absolute.
    #   - $2 (resolve_links): A boolean-like string ("true" or "false")
    #   indicating whether symbolic links in the provided paths should be
    #   resolved to their target locations before opening.

    local items=$1
    local resolve_links=$2

    # Exit if no items are provided.
    if [[ -z "$items" ]]; then
        return
    fi

    # Detect the currently running file manager.
    local file_manager=""
    if [[ -v "CAJA_SCRIPT_SELECTED_URIS" ]]; then
        file_manager="caja"
    elif [[ -v "NEMO_SCRIPT_SELECTED_URIS" ]]; then
        file_manager="nemo"
    elif [[ -v "NAUTILUS_SCRIPT_SELECTED_URIS" ]]; then
        file_manager="nautilus"
    else
        file_manager=$(pgrep -o -l \
            "caja|dolphin|nautilus|nemo|pcmanfm|pcmanfm-qt|thunar" |
            cut -f 2 -d " ")

        # If no file manager is detected, fall back to the system default.
        if [[ -z "$file_manager" ]]; then
            file_manager=$(_xdg_get_default_app "inode/directory")
        fi
    fi

    # Restore absolute paths for items if relative paths are used.
    local working_directory=""
    working_directory=$(_get_working_directory)
    items=$(sed "s|\./|$working_directory/|g" <<<"$items")

    # Prepare items to be opened by the file manager.
    local items_open=""
    local item=""
    for item in $items; do
        # Skip the root directory ("/") since opening it is redundant.
        if [[ "$item" == "/" ]]; then
            continue
        fi

        # Resolve symbolic links to their target locations if requested.
        if [[ "$resolve_links" == "true" ]] && [[ -L "$item" ]]; then
            item=$(readlink -f "$item")
        fi
        items_open+="$item$FIELD_SEPARATOR"
    done

    # Remove the trailing field separator.
    items_open=$(_str_remove_empty_tokens "$items_open")

    # Open the items using the detected file manager.
    case "$file_manager" in
    "nautilus" | "caja" | "dolphin")
        # Open the directory of each item and select it.
        # shellcheck disable=SC2086
        $file_manager --select $items_open &
        ;;
    "nemo" | "thunar")
        # Open the directory of each item (selection not supported).
        # shellcheck disable=SC2086
        $file_manager $items_open &
        ;;
    *)
        # For other file managers (e.g., "pcmanfm-qt"), open the directory of
        # each item.
        local dir=""
        for item in $items_open; do
            # Open the directory of the item.
            dir=$(_get_filename_dir "$item")
            if [[ -z "$dir" ]]; then
                continue
            fi
            $file_manager "$dir" &
        done
        ;;
    esac
}

_pkg_get_package_manager() {
    # This function detects and returns the name of the package manager
    # available on the system.
    #
    # Output:
    #   - Prints the name of the detected package manager. Possible values are:
    #     - "apt": Indicates that "apt-get" (Debian/Ubuntu) is available.
    #     - "dnf": Indicates that "dnf" (Fedora/RHEL) is available.
    #     - "pacman": Indicates that "pacman" (Arch Linux) is available.
    #     - "zypper": Indicates that "zypper" (openSUSE) is available.
    #   - If no supported package manager is found, the output is an empty
    #     string.

    local pkg_manager=""

    # Check for an installed package manager.
    if _command_exists "apt-get"; then
        pkg_manager="apt"
    elif _command_exists "dnf"; then
        pkg_manager="dnf"
    elif _command_exists "pacman"; then
        pkg_manager="pacman"
    elif _command_exists "zypper"; then
        pkg_manager="zypper"
    fi

    printf "%s" "$pkg_manager"
}

_pkg_install_packages() {
    # This function installs specified packages using the given package
    # manager.
    #
    # Parameters:
    #   - $1 (pkg_manager): The package manager to use for installation.
    #   Supported values are:
    #       - "apt": For Debian/Ubuntu systems.
    #       - "dnf": For Fedora/RHEL systems.
    #       - "pacman": For Arch Linux systems.
    #       - "zypper": For openSUSE systems.
    #   - $2 (packages): A space-separated list of package names to install.

    local pkg_manager=$1
    local packages=$2

    _display_wait_box_message "Installing the packages. Please, wait..."

    # Install the packages.
    if ! _command_exists "pkexec"; then
        _display_error_box \
            "Could not run the installer with administrator permission!"
        _exit_script
    fi

    case "$pkg_manager" in
    "apt")
        pkexec bash -c \
            "apt-get update; apt-get -y install $packages &>/dev/null"
        ;;
    "dnf")
        pkexec bash -c \
            "dnf check-update; dnf -y install $packages &>/dev/null"
        ;;
    "pacman")
        pkexec bash -c \
            "pacman -Syy; pacman --noconfirm -S $packages &>/dev/null"
        ;;
    "zypper")
        pkexec bash -c \
            "zypper refresh; zypper --non-interactive install $packages &>/dev/null"
        ;;
    esac

    _close_wait_box

    # Check if all packages were installed.
    packages=$(tr " " "$FIELD_SEPARATOR" <<<"$packages")
    local package=""
    for package in $packages; do
        if ! _pkg_is_package_installed "$pkg_manager" "$package"; then
            _display_error_box "Could not install the package '$package'!"
            _exit_script
        fi
    done

    _display_info_box "The packages have been successfully installed!"
}

_pkg_is_package_installed() {
    # This function checks if a specific package is installed using the given
    # package manager.
    #
    # Parameters:
    #   - $1 (pkg_manager): The package manager to use for the check.
    #   Supported values are:
    #       - "apt": For Debian/Ubuntu systems.
    #       - "dnf": For Fedora/RHEL systems.
    #       - "pacman": For Arch Linux systems.
    #       - "zypper": For openSUSE systems.
    #   - $2 (package): The name of the package to check.
    #
    # Returns:
    #   - "0" (true): If the package is installed.
    #   - "1" (false): If the package is not installed or an error occurs.

    local pkg_manager=$1
    local package=$2

    case "$pkg_manager" in
    "apt")
        if dpkg -s "$package" &>/dev/null; then
            return 0
        fi
        ;;
    "dnf")
        if dnf repoquery --installed | grep --quiet "$package"; then
            return 0
        fi
        ;;
    "pacman")
        if pacman -Q "$package" &>/dev/null; then
            return 0
        fi
        ;;
    "zypper")
        if zypper search --installed-only "$package" | grep --quiet "^i"; then
            return 0
        fi
        ;;
    esac
    return 1
}

_recent_scripts_organize() {
    # This function organizes the directory containing recently accessed
    # scripts ('ACCESSED_RECENTLY_DIR'). This function manages symbolic links
    # in the directory by:
    # 1. Keeping only the 'ACCESSED_RECENTLY_LINKS_TO_KEEP' most recently
    #    accessed scripts.
    # 2. Renaming retained links with numeric prefixes (e.g., "01", "02") to
    #    maintain chronological order.

    local links=()
    local link=""
    while IFS= read -r -d $'\0' link; do
        # Check if the link is broken.
        if [[ ! -e "$link" ]]; then
            # Remove broken links.
            rm -f -- "$link"
        else
            links+=("$link")
        fi
    done < <(
        find "$ACCESSED_RECENTLY_DIR" -maxdepth 1 -type l -print0 2>/dev/null |
            sort --zero-terminated --numeric-sort
    )

    # Process the links, keeping only the 'ACCESSED_RECENTLY_LINKS_TO_KEEP'
    # most recent.
    local count=1
    local link_name=""
    for link in "${links[@]}"; do
        if ((count <= ACCESSED_RECENTLY_LINKS_TO_KEEP)); then
            # Rename the link with a numeric prefix for ordering.
            link_name=$(basename "$link" |
                sed --regexp-extended 's|^.?[0-9]{2} ||')
            mv -f -- "$link" \
                "$ACCESSED_RECENTLY_DIR/$(printf '%02d' "$count") $link_name" \
                2>/dev/null
            ((count++))
        else
            # Remove excess links.
            rm -f -- "$link"
        fi
    done
}

_recent_scripts_add() {
    # This function adds the running script to the history of recently accessed
    # scripts ('ACCESSED_RECENTLY_DIR').

    local running_script=$0
    if [[ "$0" != "/"* ]]; then
        # If $0 is a relative path, resolve it relative to the current working
        # directory.
        running_script="$SCRIPT_DIR/"$(basename -- "$0")
    fi

    # Resolve symbolic links to their target locations.
    if [[ -L "$running_script" ]]; then
        running_script=$(readlink -f "$running_script")
    fi

    # Create 'ACCESSED_RECENTLY_DIR' if it does not exist.
    if [[ ! -d $ACCESSED_RECENTLY_DIR ]]; then
        mkdir -p "$ACCESSED_RECENTLY_DIR"
    fi

    _directory_push "$ACCESSED_RECENTLY_DIR" || return 1

    # Remove any existing links pointing to the same script.
    find "$ACCESSED_RECENTLY_DIR" -lname "$running_script" \
        -exec rm -f -- "{}" +

    # Create a new symbolic link with a ".00" prefix.
    ln -s -- "$running_script" ".00 $(basename -- "$running_script")"

    _directory_pop || return 1
}

_run_task_parallel() {
    # This function runs a task in parallel for a set of input files, using a
    # specified output directory for results.
    #
    # Parameters:
    #   - $1 (input_files): A field-separated list of file paths to process.
    #   - $2 (output_dir): The directory where the output files will be stored.

    local input_files=$1
    local output_dir=$2

    # Export variables to be used inside new shells (when using 'xargs').
    export \
        FIELD_SEPARATOR \
        GUI_BOX_HEIGHT \
        GUI_BOX_WIDTH \
        GUI_INFO_WIDTH \
        IGNORE_FIND_PATH \
        INPUT_FILES \
        TEMP_DATA_TASK \
        TEMP_DIR_ITEMS_TO_REMOVE \
        TEMP_DIR_LOGS \
        TEMP_DIR_STORAGE_TEXT \
        TEMP_DIR_TASK

    # Export functions to be used inside new shells (when using 'xargs').
    export -f \
        _check_output \
        _command_exists \
        _convert_delimited_string_to_text \
        _convert_text_to_delimited_string \
        _directory_pop \
        _directory_push \
        _display_password_box \
        _exit_script \
        _get_file_encoding \
        _get_file_mime \
        _get_filename_dir \
        _get_filename_extension \
        _get_filename_full_path \
        _get_filename_next_suffix \
        _get_max_procs \
        _get_output_filename \
        _get_temp_dir_local \
        _get_temp_file \
        _get_temp_file_dry \
        _get_working_directory \
        _is_directory_empty \
        _is_gui_session \
        _log_error \
        _main_task \
        _move_file \
        _move_temp_file_to_output \
        _storage_text_write \
        _storage_text_write_ln \
        _str_remove_empty_tokens \
        _strip_filename_extension \
        _text_remove_pwd \
        _text_uri_decode

    # Escape single quotes in filenames to handle them correctly in 'xargs'
    # with 'bash -c'.
    input_files=$(sed -z "s|'|'\\\''|g" <<<"$input_files")

    printf "%s" "$input_files" | xargs \
        --no-run-if-empty \
        --delimiter="$FIELD_SEPARATOR" \
        --max-procs="$(_get_max_procs)" \
        --replace="{}" \
        bash -c "_main_task '{}' '$output_dir'"
}

_storage_text_clean() {
    # This function clears all temporary text storage files.

    rm -f -- "$TEMP_DIR_STORAGE_TEXT/"* &>/dev/null
}

_storage_text_read_all() {
    # This function concatenates and outputs the content of all temporary text
    # storage files.

    cat -- "$TEMP_DIR_STORAGE_TEXT/"* 2>/dev/null
}

_storage_text_write() {
    # This function writes a given input text to temporary text storage files.
    #
    # Parameters:
    #   - $1 (input_text): The text to be stored in a temporary file.

    local input_text=$1
    local temp_file=""

    if [[ -z "$input_text" ]] || [[ "$input_text" == $'\n' ]]; then
        return
    fi

    # Save the text to be compiled into a single file.
    temp_file=$(mktemp --tmpdir="$TEMP_DIR_STORAGE_TEXT")
    printf "%s" "$input_text" >"$temp_file"
}

_storage_text_write_ln() {
    # This function writes a given input text, followed by a newline character,
    # to a temporary text storage file.

    local input_text=$1

    if [[ -z "$input_text" ]]; then
        return
    fi

    _storage_text_write "$input_text"$'\n'
}

_str_human_readable_path() {
    # This function transforms a given file path into a more human-readable
    # format.
    #
    # Parameters:
    #   - $1 (input_path): The input file path to process.

    local input_path=$1
    local output_path=""

    output_path=$(_text_remove_pwd "$input_path")
    output_path=$(_text_remove_home "$output_path")
    output_path=$(sed "s|^\./||g" <<<"$output_path")

    if [[ "$output_path" == "." ]]; then
        output_path="same"
    elif [[ "$output_path" == "~" ]]; then
        output_path="home"
    elif [[ "$output_path" == "" ]]; then
        output_path="(none)"
    else
        output_path="'$output_path'"
    fi

    printf "%s" "$output_path"
}

_str_remove_empty_tokens() {
    # This function removes empty tokens from a string, ensuring it is compact
    # and well-formed.
    #
    # Parameters:
    #   - $1 (input_str): The input string containing tokens separated by
    #     $FIELD_SEPARATOR.

    local input_str=$1
    input_str=$(tr -s "$FIELD_SEPARATOR" <<<"$input_str")
    input_str=$(sed "s|$FIELD_SEPARATOR$||" <<<"$input_str")

    printf "%s" "$input_str"
}

_strip_filename_extension() {
    # This function removes the file extension from a given filename, if one
    # exists.
    #
    # Parameters:
    #   - $1 (filename): The filename from which to strip the extension.
    #
    # Returns:
    #   - The filename without its extension.

    local filename=$1
    local extension=""
    extension=$(_get_filename_extension "$filename")

    if [[ -z "$extension" ]]; then
        printf "%s" "$filename"
        return 0
    fi

    local len_extension=${#extension}
    printf "%s" "${filename::-len_extension}"
}

_text_remove_empty_lines() {
    local input_text=$1

    grep -v "^\s*$" <<<"$input_text" || true
}

_text_remove_home() {
    # This function replaces the user's home directory path in a given string
    # with the tilde ("~") symbol for brevity.
    #
    # Parameters:
    #   - $1 (input_text): The input string that may contain the user's home
    #     directory path.
    #
    # Returns:
    #   - The modified string with the home directory replaced by "~", or the
    #     original string if "$HOME" is not defined.
    #
    # Examples:
    #   - Input: "/home/user/documents/file.txt" (assuming $HOME is
    #     "/home/user")
    #   - Output: "~/documents/file.txt"
    #
    #   - Input: "/etc/config" (assuming $HOME is "/home/user")
    #   - Output: "/etc/config"

    local input_text=$1

    if [[ -n "$HOME" ]]; then
        sed "s|$HOME|~|g" <<<"$input_text"
    else
        printf "%s" "$input_text"
    fi
}

_text_remove_pwd() {
    # This function replaces the current working directory path in a given
    # string with a dot (".") for brevity.
    #
    # Parameters:
    #   - $1 (input_text): The input string that may contain the current
    #     working directory path.
    #
    # Returns:
    #   - The modified string with the working directory replaced by ".", or
    #     the original string if the working directory is not found.
    #
    # Examples:
    #   - Input: "/home/user/project/file.txt" (assuming current directory is
    #     "/home/user/project")
    #   - Output: "./file.txt"
    #
    #   - Input: "/etc/config" (assuming current directory is
    #     "/home/user/project")
    #   - Output: "/etc/config"

    local input_text=$1
    local working_directory=""
    working_directory=$(_get_working_directory)

    if [[ "$working_directory" != "/" ]]; then
        sed "s|$working_directory|.|" <<<"$input_text"
    else
        printf "%s" "$input_text"
    fi
}

_text_sort() {
    # This function sorts the lines of a given text input in a version-aware
    # manner.
    #
    # Parameters:
    #   - $1 (input_text): The input text to be sorted, where each line is
    #     treated as a separate string.
    #
    # Returns:
    #   - The sorted text with each line in the correct order.

    local input_text=$1

    sort --version-sort <<<"$input_text"
}

_text_uri_decode() {
    # This function decodes a URI-encoded string by converting percent-encoded
    # characters back to their original form.
    #
    # Parameters:
    #   - $1 (uri_encoded): The URI-encoded string that needs to be decoded.
    #
    # Returns:
    #   - The decoded URI string, with percent-encoded characters replaced and
    #     the "file://" prefix removed.
    #
    # Example:
    #   - Input: "file:///home/user%20name/file%20name.txt"
    #   - Output: "/home/user name/file name.txt"

    local uri_encoded=$1

    uri_encoded=${uri_encoded//%/\\x}
    uri_encoded=${uri_encoded//file:\/\//}

    # shellcheck disable=SC2059
    printf "$uri_encoded"
}

_unset_global_variables_file_manager() {
    # This function unset global variables that may have been set by different
    # file managers (Caja, Nautilus, Nemo) during script execution.

    unset \
        CAJA_SCRIPT_CURRENT_URI \
        CAJA_SCRIPT_NEXT_PANE_CURRENT_URI \
        CAJA_SCRIPT_NEXT_PANE_SELECTED_FILE_PATHS \
        CAJA_SCRIPT_NEXT_PANE_SELECTED_URIS \
        CAJA_SCRIPT_SELECTED_FILE_PATHS \
        CAJA_SCRIPT_SELECTED_URIS \
        CAJA_SCRIPT_WINDOW_GEOMETRY \
        NAUTILUS_SCRIPT_CURRENT_URI \
        NAUTILUS_SCRIPT_NEXT_PANE_CURRENT_URI \
        NAUTILUS_SCRIPT_NEXT_PANE_SELECTED_FILE_PATHS \
        NAUTILUS_SCRIPT_NEXT_PANE_SELECTED_URIS \
        NAUTILUS_SCRIPT_SELECTED_FILE_PATHS \
        NAUTILUS_SCRIPT_SELECTED_URIS \
        NAUTILUS_SCRIPT_WINDOW_GEOMETRY \
        NEMO_SCRIPT_CURRENT_URI \
        NEMO_SCRIPT_NEXT_PANE_CURRENT_URI \
        NEMO_SCRIPT_NEXT_PANE_SELECTED_FILE_PATHS \
        NEMO_SCRIPT_NEXT_PANE_SELECTED_URIS \
        NEMO_SCRIPT_SELECTED_FILE_PATHS \
        NEMO_SCRIPT_SELECTED_URIS \
        NEMO_SCRIPT_WINDOW_GEOMETRY
}

_validate_conflict_filenames() {
    # This function checks if there are any files with the same base name in
    # the provided list of input files.
    #
    # Parameters:
    #   - $1 (input_files): A string containing a list of file paths, where
    #       each file path can include extensions. This list is checked for
    #       duplicates based on the base file name (excluding extensions).
    #
    # Example:
    #   - Input: "file1.txt file2.txt file1.jpg"
    #   - Output: An error box will be displayed indicating that "file1" is
    #     duplicated.

    local input_files=$1
    local filenames=""

    filenames=$(printf "%s" "$input_files" | tr "$FIELD_SEPARATOR" "\0" |
        sed -z "s|/\.|//|" | # Ignore hidden files without extension.
        sed -z --regexp-extended \
            "s|(\.tar)?\.[a-z0-9_~-]{0,15}$||I" | # Remove file extensions.
        sort --zero-terminated --version-sort |   # Sort files.
        uniq --zero-terminated --repeated |       # Find duplicate base names.
        tr "\0" "$FIELD_SEPARATOR")

    # If duplicates are found, display an error and exit the script.
    if [[ -n "$filenames" ]]; then
        local basenames=""
        # shellcheck disable=SC2086
        basenames=$(basename --multiple -- $filenames)
        _display_error_box \
            "There are selected files with the same base name:\n$basenames"
        _exit_script
    fi
}

_validate_file_mime() {
    # This function validates the MIME type and optionally the encoding of a
    # given file. It checks whether the file's MIME type matches a specified
    # pattern (regex) and, if provided, whether the file's encoding matches
    # another specified pattern.
    #
    # Parameters:
    #   - $1 (input_file): The path to the file that is being validated. This
    #       is the file whose MIME type will be checked.
    #   - $2 (par_select_mime): A MIME type pattern (or regular expression)
    #       used to validate the file's MIME type. If this parameter is empty,
    #       MIME type validation is skipped.
    #   - $3 (par_skip_encoding): An optional encoding pattern (or regular
    #       expression) used to validate the file's encoding. If this parameter
    #       is empty, encoding validation is skipped.

    local input_file=$1
    local par_select_mime=$2
    local par_skip_encoding=$3

    # Validate MIME type if a pattern is provided.
    if [[ -n "$par_select_mime" ]]; then
        local file_mime=""
        file_mime=$(_get_file_mime "$input_file")
        par_select_mime=${par_select_mime//+/\\+}
        grep --quiet --ignore-case --perl-regexp \
            "($par_select_mime)" <<<"$file_mime" || return
    fi

    # Validate encoding if a pattern is provided.
    if [[ -n "$par_skip_encoding" ]]; then
        local file_encoding=""
        file_encoding=$(_get_file_encoding "$input_file")
        par_skip_encoding=${par_skip_encoding//+/\\+}
        grep --quiet --ignore-case --perl-regexp \
            "($par_skip_encoding)" <<<"$file_encoding" && return
    fi

    # Create a temp file containing the name of the valid file.
    _storage_text_write "$input_file$FIELD_SEPARATOR"
}

_validate_file_mime_parallel() {
    # This function validates the MIME types of multiple files in parallel,
    # based on a specified MIME type pattern. It processes a list of file paths
    # concurrently using `xargs` and calls the `_validate_file_mime` function
    # for each file.
    #
    # Parameters:
    #   - $1 (input_files): A space-separated string containing the paths of
    #     the files to validate. These files will be checked for the MIME type
    #     pattern.
    #   - $2 (par_select_mime): The MIME type pattern (or regular expression)
    #       used to validate the files' MIME types. If this parameter is empty,
    #       no MIME type validation is performed, and all input files are
    #       returned as valid.
    #   - $3 (par_skip_encoding): An optional encoding pattern (or regular
    #       expression) used to validate the files' encodings. If this
    #       parameter is empty, no encoding validation is performed.
    #
    # Example:
    #   - Input: File paths "file1.txt file2.png", MIME pattern "text/plain".
    #   - Output: If "file1.txt" has a MIME type of "text/plain", it will be
    #     included in the output, but "file2.png" will be excluded if its MIME
    #     type doesn't match.

    local input_files=$1
    local par_select_mime=$2
    local par_skip_encoding=$3

    # Export variables to be used inside new shells (when using 'xargs').
    export FIELD_SEPARATOR TEMP_DIR_STORAGE_TEXT

    # Export functions to be used inside new shells (when using 'xargs').
    export -f \
        _get_file_encoding \
        _get_file_mime \
        _storage_text_write \
        _validate_file_mime

    # Escape single quotes in filenames to handle them correctly in 'xargs'
    # with 'bash -c'.
    input_files=$(sed -z "s|'|'\\\''|g" <<<"$input_files")

    # Execute the function '_validate_file_mime' for each file in parallel
    # (using 'xargs').
    printf "%s" "$input_files" | xargs \
        --no-run-if-empty \
        --delimiter="$FIELD_SEPARATOR" \
        --max-procs="$(_get_max_procs)" \
        --replace="{}" \
        bash -c \
        "_validate_file_mime '{}' '$par_select_mime' '$par_skip_encoding'"

    # Compile valid files in a single list.
    input_files=$(_storage_text_read_all)
    _storage_text_clean

    input_files=$(_str_remove_empty_tokens "$input_files")
    printf "%s" "$input_files"
}

_validate_files_count() {
    # This function validates the number of selected files or directories based
    # on several criteria, such as type, extension, MIME type, and minimum or
    # maximum item count.
    #
    # Parameters:
    #   - $1 (input_files): A space-separated string containing the paths of
    #     files or directories to be validated.
    #   - $2 (par_type): A string indicating the type of items to validate.
    #     Possible values:
    #     - "file": Validate files only.
    #     - "directory": Validate directories only.
    #     - "all": Validate both files and directories.
    #   - $3 (par_select_extension): A pipe-separated list of file extensions
    #     to filter the files. Files must have one of these extensions.
    #   - $4 (par_select_mime): A string representing MIME types to filter the
    #     files by. Only files with matching MIME types are selected.
    #   - $5 (par_min_items): The minimum number of valid items required. If
    #     fewer valid items are selected, an error is displayed.
    #   - $6 (par_max_items): The maximum number of valid items allowed. If
    #     more valid items are selected, an error is displayed.
    #   - $7 (par_recursive): A string indicating whether the validation should
    #     be recursive. If "true", directories will be searched recursively.
    #
    # Example:
    #   - Input: "dir1 dir2", "file", "txt|pdf", "", 1, 5, "true"
    #   - Output: The function checks if the directories "dir1" and "dir2"
    #     contain at least 1 and no more than 5 ".txt" or ".pdf" files,
    #     recursively.

    local input_files=$1
    local par_type=$2
    local par_select_extension=$3
    local par_select_mime=$4
    local par_min_items=$5
    local par_max_items=$6
    local par_recursive=$7

    # Define a label for a valid file.
    local valid_file_label="valid files"
    if [[ "$par_type" == "directory" ]]; then
        valid_file_label="directories"
    elif [[ -n "$par_select_mime" ]]; then
        valid_file_label="$par_select_mime"
        valid_file_label=$(sed "s|\|| or |g" <<<"$par_select_mime")
        valid_file_label=$(sed "s|/$||g; s|/ | |g" <<<"$valid_file_label")
        valid_file_label+=" files"
    elif [[ "$par_type" == "file" ]]; then
        valid_file_label="files"
    elif [[ "$par_type" == "all" ]]; then
        valid_file_label="files or directories"
    fi

    # Count the number of valid files.
    local valid_items_count=0
    valid_items_count=$(_get_items_count "$input_files")

    # Define a label for the extension.
    local extension_label=""
    if [[ -n "$par_select_extension" ]]; then
        extension_label="'.${par_select_extension//|/\' or \'.}'"
    fi

    # Check if there is at least one valid file.
    if ((valid_items_count == 0)); then
        if [[ "$par_recursive" == "true" ]]; then
            if [[ -n "$par_select_extension" ]]; then
                _display_error_box \
                    "No files with extension: $extension_label were selected!"
            else
                _display_error_box "No $valid_file_label were selected!"
            fi
        else
            if [[ -n "$par_select_extension" ]]; then
                _display_error_box \
                    "You must select files with extension: $extension_label!"
            else
                _display_error_box "You must select $valid_file_label!"
            fi
        fi
        _exit_script
    fi

    if [[ -n "$par_min_items" ]] && ((valid_items_count < par_min_items)); then
        _display_error_box \
            "You must select at least $par_min_items $valid_file_label!"
        _exit_script
    fi

    if [[ -n "$par_max_items" ]] && ((valid_items_count > par_max_items)); then
        _display_error_box \
            "You must select up to $par_max_items $valid_file_label!"
        _exit_script
    fi
}

_xdg_get_default_app() {
    # This function retrieves the default application associated with a
    # specific MIME type on a Linux system using the "xdg-mime" command.
    #
    # Parameters:
    #   - $1 (mime): The MIME type (e.g., "application/pdf", "image/png") for
    #     which to find the default application.
    #
    # Example:
    #   - Input: "application/pdf"
    #   - Output: The function prints the default application's executable for
    #     opening PDF files (e.g., "evince" or "okular").

    local mime=$1
    local desktop_file=""
    local default_app=""

    desktop_file=$(xdg-mime query default "$mime" 2>/dev/null)

    default_app=$(grep -m1 "^Exec" "/usr/share/applications/$desktop_file" |
        sed "s|Exec=||g" | cut -d " " -f 1)

    if [[ -z "$default_app" ]]; then
        _display_error_box \
            "Could not find the default application to open '$mime' files!"
        _exit_script
    fi

    printf "%s" "$default_app"
}

# Execute functions to organize the recently accessed scripts.
_recent_scripts_add
_recent_scripts_organize
