#!/bin/bash
################################################################################
# PROJECT   : Publish-AppImage for .NET
# WEBPAGE   : https://github.com/kuiperzone/Publish-AppImage
# COPYRIGHT : Andy Thomas 2021-2023
# LICENSE   : MIT
################################################################################

###############################
# CONSTANTS
###############################

declare -r _SCRIPT_VERSION="1.3.1"
declare -r _SCRIPT_TITLE="Publish-AppImage for .NET"
declare -r _SCRIPT_IMPL_MIN=1
declare -r _SCRIPT_IMPL_MAX=1
declare -r _SCRIPT_COPYRIGHT="Copyright 2023 Andy Thomas"
declare -r _SCRIPT_WEBSITE="https://github.com/kuiperzone/Publish-AppImage"

declare -r _SCRIPT_NAME="publish-appimage"
declare -r _DEFAULT_CONF="${_SCRIPT_NAME}.conf"

declare -r _APPIMAGE_KIND="appimage"
declare -r _ZIP_KIND="zip"
declare -r _DOTNET_NONE="null"


###############################
# FUNCTIONS
###############################

function assert_result
{
    local _ret=$?

    if [ ${_ret} -ne 0 ]; then
        echo
        exit ${_ret}
    fi
}

function exec_or_die
{
    echo "${1}"
    eval "${1}"
    assert_result
}

function ensure_directory
{
    local _path="${1}"

    if [ ! -d "${_path}" ]; then
        mkdir -p "${_path}"
        assert_result
    fi
}

function remove_path
{
    local _path="${1}"

    if [ -d "${_path}" ]; then
        rm -rf "${_path}"
        assert_result
    elif [ -f "${_path}" ]; then
        rm -f "${_path}"
        assert_result
    fi
}

function assert_mandatory
{
    local _name="${1}"
    local _value="${2}"

    if [ "${_value}" == "" ]; then
        echo "${_name} undefined in: ${_conf_arg_value}"
        echo
        exit 1
    fi
}

function assert_opt_file
{
    local _name="${1}"
    local _value="${2}"

    if [ "${_value}" != "" ] && [ ! -f "${_value}" ]; then
        echo "File not found: ${_value}"

        if [ "${_name}" != "" ]; then
            echo "See ${_name} in: ${_conf_arg_value}"
        fi

        echo
        exit 1
    fi
}

###############################
# HANDLE ARGUMENTS
###############################

# Specify conf file
declare -r _CONF_ARG="f"
declare -r _CONF_ARG_NAME="conf"
_conf_arg_value="${_DEFAULT_CONF}"
_arg_syntax=":${_CONF_ARG}:"

# Runtime ID
declare -r _RID_ARG="r"
declare -r _RID_ARG_NAME="runtime"
_rid_arg_value="linux-x64"
_arg_syntax="${_arg_syntax}${_RID_ARG}:"

# Package kind
declare -r _KIND_ARG="k"
declare -r _KIND_ARG_NAME="kind"
declare -l _kind_arg_value="${_APPIMAGE_KIND}"
_arg_syntax="${_arg_syntax}${_KIND_ARG}:"

# Run app
declare -r _RUNAPP_ARG="u"
declare -r _RUNAPP_ARG_NAME="run"
_runapp_arg_value=false
_arg_syntax="${_arg_syntax}${_RUNAPP_ARG}"

# Verbose
declare -r _VERBOSE_ARG="b"
declare -r _VERBOSE_ARG_NAME="verbose"
_verbose_arg_value=false
_arg_syntax="${_arg_syntax}${_VERBOSE_ARG}"

# Skip yes (no prompt)
declare -r _SKIPYES_ARG="y"
declare -r _SKIPYES_ARG_NAME="skip-yes"
_skipyes_arg_value=false
_arg_syntax="${_arg_syntax}${_SKIPYES_ARG}"

