#!/hint/bash
#
# SPDX-License-Identifier: GPL-3.0-or-later
:

# shellcheck disable=2034
CHROOT_VERSION='v6'

##
#  usage : check_root $keepenv
##
check_root() {
	local keepenv=$1
	shift
	local orig_argv=("$@")

	(( EUID == 0 )) && return
	if type -P sudo >/dev/null; then
		exec sudo --preserve-env="${keepenv}" -- "${orig_argv[@]}"
	else
		exec su root -c "$(printf ' %q' "${orig_argv[@]}")"
	fi
}

##
#  usage : is_zfs( $path )
# return : whether $path is on zfs
##
is_zfs() {
	[[ -e "$1" && "$(stat -f -c %T "$1")" == zfs ]]
}

##
#  usage : is_zfs_dataset( $path [, $dataset_name [, $dataset_mountpoint]] )
# return : whether $path is the root of a (mounted) zfs dataset.
#    out : ${$dataset_name}: the name of the zfs dataset mounted onto $path.
#    out : ${$dataset_mountpoint}: the currently visible mountpoint of the zfs dataset mounted onto $path.
##
is_zfs_dataset() {
	local _dataset _fsroot _mountpoint

	# NB: use `findmnt -o TARGET` instead of `zfs list -Ho mountpoint`
	#     because the latter is not namespace-aware
	#     (we might be in a container)
	# && _mountpoint="$(zfs list -Ho mountpoint "$_dataset")" \

	# shellcheck disable=SC2034
	[[ -e "$1" ]] \
	&& is_zfs "$1" \
	&& mountpoint -q "$1" \
	&& _fsroot="$(findmnt --list --noheading --first-only --nofsroot -o FSROOT "$1")" \
	&& [[ "$_fsroot" == / ]] \
	&& _dataset="$(findmnt --list --noheading --first-only --nofsroot -o SOURCE "$1")" \
	&& [[ "$_dataset" && "$_dataset" == */* && "$_dataset" != /* ]] \
	&& _mountpoint="$(findmnt --list --noheading --first-only --nofsroot -o TARGET "$1")" \
	&& [[ "$1" -ef "$_mountpoint" ]] \
	&& printf -v "${2-_}" "%s" "$_dataset" \
	&& printf -v "${3-_}" "%s" "$_mountpoint" \
	# EOL
}

##
#  usage : is_zfs_dataset_or_nothing( $path [, $dataset_name [, $dataset_mountpoint]] )
# return : whether $path is the root of a (mounted) zfs dataset, or true if $path does not exist.
#    out : ${$dataset_name}: the name of the zfs dataset mounted onto $path.
#    out : ${$dataset_mountpoint}: the canonical mountpoint of the zfs dataset mounted onto $path.
##
is_zfs_dataset_or_nothing() {
	[[ ! -e "$1" ]] || is_zfs_dataset "$@"
}

##
#  usage : is_zfs_dataset_or_nothing( $path [, $dataset_name] )
# return : whether $path is the root of a (mounted) zfs dataset, or true if $path does not exist.
#    out : ${$dataset_name}: the name of the zfs dataset mounted onto $path.
#    out : ${$dataset_mountpoint}: the currently visible mountpoint of the zfs dataset mounted onto $path.
##
is_zfs_dataset_subdir() {
	local _dataset _fsroot _mountpoint

	# NB: use `findmnt -o TARGET` instead of `zfs list -Ho mountpoint`
	#     because the latter is not namespace-aware
	#     (we might be in a container)
	# && _mountpoint="$(zfs list -Ho mountpoint "$_dataset")" \

	[[ -e "$1" ]] \
	&& is_zfs "$1" \
	&& _fsroot="$(findmnt --list --noheading --first-only --nofsroot -o FSROOT --target "$1")" \
	&& [[ "$_fsroot" == / ]] \
	&& _dataset="$(findmnt --list --noheading --first-only --nofsroot -o SOURCE --target "$1")" \
	&& [[ "$_dataset" && "$_dataset" == */* && "$_dataset" != /* ]] \
	&& _mountpoint="$(findmnt --list --noheading --first-only --nofsroot -o TARGET --target "$1")" \
	&& [[ "$1" == "$_mountpoint" || "$1" == "$_mountpoint"/* ]] \
	&& printf -v "${2-_}" "%s" "$_dataset" \
	&& printf -v "${3-_}" "%s" "$_mountpoint" \
	# EOL
}

zfs_dataset_clone() {
	local src="$1" dest="$2"
	local dataset clone clonename

	is_zfs_dataset "$src" dataset || return
	clone="$(dirname "$dataset")/$(basename "$dest")"
	clonename="$(basename "$clone")"

	zfs snapshot "$dataset@clone-$clonename" || return
	zfs clone -u "$dataset@clone-$clonename" "$clone" || return
	mount --mkdir -t zfs "$clone" "$dest" -o zfsutil || return
}

zfs_dataset_delete() {
	local src="$1"
	local dataset origin

	[[ -e "$src" ]] || return 0
	is_zfs_dataset "$src" dataset || return
	origin="$(zfs list -Ho origin "$dataset")" || return

	if [[ $origin == *@clone-$(basename "$dataset") ]]; then
		zfs destroy -v -R "$origin" || return
	else
		zfs destroy -v -r "$dataset" || return
	fi
	rm -fd "$src"
}

zfs_dataset_create_at() {
	local dest="$1"
	local dataset mountpoint subdir

	# if $dest is already a zfs dataset, do nothing
	is_zfs_dataset "$dest" dataset && return 0

	is_zfs_dataset_subdir "$dest" dataset mountpoint || return
	# $dest cannot be a dataset itself, it must be a subdirectory
	[[ "$dest" == "$mountpoint"/* ]] || return
	subdir="${dest#"$mountpoint/"}"

	rm -fd "$dest" || return
	zfs create -u -pp "$dataset/$subdir" || return
	mount --mkdir -t zfs "$dataset/$subdir" "$dest" -o zfsutil || return
}

##
#  usage : is_btrfs( $path )
# return : whether $path is on a btrfs
##
is_btrfs() {
	[[ -e "$1" && "$(stat -f -c %T "$1")" == btrfs ]]
}

##
#  usage : is_subvolume( $path )
# return : whether $path is a the root of a btrfs subvolume (including
#          the top-level subvolume).
##
is_subvolume() {
	[[ -e "$1" && "$(stat -f -c %T "$1")" == btrfs && "$(stat -c %i "$1")" == 256 ]]
}

##
#  usage : subvolume_delete_recursive( $path )
#
#    Find all btrfs subvolumes under and including $path and delete them.
##
subvolume_delete_recursive() {
	local subvol

	is_subvolume "$1" || return 0

	while IFS= read -d $'\0' -r subvol; do
		if ! subvolume_delete_recursive "$subvol"; then
			return 1
		fi
	done < <(find "$1" -mindepth 1 -xdev -depth -inum 256 -print0)
	if ! btrfs subvolume delete "$1" &>/dev/null; then
		error "Unable to delete subvolume %s" "$subvol"
		return 1
	fi

	return 0
}

##
#  $1: name of the output array
#  $2...: mirror URL
#  out: ${$1}: extracted cache directories
#
#    Extract potential cache directories from a list of mirror URIs.
##
_extract_cache_dirs () {
	declare -n cache_dirs__=$1
	local mirror m
	shift

	for mirror; do
		if [[ $mirror =~ ^file://(/.+)/\$repo/os/\$arch$ ]]; then
			# full pool mirrors
			mirror="${BASH_REMATCH[1]}"
			for m in "$mirror"/pool/*/; do
				in_array "$m" "${cache_dirs__[@]}" || cache_dirs__+=("$m")
			done
		elif [[ $mirror =~ ^file://(/.+)/?$ ]]; then
			# other arbitrary file:// sources
			mirror="${BASH_REMATCH[1]}"
			in_array "$mirror" "${cache_dirs__[@]}" || cache_dirs__+=("$mirror")
		fi
	done
}
# }}}

##
#  $1: path to pacman.conf
#  $2: name of the output array for R/O cache dirs
#      (inferred from file:/// mirrors)
#  $3: name of the output array for primary cache dirs
#      (if non-empty, not overwritten)
#  out: ${$2}, ${$3}: extracted cache directories
#  out: $host_mirrors, $host_cachemirrors: set according to host pacman.conf
#
#    Extract potential cache directories from a pacman.conf.
#    NOTE: R/O cache dirs must have priority over primary cache dirs
##
get_cache_dirs () {
	local pac_conf=$1
	declare -n cache_dirs_ro_=$2 cache_dirs_rw_=$3
	local use_host_mirrors
	local -a cache_dirs_host
	local repo
	local -a mirrors

	# extract primary cache dirs if it was not set
	if (( ${#cache_dirs_rw_[@]} == 0 )); then
		mapfile -t cache_dirs_rw_ < <(pacman-conf --config "$pac_conf" CacheDir)
	fi

	# check if the primary cache dir is equal to host's
	# (i.e. we are building for the same Arch flavor as the host)
	mapfile -t cache_dirs_host < <(pacman-conf CacheDir)
	if [[ "${cache_dirs_rw_[-1]}" -ef /var/cache/pacman/pkg \
	   && "${cache_dirs_host[-1]}" -ef /var/cache/pacman/pkg ]]; then
	   	use_host_mirrors=1
	fi

	# first, analyze target mirrors (for all repos)
	# we need this to add local repo file:/// mirrors to target's CacheDirs
	while read -r repo; do
		# (note `sed -n 's###p'` is NOT used)
		mapfile -t mirrors < <( \
			pacman-conf --config "$pac_conf" --repo "$repo" Server 2>/dev/null \
			| sed -r "s#(.*/)${repo}/(os/)?$(uname -m)#\1\$repo\2\$arch#" \
		)
		_extract_cache_dirs cache_dirs_ro_ "${mirrors[@]}"
	done < <(pacman-conf --config "$pac_conf" --repo-list)

	# if target's primary cache dir is same as host's, also analyze host mirrors
	# (use [extra] as an exemplar)
	# 1. to pull whole-pool file:/// mirrors to add to target's CacheDirs
	# 2. to pull any CacheServers to add to target's mirrorlist
	if (( use_host_mirrors )); then
		# extract and generalize host mirrors
		# (note `sed -n 's###p'` is used as we only want generalizable mirrors)
		mapfile -t host_mirrors < <( \
			pacman-conf --repo extra Server 2>/dev/null \
			| sed -nr "s#(.*/)extra/(os/)?$(uname -m)#\1\$repo/\2\$arch#p" \
		)
		_extract_cache_dirs cache_dirs_ro_ "${host_mirrors[@]}"

		# ...and cachemirrors
		mapfile -t host_cachemirrors < <( \
			pacman-conf --repo extra CacheServer 2>/dev/null \
			| sed -nr "s#(.*/)extra/(os/)?$(uname -m)#\1\$repo/\2\$arch#p" \
		)
		# also try to extract CacheDirs for completeness
		_extract_cache_dirs cache_dirs_ro_ "${host_cachemirrors[@]}"
	fi

	# final cache_dirs_{ro,rw} looks like this:
	#   cache_dirs_ro=(chroot-conf-mirrors host-conf-mirrors)
	#   cache_dirs_rw=(                                      global-cachedirs)
	# this way, the most relevant directories are listed first (in pacman.conf priority order)
	# this guarantees that we will pick the right cached package even if repos have duplicates
}
