#!/usr/bin/env bash
#

declare -rx VERSION='1.1.23'
declare -rx BASH5_VERSION='5.2.32'
declare -rx TCL_VERSION='8.6.14'
declare -rx TCLLIB_VERSION='1.21'
declare -rx MODULES_VERSION='3.2.10.2'
declare -rx LMOD_VERSION='8.7'

# for macOS only
declare -rx GETOPT_VERSION='1.1.6'
declare -rx FINDUTILS_VERSION='4.9.0'


if (( "${BASH_VERSINFO[0]}" < 5 )); then
	echo "BASH version 5.0 or newer is required and must be available in PATH!" 1>&2
	exit 1
fi

set -o nounset
set -o pipefail
shopt -s nullglob

declare -r BOOTSTRAP_DIR="$(cd "$(dirname "$0")" && pwd -P)"
declare -r SRC_DIR="${BOOTSTRAP_DIR}/Pmodules"

declare -r  PMOD_DIR="Tools/Pmodules/${VERSION}"
# config directory and file relative to install root
declare -rx CONFIG_DIR='config'
declare -rx CONFIG_FILE="${CONFIG_DIR}/Pmodules.yaml"

# directory where the required tools will be installed (like bash, tclsh, etc)
declare -rx UTILBIN_DIR='libexec'

# defaults
declare -rx DEFAULT_INSTALL_ROOT='/opt/psi'
declare -rx DEFAULT_DISTFILES_DIR='var/distfiles'
declare -rx DEFAULT_TMP_DIR='var/tmp/${USER}'

std::log() {
        local -ri fd=$1
        local -r fmt="$2"
        shift 2
        printf -- "${fmt}" "$@" 1>&$fd
        printf -- "\n" 1>&$fd
}

std::info() {
        std::log 2 "$1" "${@:2}"
}

std::error() {
        std::log 2 "$1" "${@:2}"
}

std::debug() {
        [[ -v PMODULES_DEBUG ]] || return 0
        std::log 2 "$@"
}

std::die() {
        local -ri ec=$1
        shift
        if [[ -n $@ ]]; then
                local -r fmt=$1
                shift
                std::log 2 "$fmt" "$@"
        fi
        exit $ec
}
std::parse_yaml() {
	#
	# parse a YAML file
	# See: https://gist.github.com/pkuczynski/8665367
	#
	local -r fname="$1"
	local -r prefix="$2"
	local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
	sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \
            -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" "${fname}" |
		awk -F$fs '{
		      indent = length($1)/2;	
		      vname[indent] = $2;	
		      for (i in vname) {
                          if (i > indent) {delete vname[i]}
                      }
		      if (length($3) > 0) {
		          vn="";
                          for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
		          printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
	              }
                }'
}

#-----------------------------------------------------------------------------
#
read_config_file() {
	local fname="$1"
	if [[ ! -r "${fname}" ]]; then
		std::die 1 "Configuration file '${fname}' does not exist or is not readable!"
	fi

	eval $(std::parse_yaml "${fname}" '') || \
		std::die 1 "Cannot read configuration file '${fname}'"

	declare -xg INSTALL_ROOT="${Overlays_base_install_root}"
	if [[ -z "${INSTALL_ROOT}" ]]; then
		std::die 1 "Error in configuration file '${fname}': install root not defined!"
	fi
	declare -xg PREFIX="${INSTALL_ROOT}/${PMOD_DIR}"
	declare -xg DOWNLOADS_DIR="${DistfilesDir:-${INSTALL_ROOT}/${DEFAULT_DISTFILES_DIR}}"
	declare -xg TMP_DIR="${TmpDir:-${INSTALL_ROOT}/${DEFAULT_TMP_DIR}}"
}

#-----------------------------------------------------------------------------
# The next functions are used in the sub-commands, if an illegal option
# or argument has been passed.
#
illegal_option(){
	local subcmd="$1"
	local opt="$2"
	std::die 1 \
		 "%s: %s -- %s" \
		 "$(basename $0) ${subcmd}" \
		 "Illegal option" \
		 "${opt}"
}