# Output name
declare -r _OUTPUT_ARG="o"
declare -r _OUTPUT_ARG_NAME="output"
_output_arg_value=""
_arg_syntax="${_arg_syntax}${_OUTPUT_ARG}:"

# Show version
declare -r _VERSION_ARG="v"
declare -r _VERSION_ARG_NAME="version"
_version_arg_value=false
_arg_syntax="${_arg_syntax}${_VERSION_ARG}"

# Show help
declare -r _HELP_ARG="h"
declare -r _HELP_ARG_NAME="help"
_help_arg_value=false
_arg_syntax="${_arg_syntax}${_HELP_ARG}"

_exit_help=0

# Transform long options to short ones
for arg in "${@}"; do
  shift
  case "${arg}" in
    ("--${_CONF_ARG_NAME}") set -- "$@" "-${_CONF_ARG}" ;;
    ("--${_RID_ARG_NAME}") set -- "$@" "-${_RID_ARG}" ;;
    ("--${_KIND_ARG_NAME}") set -- "$@" "-${_KIND_ARG}" ;;
    ("--${_RUNAPP_NAME}") set -- "$@" "-${_RUNAPP_ARG}" ;;
    ("--${_VERBOSE_ARG_NAME}") set -- "$@" "-${_VERBOSE_ARG}" ;;
    ("--${_SKIPYES_ARG_NAME}") set -- "$@" "-${_SKIPYES_ARG}" ;;
    ("--${_OUTPUT_ARG_NAME}") set -- "$@" "-${_OUTPUT_ARG}" ;;
    ("--${_VERSION_ARG_NAME}") set -- "$@" "-${_VERSION_ARG}" ;;
    ("--${_HELP_ARG_NAME}") set -- "$@" "-${_HELP_ARG}" ;;
    ("--"*)
        echo "Illegal argument: ${arg}"
        echo

        _exit_help=1
        break
        ;;
    (*) set -- "$@" "${arg}" ;;
  esac
done

if [ ${_exit_help} == 0 ]; then
    # Read arguments
    while getopts ${_arg_syntax} arg; do
        case "${arg}" in
            (${_CONF_ARG}) _conf_arg_value="${OPTARG}" ;;
            (${_RID_ARG}) _rid_arg_value="${OPTARG}" ;;
            (${_KIND_ARG}) _kind_arg_value="${OPTARG}" ;;
            (${_RUNAPP_ARG}) _runapp_arg_value=true ;;
            (${_VERBOSE_ARG}) _verbose_arg_value=true ;;
            (${_SKIPYES_ARG}) _skipyes_arg_value=true ;;
            (${_OUTPUT_ARG}) _output_arg_value="${OPTARG}" ;;
            (${_VERSION_ARG}) _version_arg_value=true ;;
            (${_HELP_ARG}) _help_arg_value=true ;;
            (*)
                echo "Illegal argument"
                echo

                _exit_help=1
                break
                ;;
        esac
    done
fi

