#!/bin/bash
#
# Copyright (C) 2023 MOXA Inc. All rights reserved.
# This software is distributed under the terms of the MOXA SOFTWARE NOTICE.
# See the file LICENSE for details.
#
# Authors:
#       2023  Wilson YS Huang  <wilsonys.huang@moxa.com>

# shellcheck disable=SC2317 # Don't warn about unreachable commands in this file

# TODO: loading animation

#
# Global variables
#
export PRODUCT_NAME=""
export BOARD_ID=""
export LINUX_MACHINE_ARCH=""
export LINUX_KERNEL_VER=""
export LINUX_KERNEL_MAJOR_VER=""
export LINUX_KERNEL_MINOR_VER=""
export OS_PACKAGE_MANAGER=""
export LINUX_DISTRO_ID=""
export LINUX_DISTRO_VERSION=""
export LINUX_DISTRO_VERSION_ID=""
export LINUX_DISTRO_VERSION_CODENAME=""
export UEFI_SECURE_BOOT_STATUS=""

INSTL_TOP_DIR="$(dirname "$(realpath "${0}")")"
export INSTL_TOP_DIR
export INSTL_PRODUCT_DIR="${INSTL_TOP_DIR}/product.d"
export INSTL_SCRIPT_DIR="${INSTL_TOP_DIR}/scripts"
export INSTL_HOOKS_DIR="${INSTL_SCRIPT_DIR}/hook.d"
INSTL_LOG_FILE="${INSTL_TOP_DIR}/install.log"
PRODUCT_NAME_MAPPING="${INSTL_PRODUCT_DIR}/mapping"
export INSTL_LOG_FILE
INSTL_VERSION="$(cat "${INSTL_TOP_DIR}"/version)"
export INSTL_VERSION
export INSTL_LOG_LEVEL="info"
export INSTL_EXIT_CODE=0
INSTL_PROMPT_AUTO_YES=false
export INSTL_PROMPT_AUTO_YES
INSTL_OPERATION="install"
INSTL_LOCKFILE="/var/lock/moxa-sdk.lock"
INSTL_CONF_DIR="/etc/moxa"
export INSTL_CONF_DIR
INSTL_SRC_VERSION_FILE="${INSTL_CONF_DIR}/x86-src-version"
export INSTL_SRC_VERSION_FILE
INSTL_INFO_FILE="/etc/moxa-version.conf"
INSTL_INFO_TOOL="mx-ver"
INSTL_FORCE_INSTALL=false
export INSTL_FORCE_INSTALL
INSTL_CUSTOM_CONFIG_FILE="${INSTL_TOP_DIR}/.config"
IS_CUSTOM_CONFIG_FILE=""
TEMP_CUSTOM_CONFIG_FILE=""
export IS_CUSTOM_CONFIG_FILE
export INSTL_CUSTOM_CONFIG_FILE
INSTL_VERBOSE=false
export INSTL_VERBOSE

source "${INSTL_SCRIPT_DIR}/lib/util.sh"
source "${INSTL_SCRIPT_DIR}/lib/logger.sh"
source "${INSTL_SCRIPT_DIR}/lib/version.sh"

read_product_name_from_dmi_type12() {
        /usr/sbin/dmidecode -t 12 |
                grep "Option " |
                awk -F ':' '{print substr($2,1,11)}' |
                sed 's/ //g'
}

read_product_name_from_dmi_type1() {
        /usr/sbin/dmidecode -t 1 |
                grep "Product Name" |
                awk -F ':' '{print $2}' |
                sed 's/ //g'
}