illegal_arg(){
	local subcmd="$1"
	local arg="$2"
	std::die 1 \
		 "%s: %s -- %s" \
		 "$(basename $0) ${subcmd}" \
		 "Illegal argument" \
		 "${arg}"
}

#-----------------------------------------------------------------------------
# help for sub-command 'help' (usage)
build::help_help(){
	local prog="$(basename "$0")"
	echo "
Usage: ${prog} help|configure|compile|install

This script must be used to
- Bootstrap/configure a new Pmodules environment,
- compile and install the required tools
- install a new Pmodules version

Run

    ${prog} help configure|compile|install

or

    ${prog} configure|compile|install --help

to get help for a specific sub-command.
"
	std::die 1 ""
}

#-----------------------------------------------------------------------------
# sub-command 'help'
#
# print help for sub-commands
#
build::help() {
	if (( $# == 0 )); then
		build::help_help
	else
		case $1 in
			configure|compile|install )
				build::help_$1
				;;
			help )
				build::help_help
				;;
			-* )
				illegal_option 'help' "$1"
				;;
			* )
				std::error "No such command -- $1"
				build::help_help
				;;
		esac
	fi
}

#-----------------------------------------------------------------------------
# help for sub-command 'configure'
#
build::help_configure() {
	echo "
Usage: $(basename $0) configure [OPTION...]

Configure and setup a new Pmodules environment. You need permissions
to write to the installation root.

Options:
--install_root=DIR
	Root of the Pmodules environment installation. Everything will be
	installed in a directory hierarchy with 'DIR' as prefix.
       	The default is '${DEFAULT_INSTALL_ROOT}'.

--distfilesdir=DIR
	Directory where downloaded files are stored.
	The default is '${DEFAULT_INSTALL_ROOT}/${DEFAULT_DISTFILES_DIR}' in the 
	Pmodules root directory.

--tmpdir=DIR
	Directory for temporary files.
	The default is '${DEFAULT_INSTALL_ROOT}/${DEFAULT_TMP_DIR}'

--force|-f
	Override existing configuration.

--help|-h|-?
	Print this help text.

" 1>&2
	std::die 1 ""
}

