#!/bin/sh

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

# Configuration file

CONFIG_FILE=%PREFIX%/etc/pkgfe.conf


# Paths

PORTS_PATH=${PORTSDIR:-/usr/ports}

PKGDB_PATH=${PKG_DBDIR:-/var/db/pkg}

TEMP_DIR=${TMPDIR:-/tmp}


# Miscellaneous

PAGER_PROGRAM=${PAGER:-less}


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

#
# Returns program name without path.
#
prog()
{
	basename "$0"
}


#
# Returns "s" if value is not "1".
#
plural()
{
	if [ "$1" -a "$1" -ne 1 ]
	then
		echo s
	fi
}


#
# Removes file
#
unlink_file()
{
	if [ "$1" -a -f "$1" ]
	then
		rm -f "$1"
	fi
}


#
# Prints error and exits.
#
error()
{
	if [ "$1" ]
	then
		echo "`prog`: $1"
		
		cleanexit 1
	fi
}


#
# Prints error.
#
notice()
{
	if [ "$1" ]
	then
		echo "`prog`: $1"
	fi
}


#
# Does clean exit.
#
cleanexit()
{
	if [ ! "$LOCKFILE" ]
	then
		error 'internal error: $LOCKFILE is empty'
	else
		unlink_file "$LOCKFILE"
	fi
	
	
	exit ${1:-0}
}


#
# Prints error when command fails.
#
exitcode()
{
	if [ $# -ne 2 ]
	then
		return 1
	fi
	
	
	echo
	error "command '$1' failed with exit code $2"
}


#
# Show command before executing it.
#
showcmd()
{
	if [ ! "$1" ]
	then
		return 1
	fi
	
	
	echo
	echo "> $1"
}


#
# Checks boolean configuration paramaters.
#
checkyesno()
{
	if [ ! "$1" ]
	then
		return 1
	fi
	
	
	eval local value=\$$1
	
	
	case "$value"
	in
		[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
			
			unset value
			
			return 0
			;;
		
		[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0)
			
			unset value
			
			return 1
			;;
		
		*)
			error "variable '$1' is not set properly in '$CONFIG_FILE'"
			;;
	esac
}


#
# Checks if command is failed
# (exit code is greater than zero).
#
cmd_failed()
{
	if [ "$1" -gt 0 ]
	then
		return 0
	else
		return 1
	fi
}


#
# Creates temporary files.
#
make_temp()
{
	if [ ! "$TEMP_PREFIX" ]
	then
		error 'internal error: $TEMP_PREFIX is empty'
	fi
	
	
	mktemp "$TEMP_PREFIX.XXX" || error 'mktemp error'
}


#
# Shows menu or checklist using dialog(1).
#
show_dialog()
{
	if [ $# -ne 3 -a $# -ne 4 ]
	then
		return 1
	fi
	
	
	local name text options type options_tempfile exitcode selected_options
	
	
	name=$1
	text=$2
	options=$3
	
	type=checklist
	
	
	case $4
	in
		''|checklist)
			
			options_tempfile=`make_temp`
			;;
		
		menu)
			type=$4
			;;
		
		*)
			unset name text options type options_tempfile exitcode selected_options
			
			return 1
			;;
	esac
	
	
	if [ ! "$options" ]
	then
		error 'internal error: $options is empty'
	fi
	
	
	eval "dialog --title '`prog`: $name' \
	--$type '\n$text\n' \
	-1 -1 $dialog_list_rows \
	$options \
	2> '${options_tempfile:-/dev/null}'"
	
	
	exitcode=$?
	
	eval selopt_${name}_exitcode=\$exitcode
	
	unset text options exitcode
	
	
	if [ "$type" = checklist ]
	then
		selected_options=`sed 's|"||g' < "$options_tempfile"`
		
		unlink_file "$options_tempfile"
		
		unset options_tempfile
		
		
		eval selopt_${name}_values="\$selected_options"
		
		unset selected_options
	fi
	
	unset name type
}