read_product_name() {
        local board_id
        local product_name
        local dmi_info

        dmi_info="$(read_product_name_from_dmi_type12) $(read_product_name_from_dmi_type1)"

        for name in ${dmi_info}; do
                log::debug "name=${name}"
                if util::is_dir "${INSTL_PRODUCT_DIR}/${name}"; then
                        board_id="${name}"
                        break
                fi
        done

        if util::is_empty_str "${board_id}"; then
                log::error "Product is not support."
                exit 1
        fi

        BOARD_ID="${board_id}"

        while IFS=", " read -r id name; do
                log::debug "id=${id}"
                log::debug "name=${name}"

                if [[ "${board_id}" == "${id}" ]]; then
                        product_name="${name}"
                        break
                else
                        continue
                fi
        done <"${PRODUCT_NAME_MAPPING}"

        if util::is_empty_str "${product_name}"; then
                log::error "Product is not support."
                exit 1
        fi

        PRODUCT_NAME="${product_name}"
}

load_product_config() {
        read_product_name

        if ! util::is_exists "${INSTL_PRODUCT_DIR}/${BOARD_ID}/config"; then
                log::error "Could not find ${BOARD_ID} configuration."
                exit 2
        fi

        source "${INSTL_PRODUCT_DIR}/${BOARD_ID}/config"
}

read_os_info() {
        local option=$1

        awk -v opt="${option}" -F= '$1==opt { print $2 ;}' /etc/os-release |
                tr -d '"'
}

check_os_env() {
        local arch
        local pkg_manager
        local kernel_version
        local kernel_major_version
        local kernel_minor_version

        arch="$(uname -m)"

        # Check machine architecture
        log::debug "arch=$arch"
        if [[ ! "${arch}" == "x86_64" ]]; then
                log::error "Machine architecture ${arch} is not support."
                exit 1
        fi

        kernel_version="$(uname -r)"
        kernel_major_version="$(ver::get_major "$kernel_version")"
        kernel_minor_version="$(ver::get_minor "$kernel_version")"
        log::debug "kernel_version=$kernel_version"
        log::debug "kernel_major_version=$kernel_major_version"
        log::debug "kernel_minor_version=$kernel_minor_version"

        if command -v apt-get &>/dev/null; then
                pkg_manager="apt"
        elif command -v yum &>/dev/null; then
                pkg_manager="yum"
        else
                log::error "Package manager is not support."
                exit 1
        fi

        # Check UEFI secure boot status
        if ! command -v mokutil &>/dev/null; then
                log::warn "mokutil could not be found."
                UEFI_SECURE_BOOT_STATUS="unknown"
        elif mokutil --sb-state | grep -q enabled &>/dev/null; then
                log::warn "The UEFI secure boot is enabled from BIOS menu."
                log::warn "The boot process or unsiged kernel modules should be loaded failed due to unauthorized policy."
                UEFI_SECURE_BOOT_STATUS="enabled"
        else
                UEFI_SECURE_BOOT_STATUS="disabled"
        fi

        LINUX_MACHINE_ARCH="${arch}"
        LINUX_DISTRO_ID="$(read_os_info "ID")"
        LINUX_DISTRO_VERSION="$(read_os_info "VERSION")"
        LINUX_DISTRO_VERSION_ID="$(read_os_info "VERSION_ID")"
        LINUX_DISTRO_VERSION_CODENAME="$(read_os_info "VERSION_CODENAME")"
        LINUX_KERNEL_VER="${kernel_version}"
        LINUX_KERNEL_MAJOR_VER="${kernel_major_version}"
        LINUX_KERNEL_MINOR_VER="${kernel_minor_version}"
        OS_PACKAGE_MANAGER="${pkg_manager}"
}

exec_hook() {
        local hook_script
        local exit_code
        hook_script="${INSTL_SCRIPT_DIR}/${1}"

        if ! util::is_exists "${hook_script}"; then
                log::warn "=== Execute hook script \"$(basename "${hook_script}")\" is not exist."
        elif ! util::is_file "${hook_script}"; then
                log::warn "=== Execute hook script \"$(basename "${hook_script}")\" is not file."
        else
                log::info ">>> Execute hook script \"$(basename "${hook_script}")\"."
                "${hook_script}"
                exit_code=${?}

                if [[ ${exit_code} -ne 0 ]]; then
                        INSTL_EXIT_CODE=${exit_code}
                fi

                log::info "<<< Execute hook script \"$(basename "${hook_script}")\" done."
        fi
}