#-----------------------------------------------------------------------------
# sub-command 'configure'
#
# Create basic directory hierachy and the configuration file in the given
# installation root directory
#
build::configure() {
	local opt_force='no'

	while (( $# > 0 )); do
		case "$1" in
			--install_root | --install_root=* )
				if [[ $1 == *=* ]]; then
					INSTALL_ROOT="${1#*=}"
				else
					INSTALL_ROOT="$2"
					shift 1
				fi
				;;
			--distfilesdir )
				if [[ $1 == *=* ]]; then
					DOWNLOADS_DIR="${1#*=}"
				else
					DOWNLOADS_DIR="$2"
					shift 1
				fi
				;;
			-f | --force )
				opt_force='yes'
				;;
			--tmpdir | --tmpdir=* )
				if [[ $1 == *=* ]]; then
					TMP_DIR="${1#*=}"
				else
					TMP_DIR="$2"
					shift 1
				fi
				;;
			--help | -h | -\? )
				build::help_configure
				;;
			-* )
				illegal_option 'configure' "$1"
				;;
			* )
				illegal_arg 'configure' "$1"
				;;
		esac
		shift 1
	done
	: ${INSTALL_ROOT:=${DEFAULT_INSTALL_ROOT}}
	: ${DOWNLOADS_DIR:=${INSTALL_ROOT}/${DEFAULT_DISTFILES_DIR}}
	: ${TMP_DIR:=${INSTALL_ROOT}/${DEFAULT_TMP_DIR}}
	PREFIX="${INSTALL_ROOT}/${PMOD_DIR}"

	#---
	# check/create the install root
	if [[ ! -d ${INSTALL_ROOT} ]]; then
		std::info "%s\n%s" \
			  "The root directory '${INSTALL_ROOT}' does not exist!" \
			  "Trying to create it..."
		if ! mkdir -p "${INSTALL_ROOT}"; then
			std::die 1 "%s\n%s" \
				 "Creating the root directory failed!" \
				 "Aborting..."
		fi
	fi
	if [[ ! -w ${INSTALL_ROOT} ]]; then
		std::die 1 "%s\n%s" \
			 "The root directory '${INSTALL_ROOT}' is not writable!" \
			 "Aborting..."
	fi

	#---
	# check/create YAML config file in install root
	mkdir -p "${INSTALL_ROOT}/${CONFIG_DIR}" || \
		std::die 1 "Aborting..."

	local config_file="${INSTALL_ROOT}/${CONFIG_FILE}"
	if [[ "${opt_force}" != 'yes' ]] && [[ -e "${config_file}" ]]; then
		std::die 1 "%s\n%s" \
			 "The Pmodules environment in '${INSTALL_ROOT}' has already been configured!" \
			 "Use the option --force to override. Aborting..."
	fi

	sed_cmd="s:@INSTALL_ROOT@:${INSTALL_ROOT}:g;"
	sed_cmd+="s:@PMODULES_DISTFILESDIR@:${DOWNLOADS_DIR}:g;"
	sed_cmd+="s:@PMODULES_TMPDIR@:${TMP_DIR}:g;"
        sed_cmd+="s:@PMODULES_VERSION@:${VERSION}:g"
        
	sed "${sed_cmd}" "${BOOTSTRAP_DIR}/${CONFIG_FILE}.in" \
		> "${config_file}" || \
		std::die 1 "Cannot create configuration file in Pmodules root\nAborting..."

	#---
	# create basic directories
	install -d -m 0755 \
		"${INSTALL_ROOT}/Tools/modulefiles/Pmodules" \
		"${INSTALL_ROOT}/Libraries/modulefiles" \
		"${INSTALL_ROOT}/Programming/modulefiles" \
		"${DOWNLOADS_DIR}" || \
		std::die 1 "%s" \
			 "Creating basic directories failed\n" \
			 "Aborting..."

	#---
	echo "Configuration:"
	echo "  root of Pmodules environment: ${INSTALL_ROOT}"
	echo "  Pmodule prefix:               ${PREFIX}"
	echo "  tmp directory:                ${TMP_DIR}"
	echo "  store for downloaded files:   ${DOWNLOADS_DIR}"
	echo "Done..."
}

#-----------------------------------------------------------------------------
# help for sub-command 'compile'
#
build::help_compile() {
	echo "
Usage: $(basename $0) compile [OPTION...]

Compile and install the required tools for a new Pmodules environment.
ou need the permissions to write to the installation root.

Options:
--install_root=DIR
	Root of the Pmodules environment installation. Everything will be
	installed in a directory hierarchy with 'DIR' as prefix.
       	The default is '${DEFAULT_INSTALL_ROOT}'.

--disable-cleanup
	Do not cleanup the tmp directory after compilation and installation.

--help
	Print this help text.

" 1>&2
	std::die 1 ""
}

