#!@BASH@
#
# The following build specific variables are set and used in libpbuild.bash:
#       ARGS
#       BUILD_SCRIPT
#       BUILDBLOCK_DIR
#
#.............................................................................

declare -r VERSION='@PMODULES_VERSION@'

unset CDPATH	# unset CDPATH, otherwise 'cd' prints the directoy!
unset IFS	# use default IFS

#set -e		# exit on error
set   -o pipefail
set   -o nounset
shopt -s nocaseglob
shopt -s extglob
shopt -s nullglob

# get absolute path of script
mydir=$(cd "$(/usr/bin/dirname "$0")" && pwd -P)
prefix=$(/usr/bin/dirname "${mydir}")

PATH="${prefix}/bin:${prefix}/libexec:/bin:/usr/bin:/sbin:/usr/sbin"
source "${prefix}/lib/libstd.bash" || {
        echo "Oops: cannot source library -- '$_'" 1>&2; exit 3;
}
source "${prefix}/lib/libpmodules.bash"  || \
        std::die 3 "Oops: cannot source library -- '$_'"
source "${prefix}/lib/libpbuild.bash"    || \
        std::die 3 "Oops: cannot source library -- '$_'"
unset mydir
unset prefix

##############################################################################
#
usage() {
	std::error "
USAGE:
        $0 [options..] [build_script] version

MANDATORY ARGUMENTS:

version
        Version of module to build.

SELECT VARIANT TO BUILD:

--system
        Specify the system for selecting a variants. Defaults to the
	OS version and release like 'rhel6'.

--with=P/V
        Select variant to compile. Use multiple '--with' arguments
	to make the selected variant unique.

BUILD-STEPS OPTIONS:

--clean-install
	Remove module if already exist before building.

--prep
	Prepare sources: unpack sources and apply patches only.

--configure
	Prepare and configure sources.

--compile
	Prepare, configure and compile everything.

--install
	Prepare, configure and compile everything. Finally run install
	step. Do not cleanup build and source directory.

--all
	Run throu all steps including cleanup.

-update-modulefiles
	Only install the modulefile and set the release.

MISCELLANEOUS OPTIONS:

-? | -h | --help
        Print usage.

-V | --version )
        Print version.

-v | --verbose )
        Verbose output.

--debug )
	Run in debug mode.

-j N | --jobs=N
        Run N parallel make jobs.

-f | --force-rebuild
        Force rebuild of module.

--dry-run
	Dry run.

--disable-cleanup-build
--disable-cleanup-src
--disable-cleanup
	Disable the cleanup of files in the build and/or source
	directory. Default is to remove all files in the build
	respective source directory at the beginning of a build
	and after a successful build.

--distdir
	Directory where to store and lookup downloaded files.

--tmpdir
        Directory used for building a module.

DOCUMENTATION:
	Full documentation is available at 
	http://pmodules.gitpages.psi.ch
"
	exit 1
}

##############################################################################
#
# parse options and arguments
#
# command line arguments are taken first
# then configuration file
# last default

# save arguments, required for building dependencies
declare -ra	ARGS=( "$@" )

# versions to be build, '.*' or none means all
declare         versions_to_build=''
declare         opt_build_target='all'
declare         opt_dry_run='no'
declare         opt_enable_cleanup_build='yes'
declare         opt_enable_cleanup_src='yes'
declare         opt_force_rebuild='no'
declare -i      opt_jobs=0
declare         opt_update_modulefiles='no'
declare         opt_system=''
declare         opt_verbose='no'
# array collecting all modules specified on the command line via '--with=module'
declare -a      opt_with_modules=()
declare -A	opt_with_dict=()
declare --	opt_config_file=''
declare --	opt_debug='no'
declare --	opt_check_mode='no'
declare --	opt_variant=''
declare --	opt_clean_install='no'
declare --	opt_parent_prefix=''

declare --	BUILD_SCRIPT=''
declare --      yaml_config_file=''
declare --	module_name=''
declare --	module_type='module'
declare --	echo=':'

