#!/bin/sh

# popcon-retirement is a neat frontend for popcon-nodependency displaying a list of
# <OLD> packages with dialog. Packages may be selected for removal
# with apt-get which is then called to do the work. 

# Author: Christophe Lohr <clohr@users.sourceforge.net>
# License: GPL
# Mon, 11 May 2009 15:27:31 +0200

# Acknowledgment: popcon-retirement is based on the tool "orphaner"
# (It is mostly a syntactic rearrangement... so check the code!)

set -e

VERSION="0.1"

OPTIONS=$@
VALIDOPTIONS='^-([rs])[[:space:]]$'
VALIDKEEPOPTIONS='^-([rs])[[:space:]]$'
SKIPAPT=0

# LC_COLLATE=pl_PL or similar breaks the script under some circumstances, see
# Debian bug #495818.
export LC_COLLATE=C

if which gettext > /dev/null; then
	. gettext.sh
else
	gettext() {
		echo "$@"
	}
fi

TEXTDOMAIN=popcon-nodependency
export TEXTDOMAIN
# xgettext:sh-format
USAGE=$(gettext 'Usage: %s [--help|--purge|--skip-apt] [popcon-nodependency options]')'\n'
# xgettext:no-sh-format
SEE_POPCON_RETIREMENT=$(gettext 'See popcon-retirement(8) and popcon-nodependency(1) for a list of valid options.')
# xgettext:sh-format
INVALID_BASENAME=$(gettext 'Invalid basename: %s.')'\n'
# xgettext:sh-format
INVALID_OPTION=$(gettext '%s: Invalid option: %s.')'\n'
# xgettext:sh-format
MISSING_DIALOG=$(gettext '%s: You need "dialog" in $PATH to run this frontend.')'\n'
# xgettext:no-sh-format
SCREEN_TOO_SMALL=$(gettext 'Screen too small or set $LINES and $COLUMNS.')

# xgettext:no-sh-format
EDIT_KEEP_INSTRUCTION=$(gettext 'Select packages that should never be recommended for removal in popcon-nodependency:')
# xgettext:no-sh-format
POPCON_RETIREMENT_INSTRUCTION=$(gettext 'Select packages for removal or cancel to quit:')

# xgettext:no-sh-format
NO_OLD_FOUND=$(gettext 'No old packages found.')
# xgettext:no-sh-format
POPCON_NODEPENDENCY_REMOVED=$(gettext '"popcon-nodependency" got removed.  Exiting.')
# xgettext:no-sh-format
APT_GET_REMOVED=$(gettext '"apt" got removed.  Exiting.')
# xgettext:no-sh-format
APT_GET_LOCKFAIL=$(gettext '"apt" is not installed, broken dependencies found or could not open lock file, are you root?  Printing "apt-get" commandline and exiting:')
SKIPAPT_SET=$(gettext 'Explicitly specified status file or requested calling "apt-get" to be skipped.  Printing "apt-get" commandline and exiting:')
# xgettext:no-sh-format
REMOVING=$(gettext 'Removing %s')'\n'

# xgettext:no-sh-format
POPCON_NODEPENDENCY_ERROR=$(gettext '"popcon-nodependency" returned with error.')
# xgettext:sh-format
APT_GET_ERROR=$(gettext '"apt-get" returned with exitcode %s.')'\n'
# xgettext:sh-format
DIALOG_ERROR=$(gettext '"dialog" returned with exitcode %s.')'\n'
# xgettext:no-sh-format
NUMBER_OF_PACKAGES_ERROR=$(gettext '"apt-get" tries to remove more packages than requested by "popcon-retirement".  Exiting.')'\n'

# xgettext:no-sh-format
SIMULATE_BUTTON=$(gettext 'Simulate')

# xgettext:no-sh-format
PRESS_ENTER_TO_CONTINUE=$(gettext 'Press enter to continue.')

# xgettext:no-sh-format
SHOULD_UPDATE_POPCON=$(gettext 'Should I update popularity-contest?')
# xgettext:no-sh-format
WAIT_UPDATE_POPCON=$(gettext 'Updating popularity-contest, please wait.')


if ! which dialog >/dev/null ; then
	printf "$MISSING_DIALOG" $0 >&2
	exit 1
fi

