#! /usr/bin/env bash

################################################################################
# Functions
################################################################################

eecho()
{
	echo "$*" 1>&2
}

panic()
{
	eecho "ERROR: $*"
	exit 1
}

get_component_dir()
{
	if [ $# -ne 5 ]; then
		eecho "get_component_dir: bad usage"
		return 1
	fi
	local root_dir="$1"
	local build_root_dir="$2"
	local context="$3"
	local component="$4"
	local key="$5"

	# The following is for debugging.
	if [ 0 -ne 0 ]; then
		eecho "get_component_dir: args: $*"
		eecho "root_dir: $root_dir"
		eecho "build_root_dir: $build_root_dir"
		eecho "context: $context"
		eecho "component: $component"
		eecho "key: $key"
	fi

	local slides_source_dir=
	local software_source_dir=
	local fmt_source_dir="$root_dir/third_party/fmt"
	case "$context" in
	companion)
		slides_source_dir="$root_dir/slides/examples"
		software_source_dir="$root_dir/miscellany/examples"
		;;
	clang_slides)
		slides_source_dir="$root_dir/slides/software"
		software_source_dir="$root_dir/software"
		;;
	*)
		eecho "get_component_dir: bad context $context"
		;;
	esac
	local fmt_build_dir="$fmt_source_dir/tmp_build"
	local slides_build_dir="$slides_source_dir/tmp_build"
	local software_build_dir="$software_source_dir/tmp_build"
	if [ -n "$build_root_dir" ]; then
		slides_build_dir="$build_root_dir/slides"
		software_build_dir="$build_root_dir/software"
		fmt_build_dir="$build_root_dir/fmt/tmp_build"
	fi

	local result_dir=
	case "$component" in
	fmt)
		case "$key" in
		source)
			result_dir="$fmt_source_dir";;
		build)
			result_dir="$fmt_build_dir";;
		*)
			eecho "get_component_dir: bad key $key";;
		esac
		;;
	slides)
		case "$key" in
		source)
			result_dir="$slides_source_dir";;
		build)
			result_dir="$slides_build_dir";;
		*)
			eecho "get_component_dir: bad key $key";;
		esac
		;;
	software)
		case "$key" in
		source)
			result_dir="$software_source_dir";;
		build)
			result_dir="$software_build_dir";;
		*)
			eecho "get_component_dir: bad key $key";;
		esac
		;;
	*)
		eecho "get_component_dir: invalid component $component"
		return 1
		;;
	esac
	if [ 0 -ne 0 ]; then
		result_dir="$(realpath "$result_dir")" || return 1
	fi
	if [ -z "$result_dir" ]; then
		eecho "get_component_dir: unexpected"
		return 1
	fi
	echo "$result_dir" || return 1
}

get_component_install_targets()
{
	if [ $# -ne 1 ]; then
		eecho "get_component_install_targets: bad usage"
		return 1
	fi
	local component="$1"
	shift 1
	local result
	case "$component" in
	slides)
		result="install-subprojects";;
	software)
		result="install-subprojects install";;
	*)
		eecho "get_component_install_targets: invalid component $component"
		return 1;;
	esac
	echo "$result"
}

