#!/bin/bash

# This file is part of The RetroPie Project
#
# The RetroPie Project is the legal property of its developers, whose names are
# too numerous to list here. Please refer to the COPYRIGHT.md file distributed with this source.
#
# See the LICENSE.md file at the top-level directory of this distribution and
# at https://raw.githubusercontent.com/RetroPie/RetroPie-Setup/master/LICENSE.md
#

## @file helpers.sh
## @brief RetroPie helpers library
## @copyright GPLv3

## @fn printMsgs()
## @param type style of display to use - dialog, console or heading
## @param message string or array of messages to display
## @brief Prints messages in a variety of ways.
function printMsgs() {
    local type="$1"
    shift
    if [[ "$__nodialog" == "1" && "$type" == "dialog" ]]; then
        type="console"
    fi
    for msg in "$@"; do
        [[ "$type" == "dialog" ]] && dialog --backtitle "$__backtitle" --cr-wrap --no-collapse --msgbox "$msg" 20 60 >/dev/tty
        [[ "$type" == "console" ]] && echo -e "$msg"
        [[ "$type" == "heading" ]] && echo -e "\n= = = = = = = = = = = = = = = = = = = = =\n$msg\n= = = = = = = = = = = = = = = = = = = = =\n"
    done
    return 0
}

## @fn printHeading()
## @param message string or array of messages to display
## @brief Calls PrintMsgs with "heading" type.
function printHeading() {
    printMsgs "heading" "$@"
}

## @fn fatalError()
## @param message string or array of messages to display
## @brief Calls PrintMsgs with "heading" type, and exits immediately.
function fatalError() {
    printHeading "Error"
    echo -e "$1"
    joy2keyStop
    exit 1
}

# @fn fnExists()
# @param name name of function to check for
# @brief Checks if function name exists.
# @retval 0 if the function name exists
# @retval 1 if the function name does not exist
function fnExists() {
    declare -f "$1" > /dev/null
    return $?
}

function ask() {
    echo -e -n "$@" '[y/n] ' ; read ans
    case "$ans" in
        y*|Y*) return 0 ;;
        *) return 1 ;;
    esac
}

## @fn runCmd()
## @param command command to run
## @brief Calls command and record any non zero return codes for later printing.
## @return whatever the command returns.
function runCmd() {
    local ret
    "$@"
    ret=$?
    if [[ "$ret" -ne 0 ]]; then
        md_ret_errors+=("Error running '$*' - returned $ret")
    fi
    return $ret
}

## @fn hasFlag()
## @param string string to search in
## @param flag flag to search for
## @brief Checks for a flag in a string (consisting of space separated flags).
## @retval 0 if the flag was found
## @retval 1 if the flag was not found
function hasFlag() {
    local string="$1"
    local flag="$2"
    [[ -z "$string" || -z "$flag" ]] && return 1

    if [[ "$string" =~ (^| )$flag($| ) ]]; then
        return 0
    else
        return 1
    fi
}

## @fn isPlatform()
## @param platform
## @brief Test for current platform / platform flags.
function isPlatform() {
    local flag="$1"
    if hasFlag "${__platform_flags[*]}" "$flag"; then
        return 0
    fi
    return 1
}

## @fn addLineToFile()
## @param line line to add
## @param file file to add line to
## @brief Adds a new line of text to a file.
function addLineToFile() {
    if [[ -f "$2" ]]; then
        cp -p "$2" "$2.bak"
    else
        sed -i --follow-symlinks '$a\' "$2"
    fi

    echo "$1" >> "$2"
}

## @fn editFile()
## @param file file to edit
## @brief Opens an editing dialog for specified file.
function editFile() {
    local file="$1"
    local cmd=(dialog --backtitle "$__backtitle" --editbox "$file" 22 76)
    local choice=$("${cmd[@]}" 2>&1 >/dev/tty)
    [[ -n "$choice" ]] && echo "$choice" >"$file"
}

## @fn inputBox()
## @param title title of dialog
## @param text default text
## @param minchars minimum chars to accept
## @brief Opens an inputbox dialog and echoes resulting text. Uses the OSK if installed.
## @details The input dialog has OK/Cancel buttons and can be cancelled by the user.
## The dialog will enforce the minimum number of characters expected, re-prompting the user.
## @retval 0 when the user entered the text and chose the OK button
## @retval != 0 when the user chose the Cancel button

function inputBox() {
    local title="$1"
    local text="$2"
    local minchars="$3"
    [[ -z "$minchars" ]] && minchars=0
    local params=(--backtitle "$__backtitle" --inputbox "Enter the $title")
    local osk="$(rp_getInstallPath joy2key)/osk.py"

    if [[ -f "$osk" ]]; then
        params+=(--minchars "$minchars")
        text=$(python3 "$osk" "${params[@]}" "$text" 2>&1 >/dev/tty) || return $?
    else
        while true; do
            text=$(dialog "${params[@]}" 10 60 "$text" 2>&1 >/dev/tty) || return $?
            [[ "${#text}" -ge "$minchars" ]] && break
            dialog --msgbox "$title must have at least $minchars characters" 8 60 2>&1 >/dev/tty
        done
    fi

    echo "$text"
}

## @fn hasPackage()
## @param package name of Debian package
## @param version requested version (optional)
## @param comparison type of comparison - defaults to `ge` (greater than or equal) if a version parameter is provided.
## @brief Test for an installed Debian package / package version.
## @retval 0 if the requested package / version was installed
## @retval 1 if the requested package / version was not installed
function hasPackage() {
    local pkg="$1"
    local req_ver="$2"
    local comp="$3"
    [[ -z "$comp" ]] && comp="ge"

    local ver
    local status
    # extract the first line only (for cases where both amd64 & i386 versions of a package are installed)
    local out=$(dpkg-query -W --showformat='${Status} ${Version}\n' $1 2>/dev/null | head -n1)
    if [[ "$?" -eq 0 ]]; then
        ver="${out##* }"
        status="${out% *}"
    fi

    local installed=0
    [[ "$status" == *"ok installed" ]] && installed=1
    # if we are not checking version
    if [[ -z "$req_ver" ]]; then
        # if the package is installed return true
        [[ "$installed" -eq 1 ]] && return 0
    else
        # if checking version and the package is not installed we need to clear "ver" as it may contain
        # the version number of a removed package and give a false positive with compareVersions.
        # we still need to do the version check even if not installed due to the varied boolean operators
        [[ "$installed" -eq 0 ]] && ver=""

        compareVersions "$ver" "$comp" "$req_ver" && return 0
    fi
    return 1
}

## @fn aptUpdate()
## @brief Calls apt-get update (if it has not been called before).
function aptUpdate() {
    if [[ "$__apt_update" != "1" ]]; then
        apt-get update --allow-releaseinfo-change
        __apt_update="1"
    fi
}

## @fn aptInstall()
## @param packages package / space separated list of packages to install
## @brief Calls apt-get install with the packages provided.
function aptInstall() {
    aptUpdate
    apt-get install -y "$@"
    return $?
}

## @fn aptRemove()
## @param packages package / space separated list of packages to install
## @brief Calls apt-get remove with the packages provided.
function aptRemove() {
    aptUpdate
    apt-get remove -y "$@"
    return $?
}