#
# Shows confirm dialog.
#
show_confirm()
{
	if [ $# -ne 2 -a "$1" -a "$2" ]
	then
		return 1
	fi
	
	
	local type package packages count dialog_options confirm_message cancel_message
	
	
	type=$1
	packages=$2
	
	
	for package in $packages
	do
		count=$(($count + 1))
		
		dialog_options="$dialog_options '$package' ' '"
	done
	
	unset package packages
	
	
	case $type
	in
		upgrade)
			confirm_message="Upgrade $count package`plural $count`?"
			
			cancel_message='Upgrade cancelled'
			;;
		
		deinstall)
			confirm_message="Deinstall $count package`plural $count`?\nARE YOU SURE?"
			
			cancel_message='Deinstall cancelled'
			;;
		
		install)
			confirm_message="Install $count package`plural $count`?"
			
			cancel_message='Install cancelled'
			;;
		
		*)
			error "show_confirm: internal error: invalid type '$type'"
			;;
	esac
	
	unset type count
	
	
	show_dialog confirm "$confirm_message" "$dialog_options" menu
	
	unset dialog_options confirm_message
	
	
	if cmd_failed $selopt_confirm_exitcode
	then
		echo
		echo "$cancel_message"
		echo
		
		cleanexit
	fi
	
	unset cancel_message
}


#
# Shows package deinstall dialog.
#
package_deinstall()
{
	local packages command exitcode
	
	
	packages=$1
	
	
	if [ ! "$packages" ]
	then
		echo
		echo 'No packages selected for deinstall'
		echo
		
		cleanexit
	fi
	
	
	show_confirm deinstall "$packages"
	
	
	command="pkg_deinstall $packages"
	
	unset packages
	
	
	if ! checkyesno enable_real_deinstall
	then
		cat <<- EOF
		
		Execute that manually:
		
		$command
		
		EOF
		
		cleanexit
	fi
	
	
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	else
		unset command exitcode
		
		if checkyesno listup_after_deinstall
		then
			echo
			do_list
		fi
	fi
	
	
	cleanexit
}


#
# Command:
#  shows help. for more info, look EOF.
#
do_help()
{
	grep -A100 '^#__HELP__$' "$0" | \
	sed "s|^#||; s|^__HELP__||; s|%P%|`prog`|g; s|%PORTS_PATH%|$PORTS_PATH|g; s|%PKGDB_PATH%|$PKGDB_PATH|g"
	
	cleanexit
}


#
# Command:
#  shows list of outdated packages.
#
do_list()
{
	local tempfile command exitcode count
	
	
	tempfile=`make_temp`
	
	
	command='portversion -vl <'
	
	showcmd "$command"
	
	$command > "$tempfile"
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		unlink_file "$tempfile"
		
		exitcode "$command" $exitcode
	fi
	
	unset command exitcode
	
	
	sed 's|<||; s|needs updating (port has ||; s|) *$||; s|) (| |' \
	< "$tempfile" \
	> "$status_file"
	
	unlink_file "$tempfile"
	
	
	if ! checkyesno ignore_held
	then
		tempfile=`make_temp`
		
		cat "$status_file" > "$tempfile"
		
		grep -v '\[held\]' < "$tempfile" > "$status_file"
		
		unlink_file "$tempfile"
	fi
	
	unset tempfile
	
	
	count=`wc -l "$status_file" | awk '{print $1}'`
	
	echo
	echo '-----------------------------------'
	echo "Found $count outdated package`plural $count`"
	echo '-----------------------------------'
	
	
	if [ "$count" -eq 0 -a "$1" = exit-if-none ]
	then
		cleanexit
	fi
	
	unset count
	
	
	echo
}


#
# Command:
#  updates ports tree using csup(1).
#
do_csup()
{
	local command exitcode
	
	
	check_supfile "$csup_supfile"
	
	
	command="csup $csup_args $csup_supfile"
	
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	
	command='portsdb -Fu'
	
	echo
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	unset command exitcode
	
	
	echo
	do_list
}


#
# Command:
#  updates ports tree using portsnap(8).
#
do_portsnap()
{
	local command exitcode
	
	
	command='portsnap fetch'
	
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	
	command='portsnap update'
	
	echo
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	
	command='portsdb -u'
	
	echo
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	unset command exitcode
	
	
	echo
	do_list
}