# Plea for help?
case " $OPTIONS " in
	*" --help "*|*" -h "*)
		printf "$USAGE" $0
		echo
		echo $SEE_POPCON_RETIREMENT
		exit 0
		;;
esac

# Adapt to terminal size
if [ -n "${LINES:-}" -a -n "${COLUMNS:-}" ]; then
	# Can't use LINES, because it colides with magic variable
	# COLUMNS ditto
	lines=$(($LINES - 7))
	columns=$(($COLUMNS - 10))

	# unset these magic variables to avoid unwished effects
	unset LINES COLUMNS
else
	size=$(stty size)
	lines=$((${size% *} - 7))
	columns=$((${size#* } - 10))

	sigwinch_handle()
	{
		size=$(stty size)
		lines=$((${size% *} - 7))
		columns=$((${size#* } - 10))

		if [ $lines -ge 12 -a $columns -ge 50 ]; then
			LISTSIZE="$lines $columns $(($lines - 7))"
			BOXSIZE="$lines $columns"
		fi
	}

	case "${BASH_VERSION:+bash}" in
		bash) trap sigwinch_handle SIGWINCH;;
		*) trap sigwinch_handle 20 28;;
	esac
fi

if [ $lines -lt 12 -o $columns -lt 50 ]; then
	echo $SCREEN_TOO_SMALL >&2
	exit 1
fi

LISTSIZE="$lines $columns $(($lines - 7))"
BOXSIZE="$lines $columns"

# Should one use "/var/lib/deborphan/keep" 
# and "deborphan --*keep*" facilities 
# or should one implements dedicated stuff?
editkeepers() { #{{{
	for each in $OPTIONS; do
		if [ "$SKIPONE" = "1" ]; then
			SKIPONE=0;
		elif [ " $each" = " -k" ]; then
			SKIPONE=1;
		elif ! echo "$each " | egrep $VALIDKEEPOPTIONS >/dev/null; then
			case "$each" in
				-k*)
					;;
				*)
					printf "$INVALID_OPTION" $0 $each >&2
					exit 1
					;;
			esac;
		fi
	done

	RETIRED=`keeping_list $OPTIONS | sort`;
	# insert clever error handling

	if [ -n "$RETIRED" ]; then
		PACKAGES=`tempfile`;
		ERROR=0
		dialog \
			--backtitle "popcon-retirement $VERSION" \
			--separate-output \
			--title "popcon-retirement $VERSION" \
			--checklist "$EDIT_KEEP_INSTRUCTION" \
			$LISTSIZE \
			$RETIRED \
			2> $PACKAGES || ERROR=$?

		case $ERROR in
			0) # OK-Button
				if LC_MESSAGES=C deborphan --help | grep -q 'Do not read debfoster'; then
					NODF="--df-keep"
				fi

				deborphan ${NODF} --zero-keep $OPTIONS
				if [ -s $PACKAGES ]; then
					deborphan --add-keep - $OPTIONS < $PACKAGES
				fi
				;;
			*) # other button or state
				# do nothing
		esac
		rm $PACKAGES
	fi
} #}}}

keeping_list() { #{{{
	{
		{ deborhpan -a $@ || echo "ERROR"; } \
			| while read SECTION PACKAGE; do
			echo $PACKAGE $SECTION off
		done
		{ deborphan -L $@ 2>/dev/null || echo "ERROR"; } \
			| while read PACKAGE; do
			echo $PACKAGE "." on
		done
	} | sort
} #}}}


popcon_nodependency_list() { #{{{
	{ popcon-nodependency $@ < $TMP_LARGEST_UNUSED || echo "ERROR"; } \
		| while read SIZE PACKAGE; do
		echo $PACKAGE $SIZE off
	done
} #}}}


doretirement() { #{{{
	# Check options {{{
	skipone=0
	for each in $OPTIONS; do
		if [ "$skipone" = "1" ]; then
			skipone=0;
		elif [ " $each" = " -k" ]; then
			skipone=1;
		elif ! echo "$each " | egrep -q $VALIDOPTIONS; then
			case "$each" in
				-k*)
					;;
				*)
					printf "$INVALID_OPTION" $0 $each >&2
					exit 1
					;;
			esac
		fi
	done #}}}

	TMPFILE=`tempfile`
	TMP_LARGEST_UNUSED=`tempfile`
	trap "rm -f $TMPFILE $TMP_LARGEST_UNUSED" EXIT INT

	EXCLUDE=
	RETIRED=
	# Don't touch the next two lines! This is correct! NL should be the newline
	# character
	NL='