process_file_list() {
        local repo_name="$1"
        local repo_tag="$2"
        local repo_ver

        if util::is_exists "${INSTL_TOP_DIR}"/src/"${repo_name}"-"${repo_tag}"/version; then
                repo_ver="$(cat "${INSTL_TOP_DIR}"/src/"${repo_name}"-"${repo_tag}"/version)"
        else
                repo_ver="unknown"
        fi

        printf " %-50s%-35s\n" "${repo_name}" "${repo_ver}"
}

process_config_list() {
        local src_name="$1"
        local dst_path="$3"

        printf " %-50s%-35s\n" "${src_name}" "${dst_path}"
}

process_package_list() {
        local pkg_name="$1"
        local pkg_ver="$2"

        for pkg_file in "${INSTL_TOP_DIR}"/pkg/"${pkg_name}"/"${pkg_ver}"/*.deb; do
                printf " %-50s%-35s\n" "${pkg_name}" "${pkg_file##*/}"
        done
}

dry_run() {
        local drivers_list
        local tools_list
        local driver_available
        local tool_available

        driver_available=true
        tool_available=true

        drivers_list="${INSTL_PRODUCT_DIR}/${BOARD_ID}/drivers"
        tools_list="${INSTL_PRODUCT_DIR}/${BOARD_ID}/tools"
        printf "%70s\n" " " | tr ' ' '-'
        printf " %-14s%s\n" "Product Name:" "${PRODUCT_NAME}"
        printf " %-14s%s\n" "Architecture:" "${LINUX_MACHINE_ARCH}"
        printf " %-14s%s\n" "Kernel Version: " "${LINUX_KERNEL_VER}"
        printf " %-14s%s\n" "UEFI Secure Boot: " "${UEFI_SECURE_BOOT_STATUS}"
        if util::is_exists "${INSTL_CUSTOM_CONFIG_FILE}"; then
                printf " %-14s%s\n" "Custom Config File: " "$(basename "${INSTL_CUSTOM_CONFIG_FILE}")"
        fi
        printf "%70s\n" " " | tr ' ' '-'
        printf " %-50s%-35s\n" "Name" "Version"
        printf "%70s\n" " " | tr ' ' '='

        if util::is_exists "${INSTL_CUSTOM_CONFIG_FILE}"; then
                # parse custom config list
                while IFS= read -r line; do
                        repo_name="${line%%(*}"
                        repo_tag="${line#*\(}"
                        repo_tag="${repo_tag%\)}"
                        printf " %-50s%-35s\n" "${repo_name}" "${repo_tag}"
                done <"${INSTL_CUSTOM_CONFIG_FILE}"
        else
                if ! util::parse_product_file "${drivers_list}" "${LINUX_KERNEL_MAJOR_VER}" "${LINUX_KERNEL_MINOR_VER}" process_file_list; then
                        driver_available=false
                fi

                if ! util::parse_product_file "${tools_list}" "${LINUX_KERNEL_MAJOR_VER}" "${LINUX_KERNEL_MINOR_VER}" process_file_list; then
                        tool_available=false
                fi

                if [[ $driver_available = false ]] && [[ $tool_available = false ]]; then
                        printf " %-50s%-35s\n" "No available" "N/A"
                fi
        fi

        printf "%70s\n" " " | tr ' ' '-'

        if [[ $INSTL_VERBOSE = true ]]; then
                local file_available
                local pkg_available
                file_available=true
                pkg_available=true
                files_list="${INSTL_PRODUCT_DIR}/${BOARD_ID}/files"
                pkg_list="${INSTL_PRODUCT_DIR}/${BOARD_ID}/packages"

                if [ -n "${LINUX_DISTRO_VERSION_CODENAME}" ]; then
                        codename="${LINUX_DISTRO_VERSION_CODENAME}"
                elif [ "${LINUX_DISTRO_ID}" = "rhel" ] || [ "${LINUX_DISTRO_ID}" = "rocky" ]; then
                        codename="${LINUX_DISTRO_ID}_$(echo "${LINUX_DISTRO_VERSION}" | sed -n 's/.*(\(.*\)).*/\1/p')"
                else
                        codename="${LINUX_DISTRO_ID}_${LINUX_DISTRO_VERSION_ID}"
                fi

                if util::is_exists "${files_list}"; then
                        printf " %-50s%-35s\n" "Filename" "Install path"
                        printf "%70s\n" " " | tr ' ' '='

                        if ! util::parse_product_file_from_codename "${files_list}" "${codename}" process_config_list; then
                                file_available=false
                        fi

                        if [[ $file_available = false ]]; then
                                printf " %-50s%-25s\n" "No available" "N/A"
                        fi

                        printf "%70s\n" " " | tr ' ' '-'
                fi

                if util::is_exists "${pkg_list}"; then
                        printf " %-50s%-35s\n" "Package" "Install deb"
                        printf "%70s\n" " " | tr ' ' '='
                        if ! util::parse_product_file_from_codename "${pkg_list}" "${codename}" process_package_list; then
                                pkg_available=false
                        fi

                        if [[ $pkg_available = false ]]; then
                                printf " %-50s%-25s\n" "No available" "N/A"
                        fi
                        printf "%70s\n" " " | tr ' ' '-'
                fi
        fi
}

