#!/usr/bin/env bash
#
# * appimage - Manage AppImages on system.
#
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>

set -o errexit
cache_dir="$HOME/.cache/appimage"
appimages_info="$HOME/.config/appimages.info"
PREFIX="${PREFIX:-$HOME/.local}"
ASSUME_YES="${ASSUME_YES:-}"

print_help() {
cat <<- EOF
Manage AppImages.

Usage: $0 [options] [<arguments>]

Options:
    -i, --install       install AppImage from URL or from FILE.
    -d, --no-desktop    don't create a desktop file. See https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
    -p, --prefix        installation prefix [default: $PREFIX]
    -r, --remove        remove AppImage.
    -l, --list          list installed AppImages.
    -s, --show          show AppImage related info.
    -y, --yes           automatic yes to prompts.
    -h, --help          print this help message and exit.
    -v, --version       print version and exit.

Environment:
    PREFIX              default installation prefix.
    ASSUME_YES          automatic yes to prompts.
EOF
}

yesno() {
    # Yes/No interactive dialog.
    #
    # Usage: if yesno 'Question'; then ...
    local answer=
    [ "$ASSUME_YES" ] && return 0

    while [ ! "$answer" ]; do
        echo -en "$* (y/n) "
        read -r reply
        case "${reply,,}" in
            y|yes)  answer=0;;
            n|no)   answer=1;;
            *)      echo "Please, answer y or n";;
        esac
    done
    return "$answer"
}

# https://stackoverflow.com/a/49197786
extract() {
    tar -xOvaf "$1" 2>/dev/null > "$2" && return 0
    case $(file "$1") in
        *bzip2*)    bzip2 -dkc  "$1" > "$2"  ;;
        *gzip*)     gunzip -c   "$1" > "$2"  ;;
        *'7-zip'*)  7z e -so    "$1" > "$2"  ;;
        *zip*)                                  ;&
        *Zip*)      unzip -p    "$1" > "$2"  ;;
        *xz*)                                   ;&
        *XZ*)       unxz -c     "$1" > "$2"  ;;
        *)          return 1;;
    esac
}

