#!/bin/sh
#
# Alice Package Dependency Resolver
#
# Single owner of the dependency graph. Used by apkg-src and apkg-bin.
#
# Backends:
#   source  resolve against recipe repos ($APKG_REPO, depends/makedepends files)
#   binary  resolve against a binary package index when $APKG_RESOLV_DB points
#           to an APKGBINDB file (apkg-bin format)
#

msg() {
	printf "%s\n" "[apkg-resolv] $*" >&2
}

die() {
	[ "$1" ] && printf "%s\n" "error: $1" >&2
	exit 1
}

solve_alias() {
	[ "$APKG_ALIAS" ] || {
		printf "%s\n" $@
		return
	}
	while [ "$1" ]; do
		d=$(printf "%s\n" $APKG_ALIAS | tr ' ' '\n' | grep ^$1:* | head -n1 | awk -F : '{print $2}')
		printf "%s\n" ${d:-$1}
		shift
	done
}

# return 0 if the backend knows about the package
resolv_have() {
	if [ "$APKG_RESOLV_DB" ]; then
		grep -q "^$1#" "$APKG_RESOLV_DB" 2>/dev/null
	else
		resolv_pkgpath "$1" >/dev/null
	fi
}

# print recipe directory of a package (source backend)
resolv_pkgpath() {
	[ "$APKG_REPO" ] || die 'No repo configured'
	for r in $APKG_REPO; do
		[ -f $r/$1/abuild ] && {
			printf "%s\n" $r/$1
			return 0
		}
	done
	return 1
}

# print direct dependencies of $1, one per line, alias-substituted
resolv_deps() {
	if [ "$APKG_RESOLV_DB" ]; then
		line=$(grep "^$1#" "$APKG_RESOLV_DB" 2>/dev/null | head -n1)
		[ "$line" ] || return 1
		deps=$(printf "%s\n" "$line" | cut -d'|' -f4)
		[ "$deps" ] && solve_alias $(printf "%s\n" "$deps" | tr ',' ' ')
		return 0
	fi
	path=$(resolv_pkgpath "$1") || return 1
	if [ "$include_makedepends" ] && [ -s $path/makedepends ]; then
		makedeps=$(grep -Ev ^'(#|$)' $path/makedepends | awk '{print $1}')
	fi
	if [ -s $path/depends ]; then
		solve_alias $(grep -Ev ^'(#|$)' $path/depends | awk '{print $1}') $makedeps
	elif [ "$makedeps" ]; then
		solve_alias $makedeps
	fi
}

# recursive depth-first traversal; appends to $RESOLV_DEPS in install order
resolv_visit() {
	# skip packages unknown to the backend
	if ! resolv_have "$1"; then
		# warn only for nested deps, like apkg-bin used to
		if [ "$APKG_RESOLV_DB" ] && [ "$RESOLV_PROCESS" ]; then
			msg "Warning: '$1' not found in APKGBINDB, skipping"
		fi
		return 0
	fi
	# track processed pkgs to avoid cycle deps
	RESOLV_PROCESS="$RESOLV_PROCESS $1"
	for d in $(resolv_deps "$1"); do
		# skip if dep is itself
		[ "$d" = "$1" ] && continue
		# if already installed, skip
		if [ "$APKG_RESOLV_SKIP_INSTALLED" ]; then
			[ -s $SPM_PKGDB/$d ] && continue
		fi
		# if dep already in process list, skip, cycle deps detected
		printf "%s\n" $RESOLV_PROCESS | tr ' ' '\n' | grep -Fxq "$d" && continue
		# skip if dep already resolved
		[ "$RESOLV_DEPS" ] && {
			printf "%s\n" $RESOLV_DEPS | tr ' ' '\n' | grep -Fxq "$d" && continue
		}
		resolv_visit "$d"
	done
	# no deps anymore to check, add it to the resolved list
	[ "$RESOLV_DEPS" ] && {
		printf "%s\n" $RESOLV_DEPS | tr ' ' '\n' | grep -Fxq "$1" && return 0
	}
	RESOLV_DEPS="$RESOLV_DEPS $1"
}