script_acquire_lock() {
        if ! (util::is_exists "${INSTL_LOCKFILE}" && kill -0 "$(cat ${INSTL_LOCKFILE})"); then
                echo $$ >"${INSTL_LOCKFILE}"
                return 0
        else
                return 1
        fi
}

script_release_lock() {
        rm -f "${INSTL_LOCKFILE}"
}

script_signal_handler() {
        script_release_lock
}

script_signal_INT_handler() {
        script_release_lock
        log::info "Interrupt!"
        exit 0
}

script_init() {
        if ! util::is_root_user; then
                echo "Permission denied. Are you root?"
                exit 13
        fi

        if ! script_acquire_lock; then
                echo "Another install.sh process is running."
                exit 16
        fi

        trap script_signal_handler ERR TERM EXIT
        trap script_signal_INT_handler INT

        if util::is_exists "${INSTL_LOG_FILE}"; then
                echo >"${INSTL_LOG_FILE}"
        else
                touch "${INSTL_LOG_FILE}"
        fi

        log::init "${INSTL_LOG_FILE}" "${INSTL_LOG_LEVEL}"
}

script_help_page() {
        cat <<EOF

                ███╗   ███╗ ██████╗ ██╗  ██╗ █████╗
                ████╗ ████║██╔═══██╗╚██╗██╔╝██╔══██╗
                ██╔████╔██║██║   ██║ ╚███╔╝ ██║  ██║
                ██║╚██╔╝██║██║   ██║ ██╔██╗ ██║  ██║
                ██║ ╚═╝ ██║╚██████╔╝██╔╝ ██╗██║  ██║
                ╚═╝     ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝
                ------------------------------------
                         X86 INSTALL WIZARD

Usage: install.sh [option]

Options:
        -h, --help         Display this help page
        -y, --yes          Automatically answer yes
        -v, --version      Display the version information
        -s, --selftest     Run the self test cases
        -c, --config       Use terminal user interface configuration
        -f, --file         Use custom config to start install procedure
            --dry-run      List available driver and tool only
            --uninstall    Uninstall driver and tool
            --force        Install driver and tool even if the
                           version is the same or older (default is
                           to install newer version)
            --verbose      Increase output verbosity

Without passing any option, it would run the installation directly.
EOF
}