function _mapPackage() {
    local pkg="$1"
    case "$pkg" in
        libraspberrypi-bin)
            isPlatform "osmc" && pkg="rbp-userland-osmc"
            isPlatform "xbian" && pkg="xbian-package-firmware"
            ;;
        libraspberrypi-dev)
            isPlatform "osmc" && pkg="rbp-userland-dev-osmc"
            isPlatform "xbian" && pkg="xbian-package-firmware"
            ;;
        mali-fbdev)
            isPlatform "vero4k" && pkg=""
            ;;
        # handle our custom package alias LINUX-HEADERS
        LINUX-HEADERS)
            if isPlatform "rpi"; then
                if [[ "$__os_debian_ver" -lt 12 ]]; then
                    pkg="raspberrypi-kernel-headers"
                else
                    # on RaspiOS bookworm and later, kernel packages are separated by arch and model
                    isPlatform "rpi0" || isPlatform "rpi1" && pkg="linux-headers-rpi-v6"
                    if isPlatform "32bit"; then
                        isPlatform "rpi2" || isPlatform "rpi3" && pkg="linux-headers-rpi-v7"
                        isPlatform "rpi4" && pkg="linux-headers-rpi-v7l"
                    else
                        isPlatform "rpi3" || isPlatform "rpi4" && pkg="linux-headers-rpi-v8"
                        isPlatform "rpi5" && pkg="linux-headers-rpi-2712"
                    fi
                fi
            elif isPlatform "armbian"; then
                local branch="$(grep -oP "BRANCH=\K.*"      /etc/armbian-release)"
                local family="$(grep -oP "LINUXFAMILY=\K.*" /etc/armbian-release)"
                pkg="linux-headers-${branch}-${family}"
            elif [[ -z "$__os_ubuntu_ver" ]]; then
                pkg="linux-headers-$(uname -r)"
            else
                pkg="linux-headers-generic"
            fi
            ;;
        # map libpng-dev to libpng12-dev for Jessie
        libpng-dev)
            [[ "$__os_debian_ver" -lt 9 ]] && pkg="libpng12-dev"
            ;;
        libsdl1.2-dev)
            rp_isEnabled "sdl1" && pkg="RP sdl1 $pkg"
            ;;
        libsdl2-dev)
            if rp_isEnabled "sdl2"; then
                # check whether to use our own sdl2 - can be disabled to resolve issues with
                # mixing custom 64bit sdl2 and os distributed i386 version on multiarch
                local own_sdl2=1
                # default to off for x11 targets due to issues with dependencies with recent
                # Ubuntu (19.04). eg libavdevice58 requiring exactly 2.0.9 sdl2.
                isPlatform "x11" && own_sdl2=0
                iniConfig " = " '"' "$configdir/all/retropie.cfg"
                iniGet "own_sdl2"
                if [[ "$ini_value" == "1" ]]; then
                    own_sdl2=1
                elif [[ "$ini_value" == "0" ]]; then
                    own_sdl2=0
                fi
                [[ "$own_sdl2" -eq 1 ]] && pkg="RP sdl2 $pkg"
            fi
            ;;
        libfreetype6-dev)
            [[ "$__os_debian_ver" -gt 10 ]] || compareVersions "$__os_ubuntu_ver" gt 23.04 && pkg="libfreetype-dev"
            ;;
    esac
    echo "$pkg"
}

