# Copyright 2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
# shellcheck shell=sh disable=3043

# This file contains functions considered experimental in nature. Any functions
# defined here may eventually be promoted to the core library or to a distinct
# module. They may also be dropped without warning, either because they were
# not considered as being sufficiently within the scope of gentoo-functions as
# a project or because they were deemed to be insufficiently useful. As such, it
# serves as a staging ground for new ideas. Note that GENFUN_API_LEVEL must
# never be incremented on account of any changes made to this module.

warn "sourcing the experimental module from gentoo-functions; no stability guarantee is provided"

#
# Considers the first parameter as an URL then attempts to fetch it with either
# curl(1) or wget(1). If the URL does not contain a scheme then the https://
# scheme shall be presumed. Both utilities shall be invoked in a manner that
# suppresses all output unless an error occurs, and whereby HTTP redirections
# are honoured. Upon success, the body of the response shall be printed to the
# standard output. Otherwise, the return value shall be greater than 0.
#
fetch()
{
	if hash curl 2>/dev/null; then
		fetch()
		{
			if [ "$#" -gt 0 ]; then
				# Discard any extraneous parameters.
				set -- "$1"
			fi
			curl -f -sS -L --connect-timeout 10 --proto-default https -- "$@"
		}
	elif hash wget 2>/dev/null; then
		fetch()
		{
			if [ "$#" -gt 0 ]; then
				# Discard any extraneous parameters.
				case $1 in
					''|ftp://*|ftps://*|https://*)
						set -- "$1"
						;;
					*)
						set -- "https://$1"
				esac
			fi
			wget -nv -O - --connect-timeout 10 -- "$@"
		}
	else
		warn "fetch: this function requires that curl or wget be installed"
		return 127
	fi

	fetch "$@"
}

#
# Prints a horizontal rule. If specified, the first parameter shall be taken as
# a string whose first character is to be repeated in the course of composing
# the rule. Otherwise, or if specified as the empty string, it shall default to
# the <hyphen-minus>. If specified, the second parameter shall define the length
# of the rule in characters. Otherwise, it shall default to the width of the
# terminal if such can be determined, or 80 if it cannot be.
#
hr()
{
	local c hr i length

	# shellcheck disable=2154
	if [ "$#" -ge 2 ] && is_int "$2"; then
		length=$2
	elif _update_tty_level <&1; [ "${genfun_tty}" -eq 2 ]; then
		length=${genfun_cols}
	else
		length=80
	fi
	c=${1--}
	c=${c%"${c#?}"}
	hr=
	i=0
	while [ "$(( i += 16 ))" -le "${length}" ]; do
		hr=${hr}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}${c}
	done
	i=${#hr}
	while [ "$(( i += 1 ))" -le "${length}" ]; do
		hr=${hr}${c}
	done
	printf '%s\n' "${hr}"
}

#
# Expects three parameters, all of which must be integers, and determines
# whether the first is numerically greater than or equal to the second, and
# numerically lower than or equal to the third.
#
int_between()
{
	if [ "$#" -lt 3 ]; then
		warn "int_between: too few arguments (got $#, expected 3)"
		false
	elif ! is_int "$2" || ! is_int "$3"; then
		_warn_for_args int_between "$@"
		false
	else
		is_int "$1" && [ "$1" -ge "$2" ] && [ "$1" -le "$3" ]
	fi
}

#
# Returns 0 provided that two conditions hold. Firstly, that the standard input
# is connected to a tty. Secondly, that the standard output has not been closed.
# This technique is loosely based on the IO::Interactive::Tiny module from CPAN.
#
is_interactive()
{
	test -t 0 && { true 3>&1; } 2>/dev/null
}