script_params() {
        local param

        # Parse long options with getopt
        param=$(getopt -l "force,uninstall,dry-run,config,selftest,verbose,version,yes,help,file:" -o "csvyhf:" -a -- "$@")
        eval set -- "$param"
        # Extract options
        while true; do
                case "$1" in
                -y | --yes)
                        INSTL_PROMPT_AUTO_YES=true
                        shift
                        ;;
                -f | --file)
                        INSTL_OPERATION="file"
                        if [[ -z "$2" && "$2" != "--" && "$2" != -* ]]; then
                                TEMP_CUSTOM_CONFIG_FILE=".config"
                        else
                                TEMP_CUSTOM_CONFIG_FILE="$2"
                        fi
                        shift 2
                        ;;
                -s | --selftest)
                        INSTL_OPERATION="selftest"
                        shift
                        ;;
                -c | --config)
                        INSTL_OPERATION="config"
                        shift
                        ;;
                -v | --version)
                        echo "${INSTL_VERSION}"
                        exit 0
                        ;;
                -h | --help)
                        script_help_page
                        exit 0
                        ;;
                --dry-run)
                        INSTL_OPERATION="dryrun"
                        shift
                        ;;
                --uninstall)
                        INSTL_OPERATION="uninstall"
                        shift
                        ;;
                --force)
                        INSTL_FORCE_INSTALL=true
                        shift
                        ;;
                --verbose)
                        INSTL_VERBOSE=true
                        shift
                        ;;
                --)
                        shift
                        break
                        ;;
                *)
                        echo "Invalid option was provided: $param"
                        script_help_page
                        exit 22
                        ;;
                esac
        done
}

show_system_info() {
        log::info "Product Name: ${PRODUCT_NAME}"
        log::info "Architecture: ${LINUX_MACHINE_ARCH}"
        log::info "Kernel Version: ${LINUX_KERNEL_VER}"
        log::info "UEFI Secure Boot: ${UEFI_SECURE_BOOT_STATUS}"
        log::info "Distro Version: ${LINUX_DISTRO_VERSION}"
}

check_config_file() {
        local custom_config_file="$1"

        if [[ -z "${custom_config_file}" ]]; then
                log::info "Custom config file is unset or empty."
                log::info "To use '.config' file as default config to start install procedure"
        else
                log::info "Use custom config file '${custom_config_file}' to start install procedure"
                INSTL_CUSTOM_CONFIG_FILE="${INSTL_TOP_DIR}/${custom_config_file}"
        fi

        if util::is_exists "${INSTL_CUSTOM_CONFIG_FILE}"; then
                IS_CUSTOM_CONFIG_FILE="y"
        else
                echo "Custom config file is not exist. Exit."
                exit 1
        fi
}

store_install_info() {
        cat <<EOF >$INSTL_INFO_FILE
PRODUCT_NAME=$PRODUCT_NAME
OPTION_CODE=
SDK_VERSION=$(util::is_exists "$INSTL_TOP_DIR"/.sdk_version && cat "$INSTL_TOP_DIR"/.sdk_version)
SDK_BUILDDATE=$(util::is_exists "$INSTL_TOP_DIR"/.sdk_builddate && cat "$INSTL_TOP_DIR"/.sdk_builddate)
KERNEL_VERSION=$LINUX_KERNEL_VER
IMAGE_VERSION=
IMAGE_BUILDDATE=
EOF
        if util::is_exists "${INSTL_SCRIPT_DIR}/mx-ver.sh"; then
                cp "${INSTL_SCRIPT_DIR}/mx-ver.sh" "/usr/sbin/${INSTL_INFO_TOOL}"
        fi
}

remove_install_info() {
        util::is_exists "${INSTL_INFO_FILE}" && rm "${INSTL_INFO_FILE}"
        util::is_exists "/usr/sbin/${INSTL_INFO_TOOL}" && rm "/usr/sbin/${INSTL_INFO_TOOL}"
}