# Handle and help and version
if [ ${_help_arg_value} == true ] || [ $_exit_help != 0 ]; then

    _indent="  "
    echo "Usage:"
    echo "${_indent}${_SCRIPT_NAME} [-flags] [-option-n value-n]"
    echo

    echo "Help Options:"
    echo "${_indent}-${_HELP_ARG}, --${_HELP_ARG_NAME}"
    echo "${_indent}Show help information flag."
    echo
    echo "${_indent}-${_VERSION_ARG}, --${_VERSION_ARG_NAME}"
    echo "${_indent}Show version and about information flag."
    echo

    echo "Build Options:"
    echo "${_indent}-${_CONF_ARG}, --${_CONF_ARG_NAME} value"
    echo "${_indent}Specifies the conf file. Defaults to ${_SCRIPT_NAME}.conf."
    echo
    echo "${_indent}-${_RID_ARG}, --${_RID_ARG_NAME} value"
    echo "${_indent}Dotnet publish runtime identifier. Valid examples include:"
    echo "${_indent}linux-x64 and linux-arm64. Default is linux-x64 if unspecified."
    echo "${_indent}See also: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog"
    echo
    echo "${_indent}-${_KIND_ARG}, --${_KIND_ARG_NAME} value"
    echo "${_indent}Package output kind. Value must be one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}."
    echo "${_indent}Default is ${_APPIMAGE_KIND} if unspecified."
    echo
    echo "${_indent}-${_VERBOSE_ARG}, --${_VERBOSE_ARG_NAME}"
    echo "${_indent}Verbose review info output flag."
    echo
    echo "${_indent}-${_RUNAPP_ARG}, --${_RUNAPP_ARG_NAME}"
    echo "${_indent}Run the application after successful build flag."
    echo
    echo "${_indent}-${_SKIPYES_ARG}, --${_SKIPYES_ARG_NAME}"
    echo "${_indent}Skip confirmation prompt flag (assumes yes)."
    echo
    echo "${_indent}-${_OUTPUT_ARG}, --${_OUTPUT_ARG_NAME}"
    echo "${_indent}Explicit final output filename (excluding directory part)."
    echo

    echo "Example:"
    echo "${_indent}${_SCRIPT_NAME} -${_RID_ARG} linux-arm64"
    echo

    exit $_exit_help
fi

if [ ${_version_arg_value} == true ]; then
    echo
    echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}"
    echo "${_SCRIPT_COPYRIGHT}"
    echo "${_SCRIPT_WEBSITE}"
    echo
    echo "MIT License"
    echo
    echo "Permission is hereby granted, free of charge, to any person obtaining a copy"
    echo "of this software and associated documentation files (the "Software"), to deal"
    echo "in the Software without restriction, including without limitation the rights"
    echo "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell"
    echo "copies of the Software, and to permit persons to whom the Software is"
    echo "furnished to do so, subject to the following conditions:"
    echo
    echo "The above copyright notice and this permission notice shall be included in all"
    echo "copies or substantial portions of the Software."
    echo
    echo "THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR"
    echo "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,"
    echo "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE"
    echo "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER"
    echo "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,"
    echo "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE"
    echo "SOFTWARE."
    echo

    exit 0
fi


###############################
# SOURCE & WORKING
###############################

# Export these now as may be
# useful in an advanced config file
export DOTNET_RID="${_rid_arg_value}"
export PKG_KIND="${_kind_arg_value}"
export ISO_DATE=`date +"%Y-%m-%d"`

if [ ! -f "${_conf_arg_value}" ]; then
    echo "Configuration file not found: ${_conf_arg_value}"
    echo
    exit 1
fi

# Export contents to any post publish command
set -a

# Source local to PWD
source "${_conf_arg_value}"
set +a

# For AppImage tool and backward compatibility
export VERSION="${APP_VERSION}"


# Then change PWD to conf file
cd "$(dirname "${_conf_arg_value}")"


###############################
# SANITY
###############################

if (( ${CONF_IMPL_VERSION} < ${_SCRIPT_IMPL_MIN} )) || (( ${CONF_IMPL_VERSION} > ${_SCRIPT_IMPL_MAX} )); then
    echo "Configuration format version ${_SCRIPT_IMPL_VERSION} not compatible"
    echo "Older conf file but newer ${_SCRIPT_NAME} implementation?"
    echo "Update from: ${_SCRIPT_WEBSITE}"
    echo
    exit 1
fi

assert_mandatory "APP_MAIN" "${APP_MAIN}"
assert_mandatory "APP_ID" "${APP_ID}"
assert_mandatory "APP_ICON_SRC" "${APP_ICON_SRC}"
assert_mandatory "DE_NAME" "${DE_NAME}"
assert_mandatory "DE_CATEGORIES" "${DE_CATEGORIES}"
assert_mandatory "PKG_OUTPUT_DIR" "${PKG_OUTPUT_DIR}"