#
# Command:
#  fixes package database.
#
do_fixdb()
{
	local command exitcode
	
	
	command='pkgdb -F'
	
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	unset command exitcode
	
	
	echo
	
	cleanexit
}


#
# Command:
#  does search in ports tree.
#
do_find()
{
	local search_string type
	
	
	search_string=$1
	
	type=name
	
	
	case $search_string
	in
		name|key)
			if [ ! "$2" ]
			then
				error 'usage: name|key searchstring'
			fi
			
			
			search_string=$2
			
			type=$1
			;;
	esac
	
	
	if [ ! "$search_string" ]
	then
		error 'empty search string'
	fi
	
	
	showcmd "cd $PORTS_PATH && make search $type=$search_string"
	
	cd "$PORTS_PATH" || cleanexit 1
	
	make search "$type=$search_string" | "$PAGER_PROGRAM"
	
	unset search_string type
	
	
	cleanexit
}


#
# Command:
#  does search in ports tree
#  and shows install dialog.
#
do_find_install()
{
	local tempfile search_string index_file name comments count dialog_options
	
	
	search_string=$1
	
	
	if [ -f "$PORTS_PATH/INDEX-6" ]
	then
		index_file="$PORTS_PATH/INDEX-6"
		
	elif [ -f "$PORTS_PATH/INDEX" ]
	then
		index_file="$PORTS_PATH/INDEX"
		
	else
		error 'no valid INDEX found'
	fi
	
	
	if [ ! "$search_string" ]
	then
		error 'search regex is required'
	fi
	
	
	tempfile=`make_temp`
	
	grep -iE "$search_string" "$index_file" | cut -d '|' -f 1,4 | tr '|' ' ' > "$tempfile"
	
	unset index_file
	
	
	if [ ! -s "$tempfile" ]
	then
		unlink_file "$tempfile"
		
		error "no packages matching '$search_string' found"
	fi
	
	unset search_string
	
	
	while read name comments
	do
		count=$(($count + 1))
		
		dialog_options="$dialog_options '$name' '`echo "$comments" | awk '{print substr($0, 0, 26)}'`...' OFF"
	done \
	< "$tempfile"
	
	unlink_file "$tempfile"
	
	unset name comments tempfile
	
	
	show_dialog find "Found $count package`plural $count`.\nSelect one`plural $count` you want to install" "$dialog_options" checklist
	
	unset count dialog_options
	
	
	if [ ! "$selopt_find_values" ]
	then
		echo
		echo 'No packages selected for install'
		echo
		
		cleanexit
	fi
	
	
	show_confirm install "$selopt_find_values"
	
	
	do_portinstall_portupgrade portinstall $selopt_find_values
}


#
# Command:
#  searches for local packages.
#
do_ls()
{
	local search_string=$1
	
	
	if [ ! "$search_string" ]
	then
		error 'search regex is required'
	fi
	
	
	ls -1 "$PKGDB_PATH" | \
	grep -v ^pkgdb\. | \
	grep -iE "$search_string"
	
	
	if cmd_failed $?
	then
		error "no packages matching '$search_string' found"
	fi
	
	unset search_string
	
	
	cleanexit
}


#
# Command:
#  deinstalls not required packages.
#
do_notreq()
{
	local old_wd name count dialog_options
	
	
	old_wd=`pwd`
	
	cd "$PKGDB_PATH" || cleanexit 1
	
	
	for name in *
	do
		if [ ! $name = pkgdb.db -a ! -f $name/+REQUIRED_BY ]
		then
			count=$(($count + 1))
			
			dialog_options="$dialog_options '$name' 'no +REQUIRED_BY' OFF"
		fi
	done
	
	cd "$old_wd" || cleanexit 1
	
	unset old_wd name
	
	
	show_dialog notrequired "Found $count not required package`plural $count`\nSelect one`plural $count` you want to deinstall" "$dialog_options"
	
	unset count dialog_options
	
	
	package_deinstall "$selopt_notrequired_values"
}