## @fn getDepends()
## @param packages package / space separated list of packages to install
## @brief Installs packages if they are not installed.
## @retval 0 on success
## @retval 1 on failure
function getDepends() {
    local own_pkgs=()
    local apt_pkgs=()
    local all_pkgs=()
    local pkg
    for pkg in "$@"; do
        pkg=($(_mapPackage "$pkg"))
        # manage our custom packages (pkg = "RP module_id pkg_name")
        if [[ "${pkg[0]}" == "RP" ]]; then
            # if removing, check if any version is installed and queue for removal via the custom module
            if [[ "$md_mode" == "remove" ]]; then
                if hasPackage "${pkg[2]}"; then
                    own_pkgs+=("${pkg[1]}")
                    all_pkgs+=("${pkg[2]}(custom)")
                fi
            else
                # if installing check if our version is installed and queue for installing via the custom module
                if hasPackage "${pkg[2]}" $(get_pkg_ver_${pkg[1]}) "ne"; then
                    own_pkgs+=("${pkg[1]}")
                    all_pkgs+=("${pkg[2]}(custom)")
                fi
            fi
            continue
        fi

        if [[ "$md_mode" == "remove" ]]; then
            # add package to apt_pkgs for removal if installed
            if hasPackage "$pkg"; then
                apt_pkgs+=("$pkg")
                all_pkgs+=("$pkg")
            fi
        else
            # add package to apt_pkgs for installation if not installed
            if ! hasPackage "$pkg"; then
                apt_pkgs+=("$pkg")
                all_pkgs+=("$pkg")
            fi
        fi

    done


    # return if no packages required
    [[ ${#apt_pkgs[@]} -eq 0 && ${#own_pkgs[@]} -eq 0 ]] && return

    # if we are removing, then remove packages, do an autoremove to clean up additional packages and return
    if [[ "$md_mode" == "remove" ]]; then
        printMsgs "console" "Removing dependencies: ${all_pkgs[*]}"
        for pkg in ${own_pkgs[@]}; do
            rp_callModule "$pkg" remove
        done
        apt-get remove --purge -y "${apt_pkgs[@]}"
        apt-get autoremove --purge -y
        return 0
    fi

    printMsgs "console" "Did not find needed dependencies: ${all_pkgs[*]}. Trying to install them now."

    # install any custom packages
    for pkg in ${own_pkgs[@]}; do
       rp_callModule "$pkg" _auto_
    done

    aptInstall --no-install-recommends "${apt_pkgs[@]}"

    local failed=()
    # check the required packages again rather than return code of apt-get,
    # as apt-get might fail for other reasons (eg other half installed packages)
    for pkg in ${apt_pkgs[@]}; do
        if ! hasPackage "$pkg"; then
            # workaround for installing samba in a chroot (fails due to failed smbd service restart)
            # we replace the init.d script with an empty script so the install completes
            if [[ "$pkg" == "samba" && "$__chroot" -eq 1 ]]; then
                mv /etc/init.d/smbd /etc/init.d/smbd.old
                echo "#!/bin/sh" >/etc/init.d/smbd
                chmod u+x /etc/init.d/smbd
                apt-get -f install
                mv /etc/init.d/smbd.old /etc/init.d/smbd
            else
                failed+=("$pkg")
            fi
        fi
    done

    if [[ ${#failed[@]} -gt 0 ]]; then
        md_ret_errors+=("Could not install package(s): ${failed[*]}.")
        return 1
    fi

    return 0
}


## @fn rpSwap()
## @param command *on* to add swap if needed and *off* to remove later
## @param memory total memory needed (swap added = memory needed - available memory)
## @brief Adds additional swap to the system if needed.
function rpSwap() {
    local command=$1
    local swapfile="$__swapdir/swap"
    case $command in
        on)
            rpSwap off
            local needed=$2
            local size=$((needed - __memory_avail))
            mkdir -p "$__swapdir/"
            if [[ $size -ge 0 ]]; then
                echo "Adding $size MB of additional swap"
                fallocate -l ${size}M "$swapfile"
                chmod 600 "$swapfile"
                mkswap "$swapfile"
                swapon "$swapfile"
            fi
            ;;
        off)
            echo "Removing additional swap"
            swapoff "$swapfile" 2>/dev/null
            rm -f "$swapfile"
            ;;
    esac
}

## @fn gitPullOrClone()
## @param dest destination directory
## @param repo repository to clone or pull from
## @param branch branch to clone or pull from (optional)
## @param commit specific commit to checkout (optional - requires branch to be set)
## @param depth depth parameter for git. (optional)
## @brief Git clones or pulls a repository.
## @details depth parameter will default to 1 (shallow clone) so long as __persistent_repos isn't set.
## A depth parameter of 0 will do a full clone with all history.
function gitPullOrClone() {
    local dir="$1"
    [[ -z "$dir" ]] && dir="$md_build"
    local repo="$2"
    local branch="$3"
    local commit="$4"
    local depth="$5"
    # if repo is blank then use the rp_module_repo info
    if [[ -z "$repo" && -n "$md_repo_url" ]]; then
        repo="$(rp_resolveRepoParam "$md_repo_url")"
        branch="$(rp_resolveRepoParam "$md_repo_branch")"
        commit="$(rp_resolveRepoParam "$md_repo_commit")"
    fi
    [[ -z "$repo" ]] && return 1
    [[ -z "$branch" ]] && branch="master"

    # if no depth is provided default to shallow clone (depth 1)
    [[ -z "$depth" ]] && depth=1
    # if we are using persistent repos or checking out a specific commit, don't shallow clone
    if [[ "$__persistent_repos" -eq 1 || -n "$commit" ]]; then
        depth=0
    fi

    # record the source directory in __mod_info[ID/repo_dir] if not previously set which will be used
    # by the packaging functions later to grab repository information
    if [[ -z "${__mod_info[$md_id/repo_dir]}" ]]; then
        __mod_info[$md_id/repo_dir]="$dir"
    fi

    if [[ -d "$dir/.git" ]]; then
        pushd "$dir" > /dev/null
        # if we are using persistent repos, fetch the latest remote changes and clean the source so
        # any patches can be re-applied as needed.
        if [[ "$__persistent_repos" -eq 1 ]]; then
            runCmd git fetch
            runCmd git reset --hard
            runCmd git clean -f -d
        fi
        runCmd git checkout "$branch"
        # only try to pull if we are on a tracking branch
        if [[ -n "$(git config --get branch.$branch.merge)" ]]; then
            runCmd git pull --ff-only
            runCmd git submodule update --init --recursive
        fi
        popd > /dev/null
    else
        local git="git clone --recursive"
        if [[ "$depth" -gt 0 ]]; then
            git+=" --depth $depth --shallow-submodules"
        fi
        git+=" --branch $branch"
        printMsgs "console" "$git \"$repo\" \"$dir\""
        runCmd $git "$repo" "$dir"
    fi

    if [[ -n "$commit" ]]; then
        printMsgs "console" "Winding back $repo->$branch to commit: #$commit"
        git -C "$dir" branch -D "$commit" &>/dev/null
        runCmd git -C "$dir" checkout -f "$commit" -b "$commit"
    fi

    branch=$(runCmd git -C "$dir" rev-parse --abbrev-ref HEAD)
    commit=$(runCmd git -C "$dir" rev-parse HEAD)
    printMsgs "console" "HEAD is now in branch '$branch' at commit '$commit'"
}

# @fn setupDirectories()
# @brief Makes sure some required retropie directories and files are created.
function setupDirectories() {
    mkdir -p "$rootdir"
    mkUserDir "$datadir"
    mkUserDir "$romdir"
    mkUserDir "$biosdir"
    mkUserDir "$configdir"
    mkUserDir "$configdir/all"

    # some home folders for configs that modules rely on
    mkUserDir "$home/.cache"
    mkUserDir "$home/.config"
    mkUserDir "$home/.local"
    mkUserDir "$home/.local/share"

    # make sure we have inifuncs.sh in place and that it is up to date
    mkdir -p "$rootdir/lib"
    local helper
    for helper in inifuncs.sh archivefuncs.sh; do
        if [[ ! -f "$rootdir/lib/$helper" || "$rootdir/lib/$helper" -ot "$scriptdir/scriptmodules/$helper" ]]; then
            cp --preserve=timestamps "$scriptdir/scriptmodules/$helper" "$rootdir/lib/$helper"
        fi
    done

    # create template for autoconf.cfg and make sure it is owned by $user
    local config="$configdir/all/autoconf.cfg"
    if [[ ! -f "$config" ]]; then
        echo "# this file can be used to enable/disable retropie autoconfiguration features" >"$config"
    fi
    chown $user:$user "$config"
}

## @fn rmDirExists()
## @param dir directory to remove
## @brief Removes a directory and all contents if it exists.
function rmDirExists() {
    if [[ -d "$1" ]]; then
        rm -rf "$1"
    fi
}

## @fn mkUserDir()
## @param dir directory to create
## @brief Creates a directory owned by the current user.
function mkUserDir() {
    mkdir -p "$1"
    chown $user:$user "$1"
}

## @fn mkRomDir()
## @param dir rom directory to create
## @brief Creates a directory under $romdir owned by the current user.
function mkRomDir() {
    mkUserDir "$romdir/$1"
    if [[ "$1" == "megadrive" ]]; then
        pushd "$romdir"
        ln -snf "$1" "genesis"
        popd
    fi
}

## @fn moveConfigDir()
## @param from source directory
## @param to destination directory
## @brief Moves the contents of a folder and symlinks to the new location.
function moveConfigDir() {
    local from="$1"
    local to="$2"

    # if we are in remove mode - remove the symlink
    if [[ "$md_mode" == "remove" ]]; then
        [[ -h "$from" ]] && rm -f "$from"
        return
    fi

    mkUserDir "$to"
    # move any old configs to the new location
    if [[ -d "$from" && ! -h "$from" ]]; then
        cp -a "$from/." "$to/"
        rm -rf "$from"
    fi
    ln -snf "$to" "$from"
    # set ownership of the actual link to $user
    chown -h $user:$user "$from"
}

## @fn moveConfigFile()
## @param from source file
## @param to destination file
## @brief Moves the file and symlinks to the new location.
function moveConfigFile() {
    local from="$1"
    local to="$2"

    # if we are in remove mode - remove the symlink
    if [[ "$md_mode" == "remove" && -h "$from" ]]; then
        rm -f "$from"
        return
    fi

    # move old file
    if [[ -f "$from" && ! -h "$from" ]]; then
        mv "$from" "$to"
    fi
    ln -sf "$to" "$from"
    # set ownership of the actual link to $user
    chown -h $user:$user "$from"
}

## @fn diffFiles()
## @param file1 file to compare
## @param file2 file to compare
## @brief Compares two files using diff.
## @retval 0 if the files were the same
## @retval 1 if they were not
## @retval >1 an error occurred
function diffFiles() {
    diff -q "$1" "$2" >/dev/null
    return $?
}

## @fn compareVersions()
## @param version first version to compare
## @param operator operator to use (lt le eq ne ge gt)
## @brief version second version to compare
## @retval 0 if the comparison was true
## @retval 1 if the comparison was false
function compareVersions() {
    dpkg --compare-versions "$1" "$2" "$3" >/dev/null
    return $?
}

## @fn dirIsEmpty()
## @param path path to directory
## @param files_only set to 1 to ignore sub directories
## @retval 0 if the directory is empty
## @retval 1 if the directory is not empty
function dirIsEmpty() {
    if [[ "$2" -eq 1 ]]; then
        [[ -z "$(ls -lA1 "$1" | grep "^-")" ]] && return 0
    else
        [[ -z "$(ls -A "$1")" ]] && return 0
    fi
    return 1
}

## @fn copyDefaultConfig()
## @param from source file
## @param to destination file
## @brief Copies a default configuration.
## @details Copies from the source file to the destination file if the destination
## file doesn't exist. If the destination is the same nothing is done. If different
## the source is copied to `$destination.rp-dist`.
function copyDefaultConfig() {
    local from="$1"
    local to="$2"
    # if the destination exists, and is different then copy the config as name.rp-dist
    if [[ -f "$to" ]]; then
        if ! diffFiles "$from" "$to"; then
            to+=".rp-dist"
            printMsgs "console" "Copying new default configuration to $to"
            cp "$from" "$to"
        fi
    else
        printMsgs "console" "Copying default configuration to $to"
        cp "$from" "$to"
    fi

    chown $user:$user "$to"
}

## @fn renameModule()
## @param from source file
## @param to destination file
## @brief Renames an existing module.
## @details Renames an existing module, moving it's install folder to the new location
## and changing any references to it in `emulators.cfg`.
function renameModule() {
    local from="$1"
    local to="$2"
    # move from old location and update emulators.cfg
    if [[ -d "$rootdir/$md_type/$from" ]]; then
        rm -rf "$rootdir/$md_type/$to"
        mv "$rootdir/$md_type/$from" "$rootdir/$md_type/$to"
        # replace any default = "$from"
        sed -i --follow-symlinks "s/\"$from\"/\"$to\"/g" "$configdir"/*/emulators.cfg
        # replace any $from = "cmdline"
        sed -i --follow-symlinks "s/^$from\([ =]\)/$to\1/g" "$configdir"/*/emulators.cfg
        # replace any paths with /$from/
        sed -i --follow-symlinks "s|/$from/|/$to/|g" "$configdir"/*/emulators.cfg
    fi
}

## @fn addUdevInputRules()
## @brief Creates a udev rule to adjust input device permissions.
## @details Creates a udev rule in `/etc/udev/rules.d/99-input.rules` to
## make everything in `/dev/input` it writable by any user in group `input`.
function addUdevInputRules() {
    if [[ ! -f /etc/udev/rules.d/99-input.rules ]]; then
        echo 'SUBSYSTEM=="input", GROUP="input", MODE="0660"' > /etc/udev/rules.d/99-input.rules
    fi
    # remove old 99-evdev.rules
    rm -f /etc/udev/rules.d/99-evdev.rules
}

## @fn setBackend()
## @param emulator.cfg key to configure backend for
## @param backend name of the backend to set
## @param force set to 1 to force the change
## @brief Set a backend rendering driver for a module
## @details Set a backend rendering driver for a module - can be currently default, dispmanx or x11.
## This function will only set a backend if
##   - It's not already configured, or
##   - The 3rd parameter (force) is set to 1
## The emulator.cfg key is usually the module_id but some modules add multiple emulator.cfg entries
## which are all handled separately. A module can use a _backend_set_MODULE function hook which is called
## from the backends module to handle calling setBackend for additional emulator.cfg entries.
## See "fuse" scriptmodule for an example.
function setBackend() {
    local config="$configdir/all/backends.cfg"
    local id="$1"
    local mode="$2"
    local force="$3"
    iniConfig "=" "\"" "$config"
    iniGet "$id"
    if [[ "$force" -eq 1 || -z "$ini_value" ]]; then
        iniSet "$id" "$mode"
        chown $user:$user "$config"
    fi
}

## @fn getBackend()
## @param emulator.cfg key to get backend for
## @brief Get a backend rendering driver for a module
## @details Get a backend rendering driver for a module
## The function echos the result so the value can be captured using var=$(getBackend "$module_id")
function getBackend() {
    local config="$configdir/all/backends.cfg"
    local id="$1"
    iniConfig " = " '"' "$config"
    iniGet "$id"
    if [[ -n "$ini_value" ]]; then
        # translate old value of 1 as dispmanx for backward compatibility
        [[ "$ini_value" == "1" ]] && ini_value="dispmanx"
     else
        ini_value="default"
     fi
     echo "$ini_value"
}

## @fn setDispmanx()
## @param module_id name of module to add dispmanx flag for
## @param status initial status of flag (0 or 1)
## @brief Sets a dispmanx flag for a module. This function is deprecated.
## @details Set a dispmanx flag for a module as to whether it should use the
## sdl1 dispmanx backend by default or not (0 for framebuffer, 1 for dispmanx).
## This function is deprecated and instead setBackend should be used.
function setDispmanx() {
    isPlatform "dispmanx" || return
    setBackend "$1" "dispmanx"
}

## @fn iniFileEditor()
## @param delim ini file delimiter eg. ' = '
## @param quote ini file quoting character eg. '"'
## @param config ini file to edit
## @brief Allows editing of ini files with a user friendly dialog based gui.
## @details Some arrays need to be configured before calling this, which are
## used to display what can be edited and the options available.
##
## The first array is `$ini_titles` which provides the titles for each
## entry..
##
## The second array is `$ini_descs` which contains a help description for each
## entry.
##
## The third array is `$ini_options` which contains multiple space separated
## strings in each element to control how each entry should be managed.
##
## The `$ini_options` array is constructed as follows:
##
## If the first string is `_function_` then the next string should be a function
## name that will handle that entry. The function will be called with a parameter
## `get` or `set`. The function should return the value for get via `echo`
## and should handle any gui functionality when called with `set`. This can be
## used for example to build custom dialogs.
##
## If the first option is anything else, it is assumed to be a key name, followed
## by a control type and a list of parameters.
##
## Control types are:
##  * `_id_` map the following values to an id
##  * `_string_` allow the value to be inputted by the user
##  * `_file_` select from a list of files. The following values are wildcard,
##    then file path.
##
## If none of the above, then the rest of the array element should be a list of
## possible values for the key.
##
## Some examples for ini_options:
##
##     ini_options=('video_smooth true false')
## Allow setting of the key `video_smooth` with the values of *true* or *false*
##
##     ini_options=('aspect_ratio_index _id_ 4:3 16:9 16:10)
## Allow setting of the key `aspect_ratio_index` with the values 0 1 or 2 which
## correspond to the ratios. The user is shown the ratios, but the ini configuration
## is set to the id (4:3 = 0, 16:9 = 1, 16:10 = 2).
##
##     ini_options=('_function_ _video_fullscreen_configedit')
## The function `_video_fullscreen_configedit` is called with *get* or *set*
## to manage this entry.
##
##     ini_options=("video_shader _file_ *.*p $rootdir/emulators/retroarch/shader")
## The key `video_shader` will be able to be set to a list of files in
## `$rootdir/emulators/retroarch/shader` that match the wildcard `*.*p`
##
## For more examples you can check out the code in supplementary/configedit.sh
function iniFileEditor() {
    local delim="$1"
    local quote="$2"
    local config="$3"
    [[ ! -f "$config" ]] && return

    iniConfig "$delim" "$quote" "$config"
    local sel
    local value
    local option
    local title
    while true; do
        local options=()
        local params=()
        local values=()
        local keys=()
        local i=0

        # generate menu from options
        for option in "${ini_options[@]}"; do
            # split into new array (globbing safe)
            read -ra option <<<"$option"
            key="${option[0]}"
            keys+=("$key")
            params+=("${option[*]:1}")

            # if the first parameter is _function_ we call the second parameter as a function
            # so we can handle some options with a custom menu etc
            if [[ "$key" == "_function_" ]]; then
                value="$(${option[1]} get)"
            else
                # get current value
                iniGet "$key"
                if [[ -n "$ini_value" ]]; then
                    value="$ini_value"
                else
                    value="unset"
                fi
            fi

            values+=("$value")

            # add the matching value to our id in _id_ lists
            if [[ "${option[1]}" == "_id_" && "$value" != "unset" ]]; then
                value+=" - ${option[value+2]}"
            fi

            # use custom title if provided
            if [[ -n "${ini_titles[i]}" ]]; then
                title="${ini_titles[i]}"
            else
                title="$key"
            fi

            options+=("$i" "$title ($value)" "${ini_descs[i]}")

            ((i++))
        done

        local cmd=(dialog --backtitle "$__backtitle" --default-item "$sel" --item-help --help-button --menu "Please choose the setting to modify in $config" 22 76 16)
        sel=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)
        if [[ "${sel[@]:0:4}" == "HELP" ]]; then
            printMsgs "dialog" "${sel[@]:5}"
            continue
        fi

        [[ -z "$sel" ]] && break

        # if the key is _function_ we handle the option with a custom function
        if [[ "${keys[sel]}" == "_function_" ]]; then
            "${params[sel]}" set "${values[sel]}"
            continue
        fi

        # process the editing of the option
        i=0
        options=("U" "unset")
        local default=""

        # split into new array (globbing safe)
        read -ra params <<<"${params[sel]}"

        local mode="${params[0]}"

        case "$mode" in
            _string_)
                options+=("E" "Edit (Currently ${values[sel]})")
                ;;
            _file_)
                local match="${params[1]}"
                local path="${params[*]:2}"
                local file
                while read file; do
                    [[ "${values[sel]}" == "$file" ]] && default="$i"
                    file="${file//$path\//}"
                    options+=("$i" "$file")
                    ((i++))
                done < <(find -L "$path" -type f -name "$match" | sort)
                ;;
            _id_|*)
                [[ "$mode" == "_id_" ]] && params=("${params[@]:1}")
                for option in "${params[@]}"; do
                    if [[ "$mode" == "_id_" ]]; then
                        [[ "${values[sel]}" == "$i" ]] && default="$i"
                    else
                        [[ "${values[sel]}" == "$option" ]] && default="$i"
                    fi
                    options+=("$i" "$option")
                    ((i++))
                done
                ;;
        esac
        [[ -z "$default" ]] && default="U"
        # display values
        cmd=(dialog --backtitle "$__backtitle" --default-item "$default" --menu "Please choose the value for ${keys[sel]}" 22 76 16)
        local choice=$("${cmd[@]}" "${options[@]}" 2>&1 >/dev/tty)

        # if it is a _string_ type we will open an inputbox dialog to get a manual value
        if [[ -z "$choice" ]]; then
            continue
        elif [[ "$choice" == "E" ]]; then
            [[ "${values[sel]}" == "unset" ]] && values[sel]=""
            cmd=(dialog --backtitle "$__backtitle" --inputbox "Please enter the value for ${keys[sel]}" 10 60 "${values[sel]}")
            value=$("${cmd[@]}" 2>&1 >/dev/tty)
        elif [[ "$choice" == "U" ]]; then
            value=""
        else
            if [[ "$mode" == "_id_" ]]; then
                value="$choice"
            else
                # get the actual value from the options array
                local index=$((choice*2+3))
                if [[ "$mode" == "_file_" ]]; then
                    value="$path/${options[index]}"
                else
                    value="${options[index]}"
                fi
            fi
        fi

        if [[ "$choice" == "U" ]]; then
            iniUnset "${keys[sel]}" "$value"
        else
            iniSet "${keys[sel]}" "$value"
        fi

    done
}

## @fn setESSystem()
## @param fullname full name of system
## @param name short name of system
## @param path rom path
## @param extension file extensions to show
## @param command command to run
## @param platform name of platform (used by es for scraping)
## @param theme name of theme to use
## @brief Adds a system entry for Emulation Station (to /etc/emulationstation/es_systems.cfg).
function setESSystem() {
    local function
    for function in $(compgen -A function _add_system_); do
        "$function" "$@"
    done
}

## @fn ensureSystemretroconfig()
## @param system system to create retroarch.cfg for
## @brief Deprecated - use defaultRAConfig
## @details Creates a default retroarch.cfg for specified system in `$configdir/$system/retroarch.cfg`.
function ensureSystemretroconfig() {
    # don't do any config work on module removal
    [[ "$md_mode" == "remove" ]] && return

    # reset "$md_conf_root" to "$configdir" as defaultRAConfig handles this whereas ensureSystemretroconfig
    # expects system to include any subdirectory in the first parameter such as "ports/$system".
    local save_conf_root="$md_conf_root"
    md_conf_root="$configdir"
    defaultRAConfig "$1"
    md_conf_root="$save_conf_root"
}

## @fn defaultRAConfig()
## @param system system to create retroarch.cfg for
## @param ... optional key then value parameters to be used in the config
## @brief Creates a default retroarch.cfg for specified system in `$md_root_dir/$system/retroarch.cfg`.
## @details Additional default configuration values can be provided as parameters to the function - eg. "fps_show" "true"
## as two parameters would add a default entry of fps_show = "true" to the default configuration.
## This function uses $md_conf_root as a base, so there is no need to use "ports/$system" for libretro ports as with
## the older ensureSystemretroconfig
function defaultRAConfig() {
    # don't do any config work on module removal
    [[ "$md_mode" == "remove" ]] && return

    local system="$1"
    shift
    local defaults=("$@")

    local config_path="$md_conf_root/$system"

    [[ ! -d "$config_path" ]] && mkUserDir "$config_path"

    local config="$(mktemp)"
    # add the initial comment regarding include order
    echo -e "# Settings made here will only override settings in the global retroarch.cfg if placed above the #include line\n" >"$config"

    # add the per system default settings
    iniConfig " = " '"' "$config"
    iniSet "input_remapping_directory" "$config_path"

    # add any additional config key / values from function parameters
    local key
    local value
    while read key value; do
        [[ -n "$key" ]] && iniSet "$key" "$value"
    done <<< "${defaults[@]}"

    # include the main retroarch config
    echo -e "\n#include \"$configdir/all/retroarch.cfg\"" >>"$config"

    copyDefaultConfig "$config" "$config_path/retroarch.cfg"
    rm "$config"
}

## @fn setRetroArchCoreOption()
## @param option option to set
## @param value value to set
## @brief Sets a retroarch core option in `$configdir/all/retroarch-core-options.cfg`.
function setRetroArchCoreOption() {
    local option="$1"
    local value="$2"
    iniConfig " = " "\"" "$configdir/all/retroarch-core-options.cfg"
    iniGet "$option"
    if [[ -z "$ini_value" ]]; then
        iniSet "$option" "$value"
    fi
    chown $user:$user "$configdir/all/retroarch-core-options.cfg"
}

## @fn setConfigRoot()
## @param dir directory under $configdir to use
## @brief Sets module config root `$md_conf_root` to subfolder from `$configdir`
## @details This is used for ports that are not actually in scriptmodules/ports
## as they would get the wrong config root otherwise.
function setConfigRoot() {
    local dir="$1"
    md_conf_root="$configdir"
    [[ -n "$dir" ]] && md_conf_root+="/$dir"
    mkUserDir "$md_conf_root"
}

## @fn loadModuleConfig()
## @param params space separated list of key=value parameters
## @brief Load the settings for a module.
## @details This allows modules to quickly load some settings from an ini file.
## It can provide a shortcut way to load a set of keys from an ini file into
## variables.
##
## It requires iniConfig to be called first to specify the format and file.
## eg.
##
##     iniConfig " = " '"' "$configdir/all/mymodule.cfg"
##     eval $(loadModuleConfig \
##        'some_option=1' \
##        'another_option=2'
##
## This would load the keys `some_option` and `another_option` into local
## variables `some_option` and `another_option`. If the keys did not exist
## in mymodule.cfg the variables would be initialised to 1 and 2.
function loadModuleConfig() {
    local options=("$@")
    local option
    local key
    local value

    for option in "${options[@]}"; do
        option=(${option/=/ })
        key="${option[0]}"
        value="${option[@]:1}"
        iniGet "$key"
        if [[ -z "$ini_value" ]]; then
            iniSet "$key" "$value"
            echo "local $key=\"$value\""
        else
            echo "local $key=\"$ini_value\""
        fi
    done
}

## @fn applyPatch()
## @param patch filename of patch to apply
## @brief Apply a patch if it has not already been applied to current folder.
## @details This is used for applying patches against upstream code.
## @retval 0 on success
## @retval 1 on failure
function applyPatch() {
    local patch="$1"
    local patch_applied="${patch##*/}.applied"

    if [[ ! -f "$patch_applied" ]]; then
        if patch -f -p1 <"$patch"; then
            touch "$patch_applied"
            printMsgs "console" "Successfully applied patch: $patch"
        else
            md_ret_errors+=("$md_id patch $patch failed to apply")
            return 1
        fi
    fi
    return 0
}

## @fn runCurl()
## @params ... commandline arguments to pass to curl
## @brief Run curl with chosen parameters and handle curl errors
## @details Runs curl with the provided parameters, whilst also capturing the output and extracting
## any error message, which is stored in the global variable __NET_ERRMSG. Function returns the return
## code provided by curl. The environment variable __curl_opts can be set to override default curl
## parameters, eg - timeouts etc.
## @retval curl return value
function runCurl() {
    local params=("$@")
    # add any user supplied curl opts - timeouts can be overridden as curl uses the last parameters given
    [[ -n "$__curl_opts" ]] && params+=($__curl_opts)

    local cmd_err
    local ret

    # get the last non zero exit status (ignoring tee)
    set -o pipefail

    # set up additional file descriptor for stdin
    exec 3>&1

    # capture stderr - while passing both stdout and stderr to terminal
    # curl like wget outputs the progress meter to stderr, so we will extract the error line later
    cmd_err=$(curl "${params[@]}" 2>&1 1>&3 | tee /dev/stderr)
    ret="$?"

    # remove stdin copy
    exec 3>&-

    set +o pipefail

    # if there was an error, extract it and put in __NET_ERRMSG
    if [[ "$ret" -ne 0 ]]; then
        # as we also capture the curl progress output, extract the last line which contains the error
        __NET_ERRMSG="${cmd_err##*$'\n'}"
    else
        __NET_ERRMSG=""
    fi
    return "$ret"
}

## @fn download()
## @param url url of file
## @param dest destination name (optional), use - for stdout
## @brief Download a file
## @details Download a file - if the dest parameter is omitted, the file will be downloaded to the current directory.
## If the destination name is a hyphen (-), then the file will be outputted to stdout, for piping to another command
## or retrieving the contents directly to a variable. If the destination is a folder, extract with the basename from
## the url to the destination folder.
## @retval 0 on success
function download() {
    local url="$1"
    local dest="$2"
    local file="${url##*/}"

    # if no destination, get the basename from the url
    [[ -z "$dest" ]] && dest="${PWD}/$file"

    # if the destination is a folder, download to that with filename from url
    [[ -d "$dest" ]] && dest="$dest/$file"

    local params=(--location)
    if [[ "$dest" == "-" ]]; then
        params+=(--silent --no-buffer)
    else
        printMsgs "console" "Downloading $url to $dest ..."
        params+=(-o "$dest")
    fi
    params+=(--connect-timeout 10 --speed-limit 1 --speed-time 60 --fail)
    # add the url
    params+=("$url")

    local ret
    runCurl "${params[@]}"
    ret="$?"

    # if download failed, remove file, log error and return error code
    if [[ "$ret" -ne 0 ]]; then
        # remove dest if not set to stdout and exists
        [[ "$dest" != "-" && -f "$dest" ]] && rm "$dest"
        md_ret_errors+=("URL $url failed to download.\n\n$__NET_ERRMSG")
    fi
    return "$ret"
}

## @fn downloadAndVerify()
## @param url url of file
## @param dest destination file (optional)
## @brief Download a file and a corresponding .asc signature and verify the contents
## @details Download a file and a corresponding .asc signature and verify the contents.
## The .asc file will be downloaded to verify the file, but will be removed after downloading.
## If the dest parameter is omitted, the file will be downloaded to the current directory
## @retval 0 on success
function downloadAndVerify() {
    local url="$1"
    local dest="$2"
    local file="${url##*/}"

    # if no destination, get the basename from the url (supported by GNU basename)
    [[ -z "$dest" ]] && dest="${PWD}/$file"

    local cmd_out
    local ret=1
    if download "${url}.asc" "${dest}.asc"; then
        if download "$url" "$dest"; then
            cmd_out="$(gpg --verify "${dest}.asc" 2>&1)"
            ret="$?"
            if [[ "$ret" -ne 0 ]]; then
                md_ret_errors+=("$dest failed signature check:\n\n$cmd_out")
            fi
        fi
    fi
    return "$ret"
}

## @fn downloadAndExtract()
## @param url url of archive
## @param dest destination folder for the archive
## @param optional additional parameters to pass to the decompression tool.
## @brief Download and extract an archive
## @details Download and extract an archive.
## @retval 0 on success
function downloadAndExtract() {
    local url="$1"
    local dest="$2"
    shift 2
    local opts=("$@")

    local ext="${url##*.}"
    local file="${url##*/}"

    local temp="$(mktemp -d)"
    # download file, removing temporary folder and returning on error
    if ! download "$url" "$temp/$file"; then
        rm -rf "$temp"
        return 1
    fi

    mkdir -p "$dest"

    local ret
    case "$ext" in
        exe|zip)
            runCmd unzip "${opts[@]}" -o "$temp/$file" -d "$dest"
            ;;
        *)
            tar -xvf "$temp/$file" -C "$dest" "${opts[@]}"
            ;;
    esac
    ret=$?

    rm -rf "$temp"

    return $ret
}

## @fn ensureFBMode()
## @param res_x width of mode
## @param res_y height of mode
## @brief Add a framebuffer mode to /etc/fb.modes
## @details Useful for adding specific resolutions used by emulators so SDL1 can
## use them and utilise the RPI hardware scaling. Without for example a 320x240
## mode in fb.modes many of the emulators that output to the framebuffer and
## were not set to use the dispmanx SDL1 backend would just show in a small
## area of the screen.
function ensureFBMode() {
    [[ ! -f /etc/fb.modes ]] && return
    local res_x="$1"
    local res_y="$2"
    local res="${res_x}x${res_y}"
    sed -i --follow-symlinks "/$res mode/,/endmode/d" /etc/fb.modes

    cat >> /etc/fb.modes <<_EOF_
# added by RetroPie-Setup - $res mode for emulators
mode "$res"
    geometry $res_x $res_y $res_x $res_y 16
    timings 0 0 0 0 0 0 0
endmode
_EOF_
}

## @fn joy2keyStart()
## @param left mapping for left
## @param right mapping for right
## @param up mapping for up
## @param down mapping for down
## @param but1 mapping for button 1
## @param but2 mapping for button 2
## @param but3 mapping for button 3
## @param butX mapping for button X ...
## @brief Start joy2key process in background to map joystick presses to keyboard
## @details Arguments are curses capability names or hex values starting with '0x'
## see: http://pubs.opengroup.org/onlinepubs/7908799/xcurses/terminfo.html
function joy2keyStart() {
    # don't start on SSH sessions
    # (check for bracket in output - ip/name in brackets over a SSH connection)
    [[ "$(who -m)" == *\(* ]] && return

    local params=("$@")

    # if joy2key is installed, run it
    if rp_isInstalled "joy2key"; then
        "$(rp_getInstallPath joy2key)/joy2key" start "${params[@]}" 2>/dev/null && return 0
    fi

    return 1
}

## @fn joy2keyStop()
## @brief Stop previously started joy2key process.
function joy2keyStop() {
    # if joy2key is installed, stop it
    if rp_isInstalled "joy2key"; then
        "$(rp_getInstallPath joy2key)/joy2key" stop
    fi
}

## @fn getPlatformConfig()
## @param key key to look up
## @brief gets a config from a platforms.cfg ini
## @details gets a config from a platforms.cfg ini first looking in
## `$configdir/all/platforms.cfg` then `$scriptdir/platforms.cfg`
## allowing users to override any parts of `$scriptdir/platforms.cfg`
function getPlatformConfig() {
    local key="$1"
    local conf
    for conf in "$configdir/all/platforms.cfg" "$scriptdir/platforms.cfg"; do
        [[ ! -f "$conf" ]] && continue
        iniConfig "=" '"' "$conf"
        iniGet "$key"
        [[ -n "$ini_value" ]] && break
    done
    # workaround for RetroPie platform
    [[ "$key" == "retropie_fullname" ]] && ini_value="RetroPie"
    echo "$ini_value"
}

## @fn addSystem()
## @param system system to add
## @brief adds an emulator entry / system
## @param fullname optional fullname for the frontend (if not present in platforms.cfg)
## @param exts optional extensions for the frontend (if not present in platforms.cfg)
## @details Adds a system to one of the frontend launchers
function addSystem() {
    local system="$1"
    local fullname="$2"
    local exts=($3)

    local platform="$system"
    local theme="$system"
    local cmd
    local path

    # if removing and we don't have an emulators.cfg we can remove the system from the frontends
    if [[ "$md_mode" == "remove" ]] && [[ ! -f "$md_conf_root/$system/emulators.cfg" ]]; then
        delSystem "$system" "$fullname"
        return
    fi

    # set system / platform / theme for configuration based on data in names field
    if [[ "$system" == "ports" ]]; then
        cmd="bash %ROM%"
        path="$romdir/ports"
    else
        cmd="$rootdir/supplementary/runcommand/runcommand.sh 0 _SYS_ $system %ROM%"
        path="$romdir/$system"
    fi

    exts+=("$(getPlatformConfig "${system}_exts")")

    local temp
    temp="$(getPlatformConfig "${system}_theme")"
    if [[ -n "$temp" ]]; then
        theme="$temp"
    else
        theme="$system"
    fi

    temp="$(getPlatformConfig "${system}_platform")"
    if [[ -n "$temp" ]]; then
        platform="$temp"
    else
        platform="$system"
    fi

    temp="$(getPlatformConfig "${system}_fullname")"
    [[ -n "$temp" ]] && fullname="$temp"

    exts="${exts[*]}"
    # add the extensions again as uppercase
    exts+=" ${exts^^}"

    setESSystem "$fullname" "$system" "$path" "$exts" "$cmd" "$platform" "$theme"
}

## @fn delSystem()
## @param system system to delete
## @brief Deletes a system
## @details deletes a system from all frontends.
function delSystem() {
    local system="$1"
    local fullname="$2"

    local temp
    temp="$(getPlatformConfig "${system}_fullname")"
    [[ -n "$temp" ]] && fullname="$temp"

    local function
    for function in $(compgen -A function _del_system_); do
        "$function" "$fullname" "$system"
    done
}

## @fn addPort()
## @param id id of the module / command
## @param port name of the port
## @param name display name for the launch script
## @param cmd commandline to launch
## @param game rom/game parameter (optional)
## @brief Adds a port to the emulationstation ports menu.
## @details Adds an emulators.cfg entry as with addSystem but also creates a launch script in `$datadir/ports/$name.sh`.
##
## Can also optionally take a game parameter which can be used to create multiple launch
## scripts for different games using the same engine - eg for quake
##
##     addPort "lr-tyrquake" "quake" "Quake" "$emudir/retroarch/bin/retroarch -L $md_inst/tyrquake_libretro.so --config $md_conf_root/quake/retroarch.cfg %ROM%" "$romdir/ports/quake/id1/pak0.pak"
##     addPort "lr-tyrquake" "quake" "Quake Mission Pack 2 (rogue)" "$emudir/retroarch/bin/retroarch -L $md_inst/tyrquake_libretro.so --config $md_conf_root/quake/retroarch.cfg %ROM%" "$romdir/ports/quake/id1/rogue/pak0.pak"
##
## Would add an entry in $configdir/ports/quake/emulators.cfg for lr-tyrquake (setting it to default if no default set)
## and create a launch script in $romdir/ports for each game.
function addPort() {
    local id="$1"
    local port="$2"
    local file="$romdir/ports/$3.sh"
    local cmd="$4"
    local game="$5"

    # move configurations from old ports location
    if [[ -d "$configdir/$port" ]]; then
        mv "$configdir/$port" "$md_conf_root/"
    fi

    # remove the emulator / port
    if [[ "$md_mode" == "remove" ]]; then
        delEmulator "$id" "$port"

        # remove launch script if in remove mode and the ports emulators.cfg is empty
        [[ ! -f "$md_conf_root/$port/emulators.cfg" ]] && rm -f "$file"

        # if there are no more port launch scripts we can remove ports from emulation station
        if [[ "$(find "$romdir/ports" -maxdepth 1 -name "*.sh" | wc -l)" -eq 0 ]]; then
            delSystem "ports"
        fi
        return
    fi

    mkUserDir "$romdir/ports"

    cat >"$file" << _EOF_
#!/bin/bash
"$rootdir/supplementary/runcommand/runcommand.sh" 0 _PORT_ "$port" "$game"
_EOF_

    chown $user:$user "$file"
    chmod +x "$file"

    [[ -n "$cmd" ]] && addEmulator 1 "$id" "$port" "$cmd"
    addSystem "ports"
}

## @fn addEmulator()
## @param default 1 to make the emulator / command default for the system if no default already set
## @param id unique id of the module / command
## @param name name of the system to add the emulator to
## @param cmd commandline to launch
## @brief Adds a new emulator for a system.
## @details This is the primary function for adding emulators to a system which can be
## switched between via the runcommand launch menu 
##
##     addEmulator 1 "vice-x64" "c64" "$md_inst/bin/x64 %ROM%"
##     addEmulator 0 "vice-xvic" "c64" "$md_inst/bin/xvic %ROM%"
##
## Would add two optional emulators for the c64 - with vice-x64 being the default if no default
## was already set. This adds entries to `$configdir/$system/emulators.cfg` with
##
##     id = "cmd"
##     default = id
##
## Which are then selectable from runcommand when launching roms
##
## For libretro emulators, cmd needs to only contain the path to the libretro library.
##
## eg. for the lr-fcuemm module
##
##     addEmulator 1 "$md_id" "nes" "$md_inst/fceumm_libretro.so"
function addEmulator() {
    local default="$1"
    local id="$2"
    local system="$3"
    local cmd="$4"

    # check if we are removing the system
    if [[ "$md_mode" == "remove" ]]; then
        delEmulator "$id" "$system"
        return
    fi

    # automatically add parameters for libretro modules
    if [[ "$id" == lr-* && "$cmd" =~ ^"$md_inst"[^[:space:]]*\.so ]]; then
        cmd="$emudir/retroarch/bin/retroarch -L $cmd --config $md_conf_root/$system/retroarch.cfg %ROM%"
    fi

    # create a config folder for the system / port
    mkUserDir "$md_conf_root/$system"

    # add the emulator to the $conf_dir/emulators.cfg if a commandline exists (not used for some ports)
    if [[ -n "$cmd" ]]; then
        iniConfig " = " '"' "$md_conf_root/$system/emulators.cfg"
        iniSet "$id" "$cmd"
        # set a default unless there is one already set
        iniGet "default"
        if [[ -z "$ini_value" && "$default" -eq 1 ]]; then
            iniSet "default" "$id"
        fi
        chown $user:$user "$md_conf_root/$system/emulators.cfg"
    fi
}

## @fn delEmulator()
## @param id id of emulator to delete
## @param system system to delete from
## @brief Deletes an emulator entry / system
## @details Delete the entry for the id from `$configdir/$system/emulators.cfg`.
## If there are no more emulators for the system present, it will also
## delete the system entry from the installed frontends.
function delEmulator() {
    local id="$1"
    local system="$2"

    local config="$md_conf_root/$system/emulators.cfg"
    # remove from apps list for system
    if [[ -f "$config" && -n "$id" ]]; then
        # delete emulator entry
        iniConfig " = " '"' "$config"
        iniDel "$id"
        # if it is the default - remove it - runcommand will prompt to select a new default
        iniGet "default"
        [[ "$ini_value" == "$id" ]] && iniDel "default"
        # if we no longer have any entries in the emulators.cfg file we can remove it
        grep -q "=" "$config" || rm -f "$config"
    fi

}

## @fn patchVendorGraphics()
## @param filename file to patch
## @details replace declared dependencies of old vendor graphics libraries with new names
## Temporary compatibility workaround for legacy software to work on new Raspberry Pi firmwares.
function patchVendorGraphics() {
    local filename="$1"

    # patchelf is not available on Raspbian Jessie
    [[ "$__os_debian_ver" -lt 9 ]] && return

    getDepends patchelf
    printMsgs "console" "Applying vendor graphics patch: $filename"
    patchelf --replace-needed libEGL.so libbrcmEGL.so \
             --replace-needed libGLES_CM.so libbrcmGLESv2.so \
             --replace-needed libGLESv1_CM.so libbrcmGLESv2.so \
             --replace-needed libGLESv2.so libbrcmGLESv2.so \
             --replace-needed libOpenVG.so libbrcmOpenVG.so \
             --replace-needed libWFC.so libbrcmWFC.so "$filename"
}

## @fn dkmsManager()
## @param mode dkms operation type
## @module_name name of dkms module
## @module_ver version of dkms module
## Helper function to manage DKMS modules installed by RetroPie
function dkmsManager() {
    local mode="$1"
    local module_name="$2"
    local module_ver="$3"
    local kernel="$(uname -r)"
    local ver

    case "$mode" in
        install)
            if dkms status | grep -q "^$module_name"; then
                dkmsManager remove "$module_name" "$module_ver"
            fi
            ln -sf "$md_inst" "/usr/src/${module_name}-${module_ver}"
            dkms install --no-initrd --force -m "$module_name" -v "$module_ver" -k "$kernel"
            if ! dkms status "$module_name/$module_ver" -k "$kernel" | grep -q installed; then
                # Force building for any kernel that has source/headers
                local k_ver
                while read k_ver; do
                    if [[ -d "$(realpath /lib/modules/$k_ver/build)" ]]; then
                        dkms install --no-initrd --force -m "$module_name/$module_ver" -k "$k_ver"
                    fi
                done < <(ls -r1 /lib/modules)
            fi
            if ! dkms status "$module_name/$module_ver" | grep -q installed; then
                md_ret_errors+=("Failed to install $md_id")
                return 1
            fi
            ;;
        remove)
            for ver in $(dkms status "$module_name" | cut -d"," -f2 | cut -d":" -f1); do
                dkms remove -m "$module_name" -v "$ver" --all
                rm -f "/usr/src/${module_name}-${ver}"
            done
            dkmsManager unload "$module_name" "$module_ver"
            ;;
        reload)
            dkmsManager unload "$module_name" "$module_ver"
            # No reason to load modules in chroot
            if [[ "$__chroot" -eq 0 ]]; then
                modprobe "$module_name"
            fi
            ;;
        unload)
            if [[ -n "$(lsmod | grep ${module_name/-/_})" ]]; then
                rmmod "$module_name"
            fi
            ;;
    esac
}

