#!/usr/bin/env bash
#
# dbw cli utility to work with course repos.
#

##
# Globals (prefer none)
# 
readonly APP_NAME="dbw"
readonly DBW_CONFIG_DIR=${DBW_CONFIG_DIR:-"$HOME/.dbw"}
readonly DBW_CONFIG_FILE=${DBW_CONFIG_FILE:-"$DBW_CONFIG_DIR/dbw_config.env"}
readonly REPO_CONFIG_FILE_DEFAULT=${REPO_CONFIG_FILE_DEFAULT:-".dbw/repo_config.env"}



##
# Print out current version
#
version()
{
    printf "v3.0.0 (2024-02-03)\\n"
}



##
# Print out how to use
#
usage()
{
    printf "\
Utility to work with course repo.

Usage:
 %s [options] [command] [arguments]

Command:
 check               Check and display details on local environment.
 config              Create/update configuration file.
 help [command]      Show general help or detailed help on a command.
 repo                Show details on the repo and all included commands, if any.
 selfupdate          Update to latest version.
 
Commands supported by each course repo:
 <command> [options] [arguments]  Execute command with options and arguments.

Options:
 --dry                   Run dry for test, limit what actually is performed.
 --help, -h              Show info on usage.
 --silent                Suppress output.
 --verbose, very-verbose Be verbose and print details on whats happening.
 --version, -v           Show details on version.
" "$APP_NAME"

#  --force             Force potential dangerous operation.
#  --help, -h          Show info on usage.
#  --source <source>   Some commands use source as alternate source.
#  --target <target>   Some commands use target as alternate target.

}



##
# Print out how to use
#
# @param string $1 error message to display.
#
bad_usage()
{
    [[ $1 ]] && printf "%s\\n" "$1"

    printf "\
For an overview of the command, execute:
 %s help
 %s repo
" "$APP_NAME" "$APP_NAME"
}



##
# Error while processing
#
# @param string $* error message to display.
#
fail()
{
    local color
    local normal

    color=$(tput setaf 1)
    normal=$(tput sgr0)

    printf "%s $*\\n" "${color}[FAILED]${normal}"
    exit 2
}



##
# Verbose output for detailed information
#
# @param string $* error message to display.
#
verbose()
{
    local color
    local normal

    color=$(tput setaf 4)
    normal=$(tput sgr0)

    [[ $OPTION_SILENT ]] \
        || printf "%s $*\\n" "${color}[VERBOSE]${normal}"
}



##
# Verbose output to show what is being done.
#
# @param string $* error message to display.
#
doing()
{
    local color
    local normal

    color=$(tput setaf 2)
    normal=$(tput sgr0)

    [[ $OPTION_SILENT ]] \
        || printf "%s $*\\n" "${color}[$APP_NAME]${normal}"
}



##
# Check if command is installed
#
# @param string $1 the command to check.
#
has_command()
{
    if ! hash "$1" 2> /dev/null; then
        return 1
    fi
    return 0
}



##
# Print confirmation message with default values.
#
# @param string $1 the message to display or use default.
# @param string $2 the default value for the response.
#
confirm()
{
    read -r -p "${1:-Are you sure? [yN]} "
    case "${REPLY:-$2}" in
        [yY][eE][sS]|[yY]) 
            true
            ;;
        *)
            false
            ;;
    esac
}



##
# Read input from user supporting a default value for reponse.
#
# @param string $1 the message to display.
# @param string $1 the default value.
#
input()
{
    read -r -p "$1 [$2]: "
    echo "${REPLY:-$2}"
}



##
# Read the configuration if it exists or silently ignore it.
#
config_read()
{
    local configFile=${1:-$DBW_CONFIG_FILE}

    if [[ -f "$configFile" ]]; then
        # shellcheck source=/dev/null
        . "$configFile"

        # while IFS='= ' read -r lhs rhs
        # do
        #     if [[ ! $lhs =~ ^\ *# && -n $lhs ]]; then
        #         rhs="${rhs%%\#*}"    # Del in line right comments
        #         rhs="${rhs%%*( )}"   # Del trailing spaces
        #         rhs="${rhs%\"*}"     # Del opening string quotes 
        #         rhs="${rhs#\"*}"     # Del closing string quotes 
        #         export "$lhs"="$rhs"
        #     fi
        # done < "$configFile"
    fi
}



##
# Create or update the configuration.
#
config_create_update()
{
    install -d "$( dirname "$DBW_CONFIG_FILE" )" \
        || fail "Could not create configuration dir: '$( dirname "$DBW_CONFIG_FILE" )'"
    touch "$DBW_CONFIG_FILE" \
        || fail "Could not create configuration file: '$DBW_CONFIG_FILE'"

    echo "$DBW_CONFIG_FILE"
    ls -l "$DBW_CONFIG_FILE"
}