install_appimage() {
    local image desktop_file name file version description desktop

    if [ -f "$1" ]; then
        image="$1"
    elif [[ "$1" =~ https?://.+ ]]; then
        echo -e "\e[1m==> Download AppImage ...\e[0m"
        mkdir -p "$cache_dir"
        image="$cache_dir/temporary.AppImage"
        wget -c "$1" -O "$image"
    else
        echo No such file: "$1" >&2; exit 1
    fi

    local uncompressed_image="$cache_dir/temporary.AppImage.uncompressed"
    if extract "$image" "$uncompressed_image"; then
        image="$uncompressed_image"
    fi

    # Check file type
    if [[ ! "$(file "$image")" =~ .*ELF.* ]]; then
        echo File is not correct Linux binary. >&2; exit 1
    fi

    chmod +x "$image"

    echo -e "\e[1m==> Extracting data ...\e[0m"

    desktop_file="$("$image" --appimage-extract '*.desktop')"
    name="$(grep -Po '(?<=Name=)(.*)' "$desktop_file")"
    version="$(grep -Po '(?<=X-AppImage-Version=)(.*)' "$desktop_file")"
    description="$(grep -Po '(?<=Comment=)(.*)' "$desktop_file")"
    file="$PREFIX/bin/${name}-${version}.AppImage"

    echo -e "\e[1m==> Install ...\e[0m"

    # Install files
    install -Dm755 -D "$image" "$file"
    if [ -n "$no_desktop" ]; then
        desktop=None
    else
        desktop="$PREFIX/share/applications/${name}-${version}.desktop"
        install -Dm644 -D "$desktop_file" "$desktop"
        sed '/Exec=/d' -i "$desktop"; echo "Exec=$file" >> "$desktop"
        rm "${desktop_file:-/nonexistent}"
        rmdir "$PWD/squashfs-root"
    fi

    # Clean cache
    [ -f "$cache_dir/temporary.AppImage" ] && rm "$cache_dir/temporary.AppImage"
    [ -f "$cache_dir/temporary.AppImage.uncompressed" ] && rm "$cache_dir/temporary.AppImage.uncompressed"

    # Save App data
    printf "%s:%s:%s:%s:%s\n" "$name" "$version" "$description" "$file" "$desktop" >> "$appimages_info"
    echo Done.

    echo -e "\e[1mRecap:\e[0m"
    echo APPIMAGE: "$name"
    echo VERSION: "$version"
    echo EXECUTABLE: "$file"
    echo DESKTOP FILE: "$desktop"

    if [[ ! "$PATH" =~ "$PREFIX" ]]; then
        echo
        echo PREFIX is not in PATH!
        echo Add this line to your \~/.bashrc file:
        echo
        echo export PATH="$PREFIX:$PATH"
        echo
        echo and restart your Shell session.
    fi
}

remove_appimage() {
    local appimage _appimage name version file desktop
    appimage="$1"

    if [ -f "$appimages_info" ]; then
        if awk -F: '{print $1 ":" $2}' "$appimages_info" | grep -i "^${appimage}\$" >/dev/null; then
            _appimage="$(grep -i "$appimage" "$appimages_info")"
            name="$(awk -F: '{print $1}' <<< "$_appimage")"
            version="$(awk -F: '{print $2}' <<< "$_appimage")"
            file="$(awk -F: '{print $4}' <<< "$_appimage")"
            desktop="$(awk -F: '{print $5}' <<< "$_appimage")"

            echo AppImage: "$name"
            echo Version: "$version"
            echo Remove:
            echo "$file"
            echo "$desktop"
            if yesno 'Are you sure?'; then
                [ -f "$file" ] && rm "$file"
                [ -f "$desktop" ] && rm "$desktop"
                sed -e "/$appimage/Id" -i "$appimages_info"  # Remove App data
                echo "$appimage" removed.
            else
                echo Abort
            fi
        else
            echo AppImage not found: "$appimage"
            echo Note: Specify AppImage name and version separated by a colon e.g. SomeApp:1.8.2, case insensitive.
        fi
    else
        echo No AppImages installed. >&2; exit 1
    fi
}

list_appimages() {
    # shellcheck disable=SC2002
    cat "$appimages_info" 2>/dev/null | awk -F: \
        'BEGIN {print "NAME:VERSION:DESCRIPTION"} {print $1 ":" $2 ":" $3}' | column -t -s :
}

show_appimage() {
    cat "$appimages_info" 2>/dev/null | grep -i -m 1 "$1" |
        awk -F: '{print "APPIMAGE: " $1 "\nVERSION: " $2 "\nDESCRIPTION: " $3 "\nEXECUTABLE: " $4 "\nDESKTOP FILE: " $5}'
}

# Print help if no arguments passed
[[ "$#" == 0 ]] && { print_help; exit 1; }

# Transform long options to short ones
for arg in "$@"; do
    shift
    case "$arg" in
        --install)      set -- "$@" "-i";;
        --no-desktop)   set -- "$@" "-d";;
        --prefix)       set -- "$@" "-p";;
        --remove)       set -- "$@" "-r";;
        --list)         set -- "$@" "-l";;
        --show)         set -- "$@" "-s";;
        --yes)          set -- "$@" "-y";;
        --help)         set -- "$@" "-h";;
        --version)      set -- "$@" "-v";;
        *)              set -- "$@" "$arg";;
    esac
done

# Parse short options
while getopts ":i:dp:r:ls:yhv" opt; do
    case "$opt" in
        i) install_appimage "$OPTARG"; exit "$?";;
        d) no_desktop='yes';;
        p) PREFIX="$OPTARG"; ;;
        r) remove_appimage "$OPTARG"; exit "$?";;
        l) list_appimages; exit $?;;
        s) show_appimage "$OPTARG"; exit "$?";;
        y) ASSUME_YES=1;;
        h) print_help; exit 0;;
        v) echo 0.2.0; exit 0;;
        *) echo "Unknown option $opt" >&2; exit 1;;
    esac
done