'
	POPULARITY_CONTEST_LOG="/var/log/popularity-contest"
	if [ ! -e $POPULARITY_CONTEST_LOG ] || dialog \
			--backtitle "popcon-retirement $VERSION" \
			--title "popcon-retirement $VERSION" \
			--yesno "$SHOULD_UPDATE_POPCON\n`ls -gh $POPULARITY_CONTEST_LOG`" \
			$BOXSIZE ; then
		dialog \
			--backtitle "popcon-retirement $VERSION" \
			--title "popcon-retirement $VERSION" \
			--infobox "$WAIT_UPDATE_POPCON\npopularity-contest > $POPULARITY_CONTEST_LOG" \
			$BOXSIZE
		popularity-contest > $POPULARITY_CONTEST_LOG
	fi
	popcon-largest-unused > $TMP_LARGEST_UNUSED

	while true; do

		OLD_RETIRED="$RETIRED"
		RETIRED=$(popcon_nodependency_list $OPTIONS ${EXCLUDE:+-e $EXCLUDE})
		if [ "$RETIRED" = "ERROR off" ] ; then
			echo $POPCON_NODEPENDENCY_ERROR >&2
			exit 1
		fi

		if [ -z "$RETIRED$EXCLUDE" ]; then #{{{# nothing to do
			dialog \
				--backtitle "popcon-retirement $VERSION" \
				--title "popcon-retirement $VERSION" \
				--msgbox "$NO_OLD_FOUND" \
				$BOXSIZE
			break #}}}
		elif [ -z "$OLD_RETIRED" ]; then #{{{# it's the first loop cycle
			SPLIT_NEW=
			SPLIT_OLD="$RETIRED" #}}}
		elif [ -z "$RETIRED" ]; then #{{{# maybe we have excluded all packages and no new <OLD> packages
			RETIRED="$OLD_RETIRED"
			SPLIT_NEW=
			SPLIT_OLD=
			while read LINE; do
				SPLIT_OLD="$SPLIT_OLD$NL${LINE%off}on"
			done <<__ORET_EOT
$OLD_RETIRED
__ORET_EOT

			SPLIT_OLD="${SPLIT_OLD#$NL}" # trim leading newline character }}}
		else #{{{# normal loop cycle
			# Idea: you have two sorted lists: the list of the
			# <OLD> packages in the last cycle and the list of
			# <OLD> packages in this cycle. Now you compare element
			# by element if the lists differ.
			exec 3<<__RET_EOT
$RETIRED
__RET_EOT
			exec 4<<__ORET_EOT
