#!/bin/bash

# simple package installs

# written by Nathaniel Maia <natemaia10@gmail.com> 2018-2022
# This program is provided free of charge and without warranty

# shellcheck disable=SC2086,SC2001,SC2059,SC2064

# to use a different build directory: BAPH_BUILDDIR=/new/path baph ...
typeset -r builddir="${BAPH_BUILDDIR:-$HOME/.cache/baph/builds}"

# aur http address
typeset -r auraddr='https://aur.archlinux.org'

typeset auronly noconf noview

use()
{ # show the manpage or version.
	{ [[ $1 == 'v' ]] && printf "baph-1.7\n"; } || man baph
}

msg()
{ # print colour $1 :: then message $2 in bold, usage: msg color text
	{ [[ $1 ]] && prnt "\e[1m%b::\e[0m \e[1m%b\e[0m\n" "$1" "$2"; } || prnt "%b\n" "$2"
}

die()
{ # print string $1 and exit with error code $2, usage: die "text" exitcode
	printf "\e[1;31merror:\e[0m %b\n" "$1" >&2
	exit "${2:-1}"
}

get()
{ # install an AUR package.. usage: get package
	rm -rf "${builddir:?}/$1"
	cd "$builddir" || die "failed to cd to build dir $builddir"

	if hash git >/dev/null 2>&1; then
		msg '\e[34m' "Cloning \e[32m$1\e[0m\e[1m package repo..."
		git clone "$auraddr/$1" || die "failed to clone package repo: $auraddr/$1"
	else
		msg '\e[34m' "Retrieving package archive: $1"
		rm -rf "${builddir:?}/$1*"
		curl -LO#m 30 --connect-timeout 10 "$auraddr/cgit/aur.git/snapshot/$1.tar.gz" || die "failed to download package: $1"
		if [[ -e "$1.tar.gz" ]] && tar -xvf "$1.tar.gz"; then
			rm -f "${builddir:?}/$1.tar.gz"
		else
			die "failed to extract package or not a tar.gz archive: $1"
		fi
	fi

	if [[ -r "$builddir/$1/PKGBUILD" ]]; then
		view "$1" || yesno "Continue building $1" || { rm -rf "${builddir:?}/$1"; return 1; }
		build "$1" || return 1
	else
		die "$builddir/$1 does not contain a PKGBUILD or it is not accessible"
	fi
	return 0
}

prnt()
{ # printf but doesn't do anything when count is set
	[[ $count ]] && return
	printf "$@"
}

view()
{ # view the given PKGBUILD if noview is unset.. usage: view package
	if [[ -z $noview ]] && yesno "View/Edit the PKGBUILD for $1"; then
		${EDITOR:-vi} "$builddir/$1/PKGBUILD"
		cd "$builddir/$1" && makepkg --printsrcinfo > .SRCINFO
		return 1
	fi
	return 0
}

keys()
{ # import PGP keys from package.. usage: keys key1 key2 ...
	for k; do
		k="${k// /}"
		pacman-key --list-keys | grep -q "$k" && continue
		msg '\e[33m' "Resolving missing pgp key for $pkg: $k"
		if ! escalate pacman-key --recv-keys "$k" || ! escalate pacman-key --finger "$k" || ! escalate pacman-key --lsign-key "$k"; then
			msg '\e[33m' "Failed to import pgp key, continuing anyway"
		fi
	done
}

deps()
{ # build package depends.. usage: deps dep1 dep2 ...
	for d; do
		d="$(sed 's/[=<>]=\?[0-9.\-]*.*//g' <<< "$d")"
		if ! pacman -Qsq "^$d$" >/dev/null 2>&1 && ! pacman -Ssq "^$d$" >/dev/null 2>&1; then
			msg '\e[33m' "Resolving \e[32m$pkg\e[0m\e[1m AUR dependency: $d"
			get "$d" || die "failed to build dependency $d"
		fi
	done
}

count()
{
	count=true
	update "$@"
}

yesno()
{ # ask confirmation if noconf is not set, usage: yesno question
	[[ $noconf ]] && return 0
	read -re -p $'\e[34m::\e[0m \e[1m'"$1"$'? [Y/n]\e[0m ' c && [[ -z $c || $c == 'y' || $c == 'Y' ]]
}