#
# Command:
#  deinstalls packages.
#
do_deinst()
{
	local old_wd name count dialog_options
	
	
	old_wd=`pwd`
	
	cd "$PKGDB_PATH" || cleanexit 1
	
	
	for name in *
	do
		if [ ! $name = pkgdb.db ]
		then
			count=$(($count + 1))
			
			dialog_options="$dialog_options '$name' ' ' OFF"
		fi
	done
	
	cd "$old_wd" || cleanexit 1
	
	unset old_wd name
	
	
	show_dialog deinstall "Found $count package`plural $count`.\nSelect one`plural $count` you want to deinstall" "$dialog_options"
	
	unset count dialog_options
	
	
	package_deinstall "$selopt_deinstall_values"
}


#
# Command:
#  updates portaudit database.
#
do_padb()
{
	local command exitcode
	
	
	command="portaudit -Fd"
	
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
	fi
	
	unset command exitcode
	
	
	echo
}


#
# Command:
#  runs portinstall(1) or portupgrade(1) with
#  verbose mode and additional arguments.
#
do_portinstall_portupgrade()
{
	local program command exitcode
	
	
	program=$1
	
	shift
	
	
	case $program
	in
		portinstall|portupgrade)
			;;
		*)
			error "do_portinstall_portupgrade: internal error: invalid program '$program'"
			;;
	esac
	
	
	if checkyesno update_portaudit_db
	then
		echo
		do_padb
	fi
	
	
	command="$program -v $portupgrade_args $*"
	
	showcmd "$command"
	
	$command
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		exitcode "$command" $exitcode
		
	elif [ $program = portupgrade ]
	then
		if checkyesno listup_after_upgrade
		then
			echo
			do_list
		fi
	fi
	
	unset program command exitcode
	
	
	echo
	
	cleanexit
}


#
# Command:
#  checks for existence of required programs.
#
do_checkbinaries()
{
	local binary exitcode
	
	
	exitcode=0
	
	
	echo 'Checking for existence of required programs...'
	echo
	
	
	for binary in portupgrade portversion \
	dialog csup portsdb pkgdb portaudit \
	pkg_deinstall $PAGER_PROGRAM \
	sed grep cat touch id uname cut portsnap
	do
		which "$binary" > /dev/null
		
		if cmd_failed $?
		then
			notice "$binary: program not found"
			
			exitcode=1
		else
			echo "$binary: OK"
		fi
	done
	
	unset binary
	
	
	cleanexit $exitcode
}


#
# Checks configuration file.
#
check_configfile()
{
	if [ ! -f "$1" ]
	then
		error "$1: configuration file not found"
		
	elif [ ! -r "$1" ]
	then
		error "$1: configuration file not readable"
	fi
}


#
# Checks (and creates) status file.
#
check_statusfile()
{
	if [ ! "$1" ]
	then
		error "please define status_file in $CONFIG_FILE"
	fi
	
	
	if [ ! -r "$1" ]
	then
		touch "$1"
		
		if cmd_failed $?
		then
			error "cannot create status file '$1'"
		fi
	fi
}


#
# Checks csup supfile.
#
check_supfile()
{
	if [ ! "$1" ]
	then
		error "please define csup_supfile in $CONFIG_FILE"
		
	elif [ ! -r "$1" ]
	then
		error "$1: file not readable"
	fi
}


#
# Checks portupgrade log directory.
#
check_portupgrade_logdir()
{
	if [ ! "$1" ]
	then
		return 0
	fi
	
	if [ ! -d "$1" ]
	then
		notice "$1: directory not found"
		
	elif [ ! -w "$1" ]
	then
		notice "$1: directory not writable"
		
	else
		return 0
	fi
	
	
	cat <<- EOF
	
	If you don't want logging,
	comment out following varibles in $CONFIG_FILE:
	
	portupgrade_logdir
	portupgrade_args
	EOF
	
	cleanexit 1
}


#
# Checks (and creates) lock file.
#
check_lockfile()
{
	if [ ! "$LOCKFILE" ]
	then
		error 'internal error: $LOCKFILE is empty'
	fi
	
	
	if [ -f "$LOCKFILE" ]
	then
		notice "lock file '$LOCKFILE' exists"
		
		cat <<- EOF
		
		This probably means that you have
		another `prog` session running.
		If there are no active sessions,
		then it's probably a result of
		unfinished session. In this case,
		you must delete the file manually,
		before you can run `prog` again.
		EOF
		
		exit 1
	fi
	
	
	touch "$LOCKFILE"
	
	if cmd_failed $?
	then
		notice "cannot create lock file '$LOCKFILE'"
		exit 1
	fi
}


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