if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
    assert_mandatory "APPIMAGETOOL_COMMAND" "${APPIMAGETOOL_COMMAND}"
fi

assert_opt_file "APP_ICON_SRC" "${APP_ICON_SRC}"
assert_opt_file "APP_XML_SRC" "${APP_XML_SRC}"

if [ "${DE_TERMINAL_FLAG}" != "true" ] && [ "${DE_TERMINAL_FLAG}" != "false" ]; then
    echo "DE_TERMINAL_FLAG invalid value: ${DE_TERMINAL_FLAG}"
    echo
    exit 1
fi

if [ "${DOTNET_PROJECT_PATH}" == "${_DOTNET_NONE}" ] && [ "${POST_PUBLISH}" == "" ]; then
    echo "No publish or build operation defined (nothing will be built)"
    echo "See DOTNET_PROJECT_PATH and POST_PUBLISH in: ${_conf_arg_value}"
    echo
    exit 1
fi

if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ] &&
    [ ! -f "${DOTNET_PROJECT_PATH}" ] && [ ! -d "${DOTNET_PROJECT_PATH}" ]; then
    echo "DOTNET_PROJECT_PATH path not found: ${DOTNET_PROJECT_PATH}"
    echo
    exit 1
fi

if [ "${_kind_arg_value}" != "${_APPIMAGE_KIND}" ] && [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then
    echo "Invalid argument value: ${_kind_arg_value}"
    echo "Use one of: ${_APPIMAGE_KIND} or ${_ZIP_KIND}"
    echo
    exit 1
fi


# Detect if publish for windows
_exec_ext=""
declare -l _tw="${_rid_arg_value}"

if [[ "${_tw}" == "win"* ]]; then

    # May use this in future
    _exec_ext=".exe"

    if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
        echo "Invalid AppImage payload"
        echo "Looks like a windows binary to be packaged as AppImage."
        echo "Use --${_KIND_ARG_NAME} ${_ZIP_KIND} instead."
        echo
        exit 1
    fi
fi


###############################
# VARIABLES
###############################

# Abbreviate RID where it maps well to arch
if [ "${_rid_arg_value}" == "linux-x64" ]; then
    _file_out_arch="-x86_64"
elif [ "${_rid_arg_value}" == "linux-arm64" ]; then
    _file_out_arch="-aarch64"
else
    # Otherwise use RID itself
    _file_out_arch="-${_rid_arg_value}"
fi

# APPDIR LOCATIONS
export APPDIR_ROOT="${PKG_OUTPUT_DIR}/AppDir"

if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then
    # AppImage
    export APPDIR_USR="${APPDIR_ROOT}/usr"
    export APPDIR_BIN="${APPDIR_ROOT}/usr/bin"
    export APPDIR_SHARE="${APPDIR_ROOT}/usr/share"

    _local_run="usr/bin/${APP_MAIN}${_exec_ext}"
else
    # Simple zip
    export APPDIR_USR=""
    export APPDIR_BIN="${APPDIR_ROOT}"
    export APPDIR_SHARE="${APPDIR_ROOT}"

    _local_run="${APP_MAIN}${_exec_ext}"
fi

export APPRUN_TARGET="${APPDIR_BIN}/${APP_MAIN}${_exec_ext}"


# DOTNET PUBLISH
if [ "${DOTNET_PROJECT_PATH}" != "${_DOTNET_NONE}" ]; then

    _publish_cmd="dotnet publish"

    if [ "${DOTNET_PROJECT_PATH}" != "" ] && [ "${DOTNET_PROJECT_PATH}" != "." ]; then
        _publish_cmd="${_publish_cmd} \"${DOTNET_PROJECT_PATH}\""
    fi

    _publish_cmd="${_publish_cmd} -r ${_rid_arg_value}"

    if [ "${APP_VERSION}" != "" ]; then
        _publish_cmd="${_publish_cmd} -p:Version=${APP_VERSION}"
    fi

    if [ "${DOTNET_PUBLISH_ARGS}" != "" ]; then
        _publish_cmd="${_publish_cmd} ${DOTNET_PUBLISH_ARGS}"
    fi

    _publish_cmd="${_publish_cmd} -o \"${APPDIR_BIN}\""

fi


# PACKAGE OUTPUT
if [ $PKG_VERSION_FLAG == true ] && [ "${APP_VERSION}" != "" ]; then
    _version_out="-${APP_VERSION}"
fi

if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then

    # AppImageTool
    if [ "${_output_arg_value}" != "" ]; then
        _package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}"
    else
        _package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}${PKG_APPIMAGE_SUFFIX}"
    fi

    _package_cmd="${APPIMAGETOOL_COMMAND}"

    if [ "${PKG_APPIMAGE_ARGS}" != "" ]; then
        _package_cmd="${_package_cmd} ${PKG_APPIMAGE_ARGS}"
    fi

    _package_cmd="${_package_cmd} \"${APPDIR_ROOT}\" \"${_package_out}\""

    if [ ${_runapp_arg_value} == true ]; then
        _packrun_cmd="${_package_out}"
    fi