#-----------------------------------------------------------------------------
# sub-command 'compile'
#
# compile all required tools like bash, tclsh etc for a Pmodules module.
# The version is defined at the beginning of this file.
#
build::compile() {
	local opt_force='no'
	local opt_cleanup='yes'

	while (( $# > 0 )); do
		case $1 in
			--install_root | --install_root=* )
				if [[ $1 == *=* ]]; then
					INSTALL_ROOT="${1#*=}"
				else
					INSTALL_ROOT="$2"
					shift 1
				fi
				;;
			--disable-cleanup )
				opt_cleanup='no'
				;;
			--help | -h | -\? )
				build::help_compile
				;;
			-* )
				illegal_option 'compile' "$1"
				;;
			* )
				illegal_arg 'compile' "$1"
				;;
		esac
		shift 1
	done
	: ${INSTALL_ROOT:=${DEFAULT_INSTALL_ROOT}}
	PREFIX="${INSTALL_ROOT}/${PMOD_DIR}"

	read_config_file "${INSTALL_ROOT}/${CONFIG_FILE}"

	echo "Configuration:"
	echo "  root of Pmodules environment: ${INSTALL_ROOT}"
	echo "  Pmodule prefix:               ${PREFIX}"

	install -m 0755 -d "${PREFIX}"/{bin,init,lib,libexec} \

	for recipe in recipes/[0-9]*; do
		"./${recipe}" "${PREFIX}" || \
			std::die 1 "Oops"
	done
	if [[ "${opt_cleanup}" == 'yes' ]]; then
		rm -rf "${TMP_DIR}/*"
		rm -f  "${PREFIX}/lib/libtcl*.a"
		rm -rf "${PREFIX}/include"
	fi
	echo "Done..."
}

#-----------------------------------------------------------------------------
# help for sub-command 'install'
#
build::help_install() {
	echo "
Usage: $(basename $0) install [OPTION...]

Install a new Pmodules version.

Options:
--install_root=DIR
	Root of the Pmodules environment installation. Everything will be
	installed in a directory hierarchy with 'DIR' as prefix.
       	The default is '${DEFAULT_INSTALL_ROOT}'.

--debug
	Enable verbose/debug output.

--help|-h|-?
	Print this help text.

" 1>&2
	std::die 1 ""
}