## @fn getIPAddress()
## @param dev optional specific network device to use for address lookup
## @brief Obtains the current externally routable source IP address of the machine
## @details This function first tries to obtain an external IPv4 route and
## otherwise tries an IPv6 route if the IPv4 route can not be determined.
## If no external route can be determined, nothing will be returned.
## This function uses Google's DNS servers as the external lookup address.
function getIPAddress() {
    local dev="$1"
    local ip_route

    # first try to obtain an external IPv4 route
    ip_route=$(ip -4 route get 8.8.8.8 ${dev:+dev $dev} 2>/dev/null)
    if [[ -z "$ip_route" ]]; then
        # if there is no IPv4 route, try to obtain an IPv6 route instead
        ip_route=$(ip -6 route get 2001:4860:4860::8888 ${dev:+dev $dev} 2>/dev/null)
    fi

    # if an external route was found, report its source address
    [[ -n "$ip_route" ]] && grep -oP "src \K[^\s]+" <<< "$ip_route"
}

## @fn isConnected()
## @brief Simple check to see if there is a connection to the Internet.
## @details Uses the getIPAddress function to check if we have a route to the Internet. Also sets
## __NET_ERRMSG with an error message for use in packages / setup to display to the user if not.
## @retval 0 on success
## @retval 1 on failure
function isConnected() {
    local ip="$(getIPAddress)"
    if [[ -z "$ip" ]]; then
        __NET_ERRMSG="Not connected to the Internet"
        return 1
    fi
    return 0
}