##
# Check details on a command and display its version.
#
# @param string $1 command to check.
# @param string $2 optionally extra string to extract version number.
# @param string $3 optionally extra string to extract version number.
#
check_command_version()
{
    local optionVersion=${2:-"--version"}
    local currentVersion=

    currentVersion="$( eval "$1" "$optionVersion" "$3" 2>&1 )"
    printf "%-10s%-10s (%s)\\n" "$1" "$currentVersion" "$( which "$1" )"
}



##
# Check details on an environment variable.
#
# @param string $1 variable to check.
#
check_environment_variable()
{
    local isSet=${!1+x}

    if [[ $isSet ]]; then
        printf "\$%s=%s\\n" "${1}" "${!1}"
    else
        printf "\$%s is not set\\n" "${1}"
    fi
}



##
# Check details on local environment
#
check_environment()
{
    local configFile="$DBW_CONFIG_DIR/config"

    printf "App utilities."
    printf "\\n------------------------------------\\n"
    check_command_version "$APP_NAME" ""  
    printf "\\n"
    
    printf "App environment."
    printf "\\n------------------------------------\\n"
    check_environment_variable "DBW_CONFIG_DIR"
    check_environment_variable "DBW_CONFIG_FILE"
    printf "\\n"

    printf "Details on installed utilities."
    printf "\\n------------------------------------\\n"
    check_command_version "bash"  ""   "| head -1 | cut -d ' ' -f 4"
    check_command_version "curl"  ""   "| head -1 | cut -d ' ' -f 2"
    check_command_version "rsync" ""   "| head -1 | cut -d ' ' -f 4"
    check_command_version "ssh"   "-V" "| cut -d ' ' -f 1"
    check_command_version "wget"  ""   "| head -1 | cut -d ' ' -f 3"
    printf "\\n"

    printf "Details on the environment."
    printf "\\n------------------------------------"
    printf "\\nOperatingsystem:    %s" "$( uname -a )"
    printf "\\nLocal user:         %s" "$USER"
    printf "\\nLocal homedir:      %s" "$HOME"
    printf "\\n"
}



##
# Download external file and save it locally.
#
# @param string $1 url to download from.
# @param string $2 filename to save in.
#
download_file()
{
    local optionSilent=

    if has_command curl; then
        [[ ! $OPTION_VERY_VERBOSE ]] \
            && optionSilent="--silent"
        [[ $OPTION_VERBOSE ]] \
            && verbose "Curl: $1"

        if ! curl --fail --output "$2" $optionSilent "$1"; then
            rm -f "$2"
            fail "Curl: $1"
        fi
    elif has_command wget; then
        :
    else
        fail "Neither curl or wget seems to be installed on this system. Install any of them."
    fi

    [[ $OPTION_VERBOSE ]] \
        && verbose "$( ls -l "$2" )"
}



##
# Download external file and verify using hash. The hash-files should
# be named with the extension .sha1 or .md5. The downloaded file is not
# removed, thats the callers responsibility.
#
# @param string $1 url to download from.
# @param string $2 filename to save in.
#
download_file_verify_hash()
{
    local url="${OPTION_SOURCE:-$1}"
    local file="$2"

    [[ $OPTION_VERBOSE ]] \
        && verbose "Download '$url' and save as '$file'."

    download_file "$url" "$file"

    # if [[ ! $OPTION_FORCE ]]; then
    #     [[ $OPTION_VERBOSE ]] \
    #         && verbose "Download SHA1 verification: '$1.sha1'"
    
    #     if ! download_file "$url.sha1" "$file.sha1"; then
    #         rm -f "$file" "$file.sha1"
    #         fail "Could not download SHA1 for the file."
    #     fi
    # fi
}



##
# Read configuration files if they exists or silently ignore them.
#
configuration_load()
{
    # dbw
    config_read

    # course repo
    repo_find_config_file
    config_read "$REPO_CONFIG_FILE"
}



###############################################################################
# Course repo functions.
#



##
# Find the course repo config file.
#
function repo_find_config_file
{
    local dir=
    local found=

    dir="$( pwd )/."
    REPO_CONFIG_FILE=
    REPO_CONFIG_DIR=

    while [ "$dir" != "/" ]; do
        dir=$( dirname "$dir" )
        found="$( find "$dir" -maxdepth 2 -wholename "$dir/$REPO_CONFIG_FILE_DEFAULT" 2> /dev/null )"
        if [ "$found" ]; then
            REPO_CONFIG_FILE="$found"
            REPO_CONFIG_DIR="$( dirname "$found" )"
            break
        fi
    done
}