if [ `id -u` -gt 0 ]
then
	error 'requires root'
fi


if [ `uname -s` != FreeBSD ]
then
	error 'requires FreeBSD'
fi


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

check_configfile "$CONFIG_FILE"

. "$CONFIG_FILE"


: ${portupgrade_args=""}
: ${csup_args=""}
: ${dialog_list_rows="10"}
: ${listup_after_upgrade="YES"}
: ${ignore_held="YES"}
: ${update_portaudit_db="YES"}
: ${enable_real_deinstall="NO"}
: ${listup_after_deinstall="YES"}


check_portupgrade_logdir "$portupgrade_logdir"

check_statusfile "$status_file"


TEMP_PREFIX="$TEMP_DIR/`prog`.tmp"


LOCKFILE="$TEMP_DIR/`prog`.lock"

check_lockfile


trap 'cleanexit 1' 2 15


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

case $1
in
	listup|list)
		
		do_list
		cleanexit
		;;
	
	cvsup|cvs|csup)
		
		do_csup
		cleanexit
		;;
	
	portsnap|snap)
		
		do_portsnap
		cleanexit
		;;
	
	fixdb|fix)
		
		do_fixdb
		;;
	
	find|portfind)
		
		do_find "$2" "$3"
		;;
	
	findi|findv)
		
		do_find_install "$2"
		;;
	
	ls|lspkg)
		
		do_ls "$2"
		;;
	
	chkreq|notreq|noreq)
		
		do_notreq
		;;
	
	deinst|deins)
		
		do_deinst
		;;
	
	padb)
		do_padb
		cleanexit
		;;
	
	all)
		do_csup
		;;
	
	listup+run|list+run)
		
		do_list
		;;
	
	install|i)
		
		shift
		do_portinstall_portupgrade portinstall $*
		;;
	
	upgrade|u)
		
		shift
		do_portinstall_portupgrade portupgrade $*
		;;
	
	chkbin)
		do_checkbinaries
		;;
	
	help)
		do_help
		;;
	
	'')
		;;
	
	*)
		error "$1: invalid parameter"
		;;
esac


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

if [ ! -s "$status_file" ]
then
	do_list exit-if-none
fi


while read name comments
do
	count=$(($count + 1))
	
	dialog_options="$dialog_options '$name' '< $comments' OFF"
done \
< "$status_file"

unset name comments


show_dialog list "Found $count outdated package`plural $count`\nSelect one`plural $count` you want to upgrade" "$dialog_options"

unset count dialog_options


if [ ! "$selopt_list_values" ]
then
	echo
	echo 'No packages selected for upgrade'
	echo
	
	cleanexit
fi


show_confirm upgrade "$selopt_list_values"


do_portinstall_portupgrade portupgrade $selopt_list_values


#__HELP__
# Package Frontend help
#-----------------------
#
#%P%           Show upgrade dialog
#
#%P% list      Update list of outdated packages,
#                using "portversion -vl <"
#
#%P% cvs       Update ports tree using csup
#                Run "portsdb -Fu"
#                Run %P% list
#
#%P% snap      Update ports tree using portsnap
#                Run %P% list
#
#%P% all       Run %P% cvs
#                Run %P%
#
#%P% snap+run  Run %P% snap
#                Run %P%
#
#%P% list+run  Run %P% list
#                Run %P%
#	
#%P% fix       Run "pkgdb -F"
#	
#%P% find      Run "make search" in %PORTS_PATH%
#
#%P% findi     Search for packages and show install dialog
#
#%P% ls        Search packages in %PKGDB_PATH%
#
#%P% noreq     Check for not required packages
#
#%P% deins     Deinstall package(s) by name
#
#%P% padb      Update portaudit database, using "portaudit -Fd"
#
#%P% i         Run portinstall with -v $portupgrade_args
#
#%P% u         Run portupgrade with -v $portupgrade_args
#
#%P% chkbin    Check for existence of required programs
#
#%P% help      This help
#