else

    # Simple zip
    if [ "${_output_arg_value}" != "" ]; then
        _package_out="${PKG_OUTPUT_DIR}/${_output_arg_value}"
    else
        _package_out="${PKG_OUTPUT_DIR}/${APP_MAIN}${_version_out}${_file_out_arch}.zip"
    fi

    _package_cmd="(cd \"${APPDIR_ROOT}\" && zip -r \"${PWD}/${_package_out}\" ./)"

    if [ ${_runapp_arg_value} == true ]; then
        _packrun_cmd="${APPRUN_TARGET}"
    fi

fi


###############################
# DESKTOP ENTRY & APPDATA
###############################

if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then

    _desktop="[Desktop Entry]\n"
    _desktop="${_desktop}Type=Application\n"
    _desktop="${_desktop}Name=${DE_NAME}\n"
    _desktop="${_desktop}Exec=AppRun\n"
    _desktop="${_desktop}Terminal=${DE_TERMINAL_FLAG}\n"
    _desktop="${_desktop}Categories=${DE_CATEGORIES}\n"

    # Follow app-id
    _desktop="${_desktop}Icon=${APP_ID}\n"

    if [ "${DE_COMMENT}" != "" ]; then
        _desktop="${_desktop}Comment=${DE_COMMENT}\n"
    fi

    if [ "${DE_KEYWORDS}" != "" ]; then
        _desktop="${_desktop}Keywords=${DE_KEYWORDS}\n"
    fi

    _desktop="${_desktop}${DE_EXTEND}\n"
fi


# Load appdata.xml
if [ "${APP_XML_SRC}" != "" ]; then

    if command -v envsubst &> /dev/null; then
        _appxml=$(envsubst <"${APP_XML_SRC}")
    else
        _appxml=$(<"${APP_XML_SRC}")
        echo "WARNING: Variable substitution not available for: ${APP_XML_SRC}"
        echo
    fi

fi


###############################
# DISPLAY & CONFIRM
###############################

echo "${_SCRIPT_TITLE}, ${_SCRIPT_VERSION}"
echo "${_SCRIPT_COPYRIGHT}"
echo

echo "APP_MAIN: ${APP_MAIN}"
echo "APP_ID: ${APP_ID}"
echo "APP_VERSION: ${APP_VERSION}"
echo "OUTPUT: ${_package_out}"
echo

if [ "${_desktop}" != "" ]; then
    echo -e "${_desktop}"
fi

if [ ${_verbose_arg_value} == true ] && [ "${_appxml}" != "" ]; then
    echo -e "${_appxml}\n"
fi