# print full dependency list of $@ in install order, one per line
resolv_deplist() {
	RESOLV_DEPS=""
	RESOLV_PROCESS=""
	for p in $@; do
		# if already resolved, dont check again
		if [ "$RESOLV_DEPS" ]; then
			printf "%s\n" $RESOLV_DEPS | tr ' ' '\n' | grep -Fxq "$p" && continue
		fi
		resolv_visit "$p"
	done
	[ "$RESOLV_DEPS" ] && printf "%s\n" $RESOLV_DEPS | tr ' ' '\n' | grep -v ^$
}

# print direct dependencies of $1, one per line
resolv_direct() {
	resolv_deps "$1"
}

# print packages that directly depend on $1, one per line
resolv_dependents() {
	if [ "$APKG_RESOLV_DB" ]; then
		[ -f "$APKG_RESOLV_DB" ] || return 0
		while IFS= read -r line; do
			[ "$line" ] || continue
			deps=$(printf "%s\n" "$line" | cut -d'|' -f4)
			for d in $(printf "%s\n" "$deps" | tr ',' ' '); do
				[ "$d" = "$1" ] && {
					printf "%s\n" "${line%%#*}"
					break
				}
			done
		done < "$APKG_RESOLV_DB"
		return 0
	fi
	[ "$APKG_REPO" ] || die 'No repo configured'
	for r in $APKG_REPO; do
		rr="$rr $r/*/depends"
	done
	if [ "$include_makedepends" ]; then
		for r in $APKG_REPO; do
			rr="$rr $r/*/makedepends"
		done
	fi
	grep -Fx "$1" $rr 2>/dev/null | rev | cut -d / -f 2 | rev
}

resolv_help() {
	cat << EOF
usage: ${0##*/} <option> <pkg(s)>

options:
  -d <pkg>      list direct dependencies
  -dd <pkg>     list direct dependencies including makedepends
  -D <pkg(s)>   list all dependencies in install order
  -DD <pkg(s)>  list all dependencies in install order including makedepends
  -j <pkg>      list all dependents
  -jj <pkg>     list all dependents including makedepends
  -h            print this help message

backends:
  source (default)  resolve against recipe repos in \$APKG_REPO
  binary            resolve against a binary package index when
                    \$APKG_RESOLV_DB points to an APKGBINDB file

environment variables:
  APKG_REPO                  space-separated recipe repo directories (source backend)
  APKG_RESOLV_DB             path to an APKGBINDB file (binary backend)
  APKG_RESOLV_SKIP_INSTALLED skip installed packages during -D/-DD traversal
  APKG_ALIAS                 dependency substitution (name:replacement pairs)
  SPM_PKGDB                  installed package database directory

EOF
	exit 0
}

umask 022

SPM_PKGDB="${SPM_PKGDB:-${APKG_ROOT%/}/var/lib/spm/db}"

[ "$APKG_RESOLV_DB" ] && [ ! -f "$APKG_RESOLV_DB" ] && \
	die "index file not found: $APKG_RESOLV_DB"

while [ "$1" ]; do
	case $1 in
		-d|-dd|-D|-DD|-j|-jj) opt=$1;;
		-h) resolv_help;;
		-*) die "invalid option '$1'";;
		*) pkgs="$pkgs $1";;
	esac
	shift
done

[ "$opt" ] || resolv_help
[ "$pkgs" ] || die "no package specified"

case $opt in
	-d)  resolv_direct $pkgs;;
	-dd) include_makedepends=1; resolv_direct $pkgs;;
	-D)  resolv_deplist $pkgs;;
	-DD) include_makedepends=1; resolv_deplist $pkgs;;
	-j)  for p in $pkgs; do resolv_dependents $p; done;;
	-jj) include_makedepends=1; for p in $pkgs; do resolv_dependents $p; done;;
esac

exit 0