## @fn adminRsync()
## @param src src folder on local system - eg "$__tmpdir/stats/"
## @param dest destination folder on remote system - eg "stats/"
## @param params additional rsync parameters - eg --delete
## @brief Rsyncs data to remote host for admin modules
## @details Used to rsync data to our server for admin modules. Default remote
## user is retropie, host is $__binary_host and default port is 22. These can be overridden with
## env vars __upload_user __upload_host and __upload_port
##
## The default parameters for rsync are "-av --delay-updates" - more can be added via the 3rd+ argument
function adminRsync() {
    local src="$1"
    local dest="$2"
    shift 2
    local params=("$@")

    local remote_user="$__upload_user"
    [[ -z "$remote_user" ]] && remote_user="retropie"
    local remote_host="$__upload_host"
    [[ -z "$remote_host" ]] && remote_host="$__binary_host"
    local remote_port="$__upload_port"
    [[ -z "$remote_port" ]] && remote_port=22

    rsync -av --delay-updates -e "ssh -p $remote_port" "${params[@]}" "$src" "$remote_user@$remote_host:$dest"
}

## @fn signFile()
## @param file path to file to sign
## @brief signs file with $__gpg_signing_key
## @details signs file with $__gpg_signing_key creating corresponding .asc file in the same folder
function signFile() {
    local file="$1"
    local cmd_out
    cmd_out=$(gpg --default-key "$__gpg_signing_key" --detach-sign --armor --yes "$1" 2>&1)
    if [[ "$?" -ne 0 ]]; then
        md_ret_errors+=("Failed to sign $1\n\n$cmd_out")
        return 1
    fi
    return 0
}