parse_args() {
	#
	# The first argument ($1) is the build-script, if called in the
	# usual way:
	#     ./<build-script> <version> [options]
	# If called via:
	#     modbuild <build-script> [options]
	# the build-script MUST be passed as first argument.
	#
	(( $# == 0 )) && usage
	while (( $# > 0 )); do
		case $1 in
		-j )
                        opt_jobs="$2"
			shift
			;;
		--jobs=[0-9]* )
			opt_jobs="${1/--jobs=}"
			;;
		-v | --verbose )
			trap 'echo "$BASH_COMMAND"' DEBUG
                        opt_verbose='yes'
			echo='echo'
			;;
		--debug )
			opt_debug='yes'
			set -x
			;;
		-f | --force-rebuild )
                        opt_force_rebuild='yes'
			;;
		-\? | -h | --help )
			usage
			;;
                -V | --version )
                        std::die 0  "\nPmodules version ${VERSION}\nCopyright GNU GPL v2\n"
                        ;;
		--dry-run )
			opt_dry_run='yes'
			;;
		--disable-cleanup )
			opt_enable_cleanup_build='no'
			opt_enable_cleanup_src='no'
			;;
		--disable-cleanup-build )
			opt_enable_cleanup_build='no'
			;;
		--disable-cleanup-src )
			opt_enable_cleanup_src='no'
			;;
		--distdir | --distdir=* )
			if [[ $1 == *=* ]]; then
				PMODULES_DISTFILESDIR="${1/--distdir=}"
			else
				PMODULES_DISTFILESDIR="$2"
				shift
			fi
			;;
		--tmpdir | --tmpdir=* )
			if [[ $1 == *=* ]]; then
				PMODULES_TMPDIR="${1#--*=}"
			else
				PMODULES_TMPDIR="$2"
				shift
			fi
			;;
		--system | --system=* )
			if [[ $1 == *=* ]]; then
				opt_system="${1#--*=}"
			else
				opt_system="$2"
				shift
			fi
			;;
		--check-mode )
			opt_check_mode='yes'
			;;
		--use-flags | --use-flags=* )
			if [[ $1 == *=* ]]; then
				USE_FLAGS=":${1#--*=}:"
			else
				USE_FLAGS=":$2:"
				shift
			fi
			;;
		--with | --with=*/* )
			if [[ $1 == *=* ]]; then
				opt_with_modules+=( "${1#--*=}" )
				opt_with_dict[${1#--*=}]=0
			else
				opt_with_modules+=( "$2" )
				opt_with_dict[$2]=0
				shift
			fi
			;;
		--prep | --configure | --compile | --install | --all )
			opt_build_target=${1:2}
			;;
		--clean-install )
			opt_force_rebuild='yes'
			opt_clean_install='yes'
			;;
		--update-modulefiles )
			opt_update_modulefiles='yes'
			;;
		--config-file | --config-file=* )
			if [[ $1 == *=* ]]; then
				opt_config_file=( "${1#--*=}" )
			else
				opt_config_file=( "$2" )
				shift
			fi
			;;
		--variant | --variant=* )
			if [[ $1 == *=* ]]; then
				opt_variant=( "${1#--*=}" )
			else
				opt_variant=( "$2" )
				shift
			fi
			;;
		--parent-prefix | --parent-prefix=* )
			if [[ $1 == *=* ]]; then
				opt_parent_prefix="${1#--*=}"
			else
				opt_parent_prefix="$2"
				shift
			fi
			module_type='sub_package'
			pbuild.set_prefix "${opt_parent_prefix}"
			;;
                -- | '' )
                        :
                        ;;
		-* )
			std::die 1 "Invalid option -- '$1'"
			;;
		* )
			local -- arg="$1"
			if [[ -f "${arg}" && -x "${arg}" ]]; then
				BUILD_SCRIPT="$(std::get_abspath "${arg}")"
				BUILDBLOCK_DIR=$(dirname "${BUILD_SCRIPT}")
			elif [[ "${arg}" == */* ]]; then
				module_name="${arg%/*}"
				versions_to_build+=" ${arg#*/}"
			else
				versions_to_build+=" $1"
			fi
			;;
		esac
		shift
	done

	[[ -z "${BUILD_SCRIPT}" ]] && \
		std::die 1 "%s " \
			 "Build script argument missing?"

	# if no version is specified on the cmd-line, build all versions
	#(( ${#versions_to_build[@]} > 0)) || versions_to_build+=( '.*' )
	[[ -z ${versions_to_build} ]] && versions_to_build='.*'
	# set system if not set on the cmd-line
	opt_system="${opt_system:-$(std::get_os_release)}"

	# set config file
	yaml_config_file="${opt_config_file:-${BUILDBLOCK_DIR}/files/config.yaml}"
	[[ -r "${yaml_config_file}" ]] || \
		std::die 2 \
			 "%s -- %s" \
			 "YAML config file doesn't exist or is not readable" \
			 "${yaml_config_file}"
	std::info "Using YAML configuration file - ${yaml_config_file}"
}

get_yaml_file_fmt(){
	: "
	Get format version of configuration file. Print the version number
	to stdout if it is valid.
	"
	${yq} -e '.' "$1" &>/dev/null || \
		std::die 3 "%s -- %s" \
			 "Error in YAML config file, please check with linter" \
			 "$1"
	local -- fmt=''
	fmt=$(${yq} -e ".format" "$1") || \
		std::die 3 "Error reading config file format -- $1"
	case "${fmt}" in
		1 )
			:
			;;
		* )
			std::die 3 "Unknown YAML Pmodules config file format -- $1"
			;;
	esac
	echo "${fmt}"
}

read_yaml_config_file() {
	: "
  	Test whether the configuration file '$1' provides configurations
	for the module '$2'. If yes, the YAML block with the configuration
	is printed to stdout.
	"
	local -- file_name="$1"
	local -- module_name="$2"
	local -- yaml=''
	yaml=$( ${yq} -Ne e ".${module_name}" "${file_name}" 2>/dev/null ) || \
		std::die 3 "Configuration for '${module_name}' missing -- ${file_name}"
	echo "${yaml}"
}

build_modules(){
	local -- name="$1"
        local -- version="$2"
	shift 2
        local -a with_modules=( "$*" )


	if [[ "${opt_check_mode}" == 'yes' ]]; then
		if ! which yamllint > /dev/null 2>&1; then
			eval $( "${modulecmd}" bash load yamllint/1.28.0 )
		fi
		which yamllint > /dev/null 2>&1 || \
			std::die 3 "yamllint not found"
		yamllint "${yaml_config_file}"
	fi
	local -- file_fmt=''
	file_fmt=$(get_yaml_file_fmt "${yaml_config_file}")
	
	local -- yaml_mod_config=''
	yaml_mod_config=$(read_yaml_config_file "${yaml_config_file}" "${name}")
	case "${file_fmt}" in
		1 )
			build_modules_yaml_v1 \
				"${yaml_mod_config}" \
				"${name}" "${version}" \
				"${with_modules[@]}"
			;;
		* )
			std::die 255 "Oops!" # we should never be here!
			;;
	esac
}

#......................................................................
#
# Initialise environment modules.
#
# Arguments:
#	none
#
init_module_environment(){
	eval $( "${modulecmd}" bash use unstable )
	eval $( "${modulecmd}" bash use deprecated )
	eval $( "${modulecmd}" bash purge )

	# :FIXME: this is a hack!!!
	# shouldn't this be set in the build-script?
	if [[ -e "${PMODULES_HOME%%/Tools*}/Libraries" ]]; then
		eval $( "${modulecmd}" bash use Libraries )
	fi
	if [[ -e "${PMODULES_HOME%%/Tools*}/System" ]]; then
		eval $( "${modulecmd}" bash use System )
	fi
	unset	C_INCLUDE_PATH
	unset	CPLUS_INCLUDE_PATH
	unset	CPP_INCLUDE_PATH
	unset	LIBRARY_PATH
	unset	LD_LIBRARY_PATH
	unset	DYLD_LIBRARY_PATH

	unset	CFLAGS
	unset	CPPFLAGS
	unset	CXXFLAGS
	unset	LIBS
	unset	LDFLAGS

	unset	CC
	unset	CXX
	unset	FC
	unset	F77
	unset	F90
}

parse_version() {
	local v="$1"
	V="$1"
	USE_FLAGS=${USE_FLAGS:-''}

	local tmp=''

	if [[ "$v" =~ "_" ]]; then
		tmp="${v#*_}"
		USE_FLAGS+=":${tmp//_/:}:"
		v="${v%%_*}"
	fi
	V_PKG="${v%%-*}"		# version without the release number
	if [[ $v == *-* ]]; then
		V_RELEASE="${v#*-}"	# release number
	fi
	case "${V_PKG}" in
		*.*.* )
			V_MAJOR="${V_PKG%%.*}"
			tmp="${V_PKG#*.}"
			V_MINOR="${tmp%%.*}"
			V_PATCHLVL="${tmp#*.}"
			;;
		*.* )
			V_MAJOR="${V_PKG%.*}"
			V_MINOR="${V_PKG#*.}"
			;;
		* )
			V_MAJOR="${V_PKG}"
			;;
	esac

	VERSIONS=()
	if [[ -n ${V_RELEASE} ]]; then
		VERSIONS+=( "${V_PKG}-${V_RELEASE}" )
	fi
	if [[ -n ${V_PATCHLVL} ]]; then
		VERSIONS+=( "${V_MAJOR}.${V_MINOR}.${V_PATCHLVL}" )
	fi
	if [[ -n ${V_MINOR} ]]; then
		VERSIONS+=( "${V_MAJOR}.${V_MINOR}" )
	fi
	VERSIONS+=( "${V_MAJOR}" )
}

# these variables must be export for envsubst(1)
declare -x P=''
declare -x V=''
declare -x V_PKG=''
declare -x V_MAJOR=''		# first number in version string
declare -x V_MINOR=''		# second number in version string (or empty)
declare -x V_PATCHLVL=''	# third number in version string (or empty)
declare -x V_RELEASE=''		# module release (or empty)
declare -x PREFIX=''

declare -A SHASUMS=()
declare -a MODULE_DOCFILES=()


declare -A Yaml_valid_keys_for_module=(
	['defaults']=1
	['shasums']=1
	['type']=1
	['versions']=1
)

declare -A Yaml_default_config=(
	['build_requires']=''		# !!seq of strings
	['compile_in_sourcetree']='no'  # !!str
	['configure_with']='auto'	# !!str
	['configure_args']=''           # !!seq of strings
	['configure_args+']=''          # !!seq of strings
	['default_variant']=''          # !!str
	['docfiles']=''	                # !!seq of strings
	['docfiles+']=''		# !!seq of strings
	['download_dir']=''             # !!str
	['group']='Tools'		# !!str
	['kernels']=''			# !!seq of strings
	['group_deps']=''		# !!map
	['overlay']='base'		# !!str
	['patch_files']=''		# !!seq
	['patch_files+']=''		# !!seq
	['relstage']='unstable'		# !!str
	['runtime_deps']=''		# !!seq of strings
	['script']='build'		# !!str
	['suffix']=''			# !!str
	['systems']=''			# !!seq of strings
	['sub_packages']=''		# !!map
	['target_cpus']=''		# !!seq of strings
	['urls']=''			# !!map
	['variant']=''			# !!str
)

declare -A Yaml_valid_vk_keys=(
	['config']='1'			# !!map
	['variants']='1'		# !!map
)

declare -A Unpackers=(
	['tar']='tar'
	['7z']='7z'
	['none']='none'
)

declare -A KernelNames=(
	['linux']='1'
	['darwin']='1'
	['any']='1'
)

declare -A TargetCPUs=(
	['x86_64']='x86_64'
	['arm64']='arm64'
	['aarch64']='arm64'
	['any']='1'
)

declare -A hierarchical_groups=(
	['compiler']='compiler'
	['mpi']='compiler mpi'
	['hdf5']='compiler mpi hdf5'
	['hdf5_serial']='compiler hdf5_serial'
)

build_modules_yaml_v1(){
	: "
	
	"
	local -- yaml_mod_config="$1"
	local -- name="$2"
        local -- version="$3"
	shift 3
        local -a with_modules=( "$@" )

	check_yaml_keys(){
		local -n valid_yaml_keys="$1"
		local -n used_yaml_keys="$2"
		used_yaml_keys=()
		local -- key=''
		local -a keys=()
		readarray -t keys < <( ${yq} '.|keys().[]' 2>/dev/null)
		debug "top-level keys: ${keys[*]}"
		for key in "${keys[@]}"; do
			[[ -v valid_yaml_keys[${key}] ]] || \
				std::die 3 "Invalid key in YAML configuration file -- ${key}"
			used_yaml_keys[${key}]=1
		done
	}

	get_config(){
		local -- yaml_input="$1"
		local -n cfg="$2"	# ref. to return configuration
		local -n dfl="$3"       # ref. to defaults
		local -- key=''
		local -- value=''

		for key in "${!dfl[@]}"; do
			cfg[${key}]="${dfl[${key}]}"
		done

		if [[ -z "${yaml_input}" ]]; then
			return 0
		fi

		local -a keys=()
		readarray -t keys < <( ${yq} -e ".|keys().[]" <<<"${yaml_input}" 2>/dev/null ) || \
			std::die 3 "Oops: retrieving keys from:\n${yaml_input}"
		debug "config keys: ${keys[*]}"
		for key in "${keys[@]}"; do
			[[ -v dfl[${key,,}] ]] || \
				std::die 3 "%s -- %s\n%s" \
					 "Invalid key in configuration" \
					 "${key}" "${yaml_input}"
			case ${key} in
				compile_in_sourcetree )
					pm::get_value "${yaml_input}" value "${key}" '!!bool'
					case ${value,,} in
						true )
							cfg[${key,,}]='yes'
							;;
						false )
							cfg[${key,,}]='no'
							;;
						* )
							std::die 3 "%s '%s' -- %s" \
								 "Invalid value for" \
								 "${key}" \
								 "${value}"
							;;
					esac
					;;
				configure_with )
					pm::get_value "${yaml_input}" value "${key}" '!!str'
					case ${value,,} in
						auto | cmake | autotools )
							cfg[${key,,}]="${value,,}"
							;;
						* )
							std::die 3 "%s '%s' -- %s" \
								 "Invalid value for" \
								 'configure_with' \
								 "${value}"
							;;
					esac
					;;
				default_variant | download_dir | group | overlay | script | suffix )
					pm::get_value "${yaml_input}" value "${key}" '!!str'
					cfg[${key,,}]="${value}"
					;;
				group_deps )
					pm::get_value "${yaml_input}" value "${key}" '!!map'
					cfg[${key,,}]="${value}"
					;;
				relstage )
					pm::get_value "${yaml_input}" value "${key}" '!!str'
					case ${value,,} in
						unstable | stable | deprecated )
							cfg[${key,,}]="${value,,}"
							;;
						remove | removed )
							cfg[${key,,}]='remove'
							;;
						* )
							std::die 3 "%s '%s' -- %s" \
								 "Invalid value for" \
								 'relstage' \
								 "${value}"
							;;
					esac
					;;
				urls | sub_packages )
					pm::get_value "${yaml_input}" value "${key}" '!!seq'
					cfg[${key,,}]="${value}"
					;;
				build_requires|configure_args|docfiles|patch_files|runtime_deps|systems|variant )
					pm::get_seq "${yaml_input}" value "${key}"
					cfg[${key,,}]="${value}"
					;;
				kernels )
					pm::get_seq "${yaml_input}" value "${key}"
					set -o noglob
					local -a items=( "${value,,}" )
					set +o noglob
					local -- item=''
					for item in "${items[@]}"; do
						[[ -v KernelNames[${item}] ]] || \
							die_invalid_value \
								"${yaml_input}" \
								'config section' \
								'kernel' \
								"${item}"
					done
					cfg[${key,,}]="${value}"
					;;
				target_cpus )
					pm::get_seq "${yaml_input}" value "${key}"
					set -o noglob
					local -a items=( "${value,,}" )
					set +o noglob
					local -- item=''
					for item in "${items[@]}"; do
						[[ -v TargetCPUs[${item}] ]] || \
							die_invalid_value \
								"${yaml_input}" \
								'config section'
								'CPU' \
								"${item}"
					done
					cfg[${key,,}]="${value}"
					;;
				'configure_args+' | 'docfiles+' | 'patch_files+' )
					pm::get_seq "${yaml_input}" value "${key}"
					key="${key:0:-1}"
					if [[ -z "${cfg[${key,,}]}" ]]; then
						cfg[${key,,}]="${value}"
					else
						cfg[${key,,}]+=$'\n'"${value}"
					fi
					;;

				* )
					std::die 3 "%s '%s' in %s" \
						 "Oops unhandled key" \
						 "${key}" \
						 "${FUNCNAME[0]}"
			esac
		done
	}

	get_matching_version_keys(){
		: "
		return list of version keys in refvar $1 matching version specified in $2.
	      	"
		local -n refvar="$1"
		local version="$2"

		local -a keys=()
		readarray -t keys < <( ${yq} -e '.versions|keys().[]' 2>/dev/null ) || \
			std::die 3 "No version keys in configuration file!"

		refvar=()
		for key in "${keys[@]}"; do
			l=()
			# loop over semicolon separated list of keys
			for k in ${key//;/ }; do
				# brace expansion of key
				local list=()
				list=( $(${bash} -c "echo $k") )
				if [[ ${list[@]} =~ ${version} ]]; then
					refvar+=("${key}")
					break
				fi
			done
		done
		(( ${#refvar[@]} == 0 )) && \
			std::die 3 "No configuration for version -- ${version}"
		return 0
	}

	get_yaml_vk_config(){
		: "
		Get configuration for version key $1.

		Please note: this can be the empty string.
		"
		local -- key="$1"
		result=$( ${yq} ".versions.\"${key}\"" 2>/dev/null ) || \
			result=""
		echo "${result}"
	}

	get_variants(){
		local yaml
		yaml=$(${yq} '.variants' 2>/dev/null)
		echo "${yaml}"
	}

	get_num_variants(){
		local -i n=0
		n=$(${yq}  '.|length' 2>/dev/null)
		echo "$n"
	}

	get_variant(){
		local n="$1"
		local yaml
		yaml=$(${yq}  -e ".[$n]" 2>/dev/null)
		[[ "${yaml}" == 'null' ]] && _result=''
		echo "${yaml}"
	}

	chk_group_deps(){
		: "
		Check the group dependencies:
		1. are all keys valid?
		2. all required group deps defined?
		3. more group deps defined as required?
		"
		:
		local -- yaml="$1"		# yaml formatted string: value of group_deps
		local -- group="$2"		# compiler|mpi|hdf5|hdf5_serial
		local -- name="$3"		# module name
		local -- version="$4"		# module version

		# query all specified group dependencies
		local -a keys=()
		readarray -t keys < <( ${yq} ".|keys|.[]" <<<"${yaml}" 2>/dev/null )

		local -- key=''
		for key in "${keys[@]}"; do
			# is this a name of a hierarchical group?
			[[ -v hierarchical_groups[${key}] ]] || \
				die_illegal_group_dep "${name}" "${version}" "${group}" "${key}"
			# is this in the list of required group dependencies?
			is_in_array "${key}" "${hierarchical_groups[${key}]}" || \
				die_invalid_group_dep "${name}" "${version}" "${group}"
		done
		# are all required group dependencies defined?
		for key in ${hierarchical_groups[${group,,}]}; do
			is_in_array "${key,,}" "${keys[@]}" || \
				die_missing_group_dep "${name}" "${version}" "${group}"
		done
	}

	get_group_deps(){
		local -- yaml="$1"		# yaml formatted string with group config
		local -- group="$2"		# compiler|mpi|hdf5|hdf5_serial
		local -n with_modules="$3"	# refvar to return a list of modules

		local -a modules=()
		local keys=()
		readarray -t keys < <( ${yq} ".${group,,}|keys|.[]" <<<"${yaml}" 2>/dev/null )
		local key
		for key in "${keys[@]}"; do
			local versions=()
			readarray -t versions < <( ${yq} -e ".${group,,}.${key}[]" <<<"${yaml}" 2>/dev/null )
			local version
			for version in "${versions[@]}"; do
				if [[ -v opt_with_dict[${key}/${version}] ]]; then
					with_modules+=( "${key}/${version}" )
				fi
				modules+=( "${key}/${version}" )
			done
		done
		if (( ${#with_modules[@]} == 0 )); then
			with_modules=( "${modules[@]}" )
		fi
	}

	die_missing_group_dep(){
		std::die 3 "%s/%s: %s" \
			 "${1}" "${2}" \
			 "is in group '$3', but the group dependency for this group is missing!" 
	}
	die_invalid_group_dep(){
		std::die 3 "%s/%s: %s" \
			 "${1}" "${2}" \
			 "invalid group dependency '$3' for module in group '$4'!"
	}

	die_illegal_group_dep(){
		std::die 3 "%s/%s: %s" \
			 "${1}" "${2}" \
			 "illegal group dependency '$4' for module in group '$3'!"
	}

	is_in_array(){
		local -r key="$1"
		shift 1
		[[ $* =~ (^|[[:space:]])"${key}"($|[[:space:]]) ]]
	}

	is_subset(){
		local -n subset="$1"
		shift 1
		local el=''
		for el in "${subset[@]}"; do
			is_in_array "${el}" "$@" || return 1
		done
		return 0
	}

	: "
	To compile a module with a certain compiler||mpi||hdf5 dependency
	the '--with' option can be used. Depending on the hierarchical
	group the modules specified with the option '--with' must be a
	subset of
	- compiler: ( compiler)
	- mpi: ( compiler mpi )
	- hdf5: ( compiler mpi hdf5 )
	- hdf5_serial: ( compiler hdf5_serial )
	"

	build_modules_compiler(){
		local -- module_name="$1"
		local -- module_version="$2"
		local -n module_cfg="$3"

		chk_group_deps  "${module_cfg['group_deps']}" 'Compiler' \
				"${module_name}" "${module_version}"

		local -a with_compiler=()
		get_group_deps  "${module_cfg['group_deps']}" 'Compiler' with_compiler
		debug "${with_compiler[@]}"
		
		local compiler=''
		for compiler in "${with_compiler[@]}"; do
			# build if opt_with_modules is empty or compiler is in this array
			(( ${#opt_with_modules[@]} != 0 )) \
				&& [[ "${compiler}" != "${opt_with_modules[0]}" ]] \
				&& continue
			[[ "${opt_check_mode}" == 'yes' ]] && continue
			pbuild.build_module_yaml \
				"${module_name}" "${module_version}" \
				"$3" \
				"${compiler}" \
				"${@:4}"
		done
	}

	build_modules_hdf5_serial(){
		local -- module_name="$1"
		local -- module_version="$2"
		local -n module_cfg="$3"

		chk_group_deps  "${module_cfg['group_deps']}" 'HDF5_serial' \
				"${module_name}" "${module_version}"

		local -a with_compiler=()
		get_group_deps  "${module_cfg['group_deps']}" 'Compiler' with_compiler

		local -a with_hdf5=()
		get_group_deps  "${module_cfg['group_deps']}" 'HDF5_serial' with_hdf5

		local -- compiler
		local -- hdf5
		for compiler in "${with_compiler[@]}"; do
			for hdf5 in "${with_hdf5[@]}"; do
				# build if opt_with_modules is empty or compiler is in this array
				(( ${#opt_with_modules[@]} != 0 )) \
					&& ! is_subset opt_with_modules "${compiler}" "${hdf5}" \
					&& continue

				debug "build $module_name/$module_version with $compiler and $hdf5"
				debug "  runtime deps: ${runtime_deps[*]}"
				debug "  build requires: ${build_requires[*]}"
				pbuild.build_module_yaml \
					"${module_name}" "${module_version}" \
					"$3" \
					"${compiler}" \
					"${hdf5}" \
					"${@:4}"
			done
		done
	}


	build_modules_mpi(){
		local -- module_name="$1"
		local -- module_version="$2"
		local -n module_cfg="$3"

		chk_group_deps  "${module_cfg['group_deps']}" 'MPI' "${module_name}" "${module_version}"

		local -a with_compiler=()
		get_group_deps  "${module_cfg['group_deps']}" 'Compiler' with_compiler

		local -a with_mpi=()
		get_group_deps  "${module_cfg['group_deps']}" 'MPI' with_mpi

		local -- compiler
		local -- mpi
		for compiler in "${with_compiler[@]}"; do
			for mpi in "${with_mpi[@]}"; do
				# build if opt_with_modules is empty or compiler is in this array
				(( ${#opt_with_modules[@]} != 0 )) \
					&& ! is_subset opt_with_modules "${compiler}" "${mpi}" \
					&& continue

				debug "build $module_name/$module_version with $compiler and $mpi"
				debug "  runtime deps: ${runtime_deps[*]}"
				debug "  build requires: ${build_requires[*]}"
				pbuild.build_module_yaml \
					"${module_name}" "${module_version}" \
					"$3" \
					"${compiler}" \
					"${mpi}" \
					"${@:4}"
			done
		done
	}

	build_modules_hdf5(){
		local -- module_name="$1"
		local -- module_version="$2"
		local -n module_cfg="$3"

		chk_group_deps  "${module_cfg['group_deps']}" 'HDF5' "${module_name}" "${module_version}"

		local -a with_compiler=()
		get_group_deps  "${module_cfg['group_deps']}" 'Compiler' with_compiler

		local -a with_mpi=()
		get_group_deps  "${module_cfg['group_deps']}" 'MPI' with_mpi

		local -a with_hdf5=()
		get_group_deps  "${module_cfg['group_deps']}" 'HDF5' with_hdf5

		local -- compiler
		local -- mpi
		local -- hdf5
		for compiler in "${with_compiler[@]}"; do
			for mpi in "${with_mpi[@]}"; do
				for hdf5 in "${with_hdf5[@]}"; do
					debug "build $module_name/$module_version with $compiler, $mpi and $hdf5"
					debug "  runtime deps: ${runtime_deps[*]}"
					debug "  build requires: ${build_requires[*]}"

					# build if opt_with_modules is empty or compiler is in this array
					(( ${#opt_with_modules[@]} != 0 )) \
						&& ! is_subset opt_with_modules \
						     "${compiler}" "${mpi}" "${hdf5}" \
						&& continue

					pbuild.build_module_yaml \
						"${module_name}" "${module_version}" \
						"$3" \
						"${compiler}" \
						"${mpi}" \
						"${hdf5}" \
						"${@:4}"
				done
			done
		done

	}

	build_modules_other(){
		local -- module_name="$1"
		local -- module_version="$2"
		local -n module_cfg="$3"
		pbuild.build_module_yaml \
                        "${module_name}" "${module_version}" \
                        "$3" \
			"${@:4}"
	}

	die_parsing(){
		std::die 3 "error parsing YAML:\n----\n$1\n----"
	}

	die_invalid_value(){
		std::die 3 "Invalid value for key '$3' in $2 -- '$4'\n----\n$1\n----"
	}

	die_invalid_key(){
		std::die 3 "Invalid key '$3' in $2\n----\n$1\n----"
	}

	die_missing_key(){
		std::die 3 "Key '$3' missing in $2\n----\n$1\n----"
	}

	set_urls() {
		local -- yaml="$1"
		local -i l=0
		l=$( ${yq} -Ne e '.|length'  <<<"${yaml}" 2>/dev/null) || \
			die_parsing "{yaml}"
		local -i i=0
		local -- url_yaml=''
		for ((i=0; i<l; i++)); do
			url_yaml=$(${yq} -Ne e ".[$i]" <<<"${yaml}" 2>/dev/null) || \
				die_parsing "{yaml}"
			local url=''
			local fname=''
			local strip_dirs=''
			local unpacker=''
			local key=''
			local value=''
			while read -r key value; do
				key=${key:0:-1}
				case "${key}" in
					url )
						url=$(${envsubst} <<<"${value}")
						;;
					name )
						fname=$(${envsubst} <<<"${value}")
						;;
					strip_dirs )
						[[ ${value} =~ ^[0-9]+$ ]] || \
							die_invalid_value \
								"${url_yaml}" \
								'list of URLS' \
								'strip_dirs' \
								"${value}"
						strip_dirs="${value}"
						;;
					unpacker )
						[[ -v Unpackers[${value}] ]] || \
							die_invalid_value \
								"${url_yaml}" \
								'list of URLs' \
								'unpacker' \
								"${value}"
						unpacker="${value}"
						;;
					* )
						die_invalid_key \
							"${url_yaml}" \
							'list of URLs' \
							"${key}"
						;;
				esac
			done <<<"${url_yaml}"
			[[ -z "${url}" ]] && \
				die_missing_key \
					"${url_yaml}" \
					'list of URLs' \
					'url'
			[[ -z "${fname}" ]] && fname="${url##*/}"
			[[ -z "${strip_dirs}" ]] && strip_dirs=1
			[[ -z "${unpacker}" ]] && unpacker='tar'
			pbuild.set_urls "${url}" "${fname}" "${strip_dirs}" "${unpacker}"
		done
	}

	set_configure_args(){
		local -a args=()
		readarray -t args <<< "$1"
		pbuild.add_configure_args "${args[@]}"
	}

	set_patch_files(){
		local -a args=()
		readarray -t args <<< "$1"
		pbuild.add_patch_files "${args[@]}"
	}

	die_sub_package_name_missing(){
		std::die 3 "Name of sub-package not specified in \n===\n$1\n===\n"
	}
	die_sub_package_version_missing(){
		std::die 3 "Version of sub-package not specified in \n===\n$1\n===\n"
	}
	build_sub_packages(){
		local -- yaml="$1"
		local -i l=0
		l=$( ${yq} -Ne e '.|length'  <<<"${yaml}" 2>/dev/null) || \
			die_parsing "${yaml}"
		local -i i=0
		local -- fname=''
		local -- pkgs_yaml=''
		for ((i=0; i<l; i++)); do
			pkgs_yaml=$(${yq} -Ne e ".[$i]" <<<"${yaml}" 2>/dev/null) || \
				die_parsing "${yaml}"
			local -- pkg_name=''
			local -- pkg_version=''
			local -a pkg_build_args=()
			local -a keys=()
			readarray -t keys < <( ${yq} -e ".|keys().[]" <<<"${pkgs_yaml}" 2>/dev/null ) || \
				die_parsing "${pkgs_yaml}"
			local -- key=''
			for key in "${keys[@]}"; do
				case ${key,,} in
					'name' )
						pm::get_value "${pkgs_yaml}" pkg_name "${key}" '!!str'
						;;
					'version' )
						pm::get_value "${pkgs_yaml}" pkg_version "${key}" '!!str'
						;;
					'build_args' )
						local -- value=''
						pm::get_seq "${pkgs_yaml}" value "${key}"
						readarray -t pkg_build_args <<< "${value}"
						;;
					* )
						die_invalid_key \
							"${pkgs_yaml}" \
							"in subpackage '$i'" \
							"${key}"
						;;
				esac
			done
			[[ -n "${pkg_name}" ]] || \
				die_sub_package_name_missing "${pkgs_yaml}"
			[[ -n "${pkg_version}" ]] || \
				die_sub_package_version_missing "${pkgs_yaml}"

			[[ "${opt_verbose}" == 'yes' ]] && \
				pkg_build_args+=( '--verbose' )
			[[ "${opt_debug}" == 'yes' ]] && \
				pkg_build_args+=( '--debug' )
			[[ "${opt_force_rebuild}" == 'yes' ]] && \
				pkg_build_args+=( '-f' )
			pkg_build_args+=( "--parent-prefix=${PREFIX}" )
			"$BUILDBLOCK_DIR/build-${pkg_name}" \
				"${pkg_name}/${pkg_version}" \
				"${pkg_build_args[@]}"
		done
	}

	build_modules_variant(){
		local -- module_name="$1"
		local -- module_version="$2"
		local -n module_config="$3"

		check_system(){
			[[ -z ${module_config['systems']} ]] && return 0

			set -o noglob
			local -a systems=( ${module_config['systems']} )
			set +o noglob

			local -- system
			for system in "${systems[@]}"; do
				[[ "${opt_system}" == ${system} ]] && return 0
				[[ "${HOSTNAME}" == ${system} ]] && return 0
			done
			std::info "Skipping variant '${module_version}', neither OS nor hostname match:"
			std::info "  This system: ${opt_system}; hostname: ${HOSTNAME}"
			std::info "  Systems to build on: ${systems[@]}"
			return 1
		}
		die_invalid_kernel_name(){
			std::die 3 "Invalid kernel name in configuration!"
		}
		check_kernel(){
			[[ -z ${module_config['kernels']} ]] && return 0
			set -o noglob
			local -a kernels=( "${module_config['kernels'],,}" )
			set +o noglob
			local -- kernel=''
			for kernel in "${kernels[@]}"; do
				[[ ${kernel} == 'any' ]] && return 0
				[[ ${kernel} == ${KernelName,,} ]] & return 0
			done
			std::info "Skipping variant '${module_version}':"
			std::info "  The kernel of this systems is: ${KernelName}"
			std::info "  But the variant is for the following kernels: ${module_config['kernels']}"
			return 1
		}

		check_target_cpu(){
			[[ -z ${module_config['target_cpus']} ]] && return 0
			set -o noglob
			local -a target_cpus=( "${module_config['target_cpus'],,}" )
			set +o noglob
			local -- system_cpu=$(uname -p)
			local -- cpu=''
			for cpu in "${target_cpus[@]}"; do
				[[ ${cpu} == 'any' ]] && return 0
				[[ ${cpu} == ${system_cpu} ]] && return 0
			done
			std::info "Skipping variant '${module_version}':"
			std::info "  The CPU of this systems is: ${system_cpu}"
			std::info "  But this variant is for the following CPUs: ${module_config['target_cpus']}"
			return 1

		}

		P="${module_name}"
		parse_version "${module_version}"

		local build_variant="${opt_variant:-${module_config['default_variant']}}"

		# build this variant?
		if [[ ":${module_config['variant']}:" != *:${build_variant}:* ]]; then
			debug "don't build this variant: ${module_config['variant']} !=  *:${build_variant}:*"
			return 0
		fi
		
		# build for this system, kernel and target_cpu?
		check_system || return 0
		check_kernel || return 0
		check_target_cpu || return 0

		debug "build variant ${module_name}/${module_version}"

		local ol_name="${module_config['overlay']}"
		[[ -v OverlayInfo[${ol_name}:install_root] ]] || \
			std::die 2 "%s" \
				 "Overlay doesn't exist - ${ol_name}"
		declare ol_install_root="${OverlayInfo[${ol_name}:install_root]}"
		declare ol_modulefiles_root="${OverlayInfo[${ol_name}:modulefiles_root]}"

		module_version+="${module_config['suffix']}"

		pbuild.compile_in_sourcetree "${module_config['compile_in_sourcetree']}"
		pbuild.configure_with "${module_config['configure_with']}"
		pbuild.add_to_group "${module_config['group']}"

		set_urls "${module_config['urls']}"
		set_configure_args "${module_config['configure_args']}"
		set_patch_files "${module_config['patch_files']}"

		if [[ -n "${module_config['download_dir']}" ]]; then
			PMODULES_DISTFILESDIR="${module_config['download_dir']}"
		fi
		local -a runtime_deps=()
		if [[ -n ${module_config['runtime_deps']} ]]; then
			readarray -t runtime_deps <<<"${module_config['runtime_deps']}"
			debug "runtime_deps=${runtime_deps[@]} length: ${#runtime_deps[@]}"
		fi

		local -a build_requires=()
		if [[ -n ${module_config['build_requires']} ]]; then
			readarray -t build_requires <<<"${module_config['build_requires']}"
			build_requires=( "${build_requires[@]/#/b:}" )
			debug "build_requires=${build_requires[@]} length: ${#build_requires[@]}"
		fi

		if [[ -n ${module_config['docfiles']} ]]; then
			readarray -t MODULE_DOCFILES <<<"${module_config['docfiles']}"
		fi

		local -A build_functions=(
			['Compiler']=build_modules_compiler
			[HDF5_serial]=build_modules_hdf5_serial
			[MPI]=build_modules_mpi
			[HDF5]=build_modules_hdf5
		)
		local func=build_modules_other
		if [[ -v build_functions[${module_config['group']}] ]]; then
			func=${build_functions[${module_config['group']}]}
		fi
		${func}	\
			"${module_name}" \
			"${module_version}" \
			module_config \
			"${runtime_deps[@]}" "${build_requires[@]}"
		build_sub_packages "${module_config['sub_packages']}"
	}

	expand_version_key(){
		local -n ev_result="$1"
		local -- key="$2"
		local -- version="$3"

		ev_result=()
		# loop over comma separated list of keys
		for k in ${key//;/ }; do
			# do curly brackets expansion {}
			local l
			local list=()
			list=( $(${bash} -c "echo $k" ) )
			for l in "${list[@]}"; do
				if [[ $l =~ ${version} ]]; then
					ev_result+=("${l}")
				fi
			done
		done
	}

	local -A used_keys=()
	local -- yaml_input=''
	local -A default_config=()
	local -- version_key=''
	local -a version_keys=()
	local -A shasums=()

	check_yaml_keys Yaml_valid_keys_for_module used_keys <<<"${yaml_mod_config}"
	[[ -v used_keys['versions'] ]] || \
		std::die 3 "No version(s) specified in YAML configuration file."

	if [[ -v used_keys['defaults'] ]]; then
		yaml_input=$(${yq} '.defaults' <<<"${yaml_mod_config}" 2>/dev/null)
	fi
	get_config "${yaml_input}" default_config Yaml_default_config

	if [[ -v used_keys['shasums'] ]]; then
		local yaml_input=''
		yaml_input=$(${yq} '.shasums' <<<"${yaml_mod_config}" 2>/dev/null)
		while read -r key value; do
			[[ -z ${key} ]] && continue
			SHASUMS[${key//:}]="${value}"
		done <<<"${yaml_input}"
	fi
	if [[ -v used_keys['type'] ]]; then
		local -- value=''
		pm::get_value "${yaml_mod_config}" value 'type' '!!str'
		case "${value,,}" in
			'module' )
				[[ "${module_type}" == 'sub_package' ]] && \
					std::die 3 "Module type is 'module' but was called as 'sub_package'!"
				;;
			'sub_package' )
				[[ "${module_type}" == 'module' ]] && \
					std::die 3 "Module type is 'sub_package' but was called as 'module'!"
				;;
			* )
				std::die 3 "Invalid module type -- '${value}'!"
				;;
		esac
	fi
	get_matching_version_keys version_keys "${version}" <<<"${yaml_mod_config}"
	for version_key in "${version_keys[@]}"; do
		local yaml_vk_config=''
		yaml_vk_config=$(get_yaml_vk_config "${version_key}" <<<"${yaml_mod_config}")

		# check keys: allowed are 'config' and 'variants'
		used_keys=()
		check_yaml_keys Yaml_valid_vk_keys used_keys <<<"${yaml_vk_config}"

		# read config if set
		local -A vk_config=()
		yaml_input=''
		if [[ -v used_keys['config'] ]]; then
			yaml_input=$(${yq} '.config' <<<"${yaml_vk_config}" 2>/dev/null)
			debug "vk input: ${yaml_input}"
		fi
		# reminder: if YAML input is empty, next line copies defaults to 'vk_config'
		get_config "${yaml_input}" vk_config default_config

		local -- yaml_variants=''
		yaml_variants=$(get_variants <<<"${yaml_vk_config}")
		local -i num_variants=0
		num_variants=$(get_num_variants <<<"${yaml_variants}")
		local versions=()
		expand_version_key versions "${version_key}" "${version}"
		local v=''
		for v in "${versions[@]}"; do
			debug "version: $v"
			if (( num_variants == 0 )); then
				build_modules_variant \
					"${name}" "${v}" \
					vk_config
			else
				local -i n=0
				local -- yaml_variant=''
				for ((n=0; n<num_variants; n++)); do
					yaml_variant=$(get_variant "${n}" <<<"${yaml_variants}")
					
					local -A mod_config=()
					get_config "${yaml_variant}" mod_config vk_config
					build_modules_variant \
						"${name}" "${v}" \
						mod_config
				done
			fi
		done
	done
} # build_modules()

debug(){
	${echo} "INFO: " "$@" 1>&2
}

#.............................................................................
# main

init_module_environment
parse_args "$@"
pbuild.jobs "${opt_jobs}"
pbuild.force_rebuild "${opt_force_rebuild}"
pbuild.build_target "${opt_build_target}"
pbuild.dry_run "${opt_dry_run}"
pbuild.enable_cleanup_build "${opt_enable_cleanup_build}"
pbuild.enable_cleanup_src "${opt_enable_cleanup_src}"
pbuild.update_modulefiles "${opt_update_modulefiles}"
pbuild.system "${opt_system}"
pbuild.verbose "${opt_verbose}"

#
# read configuration for modbuild
#
pm::read_config

# :FIXME: should dist files go to
#         ${pm_root}/var/distfiles
#         or
#         ${overlay}/var/distfiles
#         ?
PMODULES_DISTFILESDIR=${PMODULES_DISTFILESDIR:-"${PMODULES_HOME%%/Tools*}/var/distfiles"}
PMODULES_TMPDIR="${PMODULES_TMPDIR:-/var/tmp/${USER}}"
export PMODULES_DISTFILESDIR PMODULES_TMPDIR

declare -r BUILD_SCRIPT
declare -r BUILDBLOCK_DIR

if [[ -z ${module_name} ]]; then
	# the module name is defined by the directory the build script is in
	IFS=/ read -r -a fname <<< "${BUILD_SCRIPT:1}"
	module_name=${fname[${#fname[@]}-2]}
fi

for version in ${versions_to_build}; do
        build_modules "${module_name}" "${version}" "${opt_with_modules[@]}"
done

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