echo "Build Commands:"

if [ "${_publish_cmd}" != "" ]; then
    echo
    echo "${_publish_cmd}"
fi

if [ "${POST_PUBLISH}" != "" ]; then
    echo
    echo "${POST_PUBLISH}"
fi

echo
echo "${_package_cmd}"
echo

# Prompt
if [ $_skipyes_arg_value == false ]; then

    echo
    read -p "Build now [N/y]? " prompt

    if [ "${prompt}" != "y" ] && [ "${prompt}" != "Y" ]; then
        echo
        exit 1
    fi

    # Continue
    echo
fi


###############################
# PUBLISH & BUILD
###############################

# Clean and ensure directoy exists
ensure_directory "${PKG_OUTPUT_DIR}"
remove_path "${APPDIR_ROOT}"
remove_path "${_package_out}"

# Create AppDir structure
ensure_directory "${APPDIR_BIN}"

if [ "${_kind_arg_value}" != "${_ZIP_KIND}" ]; then
    # We also create usr/share/icons, as some packages require this.
    # See: https://github.com/kuiperzone/Publish-AppImage/issues/7
    ensure_directory "${APPDIR_SHARE}/icons"
fi

echo

# Publish dotnet
if [ "${_publish_cmd}" != "" ]; then
    exec_or_die "${_publish_cmd}"
    echo
fi

# Post-publish
if [ "${POST_PUBLISH}" != "" ]; then

    exec_or_die "${POST_PUBLISH}"
    echo

fi

# Application file must exist!
if [ ! -f "${APPRUN_TARGET}" ]; then
    echo "Expected application file not found: ${APPRUN_TARGET}"
    echo
    exit 1
fi

if [ "${_kind_arg_value}" == "${_APPIMAGE_KIND}" ]; then

    echo

    # Create desktop
    if [ "${_desktop}" != "" ]; then
        _file="${APPDIR_ROOT}/${APP_ID}.desktop"
        echo "Creating: ${_file}"
        echo -e "${_desktop}" > "${_file}"
        assert_result
    fi

    if [ "${_appxml}" != "" ]; then
        _dir="${APPDIR_SHARE}/metainfo"
        _file="${_dir}/${APP_ID}.appdata.xml"
        echo "Creating: ${_file}"
        ensure_directory "${_dir}"
        echo -e "${_appxml}" > "${_file}"
        assert_result

        if [ "${_desktop}" != "" ]; then
            # Copy of desktop under "applications"
            # Needed for launchable in appinfo.xml (if used)
            # See https://github.com/AppImage/AppImageKit/issues/603
            _dir="${APPDIR_SHARE}/applications"
            _file="${_dir}/${APP_ID}.desktop"
            echo "Creating: ${_file}"
            ensure_directory "${_dir}"
            echo -e "${_desktop}" > "${_file}"
            assert_result
        fi
    fi

    # Copy icon
    if [ "${APP_ICON_SRC}" != "" ]; then

        _icon_ext="${APP_ICON_SRC##*.}"

        if [ "${_icon_ext}" != "" ]; then
            _icon_ext=".${_icon_ext}"
        fi

        _temp="${APPDIR_ROOT}/${APP_ID}${_icon_ext}"
        echo "Creating: ${_temp}"

        cp "${APP_ICON_SRC}" "${_temp}"
        assert_result
    fi

    # AppRun
    _temp="${APPDIR_ROOT}/AppRun"

    if [ ! -f "${_temp}" ]; then

        echo "Creating: ${_temp}"
        ln -s "${_local_run}" "${_temp}"
        assert_result
    fi
fi

# Build package
echo
exec_or_die "${_package_cmd}"
echo

echo "OUTPUT OK: ${_package_out}"
echo

if [ "${_packrun_cmd}" != "" ]; then
    echo "RUNNING ..."
    exec_or_die "${_packrun_cmd}"
    echo
fi

exit 0