# append to colon-delimited list
append_to_cd_list()
{
	[ $# -ge 1 ] || return 1
	local list="$1"
	shift 1
	local item=
	for item in "$@"; do
		if [ -n "$list" ]; then
			list="$list:$item"
		else
			list="$item"
		fi
	done
	echo "$list"
}

################################################################################
# Command-Line Processing
################################################################################

usage()
{
	cat <<- EOF
	usage: $0 -s dir -d dir
	NAME
	====

	build - Perform various stages of the software build.

	SYNOPSIS
	========

	build [options]

	OPTIONS
	=======

	--help (alias -h)
	    Print help information and exit.

	--verbose (alias -v)
	    Increase the verbosity of output.

	--print-only (alias -n)
	    Show what commands would be run without actually running them.

	--defaults (alias -a)
	    Initialize all of the settings to defaults that are likely to be
	    reasonable for many users.
	    [Since some users may not want to use the settings selected by
	    this option (such as the author of this software :P), they require
	    explicit opt-in via this option.]

	--clean (alias -c)
	    Specify that all files generated by the build process should be
	    removed prior to other actions.
	    The --no-clean option has the opposite effect.

	--build (alias -b)
	    Configure and build the code using CMake.
	    The --no-build causes the configure/build steps to be skipped.

	--demo (alias -t)
	    Run the demo scripts.
	    The --no-demo option has the opposite effect.

	--cxx-compiler \$path
	    Set the pathname of the C++ compiler program to \$path.

	--c-compiler \$path
	    Set the pathname of the C compiler program to \$path.

	--verbose-makefile
	    Enable verbose makefiles with CMake.
	    The --no-verbose-makefile option has the opposite effect.

	--debug
	    Use a CMake build type of "Build".

	--release
	    Use a CMake build type of "Release".

	--asan (alias -A)
	    Enable the use of Address Sanitizer (ASan).
	    The --no-asan option has the opposite effect.

	--ubsan (alias -U)
	    Enable the use of Undefined Behavior Sanitizer (UBSan).
	    The --no-ubsan option has the opposite effect.

	--fmt (alias -f)
	    Enable the downloading and building of a slightly modified version
	    of the fmt library for std::format support.
	    The --no-fmt option has the opposite effect.

	--install-dir \$install_dir
	    Set the directory under which to install the software to \$install_dir.
	    This directory defaults to \$root_dir/install, where \$root_dir is the
	    top-level directory of the source tree.

	--dev
	--extras
	--dir \$path (alias -s)
	    These options are for internal use only and only mentioned for
	    for completeness.

	EXAMPLES
	========
	EOF

	if [ $# -gt 0 ]; then
		echo
		echo "BAD USAGE: $*"
	fi

	exit 2
}

self_canonpath="$(realpath "$0")" || \
  panic "cannot get real path of program"
self_dir="$(dirname "$self_canonpath")" || \
  panic "cannot get directory of program"

root_dir="$self_dir/.."
if [ -f "$root_dir/README.md" ]; then
	context=companion
else
	context=clang_slides
fi

root_dir="$(realpath "$root_dir")" || \
  panic "cannot canonicalize path $root_dir"
if [ -n "$build_root_dir" ]; then
	build_root_dir="$(realpath "$build_root_dir")" || \
	  panic "cannot canonicalize path $build_root_dir"
fi

components=(
	slides
	software
)

software_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
  software source)" || \
  panic "cannot get software directory"
get_clang_include_dir="$self_dir/get_clang_include_dir"

build_root_dir=
build_type=Debug
c_compiler="$CC"
cxx_compiler="$CXX"
use_fmtlib=0
do_clean=0
do_build=0
do_install=0
do_demo=0
enable_extras=
verbose=0
verbose_makefile=0
enable_asan=
enable_ubsan=
print_only=0
stop_after_fmtlib=0
enable_asan_user_poisoning=
enable_asan_leak_detect=
enable_ubsan_halt_on_error=
num_jobs=8
clangfoo_comps=

install_dir="$root_dir/install"

while [ $# -gt 0 ]; do
	option="$1"
	case "$option" in
	--help|-h)
		usage;;
	--verbose|-v)
		shift 1
		verbose=$((verbose + 1))
		;;
	-n)
		shift 1
		print_only=1
		;;
	--defaults|-a)
		shift 1
		do_clean=1
		do_build=1
		do_demo=0
		enable_asan=1
		enable_ubsan=1
		build_type=Debug
		verbose=0
		enable_extras=0
		verbose_makefile=0
		use_fmtlib=1
		enable_asan_user_poisoning=0
		enable_asan_leak_detect=0
		;;
	--dev)
		shift 1
		do_clean=1
		do_build=1
		do_demo=1
		enable_asan=1
		enable_ubsan=1
		build_type=Debug
		verbose=1
		enable_extras=1
		verbose_makefile=1
		use_fmtlib=1
		;;
	--no-clean)
		shift 1
		do_clean=0
		;;
	--very-clean)
		shift 1
		do_clean=2
		;;
	--clean|-c)
		shift 1
		do_clean=1
		;;
	--no-build)
		shift 1
		do_build=0
		;;
	--build|-b)
		shift 1
		do_build=1
		;;
	--no-demo)
		shift 1
		do_demo=0
		;;
	--demo|-t)
		shift 1
		do_demo=1
		;;
	--num-jobs)
		shift 1
		[ $# -gt 0 ] || usage "missing argument"
		num_jobs="$1"
		shift 1
		;;
	--cxx-compiler)
		shift 1
		[ $# -gt 0 ] || usage "missing argument"
		cxx_compiler="$1"
		shift 1
		;;
	--c-compiler)
		shift 1
		[ $# -gt 0 ] || usage "missing argument"
		c_compiler="$1"
		shift 1
		;;
	--no-verbose-makefile)
		shift 1
		verbose_makefile=0
		;;
	--verbose-makefile)
		shift 1
		verbose_makefile=1
		;;
	--debug)
		shift 1
		build_type=Debug
		;;
	--relwithdebinfo)
		shift 1
		build_type=RelWithDebInfo
		;;
	--release)
		shift 1
		build_type=Release
		;;
	--no-asan)
		shift 1
		enable_asan=0
		;;
	--asan|-A)
		shift 1
		enable_asan=1
		;;
	--no-ubsan)
		shift 1
		enable_ubsan=0
		;;
	--ubsan|-U)
		shift 1
		enable_ubsan=1
		;;
	--no-fmt)
		shift 1
		use_fmtlib=0
		;;
	--fmt|-f)
		shift 1
		use_fmtlib=1
		;;
	--no-extras)
		shift 1
		enable_extras=0
		;;
	--extras)
		shift 1
		enable_extras=1
		;;
	--install-dir)
		shift 1
		[ $# -gt 0 ] || usage "missing argument"
		install_dir="$1"
		shift 1
		;;
	--build-dir)
		shift 1
		[ $# -gt 0 ] || usage "missing argument"
		build_root_dir="$1"
		shift 1
		;;
	-0)
		shift 1
		stop_after_fmtlib=1
		;;

	--default-ubsan-halt-on-error)
		shift 1
		enable_ubsan_halt_on_error=
		;;
	--no-ubsan-halt-on-error)
		shift 1
		enable_ubsan_halt_on_error=0
		;;
	--ubsan-halt-on-error)
		shift 1
		enable_ubsan_halt_on_error=1
		;;
	--default-asan-allow-user-poisoning)
		shift 1
		enable_asan_user_poisoning=
		;;
	--no-asan-allow-user-poisoning)
		shift 1
		enable_asan_user_poisoning=0
		;;
	--asan-allow-user-poisoning)
		shift 1
		enable_asan_user_poisoning=1
		;;
	--default-asan-detect-leaks)
		shift 1
		enable_asan_leak_detect=
		;;
	--no-asan-detect-leaks)
		shift 1
		enable_asan_leak_detect=0
		;;
	--asan-detect-leaks)
		shift 1
		enable_asan_leak_detect=1
		;;

	--no-clangfoo-comps)
		shift 1
		clangfoo_comps=0
		;;
	--clangfoo-comps)
		shift 1
		clangfoo_comps=1
		;;

	--*|-*)
		usage "invalid option $option"
		;;
	*)
		break
		;;
	esac