#-----------------------------------------------------------------------------
# sub-command 'install'
#
# Install Pmodules files.
#
build::install() {
	while (( $# > 0 )); do
		case $1 in
			--debug )
				set -x
				;;
			--install_root | --install_root=* )
				if [[ $1 == *=* ]]; then
					INSTALL_ROOT="${1#*=}"
				else
					INSTALL_ROOT="$2"
					shift 1
				fi
				;;
			--help | -h | -\? )
				build::help_install
				;;
			-* )
				illegal_option 'install' "$1"
				;;
			* )
				illegal_arg 'install' "$1"
				;;
		esac
		shift 1
	done

	: ${INSTALL_ROOT:=${DEFAULT_INSTALL_ROOT}}
	PREFIX="${INSTALL_ROOT}/${PMOD_DIR}"

	read_config_file "${INSTALL_ROOT}/${CONFIG_FILE}"

	###
	#
	# begin installation
	#
	echo "Configuration:"
	echo "  root of Pmodules environment: ${INSTALL_ROOT}"
	echo "  Pmodule prefix:               ${PREFIX}"
	sed_cmd+="s:@PMODULES_VERSION@:${VERSION}:g;"
	sed_cmd+="s:@MODULES_VERSION@:${MODULES_VERSION}:g;"
	sed_cmd+="s:@VERSIONING@:#:g;"
	sed_cmd+="s:@BASH@:${PREFIX}/${UTILBIN_DIR}/bash:g;"
	sed_cmd+="s:@MODULECMD@:${PREFIX}/${UTILBIN_DIR}/modulecmd.bash:g;"
	sed_cmd+="s:@TCL_VERSION@:${TCL_VERSION%.*}:g;"

	sed "${sed_cmd}" "${SRC_DIR}/profile.bash.in" \
	    > "${INSTALL_ROOT}/${CONFIG_DIR}/profile.bash-${VERSION}"
	sed "${sed_cmd}" "${SRC_DIR}/profile.csh.in" \
	    > "${INSTALL_ROOT}/${CONFIG_DIR}/profile.csh-${VERSION}"
	sed "${sed_cmd}" "${SRC_DIR}/profile.zsh.in" \
	    > "${INSTALL_ROOT}/${CONFIG_DIR}/profile.zsh-${VERSION}"
	chmod 0644 "${INSTALL_ROOT}/${CONFIG_DIR}"/*-${VERSION}

	test -e "${INSTALL_ROOT}/${CONFIG_DIR}/profile.bash" || \
		install -m 0644 "$_-${VERSION}" "$_"

	test -e "${INSTALL_ROOT}/${CONFIG_DIR}/profile.csh" || \
		install -m 0644 "$_-${VERSION}" "$_"

	test -e "${INSTALL_ROOT}/${CONFIG_DIR}/profile.zsh" || \
		install -m 0644 "$_-${VERSION}" "$_"

	sed "${sed_cmd}" "${SRC_DIR}/modulecmd.in" \
            > "${PREFIX}/bin/modulecmd"
	chmod 0755 "${PREFIX}/bin/modulecmd"
	sed "${sed_cmd}" "${SRC_DIR}/modulecmd.bash.in" \
	    > "${PREFIX}/libexec/modulecmd.bash"
	chmod 0755 "${PREFIX}/libexec/modulecmd.bash"

	sed "${sed_cmd}" "${SRC_DIR}/libpmodules.bash.in" \
	    > "${PREFIX}/lib/libpmodules.bash"
	chmod 0755 "${PREFIX}/lib/libpmodules.bash"

        sed "${sed_cmd}" "${SRC_DIR}/modbuild.in" \
            > "${PREFIX}/bin/modbuild"
	chmod 0755 "${PREFIX}/bin/modbuild"

	test -e "${INSTALL_ROOT}/${CONFIG_FILE}" || \
		install -m 0644 "$_"			"${INSTALL_ROOT}/${CONFIG_DIR}"
	
	install -m 0755 "${SRC_DIR}/yq.$(uname -m)_$(uname -s)"	"${PREFIX}/libexec/yq"
	install -m 0644 "${SRC_DIR}/bash"		"${PREFIX}/init"
	install -m 0644 "${SRC_DIR}/bash_completion"	"${PREFIX}/init"
	install -m 0644 "${SRC_DIR}/csh"		"${PREFIX}/init"
	install -m 0644 "${SRC_DIR}/Pmodules.py"	"${PREFIX}/init"
	install -m 0644 "${SRC_DIR}/zsh"		"${PREFIX}/init"

	install -m 0644 "${SRC_DIR}/libpbuild.bash"	"${PREFIX}/lib"
	install -m 0644 "${SRC_DIR}/libstd.bash"	"${PREFIX}/lib"
	install -m 0755 -d				"${PREFIX}/lib/Pmodules"
	install -m 0644 "${SRC_DIR}/libmodules.tcl"	"${PREFIX}/lib/Pmodules"

	{
		PATH="${PREFIX}/${UTILBIN_DIR}:${PATH}"
		cd "${PREFIX}/lib/Pmodules"
		"${BOOTSTRAP_DIR}/mkindex.tcl"
	}

	install -m 0644 \
		"${SRC_DIR}/modulefile" \
		"${INSTALL_ROOT}/Tools/modulefiles/Pmodules/${VERSION}"

	echo "Done..."
}

#=============================================================================
#
declare subcmd=''
declare -a subcmd_args=()

while (( $# > 0 )); do
	case "$1" in
		--help | -h | -\? )
			usage
			;;
		--debug )
			set -x
			;;
		-* )
			std::die 1 "$1: illegal option"
			;;
		help | configure | compile | install )
			subcmd="$1"
			shift 1
			subcmd_args=( "$@" )
			shift $#
			break
			;;
		* )
			std::die 1 "Invalid sub-command '$1'.\n\nUse 'build --help' to get help..."
			;;
	esac
	shift 1
done

[[ -n "${subcmd}" ]] || std::die 1 "Missing sub-command.\n\nUse 'build --help' to get help..."

build::${subcmd} "${subcmd_args[@]}"

# Local Variables:
# mode: sh
# sh-basic-offset: 8
# tab-width: 8
# End:
