#!@BASH@ --noprofile

PATH='/bin:/usr/bin'
unset CDPATH	# unset CDPATH, otherwise 'cd' prints the directoy!
unset IFS	# use default IFS

shopt -s nullglob
shopt -s extglob

# used for some output only
declare -r  CMD='modmanage'

declare     mydir=$(cd $(dirname "$0") && pwd)
declare     prefix=$(dirname "${mydir}")
declare     libdir="${prefix}/lib"
declare     libexecdir="${prefix}/libexec"
declare -r  bindir="${prefix}/bin"

source "${libdir}/libstd.bash"
source "${libdir}/libpmodules.bash"

_exit () {
	std::die 1 "\nInterrupted..."
}
trap '_exit' INT TERM

_err () {
	std::info "\nOops: got an error in function '${FUNCNAME[1]}', line ${BASH_LINENO[0]}"
	std::die 1 "Aborting ..."
}
trap '_err' ERR

path="/bin:/usr/bin:${bindir}"
[[ $(uname -s) == 'Darwin' ]] && path+=":${libexecdir}" 
std::def_cmds "${path}" 'chown' 'dirname' 'mkdir' 'rsync' 'rm' 'getopt' 'find' 'modulecmd'

unset mydir
unset prefix
unset libdir
unset libexecdir
# bindir we still need

declare PMODULES_VERSION='@PMODULES_VERSION@'

##############################################################################
#
# help [module|sub-command]
#
Subcommands[help]='help'
Options[help]='-o hHV\? -l version -l help'
Help[help]='
USAGE:
        modmanage [switches] subcommand [subcommand-args]...

SWITCHES:
        -h|-H|-?|--help         this usage info
        -V|--version            modules version & configuration options
        --debug                 enable debug output
	--dry-run               dry run

SUBCOMMANDS:
        + init [switches]       TARGET_DIR
        + install [switches]    module...
	+ search [switches]	[string|pattern]...
        + help                  [subcommand]
'

subcommand_help() {
        local -r subcommand='help'
	local -a args=()
	while (( $# > 0 )); do
		case $1 in
			-h | -\? | -H | --help )
				print_help "${subcommand}"
				;;
			-V | --version )
				print_help 'version'
				;;
			-- )
				shift 1
				args+=( "$@" )
				break
				;;
			* )
				args+=( "$1" )
				;;
		esac
		shift
	done
	for arg in "${args[@]}"; do
        	if [[ -n "${Help[${arg}]}" ]] ; then
                	print_help "${arg}"
                else
			std::die 1 "Unknown sub-command -- ${subcommand}"
		fi
	done
}

##############################################################################
#
# Derive the module installation path from the modulefile path.
# The passed modulefile must be absolute.
# 
# Arguments:
#	$1: absolute module file path
#
get_module_prefix() {
	"${modulecmd}" bash show "$1"  2>&1 \
		|awk '/_HOME |_PREFIX / {print $3; exit}'
}

##############################################################################
#
# Derive the module release-file path from the module file-path.
#
# Arguments:
#	$1: module file-path
#
get_releasefile_name() {
	echo "$(${dirname} "$1")/.release-$(basename "$1")"
}