#
# Collects the intersection of the parameters up to - but not including - a
# sentinel value then determines whether the resulting set is a subset of the
# intersection of the remaining parameters. If the SENTINEL variable is set, it
# shall be taken as the value of the sentinel. Otherwise, the value of the
# sentinel shall be defined as <hyphen-dash><hyphen-dash>. If the sentinel value
# is not encountered or if either set is empty then the return value shall be
# greater than 1.
#
is_subset()
{
	SENTINEL=${SENTINEL-'--'} awk -f - -- "$@" <<-'EOF'
	BEGIN {
		argc = ARGC
		ARGC = 1
		for (i = 1; i < argc; i++) {
			word = ARGV[i]
			if (word == ENVIRON["SENTINEL"]) {
				break
			} else {
				set1[word]
			}
		}
		if (i == 1 || argc - i < 2) {
			exit 1
		}
		for (i++; i < argc; i++) {
			word = ARGV[i]
			set2[word]
		}
		for (word in set2) {
			delete set1[word]
		}
		for (word in set1) {
			exit 1
		}
	}
	EOF
}

#
# Continuously reads lines from the standard input, prepending each with a
# timestamp before printing to the standard output. Timestamps shall be in the
# format of "%FT%T%z", per strftime(3). Output buffering shall not be employed.
#
prepend_ts()
{
	if hash gawk 2>/dev/null; then
		prepend_ts()
		{
			gawk '{ print strftime("%FT%T%z"), $0; fflush(); }'
		}
	elif hash ts 2>/dev/null; then
		prepend_ts()
		{
			ts '%FT%T%z'
		}
	elif bash -c '(( BASH_VERSINFO >= 4 ))' 2>/dev/null; then
		prepend_ts()
		{
			bash -c 'while read -r; do printf "%(%FT%T%z)T %s\n" -1 "${REPLY}"; done'
		}
	else
		warn "prepend_ts: this function requires that either bash, gawk or moreutils be installed"
		return 1
	fi

	prepend_ts
}

#
# Expects three parameters and determines whether the first is lexicographically
# greater than or equal to the second, and lexicographically lower than or equal
# to the third. The effective system collation shall affect the results, given
# the involvement of the sort(1) utility.
#
str_between()
{
	local i

	if [ "$#" -ne 3 ]; then
		warn "str_between: wrong number of arguments (got $#, expected 3)"
		false
	else
		set -- "$2" "$1" "$3"
		i=0
		# shellcheck disable=2034
		printf '%s\n' "$@" |
		sort |
		while IFS= read -r line; do
			eval "[ \"\${line}\" = \"\${$(( i += 1 ))}\" ]" || return
		done
	fi
}

#
# Takes the first parameter as a string (s), the second parameter as a numerical
# position (m) and, optionally, the third parameter as a numerical length (n).
# It shall then print a <newline> terminated substring of s that is at most, n
# characters in length and which begins at position m, numbering from 1. If n is
# omitted, or if n specifies more characters than are left in the string, the
# length of the substring shall be limited by the length of s. The function
# shall return 0 provided that none of the parameters are invalid.
#
substr()
{
	local i str

	if [ "$#" -lt 2 ]; then
		warn "substr: too few arguments (got $#, expected at least 2)"
		return 1
	elif ! is_int "$2"; then
		_warn_for_args substr "$2"
		return 1
	elif [ "$#" -ge 3 ]; then
		if ! is_int "$3"; then
			_warn_for_args substr "$3"
			return 1
		elif [ "$3" -lt 0 ]; then
			set -- "$1" "$2" 0
		fi
	fi
	str=$1
	i=0
	while [ "$(( i += 1 ))" -lt "$2" ]; do
		str=${str#?}
	done
	i=0
	while [ "${#str}" -gt "${3-${#str}}" ]; do
		str=${str%?}
	done
	if [ "${#str}" -gt 0 ]; then
		printf '%s\n' "${str}"
	fi
}

#
# Takes the first parameter as either a relative pathname or an integer
# referring to a number of iterations. To be recognised as a pathname, the first
# four characters must form the special prefix, ".../". It recurses upwards from
# the current directory until either the relative pathname is found to exist,
# the specified number of iterations has occurred, or the root directory is
# encountered. In the event that the root directory is reached without either of
# the first two conditions being satisfied, the return value shall be 1.
# Otherwise, the value of PWD shall be printed to the standard output.
#
up()
{
	local i

	i=0
	while [ "${PWD}" != / ]; do
		chdir ../
		case $1 in
			.../*)
				test -e "${1#.../}"
				;;
			*)
				test "$(( i += 1 ))" -eq "$1"
		esac \
		&& pwd && return
	done
}