$OLD_RETIRED
__ORET_EOT
			read LINE <&3
			read OLD_LINE <&4
			SPLIT_NEW=
			SPLIT_OLD=
			if [ -n "$EXCLUDE" ]; then
				# If we exclude some packages, the list of <OLD>
				# packages is incomplete. So we build up the list from
				# scratch
				RETIRED=
			fi
			while true; do #{{{
				# Note: Contrary to orphaner we have two lists 
				# sorted by packets size, no more by string value...
				# What is the impact on the following algorithm?
				# On the following line we replace ">" by "!="...
				if [ "$LINE" != "$OLD_LINE" ]; then
					# The package from the old <OLD> packages list was removed
					if [ -n "$EXCLUDE" ]; then
						# ...but not really, it is only excluded
						RETIRED="$RETIRED$NL$OLD_LINE"
						SPLIT_OLD="$SPLIT_OLD$NL${OLD_LINE%off}on"
					fi

					read OLD_LINE <&4 || break
				else
					if [ -n "$EXCLUDE" ]; then
						RETIRED="$RETIRED$NL$LINE"
					fi

					if [ "$LINE" = "$OLD_LINE" ]; then
						# <OLD> packages are equal no changes
						SPLIT_OLD="$SPLIT_OLD$NL$LINE"
						LINE=
						read OLD_LINE <&4 || break
					else # $LINE < $OLD_LINE
						# there is a new package in the <OLD> packages list
						SPLIT_NEW="$SPLIT_NEW$NL$LINE"
					fi

					if ! read LINE <&3; then
						# the new <OLD> packages list reached the end, all
						# packages from the old <OLD> packages list are
						# removed
						if [ -n "$EXCLUDE" ]; then
							# ...but not really, they are only excluded
							RETIRED="$RETIRED$NL$OLD_LINE"
							SPLIT_OLD="$SPLIT_OLD$NL${OLD_LINE%off}on"
							while read OLD_LINE; do
								RETIRED="$RETIRED$NL$OLD_LINE"
								SPLIT_OLD="$SPLIT_OLD$NL${OLD_LINE%off}on"
							done <&4
						fi
						break
					fi
				fi
			done #}}}
			exec 4<&-

			# The list of old <OLD> packages packages reached the end. So
			# all remaining new <OLD> packages packages are new
			if [ -n "$LINE" ]; then
				if [ -n "$EXCLUDE" ]; then
					RETIRED="$RETIRED$NL$LINE"
				fi
				SPLIT_NEW="$SPLIT_NEW$NL$LINE"
			fi
			while read LINE; do
				if [ -n "$EXCLUDE" ]; then
					RETIRED="$RETIRED$NL$LINE"
				fi
				SPLIT_NEW="$SPLIT_NEW$NL$LINE"
			done <&3
			exec 3<&-

			# trim leading newline characters
			RETIRED="${RETIRED#$NL}"
			SPLIT_OLD="${SPLIT_OLD#$NL}"
			SPLIT_NEW="${SPLIT_NEW#$NL}"
		fi #}}}
		# Display dialog box and handle buttons {{{
		while true; do
			PKG_NB=$(echo "$RETIRED"|wc -l)
			PKG_SZ=$(($(echo "$RETIRED"|cut -d' ' -f2|tr '\012' +)0))k
			SEL_NB=$(echo "$SPLIT_OLD"|grep ' on$'|wc -l)
			SEL_SZ=$(($(echo "$SPLIT_OLD"|grep ' on$'|cut -d' ' -f2|tr '\012' +)0))k
			ERROR=0
			dialog --backtitle "popcon-retirement $VERSION" \
				--defaultno \
				${DEFAULT_PKG:+--default-item $DEFAULT_PKG} \
				--separate-output \
				--title "popcon-retirement $VERSION" \
				--help-button --help-status --extra-button --extra-label "$SIMULATE_BUTTON" \
				--checklist "Packets: $PKG_NB ($PKG_SZ) - Selected: $SEL_NB ($SEL_SZ)\n$POPCON_RETIREMENT_INSTRUCTION" \
				$LISTSIZE ${SPLIT_NEW:+$SPLIT_NEW ---- _new_packages_above_ off} \
				$SPLIT_OLD 2> $TMPFILE || ERROR=$?

			unset DEFAULT_PKG EXCLUDE

			case $ERROR in
				0) # OK-Button {{{
					if [ ! -s $TMPFILE ]; then
						# nothing's selected
						break 2
					fi
					clear
					# tr , ' ' is used for compatibility with something else...
					PACKAGES_TO_REMOVE="$(printf '%s ' $(grep -v '^----$' $TMPFILE | tr , ' '))"
					PACKAGES_TO_REMOVE="${PACKAGES_TO_REMOVE% }"