##
# List the available commands in the repo.
#
function repo_command_list
{
    local dir="$REPO_CONFIG_DIR/command"

    [[ ! $REPO_CONFIG_FILE ]] \
        && fail "You are not within a course repo!"

    [[ -d "$dir" ]] \
        || fail "There is no directory '$dir' and then no commands in this course repo."

    ls "$dir"
}



##
# Return 0 if the command exists, otherwise 1.
#
function repo_command_exists
{
    local command="$REPO_CONFIG_DIR/command/$1"

    [[ ! $REPO_CONFIG_FILE ]] \
        && return

    [[ -d "$command" ]] \
        && echo "$1"
}



##
# Execute a repo command.
#
function repo_command_execute
{
    local commandDir="$REPO_CONFIG_DIR/command/$1"
    local commandFile="$commandDir/command.bash"

    [[ $OPTION_VERBOSE ]] \
        && verbose "Execute command: $commandFile" \
        && verbose "Arguments: " "${ARGS[@]}" \
        && verbose bash "$commandFile" "${ARGS[@]}"

    [[ -d $commandDir ]] \
        || fail "This is not a valid command in this course repo (missing dir)!"

    [[ -f $commandFile ]] \
        || fail "This is not a valid command in this course repo (missing command.bash)!"

    [[ -d $REPO_CONFIG_DIR ]] \
        || fail "It is not a valid config dir in this course repo!"

    # Always execute from the course repo base dir
    cd "$( dirname "$REPO_CONFIG_DIR" )" \
        || fail "Could not move to the root dir of the course repo."

    # shellcheck source=/dev/null
    . "$commandFile" #"${ARGS[@]}"
}



###############################################################################
# Application commands.
#



##
# Check details on local environment
#
app_check()
{
    check_environment
}



##
# Help for the command.
#
app_check_help()
{
    printf "\
Usage:
 %s

Help:
 Check and print out details on the current environment and
 configuration used.

 $ %s check

 Use this as a base for debugging.
" "$1" "$APP_NAME"
}



##
# Create/update the configuration directory.
#
app_config()
{
    config_create_update
}



##
# Help for the command.
#
app_config_help()
{
    printf "\
Usage:
 %s

Help:
 Create or update the configuration.

 $ %s config

 The configuration is stored in the directory '\$HOME/.dbw' and
 its location can be overridden setting the environment variable
 \$DBW_CONFIG_DIR.

 The configuration file is 'dbw_config.env' is stored in the configuration
 directory by default. Its name and location can be set with the
 environment variable \$DBW_CONFIG_FILE.

 You can check details on the current configuration through the
 'check' command.
" "$1" "$APP_NAME"
}