done
shift $((OPTIND - 1))

if [ "$do_build" -ne 0 ]; then
	do_install=1
fi

################################################################################
#
################################################################################

run_command()
{
	echo "RUNNING: $*"
	"$@"
	local status=$?
	echo "EXIT STATUS: $status"
	return "$status"
}

run_command llvm-config --bindir
run_command llvm-config --includedir
run_command llvm-config --libdir
run_command llvm-config --link-shared
run_command llvm-config --libs all

run_command "$get_clang_include_dir" -v

base_cmake_configure_options=(
	-DCMAKE_INSTALL_PREFIX="$install_dir"
)
if [ -n "$build_type" ]; then
	base_cmake_configure_options+=(-DCMAKE_BUILD_TYPE=Debug)
fi
if [ -n "$verbose_makefile" ]; then
	base_cmake_configure_options+=(-DCMAKE_VERBOSE_MAKEFILE="$verbose_makefile")
fi
if [ -n "$enable_asan" ]; then
	base_cmake_configure_options+=(-DENABLE_ASAN="$enable_asan")
fi
if [ -n "$enable_ubsan" ]; then
	base_cmake_configure_options+=(-DENABLE_UBSAN="$enable_ubsan")
fi

base_cmake_env=()
if [ -n "$cxx_compiler" ]; then
	base_cmake_env+=(CXX="$cxx_compiler")