#					printf "$REMOVING" "$PACKAGES_TO_REMOVE"
					APT_GET_CMDLN="apt-get $PURGE --show-upgraded --assume-yes remove $PACKAGES_TO_REMOVE"
					if apt-get check -q -q 2> /dev/null && [ $SKIPAPT -eq 0 ]; then
						LC_ALL=C $APT_GET_CMDLN --simulate \
						| grep -q "^0 upgraded, 0 newly installed, `echo "$PACKAGES_TO_REMOVE" | wc -w` to remove and"
						if [ $? -ne 0 ]; then
							LC_ALL=C $APT_GET_CMDLN --simulate >&2
							printf '%s\n' "$NUMBER_OF_PACKAGES_ERROR" >&2
							exit 1
						fi
						$APT_GET_CMDLN || ERROR=$?
					else
						if [ $SKIPAPT -eq 0 ]; then
							printf '%s\n' "$APT_GET_LOCKFAIL" >&2
						else
							printf '%s\n' "$SKIPAPT_SET" >&2
						fi
						printf '%s\n' "$APT_GET_CMDLN"
						exit 1
					fi
					unset APT_GET_CMDLN PACKAGES_TO_REMOVE
					if [ $ERROR -ne 0 ]; then
						printf "$APT_GET_ERROR" $ERROR >&2
						exit 1
					fi
					if ! which popcon-nodependency >/dev/null 2>&1; then
						echo $POPCON_NODEPENDENCY_REMOVED
						exit 0;
					fi
					if ! which apt-get >/dev/null 2>&1; then
						echo $APT_GET_REMOVED
						exit 0;
					fi
					echo
					echo "$PRESS_ENTER_TO_CONTINUE"
					read UNUSED_VARIABLE_NAME
					break
					;; #}}}
				1) # Cancel-Button #{{{
					break 2
					;; #}}}
				2) # Help-Button #{{{
					SEL_LIST=
					while read pkg; do
						case "$pkg" in
							"HELP "*)
								# DEFAULT_PKG is default item in the
								# next dialog
								DEFAULT_PKG=${pkg#HELP }
								;;
							*)
								SEL_LIST="$SEL_LIST $pkg"
								;;
						esac
					done < $TMPFILE

					if test -n "$SPLIT_NEW"; then
						while read pkg rest; do
							new_SPLIT_NEW="$new_SPLIT_NEW$NL$pkg $rest"
							# check if the selection for every new
							# <OLD> package changed
							case "$SEL_LIST " in
								*" $pkg "*) # now it is selected...
									case "$rest" in
										*' off') # ...but wasn't before
											new_SPLIT_NEW="${new_SPLIT_NEW%off}on"
									esac
									;;
								*) # now it is deselected...
									case "$rest" in
										*' on') # ...but it was selected before
											new_SPLIT_NEW="${new_SPLIT_NEW%on}off"
									esac
									;;
							esac
						done <<__EOT
$SPLIT_NEW
__EOT
						SPLIT_NEW="${new_SPLIT_NEW#$NL}"
						unset new_SPLIT_NEW
					fi

					while read pkg rest; do
						new_SPLIT_OLD="$new_SPLIT_OLD$NL$pkg $rest"
						# check if the selection for every old <OLD>
						# package changed
						case "$SEL_LIST " in
							*" $pkg "*) # now it is selected...
								case "$rest" in
									*' off') # ...but wasn't before
										new_SPLIT_OLD="${new_SPLIT_OLD%off}on"
								esac
								;;
							*) # now it is deselected...
								case "$rest" in
									*' on') # ...but it was selected before
										new_SPLIT_OLD="${new_SPLIT_OLD%on}off"
								esac
								;;
						esac
					done <<__EOT
$SPLIT_OLD
__EOT
					SPLIT_OLD="${new_SPLIT_OLD#$NL}"
					unset new_SPLIT_OLD

					dpkg -s $DEFAULT_PKG > $TMPFILE
					dialog --backtitle "popcon-retirement $VERSION" \
						--title "popcon-retirement $VERSION" \
						--textbox $TMPFILE $BOXSIZE
					;; #}}}
				3) # Simulate-Button #{{{
					EXCLUDE=$(grep -v '^----$' $TMPFILE | while read pkg; do printf $pkg,; done)
					EXCLUDE=${EXCLUDE%,}
					break
					;; #}}}
				*) #{{{
					printf "$DIALOG_ERROR" $ERROR >&2
					cat $TMPFILE
					exit 1 #}}}
			esac
		done #}}}
	done #}}}
} #}}}

# parse options # {{{
case " $OPTIONS " in
	*" --purge "*)
		OPTIONS="${OPTIONS%%--purge*}${OPTIONS#*--purge}"
		PURGE=--purge
		;;
esac

case " $OPTIONS " in
	*" --skip-apt "*)
		OPTIONS="${OPTIONS%%--skip-apt*}${OPTIONS#*--skip-apt}"
		SKIPAPT=1
		;;
esac
# }}}

case $0 in
	*popcon-retirement|*popcon-retirement.sh) doretirement;;
# Do one need another editkeeper?
#	*editkeep) editkeepers;;
	*)
		printf "$INVALID_BASENAME" $0 >&2
		exit 1
		;;
esac

clear