main() {
        local drivers_vermagic
        script_params "$@"
        script_init
        load_product_config
        check_os_env

        case "${INSTL_OPERATION}" in
        "install")
                show_system_info

                if [[ $INSTL_FORCE_INSTALL = false ]] && util::is_exists "/usr/sbin/${INSTL_INFO_TOOL}"; then
                        drivers_vermagic="$(/usr/sbin/${INSTL_INFO_TOOL} -k)"

                        if [[ "${drivers_vermagic}" != "${LINUX_KERNEL_VER}" ]]; then
                                log::warn "The installed drivers vermagic (${drivers_vermagic}) is not compatible with the kernel version (${LINUX_KERNEL_VER})"
                                INSTL_FORCE_INSTALL=true
                        fi
                fi

                if ! util::confirm_prompt "Do you want to continue?" "${INSTL_PROMPT_AUTO_YES}"; then
                        log::info "Abort"
                        exit 0
                fi

                exec_hook "prepare.sh"
                if [[ "$INSTL_EXIT_CODE" -ne 0 ]]; then
                        log::error "Failed, exit code: ${INSTL_EXIT_CODE}. See log at install.log for details."
                        exit ${INSTL_EXIT_CODE}
                fi

                exec_hook "build-sources.sh"
                if [[ "$INSTL_EXIT_CODE" -eq 0 ]]; then
                        exec_hook "self-test.sh"
                        log::info "Done. Please reboot machine for installation to take effect."
                        store_install_info

                        if util::confirm_prompt "Do you want to reboot now?" false; then
                                systemctl reboot
                        fi
                        exit 0
                elif [[ "$INSTL_EXIT_CODE" -eq 3 ]]; then
                        exec_hook "self-test.sh"
                        log::info "Done. Nothing was installed"
                        exit 0
                else
                        log::error "Failed, exit code: ${INSTL_EXIT_CODE}. See log at install.log for details."
                        exit ${INSTL_EXIT_CODE}
                fi
                ;;
        "uninstall")
                show_system_info
                if ! util::confirm_prompt "Do you want to continue?" "${INSTL_PROMPT_AUTO_YES}"; then
                        log::info "Abort"
                        exit 0
                fi

                exec_hook "uninstall.sh"

                if [[ "$INSTL_EXIT_CODE" -eq 0 ]]; then
                        log::info "Done. Please reboot machine for uninstallation to take effect."
                        remove_install_info

                        if util::confirm_prompt "Do you want to reboot now?" false; then
                                systemctl reboot
                        fi
                        exit 0
                elif [[ "$INSTL_EXIT_CODE" -eq 3 ]]; then
                        log::info "Done. Nothing was uninstalled"
                        exit 0
                else
                        log::error "Failed, exit code: ${INSTL_EXIT_CODE}. See log at install.log for details."
                        exit ${INSTL_EXIT_CODE}
                fi
                ;;
        "selftest")
                show_system_info
                exec_hook "self-test.sh"
                exit 0
                ;;
        "dryrun")
                dry_run
                exit 0
                ;;
        "config")
                exec_hook "config.sh" "${BOARD_ID}"
                exit 0
                ;;
        "file")
                show_system_info
                check_config_file "$TEMP_CUSTOM_CONFIG_FILE"
                if ! util::confirm_prompt "Do you want to continue?" "${INSTL_PROMPT_AUTO_YES}"; then
                        log::info "Abort"
                        exit 0
                fi
                exec_hook "prepare.sh"
                if [[ "$INSTL_EXIT_CODE" -ne 0 ]]; then
                        log::error "Failed, exit code: ${INSTL_EXIT_CODE}. See log at install.log for details."
                        exit ${INSTL_EXIT_CODE}
                fi

                exec_hook "build-sources.sh"
                if [[ "$INSTL_EXIT_CODE" -eq 0 ]]; then
                        exec_hook "self-test.sh"
                        log::info "Done. Please reboot machine for installation to take effect."
                        store_install_info

                        if util::confirm_prompt "Do you want to reboot now?" false; then
                                systemctl reboot
                        fi
                        exit 0
                elif [[ "$INSTL_EXIT_CODE" -eq 3 ]]; then
                        exec_hook "self-test.sh"
                        log::info "Done. Nothing was installed"
                        exit 0
                else
                        log::error "Failed, exit code: ${INSTL_EXIT_CODE}. See log at install.log for details."
                        exit ${INSTL_EXIT_CODE}
                fi

                exit 0
                ;;
        *)
                log::error "Unknown operation: ${INSTL_OPERATION}"
                exit 1
                ;;
        esac

        script_release_lock
}

main "$@"