##############################################################################
#
# Sync a module from one Pmodules environment to another:
# - sync module installation
# - sync modulefile
# - sync release file
#
# Arguments:
#	$1: relative modulefile path (something like: Tools/gnuplot/5.0.0)
#	$2: source prefix of Pmodule environment
#	$3: target prefix of Pmodule environment
#
sync_module() {
	local -r rel_modulefile="$1"
	local -r src_root="$2"
	local -r target_root="$3"

	local -r src_prefix=$( get_module_prefix "${src_root}/${rel_modulefile}" )
	local -r rel_prefix=${src_prefix#${src_root}/}
	local -r target_prefix="${target_root}/${rel_prefix}"

	# install/update module
	if [[ ! -d "${target_prefix}" ]] || [[ "${force}" == 'yes' ]]; then
		${mkdir} -p "${target_prefix}" || exit $?
		${rsync} --links --perms --recursive --delete \
			 "${src_prefix}/" \
			 "${target_prefix}/" || exit $?
	fi

	# create modulefile direcrory and install modulefile
	local -r src_modulefile="${src_root}/${rel_modulefile}"
	local -r target_modulefile="${target_root}/${rel_modulefile}"
	
	${mkdir} -p "$(${dirname} "${target_modulefile}")" || exit $?

	if [[ -e "${src_modulefile}" ]]; then
	        ${rsync} --links --perms --recursive \
			 "${src_modulefile}" "${target_modulefile}" || exit $?
	fi

	# install release-file
	local -r rel_releasefile=$( get_releasefile_name "${rel_modulefile}" )
	local -r src_releasefile="${src_root}/${rel_releasefile}"
	local -r target_releasefile="${target_root}/${rel_releasefile}"

	if [[ -e "${src_releasefile}" ]]; then
	        ${rsync} --links --perms --recursive \
			  "${src_releasefile}" "${target_releasefile}" || exit $?
	fi

}

##############################################################################
#
# initialize a new module environment
#
# 
#
Subcommands[init]='init'
Options[init]='-o \?hfy -l src: -l user: -l help -l force -l yes'
Help[init]="
USAGE:
        modmanage init [switches] TARGET_DIR
                Initialize a new minimal Pmodule environment in TARGET_DIR.
                A user must be specified with '--user=<USER>' if the 
                programm is executed as root.

SWITCHES:
	--user <USER>
		If this scripts runs with root privileges, a user name
		ore ID must be specified.
        --force|--yes|-f|-y
                re-initialise an already existing Pmodule environment.
"
 		
subcommand_init() {
	check_env() {
		[[ -n "${PMODULES_ROOT}" ]] &&
			[[ -d "${PMODULES_ROOT}" ]] &&
			[[ -n "${PMODULES_HOME}" ]] &&
			[[ -n "${PMODULES_VERSION}" ]] || \
				std::die 1 "
Error: the module environment you are going to use as source has not been
initialized properly!"
	}

	#.....................................................................
	#
	# Sync the Pmodules configuration and templates
	#
	# Arguments:
	#	$1: source prefix of Pmodule environment
	#	$2: target prefix of Pmodule environment
	#
	sync_config() {
		src="$1/${PMODULES_CONFIG_DIR}/"
		dst="$2/${PMODULES_CONFIG_DIR}/"
		${rsync} --links --perms \
			 "${src}"/profile.{bash,csh,zsh} "${dst}" \
			|| return $?
		${rsync} --links --perms \
			 "${src}"/profile.{bash,csh,zsh}-"${PMODULES_VERSION}" "${dst}" \
			|| return $?
		${rsync} --links --perms \
			 "${src}/Pmodules.conf" "${dst}" \
			|| return $?
		${rsync} --links --perms \
			 "${src}/modbuild.conf" "${dst}" \
			|| return $?
	}

	local target_root=()
	local user=''
	while (($# > 0)); do
		case $1 in
			-h | -H | -\? | --help | -help )
				print_help "${subcommand}"
				;;
			--force | --yes | -f | -y )
				force='yes'
				;;
			--user | --user=* )
				if [[ "$1" == '--user' ]]; then
					user="$2"
					shift
				else
					user="${1#--*=}"
				fi
				;;
			-- )
				:
				;;
			* )
				# assign and remove trailing slashes
				target_root="${1%%*([\/])}"
				;;
		esac
		shift
	done
	if [[ -z ${target_root} ]]; then
		std::die 1 "Error: no target directory specified!"
	fi

	local -i euid=$(id -u)
	if (( euid == 0 )); then
	        [[ -n "${user}" ]] || \
		    std::die 1 "Error: --user parameter is required!"
		id -u "${user}" > /dev/null 2>&1 || \
		    std::die 1 "Error: Unable to retrieve user id of user '${user}'"
	else
		[[ -z "${user}" ]] || \
		    std::die 1 "Error: --user option is only allowed if running as root!"
	fi

	local src_root="$(std::get_abspath "${bindir}/../../../..")"
	local config_file="${src_root}/${PMODULES_CONFIG_DIR}/profile.bash"
	if [[ -r "${config_file}" ]]; then
		source "${config_file}"
	fi
	check_env || \
		std::die 1 "Giving up..."

	echo "Creating a minimal Pmodule environment from the environment at"
	echo "    ${PMODULES_ROOT}"
	echo "in"
	echo "    ${target_root}"
	if [[ -d "${target_root}" ]]  && [[ ${force} == no ]]; then
		echo "Warning: ${target_root} already exists."
		std::get_YN_answer \
			"Do you really want to re-run the initialization? (y/N) " \
			|| std::die 1 "Abort ..."
	fi
	force='yes'
	echo "Creating target directory '${target_root}'..."
	${mkdir} -p "${target_root}" || \
		std::die 1 "Error: make directory failed!"

	echo "Syncing configuration ..."
	sync_config "${src_root}" \
		    "${target_root}" || \
		std::die 1 "Error: configuration synchronization failed!"
	
	echo "Syncing Pmodules ${PMODULES_VERSION} from '${src_root}' to '${target_root}'..."
	sync_module "Tools/${PMODULES_MODULEFILES_DIR}/Pmodules/${PMODULES_VERSION}" \
		    "${src_root}" \
		    "${target_root}" || \
		std::die 1 "Error: sync Pmodules failed!"

	for d in "${src_root}"/*/${PMODULES_MODULEFILES_DIR}; do
		${mkdir} -p "${target_root}/${d#${src_root}/}"
	done
	
	if [[ -n "${user}" ]]; then
	        echo "Changing user of new module environment to '${user}'..."
                ${chown} -R "${user}" "${target_root}" || \
			std::die 1 "Error: changing owner failed!"
                echo
	fi
	echo "SourceRoot=${src_root}" > "${target_root}/${PMODULES_CONFIG_DIR}/modmanage.conf"
	echo "New minimal module environment created at '${target_root}'."
	echo "To use this environment, execute"
	echo "   sudo ln -fs ${target_root} /opt/psi"
	echo "   source /opt/psi/${PMODULES_CONFIG_DIR}/profile.bash"

}


##############################################################################
#
# sub-command 'install'
# 
# Arguments:
Subcommands[install]='install'
Options[install]='-o hf -l force -l with: -l help -l src: -l target:'
Help[install]='
USAGE:
        modmanage install [switches] <module>...
                Install modules

SWITCHES:
	--force] | -f
		Install module even it already exists

	--src <src>
		Install from module environment in <src>

	--with <module>
		Install module(s) in this sub-group only

	<module_pattern>
		Install modules matching given pattern
'

subcommand_install() {
	local -r    subcommand='install'
	local	    opts=''
	local -a    with=()
	local -a    module_pattern=()
	local	    src_root="${PMODULES_INSTALL_SOURCE}"
	local       target_root="${PMODULES_ROOT}"
	local	    modulefile=''
	local -A    modules_to_install
	local -A    dependencies_to_install
	local -A    group_map
	local -a    modulepath=()

	#......................................................................
	#
	set_initial_modulepath() {
		local group
		for  group in "${!GroupDepths[@]}"; do
			(( ${GroupDepths[${group}]} == 0 )) || continue
			modulepath+=( "${src_root}/${group}/${PMODULES_MODULEFILES_DIR}" )
		done
	}

	#......................................................................
	#
	create_group_map() {
		#
		# For the dependency resolution we need to know, whether a
		# module - if loaded - adds a hierarchical group to MODULEPATH
		# or not.
		# 
		# Examples:
		#     Loading a compiler adds the hierarchical group for
		#     this compiler. The command
		#         module load gcc/10.3.0
		#     prepends 
		#         <pmodules_root>/Compiler/modulefiles/gcc/10.3.0
		#     to MODULEPATH.
		#
		# The dependency files do not convey the information whether
		# loading a module extends MODULEPATH or not. What we need to
		# know is
		# 1) does loading a specific module extends MODULEPATH?
		# 2) if yes: what is the hierarchical group?
		# 
		# Example:
		#     If we know that loading 'gcc/10.3.0' adds a directory 
		#     in the hierarchical group 'Compiler' to MODULEPATH, we
		#      know that this directory is
		#         <pmodules_root>/Compiler/modulefiles/gcc/10.3.0
		#
		# This information we store in the dictionary 'group_map'.
		# For concinience reasons we store the string 'src_root/group'.
		# So, 'group_map' maps
		#     module/version -> src_root/group
		#
		# Example:
		#     group_map[gcc/10.3.0]="${src_root}/Compiler"
		#
		local group=''
		for group in "${!GroupDepths[@]}"; do
			(( ${GroupDepths[${group}]} > 0 )) || continue
			local fname=''
			while read fname; do
				local -a parts=()
				std::split_relpath parts "${fname}"
				if (( ${#parts[@]} - 2 != ${GroupDepths[${group}]} )); then
					std::warn "error in source group ${group}:"
					std::warn "modulefile: ${fname}"
					continue
				fi
				if [[ ${#parts[@]} < 4 ]]; then
					echo "${group} ${parts[@]}"
				fi
				local key="${parts[-4]}/${parts[-3]}"
				[[ -z "${group_map[${key}]}" ]] || continue
				group_map[${key}]="${src_root}/${group}"
			done < <(${find} -L "${src_root}/${group}/${PMODULES_MODULEFILES_DIR}" \
					 \( -type l -o -type f \) \
					 \! -name ".*" \
					 -printf "%P\n" \
				)
		done
	}
	
	#......................................................................
	#
	# Resolve dependencies to given module
	#
	# Arguments:
	#   $1	absolute module file name
	#
	# Notes:
	#   Following variables from the enclosing function are used:
	#	modulepath
	#	group_map (read-only)
	#
	resolve_dependencies () {
	 	local -r modulefile="$1"

		local -- prefix=$(get_module_prefix "${modulefile}")
		local -a rdeps=()
		local -- rdeps_file="${prefix}/.dependencies"
		local -a ideps=()
		local -- ideps_file="${prefix}/.install_dependencies"
		
		if [[ -r "${rdeps_file}" ]]; then
			mapfile -t rdeps < <(grep -v '^ *#' "${rdeps_file}" )
		fi
		if [[ -r "${ideps_file}" ]]; then
			mapfile -t ideps < <(grep -v '^ *#' "${ideps_file}" )
		fi

		# loop over all dependecies
		local dep
		for dep in "${rdeps[@]}" "${ideps}"; do
			[[ -n ${dep} ]] || continue
			# search module with current modulepath
			local modulename=$(${find} "${modulepath[@]}" -path "*/${dep}" \
						   -print -quit 2>/dev/null)
			[[ -n ${modulename} ]] || \
				std::die 3 "Oops: required module '${dep}' not found!"

			local rel_modulename="${modulename#${src_root}/}"
			dependencies_to_install[${rel_modulename}]='.'
			resolve_dependencies "${modulename}"
			[[ -v group_map[${dep}] ]] || continue
			local dir="${group_map[${dep}]}"     # = ${src_root}/<group>
			dir+="/${rel_modulename##+([!/])/}"  # += rel.name with group removed
			modulepath+=( "${dir}" )
		done
	}

	#......................................................................
	#
	# Print list of modules which will be installed and ask user wheter
	# he wants to continue or abort.
	#
	# Arguments:
	#   none
	#
	# Notes:
	#   Following variables from the enclosing function are used:
	#	target_root (read-only)
	#	modules_to_install (read-only)
	#	dependencies_to_install (read-only)
	#
	print_modules() {
		local modulefile
		for modulefile in "$@"; do
			local -a parts
			std::split_relpath parts "${modulefile}"
			local s=''
			if (( ${#parts[@]} >= 6 )); then
				s="(${parts[2]}/${parts[3]}"
				for ((i = 4; i < ${#parts[@]}-2; i+=2)); do
					s+=" ${parts[i]}/${parts[i+1]}"
				done
				s+=')'
			fi
			std::info "%-20s %s" "${parts[-2]}/${parts[-1]}" "$s"
		done 2>&1 | sort
	}
	print_modules_to_install() {
		std::info "The following modules will be installed/updated:"
		print_modules "${!modules_to_install[@]}"
		if (( ${#dependencies_to_install[@]} > 0 )); then
			std::info "\nThe following dependencies will be installed/updated:"
			print_modules "${!dependencies_to_install[@]}"
		fi
		std::info ""
		std::get_YN_answer "Do you want to continue? [n] " || \
			std::die 1 "Aborting..."
		std::info ""
	}
	
	while (($# > 0)); do
		case $1 in
			-h | -H | -\? | --help | -help )
				print_help "${subcommand}"
				;;
			--force | -f )
				force='yes'
				;;
			--src | --src=*)
                                if [[ $1 == --src ]]; then
                                        src_root="$2"
                                        shift
				else
                                        src_root="${1#--*=}"
                                fi
				;;
			--target | --target=*)
                                if [[ $1 == --target ]]; then
                                        target_root="$2"
                                        shift
				else
                                        target_root="${1#--*=}"
                                fi
				;;
			--with | --with=* )
                                if [[ "$1" == --with ]]; then
                                        with+=( "$2" )
                                        shift
                                else
                                        with+=( "${1#--*=}" )
                                fi
				;;
			-- )
				:
				;;
			* )
				module_pattern+=( "$1" )
				;;
		esac
		shift
	done

	if [[ -z ${src_root} ]]; then
		local conf_file="${PMODULES_ROOT}/${PMODULES_CONFIG_DIR}/modmanage.conf"
		if [[ -r ${conf_file} ]]; then
			source "${conf_file}"
			src_root="${SourceRoot}"
		fi
	fi
	[[ -n ${src_root} ]] \
		|| std::die 3 "Oops: no installation source given."
	[[ -d ${src_root} ]] \
		|| std::die 3 "Oops: '${src_root}' is not a valid installation source."

	source "${src_root}/${PMODULES_CONFIG_DIR}/profile.bash"
	scan_groups "${src_root}"
	set_initial_modulepath
	create_group_map

	# search for to be installed modules and their dependencies
	while read modulefile; do
		modules_to_install["${modulefile#${src_root}/}"]+='.'
		resolve_dependencies "${modulefile}"
	done < <("${modulecmd}" bash search \
				 "${module_pattern[@]}" \
				 "${with[@]/#/--with=}" \
				 -a --glob \
				 --no-header --print-modulefiles \
				 --src="${src_root}"  2>&1 1>/dev/null)
	(( ${#modules_to_install[@]} == 0 )) && \
		std::die 0 "No matching modules found ..."
	print_modules_to_install

	# install/update ...
	for modulefile in "${!modules_to_install[@]}" "${!dependencies_to_install[@]}"; do
		std::split_relpath parts "${modulefile}"
		std::info "    ${parts[-2]}/${parts[-1]}"
		sync_module "${modulefile}" "${src_root}" "${target_root}"
	done
	std::info "\nDone!\n"
} # subcommand_install

##############################################################################
#
# sub-command 'search'
# 
Subcommands[search]='search'
Options[search]='-o \?h -l with: -l help -l all-dep -l wrap -l glob -l src:'
Help[install]='
USAGE:
        modmanage search [switches] <string>...
                search modules

SWITCHES:
	--src <src>
		Search modules in environment <src>.
		Default is the source defined in modmanage.conf.

	--with <module>
		Search module(s) in this sub-group.

	<string>
		Search modules matching given string.

	<pattern>
		Search modules matching given shell glob-pattern.
'

subcommand_search() {
	local -a args=()
	while (($# > 0)); do
		case $1 in
			-h | -H | -\? | --help | -help )
				print_help "${subcommand}"
				;;
			--src | --src=*)
                                if [[ $1 == --src ]]; then
                                        src_root="$2"
                                        shift
				else
                                        src_root="${1#--*=}"
                                fi
				;;
			--with | --with=* )
                                if [[ "$1" == --with ]]; then
                                        args+=( '--with' "$2" )
                                        shift
                                else
                                        args+=( "$1" )
                                fi
				;;
			--all-deps | --glob | --wrap )
				args+=( "$1" )
				;;
			-- )
				:
				;;
			* )
				args+=( "$1" )
				;;
		esac
		shift
	done
	if [[ -z ${src_root} ]]; then
		local conf_file="${PMODULES_ROOT}/${PMODULES_CONFIG_DIR}/modmanage.conf"
		if [[ -r ${conf_file} ]]; then
			source "${conf_file}"
			src_root="${SourceRoot}"
		fi
	fi
	[[ -n ${src_root} ]] \
		|| std::die 3 "Oops: no installation source given."
	[[ -d ${src_root} ]] \
		|| std::die 3 "Oops: '${src_root}' is not a valid installation source."
	${modulecmd} bash search --src="${src_root}" --all-release-stages \
		     "${args[@]}" 2>&1 1>/dev/null
}

declare force='no'
declare subcommand=''
declare -a opts=()
while (($# > 0)); do
	case $1 in
		-h | -H | -\? | --help | -help )
			print_help 'help'
			;;
		-V | --version )
			print_help 'version'
			;;
		--debug )
			set -x
			;;
		--dry-run )
			chown="echo ${chown}"
			mkdir="echo ${mkdir}"
			rsync="echo ${rsync}"
			;;
		-* )
			opts+=( "$1" )
			;;
		* )
			subcommand="$1"
			shift
			break
			;;
	esac
	shift || :
done

if [[ -z "${subcommand}" ]]; then
	std::die 1 "${CMD}: no sub-command specified.\n"
	print_help 'help'
fi

if [[ -z "${Subcommands[${subcommand}]}" ]]; then
	std::die 1 "${CMD}: unknown sub-command -- ${subcommand}\n"
fi

if [[ "${subcommand}" != "init" ]] && [[ -z "${PMODULES_ROOT}" ]]; then
        std::die 1 "Error: No current module environment configured!"
fi

tmp=$("${getopt}" --name="${CMD}" ${Options[${subcommand}]} -- "${opts[@]}" "$@" ) \
        || print_help "${subcommand}"
eval args=( "$tmp" )
unset tmp

umask 022

subcommand_${Subcommands[$subcommand]} "${args[@]}"

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