#!/usr/bin/env bash

### GLOBAL CONSTANTS ###
# ANSI ESCAPE SEQUENCES
readonly ERROR_TEXT="\033[1;41;37m" # Bold + White + Red Background
readonly CLEAR_TEXT="\033[0m"       # Clear

# ERROR CODES
readonly EC_MISSING_CONFIG=1
readonly EC_MISSING_FREERDP=2
readonly EC_NOT_IN_GROUP=3
readonly EC_NOT_RUNNING=4
readonly EC_NO_IP=5
readonly EC_BAD_PORT=6
readonly EC_UNSUPPORTED_APP=7
readonly EC_INVALID_FLAVOR=8

# PATHS
readonly APPDATA_PATH="${HOME}/.local/share/winapps"
readonly SYS_APP_PATH="/usr/local/share/winapps"
readonly LASTRUN_PATH="${APPDATA_PATH}/lastrun"
readonly LOG_PATH="${APPDATA_PATH}/winapps.log"
readonly CONFIG_PATH="${HOME}/.config/winapps/winapps.conf"
# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
readonly SCRIPT_DIR_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"

# OTHER
readonly VM_NAME="RDPWindows" # FOR 'libvirt' ONLY
readonly RDP_PORT=3389
readonly DOCKER_IP="127.0.0.1"
# shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
readonly RUN="$(date)-${RANDOM}"

### GLOBAL VARIABLES ###
# WINAPPS CONFIGURATION FILE
RDP_USER=""
RDP_PASS=""
RDP_DOMAIN=""
RDP_IP=""
WAFLAVOR="docker"
RDP_FLAGS=""
FREERDP_COMMAND=""
RDP_SCALE=100
MULTIMON="false"
DEBUG="true"
MULTI_FLAG=""

### FUNCTIONS ###
# Name: 'waThrowExit'
# Role: Throw an error message and exit the script.
function waThrowExit() {
    # Declare variables.
    local ERR_CODE="$1"

    # Throw error.
    case "$ERR_CODE" in
    "$EC_MISSING_CONFIG")
        # Missing WinApps configuration file.
        dprint "ERROR: MISSING WINAPPS CONFIGURATION FILE. EXITING."
        echo -e "${ERROR_TEXT}ERROR: MISSING WINAPPS CONFIGURATION FILE.${CLEAR_TEXT}"
        echo "Please create a WinApps configuration file at '${CONFIG_PATH}'".
        ;;
    "$EC_MISSING_FREERDP")
        dprint "ERROR: FREERDP VERSION 3 IS NOT INSTALLED. EXITING."
        echo -e "${ERROR_TEXT}ERROR: FREERDP VERSION 3 IS NOT INSTALLED.${CLEAR_TEXT}"
        ;;
    "$EC_NOT_IN_GROUP")
        dprint "ERROR: USER NOT PART OF REQUIRED GROUPS. EXITING."
        echo -e "${ERROR_TEXT}ERROR: USER NOT PART OF REQUIRED GROUPS.${CLEAR_TEXT}"
        echo "Please run:"
        echo "  sudo usermod -a -G libvirt $(whoami)"
        echo "  sudo usermod -a -G kvm $(whoami)"
        ;;
    "$EC_NOT_RUNNING")
        dprint "ERROR: WINDOWS NOT RUNNING. EXITING."
        echo -e "${ERROR_TEXT}ERROR: WINDOWS NOT RUNNING.${CLEAR_TEXT}"
        echo "Please ensure Windows is running."
        ;;
    "$EC_NO_IP")
        dprint "ERROR: WINDOWS UNREACHABLE. EXITING."
        echo -e "${ERROR_TEXT}ERROR: WINDOWS UNREACHABLE.${CLEAR_TEXT}"
        echo "Please ensure Windows is assigned an IP address."
        ;;
    "$EC_BAD_PORT")
        dprint "ERROR: RDP PORT CLOSED. EXITING."
        echo -e "${ERROR_TEXT}ERROR: RDP PORT CLOSED.${CLEAR_TEXT}"
        echo "Please ensure Remote Desktop is correctly configured on Windows."
        ;;
    "$EC_UNSUPPORTED_APP")
        dprint "ERROR: APPLICATION NOT FOUND. EXITING."
        echo -e "${ERROR_TEXT}ERROR: APPLICATION NOT FOUND.${CLEAR_TEXT}"
        echo "Please ensure the program is correctly configured as an officially supported application."
        ;;
    "$EC_INVALID_FLAVOR")
        dprint "ERROR: INVALID FLAVOR. EXITING."
        echo -e "${ERROR_TEXT}ERROR: INVALID FLAVOR.${CLEAR_TEXT}"
        echo "Please ensure 'docker', 'podman' or 'libvirt' are specified as the flavor in the WinApps configuration file."
        ;;
    esac

    # Provide generic advice.
    echo "Check the WinApps project README for more information."

    # Terminate the script.
    echo "Exiting with status '${ERR_CODE}'."
    exit "$ERR_CODE"
}

# Name: 'dprint'
# Role: Conditionally print debug messages to a log file, creating it if it does not exist.
function dprint() {
    [ "$DEBUG" = "true" ] && echo "[$RUN] $1" >>"$LOG_PATH"
}

# Name: 'waFixScale'
# Role: Since FreeRDP only supports '/scale' values of 100, 140 or 180, find the closest supported argument to the user's configuration.
function waFixScale() {
    # Define variables.
    local OLD_SCALE=100
    local VALID_SCALE_1=100
    local VALID_SCALE_2=140
    local VALID_SCALE_3=180

    # Check for an unsupported value.
    if [ "$RDP_SCALE" != "$VALID_SCALE_1" ] && [ "$RDP_SCALE" != "$VALID_SCALE_2" ] && [ "$RDP_SCALE" != "$VALID_SCALE_3" ]; then
        # Save the unsupported scale.
        OLD_SCALE="$RDP_SCALE"

        # Calculate the absolute differences.
        local DIFF_1=$(( RDP_SCALE > VALID_SCALE_1 ? RDP_SCALE - VALID_SCALE_1 : VALID_SCALE_1 - RDP_SCALE ))
        local DIFF_2=$(( RDP_SCALE > VALID_SCALE_2 ? RDP_SCALE - VALID_SCALE_2 : VALID_SCALE_2 - RDP_SCALE ))
        local DIFF_3=$(( RDP_SCALE > VALID_SCALE_3 ? RDP_SCALE - VALID_SCALE_3 : VALID_SCALE_3 - RDP_SCALE ))

        # Set the final scale to the valid scale value with the smallest absolute difference.
        if (( DIFF_1 <= DIFF_2 && DIFF_1 <= DIFF_3 )); then
            RDP_SCALE="$VALID_SCALE_1"
        elif (( DIFF_2 <= DIFF_1 && DIFF_2 <= DIFF_3 )); then
            RDP_SCALE="$VALID_SCALE_2"
        else
            RDP_SCALE="$VALID_SCALE_3"
        fi

        # Print feedback.
        dprint "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}'. Defaulting to '${RDP_SCALE}'."
        echo "WARNING: Unsupported RDP_SCALE value '${OLD_SCALE}' detected. Defaulting to '${RDP_SCALE}'."
    fi
}

# Name: 'waLoadConfig'
# Role: Load the variables within the WinApps configuration file.
function waLoadConfig() {
    # Load WinApps configuration file.
    if [ -f "$CONFIG_PATH" ]; then
        # shellcheck source=/dev/null # Exclude WinApps configuration file from being checked by ShellCheck.
        source "$CONFIG_PATH"
    else
        waThrowExit $EC_MISSING_CONFIG
    fi

    # Update 'MULTI_FLAG' based on 'MULTIMON'.
    MULTI_FLAG=$([[ $MULTIMON == "true" ]] && echo "/multimon" || echo "+span")

    # Update $RDP_SCALE.
    waFixScale
}

# Name: 'waLastRun'
# Role: Determine the last time this script was run.
function waLastRun() {
    # Declare variables.
    local LAST_RUN_UNIX_TIME=0
    local CURR_RUN_UNIX_TIME=0

    # Store the time this script was run last as a unix timestamp.
    if [ -f "$LASTRUN_PATH" ]; then
        LAST_RUN_UNIX_TIME=$(stat -t -c %Y "$LASTRUN_PATH")
        dprint "LAST_RUN: ${LAST_RUN_UNIX_TIME}"
    fi

    # Update the file modification time with the current time.
    touch "$LASTRUN_PATH"
    CURR_RUN_UNIX_TIME=$(stat -t -c %Y "$LASTRUN_PATH")
    dprint "THIS_RUN: ${CURR_RUN_UNIX_TIME}"
}

# Name: 'waGetFreeRDPCommand'
# Role: Determine the correct FreeRDP command to use.
function waGetFreeRDPCommand() {
    # Declare variables.
    local FREERDP_MAJOR_VERSION="" # Stores the major version of the installed copy of FreeRDP.

    # Attempt to set a FreeRDP command if the command variable is empty.
    if [ -z "$FREERDP_COMMAND" ]; then
        # Check for 'xfreerdp'.
        if command -v xfreerdp &>/dev/null; then
            # Check FreeRDP major version is 3 or greater.
            FREERDP_MAJOR_VERSION=$(xfreerdp --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1)
            if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then
                FREERDP_COMMAND="xfreerdp"
            fi
        # Check for 'xfreerdp3'.
        elif command -v xfreerdp3 &>/dev/null; then
            # Check FreeRDP major version is 3 or greater.
            FREERDP_MAJOR_VERSION=$(xfreerdp3 --version | head -n 1 | grep -o -m 1 '\b[0-9]\S*' | head -n 1 | cut -d'.' -f1)
            if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then
                FREERDP_COMMAND="xfreerdp3"
            fi
        fi

        # Check for FreeRDP Flatpak (fallback option).
        if [ -z "$FREERDP_COMMAND" ]; then
            if command -v flatpak &>/dev/null; then
                if flatpak list --columns=application | grep -q "^com.freerdp.FreeRDP$"; then
                    # Check FreeRDP major version is 3 or greater.
                    FREERDP_MAJOR_VERSION=$(flatpak list --columns=application,version | grep "^com.freerdp.FreeRDP" | awk '{print $2}' | cut -d'.' -f1)
                    if [[ $FREERDP_MAJOR_VERSION =~ ^[0-9]+$ ]] && ((FREERDP_MAJOR_VERSION >= 3)); then
                        FREERDP_COMMAND="flatpak run --command=xfreerdp com.freerdp.FreeRDP"
                    fi
                fi
            fi
        fi
    fi

    if command -v "$FREERDP_COMMAND" &>/dev/null || [ "$FREERDP_COMMAND" = "flatpak run --command=xfreerdp com.freerdp.FreeRDP" ]; then
        dprint "Using FreeRDP command '${FREERDP_COMMAND}'."

        # Append additional flags or parameters to FreeRDP.
        # These additional flags are loaded prior in 'waLoadConfig'.
        [[ -n $RDP_FLAGS ]] && FREERDP_COMMAND="${FREERDP_COMMAND} ${RDP_FLAGS}"
    else
        waThrowExit "$EC_MISSING_FREERDP"
    fi
}

# Name: 'waCheckGroupMembership'
# Role: Ensures the current user is part of the required groups.
function waCheckGroupMembership() {
    # Identify groups the current user belongs to.
    # shellcheck disable=SC2155 # Silence warnings regarding masking return values through simultaneous declaration and assignment.
    local USER_GROUPS=$(groups "$(whoami)")

    if ! (echo "$USER_GROUPS" | grep -q -E "\blibvirt\b") || ! (echo "$USER_GROUPS" | grep -q -E "\bkvm\b"); then
        waThrowExit "$EC_NOT_IN_GROUP"
    fi
}

# Name: 'waCheckVMRunning'
# Role: Throw an error if the Windows 'libvirt' VM is not running.
function waCheckVMRunning() {
    ! virsh list --state-running --name | grep -q "^${VM_NAME}$" && waThrowExit "$EC_NOT_RUNNING"
}

# Name: 'waCheckContainerRunning'
# Role: Throw an error if the Docker container is not running.
function waCheckContainerRunning() {
    # Declare variables.
    local CONTAINER_STATE=""

    # Determine container state (docker).
    if command -v docker &>/dev/null; then
        CONTAINER_STATE=$(docker ps --filter name="WinApps" --format '{{.Status}}')
    fi

    # Determine container state (podman).
    if [ -z "$CONTAINER_STATE" ]; then
        CONTAINER_STATE=$(podman ps --filter name="WinApps" --format '{{.Status}}')
    fi

    CONTAINER_STATE=${CONTAINER_STATE,,} # Convert the string to lowercase.
    CONTAINER_STATE=${CONTAINER_STATE%% *} # Extract the first word.

    # Check container state.
    [[ "$CONTAINER_STATE" != "up" ]] && waThrowExit "$EC_NOT_RUNNING"
}

# Name: 'waCheckPortOpen'
# Role: Assesses whether the RDP port on Windows is open.
function waCheckPortOpen() {
    # Declare variables.
    local VM_MAC="" # Stores the MAC address of the Windows VM.

    # Obtain Windows VM IP Address ('libvirt' ONLY)
    # Note: 'RDP_IP' should not be empty if 'WAFLAVOR' is 'docker', since it is set to localhost before this function is called.
    if [ -z "$RDP_IP" ] && [ "$WAFLAVOR" = "libvirt" ]; then
        VM_MAC=$(virsh domiflist "$VM_NAME" | grep -oE "([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})") # VM MAC address.
        RDP_IP=$(arp -n | grep "$VM_MAC" | grep -oE "([0-9]{1,3}\.){3}[0-9]{1,3}")                # VM IP address.
        [ -z "$RDP_IP" ] && waThrowExit "$EC_NO_IP"
    fi

    # Check for an open RDP port.
    timeout 5 nc -z "$RDP_IP" "$RDP_PORT" &>/dev/null || waThrowExit "$EC_BAD_PORT"
}

# Name: 'waRunCommand'
# Role: Run the requested WinApps command.
function waRunCommand() {
    # Declare variables.
    local ICON=""
    local FILE_PATH=""

    # Run option.
    if [ "$1" = "windows" ]; then
        # Open Windows RDP session.
        dprint "WINDOWS"
        $FREERDP_COMMAND \
            /d:"$RDP_DOMAIN" \
            /u:"$RDP_USER" \
            /p:"$RDP_PASS" \
            /scale:"$RDP_SCALE" \
            +dynamic-resolution \
            +auto-reconnect \
            +home-drive \
            /audio-mode:1 \
            /wm-class:"Microsoft Windows" \
            /v:"$RDP_IP" &>/dev/null &
    elif [ "$1" = "manual" ]; then
        # Open specified application.
        dprint "MANUAL: ${2}"
        $FREERDP_COMMAND \
            /cert:tofu \
            /d:"$RDP_DOMAIN" \
            /u:"$RDP_USER" \
            /p:"$RDP_PASS" \
            /scale:"$RDP_SCALE" \
            +auto-reconnect \
            +clipboard \
            +home-drive \
            /audio-mode:1 \
            -wallpaper \
            +dynamic-resolution \
            "$MULTI_FLAG" \
            /app:program:"$2" \
            /v:"$RDP_IP" &>/dev/null &
    else
        # Script summoned from right-click menu with officially supported application name plus/minus a file path.
        if [ -e "${SCRIPT_DIR_PATH}/../apps/${1}/info" ]; then
            # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck.
            source "${SCRIPT_DIR_PATH}/../apps/${1}/info"
            ICON="${SCRIPT_DIR_PATH}/../apps/${1}/icon.svg"
        elif [ -e "${APPDATA_PATH}/apps/${1}/info" ]; then
            # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck.
            source "${APPDATA_PATH}/apps/${1}/info"
            ICON="${APPDATA_PATH}/apps/${1}/icon.svg"
        elif [ -e "${SYS_APP_PATH}/apps/${1}/info" ]; then
            # shellcheck source=/dev/null # Exclude this file from being checked by ShellCheck.
            source "${SYS_APP_PATH}/apps/${1}/info"
            ICON="${SYS_APP_PATH}/apps/${1}/icon.svg"
        else
            waThrowExit "$EC_UNSUPPORTED_APP"
        fi

        # Check if a file path was specified, and pass this to the application.
        if [ -z "$2" ]; then
            # No file path specified.
            $FREERDP_COMMAND \
                /d:"$RDP_DOMAIN" \
                /u:"$RDP_USER" \
                /p:"$RDP_PASS" \
                /scale:"$RDP_SCALE" \
                +auto-reconnect \
                +clipboard \
                +home-drive \
                /audio-mode:1 \
                -wallpaper \
                +dynamic-resolution \
                "$MULTI_FLAG" \
                /wm-class:"$FULL_NAME" \
                /app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:"$FULL_NAME" \
                /v:"$RDP_IP" &>/dev/null &
        else
            # Convert path from UNIX to Windows style.
            FILE_PATH=$(echo "$2" | sed 's|'"${HOME}"'|\\\\tsclient\\home|;s|/|\\|g;s|\\|\\\\|g')
            dprint "UNIX_FILE_PATH: ${2}"
            dprint "WINDOWS_FILE_PATH: ${FILE_PATH}"

            $FREERDP_COMMAND \
                /cert:tofu \
                /d:"$RDP_DOMAIN" \
                /u:"$RDP_USER" \
                /p:"$RDP_PASS" \
                /scale:"$RDP_SCALE" \
                +auto-reconnect \
                +clipboard \
                +home-drive \
                /audio-mode:1 \
                -wallpaper \
                +dynamic-resolution \
                "$MULTI_FLAG" \
                /wm-class:"$FULL_NAME" \
                /app:program:"$WIN_EXECUTABLE",icon:"$ICON",name:$"FULL_NAME",cmd:\""$FILE_PATH"\" \
                /v:"$RDP_IP" &>/dev/null &
        fi
    fi
}

### MAIN LOGIC ###
#set -x # Enable for debugging.
dprint "START"
dprint "SCRIPT_DIR: ${SCRIPT_DIR_PATH}"
dprint "SCRIPT_ARGS: ${*}"
dprint "HOME_DIR: ${HOME}"
mkdir -p "$APPDATA_PATH"
waLastRun
waLoadConfig
waGetFreeRDPCommand

# If using podman backend, modify the FreeRDP command to enter a new namespace.
if [ "$WAFLAVOR" = "podman" ]; then
    FREERDP_COMMAND="podman unshare --rootless-netns ${FREERDP_COMMAND}"
fi

if [ "$WAFLAVOR" = "docker" ] || [ "$WAFLAVOR" = "podman" ]; then
    RDP_IP="$DOCKER_IP"
    waCheckContainerRunning
elif [ "$WAFLAVOR" = "libvirt" ]; then
    waCheckGroupMembership
    waCheckVMRunning
else
    waThrowExit "$EC_INVALID_FLAVOR"
fi

waCheckPortOpen
waRunCommand "$@"

dprint "END"