fi
if [ -n "$c_compiler" ]; then
	base_cmake_env+=(CC="$c_compiler")
fi
if [ -n "$clangfoo_comps" ]; then
	base_cmake_env+=(ClangFoo_USE_LLVM_COMPONENTS="$clangfoo_comps")
	base_cmake_env+=(ClangFoo_USE_CLANGCPP_COMPONENTS="$clangfoo_comps")
fi

base_cmake_build_options=(
	--parallel "$num_jobs"
)

################################################################################
#
################################################################################

if [ "$do_clean" -ge 2 ]; then
	command=(rm -rf "$install_dir")
	echo "RUNNING: ${command[*]}"
	if [ "$print_only" -eq 0 ]; then
		"${command[@]}" || \
		  panic "cannot remove build directory"
	fi
fi

################################################################################
#
################################################################################

fmt_source_dir="$(get_component_dir "$root_dir" "$build_root_dir" \
  "$context" fmt source)" || \
  panic "cannot get fmt source directory"
fmt_build_dir="$(get_component_dir "$root_dir" "$build_root_dir" \
  "$context" fmt build)" || \
  panic "cannot get fmt build directory"

if [ "$do_clean" -ge 1 ]; then
	command=(rm -rf "$fmt_build_dir")
	echo "RUNNING: ${command[*]}"
	if [ "$print_only" -eq 0 ]; then
		"${command[@]}" || \
		  panic "cannot remove build directory"
	fi
fi
if [ "$use_fmtlib" -ne 0 ]; then
	env "${base_cmake_env[@]}" \
	  cmake -H"${fmt_source_dir}" -B"${fmt_build_dir}" \
	  "${base_cmake_configure_options[@]}" || \
	  panic "configure failed"
	env "${base_cmake_env[@]}" \
	  cmake --build "${fmt_build_dir}" "${base_cmake_build_options[@]}" || \
	  panic "build failed"
fi
if [ "$stop_after_fmtlib" -ne 0 ]; then
	exit 0
fi

################################################################################
# Clean
################################################################################

for component in "${components[@]}"; do

	source_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" source)" || \
	  panic "cannot get source directory for $component"
	build_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" build)" || \
	  panic "cannot get build directory for $component"

	if [ "$do_clean" -ge 1 ]; then
		command=(rm -rf "$build_dir")
		echo "RUNNING: ${command[*]}"
		if [ "$print_only" -eq 0 ]; then
			"${command[@]}" || \
			  panic "cannot remove build directory"
		fi
	fi

done

################################################################################
# Configure and Build
################################################################################