build()
{ # build package $1.. usage: build package
	typeset -ga depends=() makedepends=() validpgpkeys=()

	cd "$builddir/$1" || die "failed to cd $builddir/$1"
	[[ -e '.SRCINFO' ]] || makepkg --printsrcinfo > .SRCINFO
	eval "$(sed 's/\s*//' .SRCINFO | grep '^depends\|^makedepends\|^validpgpkeys' | sed 's/ = \(.*\)/+=("\1")/')"
	keys "${validpgpkeys[@]}"
	deps "${depends[@]}" "${makedepends[@]}"
	cd "$builddir/$1" || die "failed to cd $builddir/$1"
	makepkg -sicr $noconf || return 1
	rm -rf ./*.tar.* >/dev/null 2>&1 || return 0 # */
}

search()
{ # search the AUR for queries, usage: search query1 query2 ...
	typeset r

	for q; do
		msg '\e[34m' "Searching AUR for '$q'...\n"
		r="$(curl -sLm 30 --connect-timeout 10 "$auraddr/rpc?v=5&type=search&arg=${q}" 2>/dev/null)"

		if [[ -z $r || $r == *'"resultcount":0'* ]]; then
			prnt "\e[1;31m:: \e[0mno results found for '%s'\n" "$q"
		else
			echo -e "$(sed 's/[]{},]/\n/g' <<< "$r" |
				awk 'BEGIN{s = ""; i = 0}
					/^"Name":/ { i++; gsub(/^"Name":|"/, ""); printf("\\e[1;33m%d \\e[1;37m%s ", i, $0); }
					/^"Version":/ { gsub(/^"Version":|"/, ""); printf("\\e[1;32m%s ", $0); }
					/^"Description":/ { gsub(/^"Description":|"/, ""); s = $0 }
					/^"OutOfDate":/ { if ($0 !~ "null") { printf("\\e[1;31m(Out of Date!)"); } }
					/^$/ { if (s) { printf("\n\\e[0m    %s\n", s); s = "" } }'
					)"
		fi
	done
}

update()
{ # check updates for passed AUR packages or all when none specified
	typeset -i i
	typeset -a up=()
	typeset -A old new
	typeset p='' s='' prnt=''

	# don't ask to view PKGBUILD for already installed AUR packages
	noview='--noview'

	if (( ${#aurpkgs[@]} == 0 )); then
		mapfile -t aurpkgs < <(pacman -Qqm 2>/dev/null)
		[[ $auronly || $count ]] || escalate pacman -Syyu $noconf
	fi

	if (( ${#aurpkgs[@]} == 0 )); then
		if [[ $auronly ]]; then
			msg '\e[34m' "No AUR packages installed.."
			[[ $count ]] && printf "0\n"
		else
			[[ $count ]] && printf "0 %d\n" "$(checkupdates 2>dev/null | wc -l)"
		fi
	else
		if [[ $count && ! $auronly ]]; then
			( checkupdates 2>/dev/null | wc -l > /tmp/paccount ) &
			cpid=$!
		fi

		prnt "\e[1m\e[34m::\e[0m \e[1mSynchronizing AUR package versions...\e[0m\r"
		(
			s="${aurpkgs[*]}"
			pacman -Q "${aurpkgs[@]}" | awk '{print "old["$1"]="$2}' > /tmp/oldver
			curl -sLZm 30 --connect-timeout 10 "$auraddr/rpc?v=5&type=info&by=name&arg[]=${s// /&arg[]=}" 2>/dev/null |
				sed 's/[]{},]/\n/g' | awk '/^"Name":/ { gsub(/^"Name":|"/, ""); printf("new[%s]=", $0); }
							/^"Version":/ { gsub(/^"Version":|"/, ""); printf("%s\n", $0); }' > /tmp/newver
		) &
		pid=$!
		trap "kill $pid $cpid 2> /dev/null" EXIT

		if [[ $count ]]; then
			# wait for curl and checkupdates
			wait $pid $cpid
			unset pid cpid
		else
			# print loading dots while we wait for curl
			i=0
			while [[ $pid ]] && kill -0 $pid 2> /dev/null; do
				if (( ++i + 40 <= $(tput cols) )); then
					s="$(prnt "%${i}s")"
					prnt "\e[1m\e[34m::\e[0m \e[1mSynchronizing AUR package versions${s// /.}\e[0m\r"
				fi
				sleep 0.5
			done
			prnt "\n"
			unset pid
		fi

		eval "$(cat /tmp/oldver /tmp/newver)" # TFW using cat properly O_O

		i=0
		for p in "${aurpkgs[@]}"; do
			if [[ ${new[$p]} && ${old[$p]} && ${new[$p]} != "${old[$p]}" && $(vercmp "${new[$p]}" "${old[$p]}") -gt 0 ]]; then
				up+=("$p")
				prnt+="${p}\e[2m-${new[$p]}\e[0m "
				(( i++ ))
			fi
		done

		# output package update count(s)
		if [[ $count ]]; then
			if [[ $auronly ]]; then
				printf "%d\n" "$i"
			else
				printf "%d %d\n" "$i" "$(< /tmp/paccount)"
			fi
		elif (( i )); then
			msg '\e[34m' "Starting AUR package upgrade..."
			prnt "\n\e[1mPackages (%s)\e[0m %b\n\n" "$i" "$prnt"
			if yesno "Proceed with package upgrade"; then
				for pkg in "${up[@]}"; do
					get "$pkg"
				done
			fi
		else
			msg '' ' there is nothing to do'
		fi
	fi

	return 0
}

install()
{ # loop over input packages and install each
	if (( ${#auronly} && ! ${#aurpkgs[@]} )); then
		if (( ${#pacpkgs[@]} )); then
			die "targets given are official packages (no -a/--auronly)"
		else
			die "no targets specified for install"
		fi
	elif (( ! ${#auronly} && ${#pacpkgs[@]} )); then
		escalate pacman -Syyu "${pacpkgs[@]}" $noconf || exit 1
	fi

	for pkg in "${aurpkgs[@]}"; do
		if curl -sLm 30 --connect-timeout 10 "$auraddr/rpc?v=5&type=info&by=name&arg[]=$pkg" 2>/dev/null | grep -q '"resultcount":[1-9]\+'; then
			get "$pkg" || msg '\e[33m' "Exited $pkg build early"
		else
			die "unable to find package '$pkg', is the name spelled correctly?"
		fi
	done
}

escalate()
{ # escalate the privilege of the command passed in
	if hash sudo >/dev/null 2>&1; then
		sudo "$@"
	elif hash doas >/dev/null 2>&1; then
		doas "$@"
	else
		su -c "$*"
	fi
}

main()
{
	typeset cmd
	typeset -g pkg count
	typeset -ga pacpkgs aurpkgs
	typeset -A desc=(
		[s]='search' [u]='update' [i]='install' [c]='count'
	)
	typeset -A opts=(
		[s]='cmd=search' [u]='cmd=update' [i]='cmd=install' [c]='cmd=count'
		[n]='noview=--noview' [N]='noconf=--noconfirm' [a]='auronly=--auronly'
	)
	while getopts ":hvuicsandN" OPT; do
		case "$OPT" in
			h|v) use "$OPT"; exit 0 ;;
			d)
				export PS4='| ${BASH_SOURCE} LINE:${LINENO} FUNC:${FUNCNAME[0]:+ ${FUNCNAME[0]}()} |>  '
				set -x
				exec 3>| baph.dbg
				BASH_XTRACEFD=3
				;;
			n|N|a|s|u|i|c)
				if [[ $OPT =~ (c|s|u|i) && $cmd == "${desc[$OPT]}" ]]; then
					die "'${desc[$OPT]}' does not support being passed multiple times"
				elif [[ $OPT =~ (c|s|u|i) && $cmd && $cmd != "${desc[$OPT]}" ]]; then
					die "'${desc[${cmd:0:1}]}' and '${desc[$OPT]}' can't be used together"
				fi
				eval "${opts[$OPT]}"
				;;
			\?) die "invalid option: '$OPTARG'" ;;
		esac
	done
	shift $((OPTIND - 1))

	if ! hash checkupdates >/dev/null 2>&1 && [[ $cmd == 'count' ]]; then
		[[ $auronly ]] || die "-c without -a requires the 'pacman-contrib' package installed"
	fi

	[[ $cmd ]] || die "no operation specified (use -h for help)"
	[[ $cmd =~ (update|count) || $# -gt 0 ]] || die "no targets specified for ${desc[${cmd:0:1}]}"

	for arg; do # remaining args are considered packages
		{ pacman -Ssq "^$arg$" >/dev/null 2>&1 && pacpkgs+=("$arg"); } || aurpkgs+=("$arg")
	done

	$cmd "$@"
}

(( UID )) || die "do not run $0 as root"
hash makepkg pacman >/dev/null 2>&1 || die "$0 only supports systems that use pacman as their package manager"
hash su curl >/dev/null 2>&1 || die "$0 requires to following packages: su, curl\n\n\toptional packages: git sudo doas pacman-contrib"
[[ -e "$builddir" && ! -d "$builddir" ]] && die "build directory location already exists and is not a directory: $builddir"
mkdir -p "$builddir" || die "unable to create build directory: $builddir"

trap 'echo ^C; exit' INT
main "$@"


# vim:fdm=marker:fmr={,}