##
# Show general help information or detailed help on a command.
#
app_help()
{
    set -- "${ARGS[@]}"

    (( $# > 1 )) \
        && fail "This command only takes max one extra argument, you supplied $#."

    if (( $# == 0 )); then
        usage
        return
    fi

    if type -t app_"$1"_help | grep -q function; then
        app_"$1"_help "$1"
        exit 0
    else
        bad_usage "There is no extra help on command '$1'."
        exit 1
    fi
}



##
# Help for the command.
#
app_help_help()
{
    printf "\
Usage:
 %s [command_name]

Command:
 command_name       The command to display help for.

Help:
 The help command displays help for a given command.
 $ %s help config

 You can list all available commands.
 $ %s commands
" "$1" "$APP_NAME" "$APP_NAME"
}



##
# Create/update the configuration directory.
#
app_repo()
{
    repo_find_config_file
    [[ ! $REPO_CONFIG_FILE ]] \
        && fail "You are not within a course repo!"

    config_read "$REPO_CONFIG_FILE"
    [[ ! $REPO_COURSE ]] \
        && fail "The repo does not have a setting for REPO_COURSE in the configuration file!\n$REPO_CONFIG_FILE"

    [[ $OPTION_VERBOSE ]] \
        && verbose "Course dir found: $REPO_CONFIG_FILE\n"

    printf "\
Course repo: %s
================================
Commands available:
$( repo_command_list )
" "$REPO_COURSE"
}



##
# Help for the command.
#
app_repo_help()
{
    printf "\
Usage:
 %s

Help:
 Show details on the course repo and what commands it supplies.

 $ %s repo

 The course repo can include a directory '.dbw/' with details about the repo
 and what commands it implements.

 The first config file that is read is 'repo_config.env'and should at least
 contain a setting for '\$COURSE'.
" "$1" "$APP_NAME"
}



##
# Selfupdate to latest version.
#
app_selfupdate()
{
    local url="${OPTION_SOURCE:-https://raw.githubusercontent.com/dbwebb-se/dbwebb-cli/master/release/latest/install}"
    local file="/tmp/dbw.$$"

    download_file_verify_hash "$url" "$file"

    [[ $OPTION_DRY ]] || \
        DBW_INSTALL_SOURCE="$OPTION_SOURCE_BIN" \
        DBW_INSTALL_TARGET="$OPTION_TARGET"     \
        bash "$file"

    #rm -f "$file" "$file.sha1"

    # local tmp="/tmp/dbwebb-cli.$$"
    # local tmpSha1="$tmp.sha1"
    # local url="${OPTION_SOURCE:-https://raw.githubusercontent.com/dbwebb-se/dbwebb-cli/master/release/latest/install}"
    # local urlSha1="$url.sha1"
    # local optionSilent="--silent"
    # 
    # [[ $OPTION_VERBOSE ]] && optionSilent=
    # [[ $OPTION_VERBOSE ]] && printf "Download install program: %s.\\n" "$url"
    # 
    # if ! curl --fail $optionSilent "$url" > "$tmp"; then
    #     rm -f "$tmp"
    #     fail "Could not download installation program. You need curl and a network connection."
    # fi
    # 
    # if [[ ! $OPTION_FORCE ]]; then
    #     [[ $OPTION_VERBOSE ]] && printf "Download SHA1 verification: %s.\\n" "$urlSha1"
    # 
    #     if ! curl --fail $optionSilent "$urlSha1" > "$tmpSha1"; then
    #         rm -f "$tmpSha1"
    #         fail "Could not download SHA1 for installation program."
    #     fi
    # fi
    # 
    # if [[ ! $OPTION_DRY ]]; then
    #     SOURCE="$OPTION_SOURCE" TARGET="$OPTION_TARGET" bash < "$tmp"
    # fi
    # rm -f "$tmp" "$tmpSha1"
}



##
# Help for the command.
#
app_selfupdate_help()
{
    printf "\
Usage:
 %s [options]

Options:
 --dry                  Download but do not actually perform the update.
 --force                Install and ignore validation of checksums.
 --source <source>      Source to download installation program.
 --source-bin <install> Source to installation program to download and install.
 --target <target>      Target dir for installation.
 --verbose,very-verbose Be more verbose in output.

Help:
 Update the utility to the latest version through an automated
 installation script.

 $ $APP_NAME selfupdate
 $ $APP_NAME selfupdate --verbose
 $ $APP_NAME selfupdate --verbose --dry

 You can perform a local installation by specifying the installation program
 to use, the program file to download and the installation directory. This is
 how the test program verifies the installation and it works if you are in
 the root of the development git-repo.
 
 $ $APP_NAME selfupdate                           \\
     --source file://\$PWD/release/latest/install \\
     --source-bin file://\$PWD/release/latest/dbw \\
     --target build                               \\
     --force
" "$1"
}



###############################################################################
# Main.
#



##
# For development and test.
#
app_develop()
{
    config_read
    echo "$DBW_HOST"
    :
}



#
# Always have a main
# 
main()
{
    configuration_load

    # Parse incoming options and arguments
    while (( $# )); do
        case "$1" in
            --no-main)
                exit 0
            ;;

            --dry)
                readonly OPTION_DRY=1
                shift
            ;;

            --force | -f)
                #readonly OPTION_FORCE=1
                shift
            ;;

            --help | -h)
                usage
                exit 0
            ;;

            --silent)
                readonly OPTION_SILENT=1
                shift
            ;;

            --source)
                shift
                readonly OPTION_SOURCE=$1
                shift
            ;;

            --source-bin)
                shift
                readonly OPTION_SOURCE_BIN=$1
                shift
            ;;

            --target)
                shift
                readonly OPTION_TARGET=$1
                shift
            ;;

            --version | -v)
                version
                exit 0
            ;;

            --verbose)
                readonly OPTION_VERBOSE=1
                shift
            ;;

            --very-verbose)
                readonly OPTION_VERBOSE=1
                readonly OPTION_VERY_VERBOSE=1
                shift
            ;;

            check       | \
            commands    | \
            config      | \
            develop     | \
            help        | \
            repo        | \
            selfupdate    )
                if [[ ! $COMMAND ]]; then
                    COMMAND=$1
                else
                    ARGS+=("$1")
                fi
                shift
            ;;

            -*)
                bad_usage "Unknown option '$1'."
                exit 1
            ;;

            *)
                if [[ (! $COMMAND) && $( repo_command_exists "$1" ) ]]; then
                    COMMAND=$1
                    shift
                elif [[ ! $COMMAND ]]; then
                    bad_usage "Unknown command '$1'."
                    exit 1
                else
                    ARGS+=("$1")
                    shift
                fi
            ;;
        esac
    done



    # Execute the command 
    if type -t app_"$COMMAND" | grep -q function; then
        app_"$COMMAND"
    elif [[ $( repo_command_exists "$COMMAND" ) ]]; then
        repo_command_execute "$COMMAND"
    else
        bad_usage "Missing option or command."
        exit 1
    fi
}



main "$@"