for component in "${components[@]}"; do

	source_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" source)" || \
	  panic "cannot get source directory for $component"
	build_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" build)" || \
	  panic "cannot get build directory for $component"

	if [ "$do_build" -ne 0 ]; then

		cmake_env=("${base_cmake_env[@]}")
		cmake_configure_options=("${base_cmake_configure_options[@]}")
		# ENABLE_EXPERIMENTAL?
		if [ -n "$enable_extras" ]; then
			cmake_configure_options+=(-DENABLE_EXTRAS="$enable_extras")
		fi
		if [ -n "$use_fmtlib" ]; then
			cmake_configure_options+=(-DUSE_FMTLIB="$use_fmtlib")
		fi

		command=(
			env "${cmake_env[@]}"
			cmake -H"$source_dir"
			-B"$build_dir"
			"${cmake_configure_options[@]}"
		)
		echo "RUNNING: ${command[*]}"
		if [ "$print_only" -eq 0 ]; then
			"${command[@]}" || \
			  panic "cmake configure failed"
		fi

		cmake_build_options=("${base_cmake_build_options[@]}")

		command=(
			env "${cmake_env[@]}"
			cmake --build "$build_dir" "${cmake_build_options[@]}"
		)
		echo "RUNNING: ${command[*]}"
		if [ "$print_only" -eq 0 ]; then
			"${command[@]}" || \
			  panic "cmake build failed"
		fi

	fi

done

################################################################################
# Install
################################################################################

for component in "${components[@]}"; do

	source_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" source)" || \
	  panic "cannot get source directory for $component"
	build_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" build)" || \
	  panic "cannot get build directory for $component"
	targets=($(get_component_install_targets "$component")) || \
	  panic "cannot get build directory for $component"

	if [ "$do_install" -ne 0 ]; then

		cmake_build_options=("${base_cmake_build_options[@]}")

		target_options=()
		for target in "${targets[@]}"; do
			target_options+=(--target "$target")
		done

		command=(
			env
			"${cmake_env[@]}"
			cmake
			--build "$build_dir"
			"${target_options[@]}"
			"${cmake_build_options[@]}"
		)
		echo "RUNNING: ${command[*]}"
		if [ "$print_only" -eq 0 ]; then
			"${command[@]}" || \
			  panic "cmake build failed"
		fi
	fi

done

################################################################################
# Run Demos
################################################################################

for component in "${components[@]}"; do

	source_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" source)" || \
	  panic "cannot get source directory for $component"
	build_dir="$(get_component_dir "$root_dir" "$build_root_dir" "$context" \
	  "$component" build)" || \
	  panic "cannot get build directory for $component"

	asan_options="$ASAN_OPTIONS"
	if [ -n "$enable_asan_user_poisoning" ]; then
		asan_options="$(append_to_cd_list "$asan_options" \
		  "allow_user_poisoning=$enable_asan_user_poisoning")" || \
		  panic "cannot append to list"
	fi
	if [ -n "$enable_asan_leak_detect" ]; then
		asan_options="$(append_to_cd_list "$asan_options" \
		  "detect_leaks=$enable_asan_leak_detect")" || \
		  panic "cannot append to list"
	fi
	ubsan_options="$UBSAN_OPTIONS"
	if [ -n "$enable_ubsan_halt_on_error" ]; then
		ubsan_options="$(append_to_cd_list "$ubsan_options" \
		  "halt_on_error=$enable_ubsan_halt_on_error")" || \
		  panic "cannot append to list"
	fi
	run_env=()
	run_env+=(ASAN_OPTIONS="$asan_options")
	run_env+=(UBSAN_OPTIONS="$ubsan_options")

	if [ "$do_demo" -ne 0 ]; then
		cmake_build_options=(
			--parallel "$num_jobs"
		)
		command=(
			env "${cmake_env[@]}"
			"${run_env[@]}"
			cmake
			--build "$build_dir"
			--target demo
			"${cmake_build_options[@]}"
		)
		echo "RUNNING: ${command[*]}"
		if [ "$print_only" -eq 0 ]; then
			"${command[@]}" || \
			  panic "cmake demo failed"
		fi
	fi

done
