#!/bin/dash
# Copyright (C) James Budiono, Aug 2015, 2016, 2019
# License: GNU GPL Version 3 or later
#
# Version 1: Aug 2015
# Version 2: Jan 2016 - add iso and sfs updater
#
# $1-method to use (sfs,pkg,iso,kernel) - optional
# $2-mirror (for iso/sfs) - optional

### config
APPTITLE="Fatdog Updater"
REPO_VERSION=${REPO_VERSION:-$(cat /etc/fatdog-version)}
REPO_VERSION=${REPO_VERSION%%[!0-9]*} # remove pre-release tags, if any (rc, alpha5, beta33, etc.)
REPO_VERSION=${REPO_VERSION%?}0 # replace last digit with zero (70x becomes 700, 71x becomes 710, etc)
OFFICIAL_URL="http://distro.ibiblio.org/fatdog/packages/$REPO_VERSION/"
CONTRIB_URL="http://distro.ibiblio.org/fatdog/contrib/packages/$REPO_VERSION/"
EXCLUDES="^aaa-.*|^fatdog-scripts|^fatdog-bins|^glibc$|^xorg-server$"
SRC_TZ_OFFSET="-04:00" # offset from UTC. This is ibiblio's server timezone

MIRROR=${2:-http://distro.ibiblio.org/fatdog}
ISO_URL="$MIRROR/iso/"
SFS_URL="$MIRROR/sfs/$REPO_VERSION/"
KERNEL_URL="$MIRROR/kernels/$REPO_VERSION/"
WORKDIR=/tmp/fatdog-updater.$$
METHOD=$1

export $(grep -m 1 -h ^BASE_SFS_DEFAULT_PATH /sbin/system-init)
BASESFS=${BASE_SFS_DEFAULT_PATH:-fd64.sfs}
BASESFS=${BASESFS##*/}
unset BASE_SFS_DEFAULT_PATH

############################################################
####################### COMMON HELPERS #####################
############################################################


########### GUI helper ###########
check_root_and_terminal() {
	test -z "$DISPLAY" && ! test -t 1 && exit 1 # not running in terminal, exit
	if test $(id -u) -ne 0; then
		msg "You need to be root to do this."
		exit 1
	fi
}

dlg() {
	if [ "$DISPLAY" ]; then
		Xdialog --title "$APPTITLE" "$@"
	else
		dialog --backtitle "$APPTITLE" "$@"
	fi
}

info() {
	if [ "$DISPLAY" ]; then
		if [ "$1" ]; then
			[ $XPID ] && kill $XPID
			Xdialog --title "$APPTITLE" --no-buttons --infobox "$@" 0 0 1000000 &
			XPID=$!
		else
			kill $XPID
			XPID=
		fi
	else
		dlg --infobox "$@" 15 60
	fi
}

msg() {
	if [ "$DISPLAY" ]; then
		dlg --msgbox "$@" 0 0
	else
		dlg --msgbox "$@" 15 60
	fi
	
}

checklist() {
	if [ "$DISPLAY" ]; then
		dlg --separator " " --stdout --no-tags --checklist "$@"
	else
		dlg --stdout --no-tags --checklist "$@"
	fi
}

menu_notags() {
	if [ "$DISPLAY" ]; then
		dlg --separator " " --stdout --no-tags --menu "$@"
	else
		dlg --stdout --no-tags --menu "$@"
	fi
}

fselect() {
	if [ "$DISPLAY" ]; then
		dlg --separator " " --stdout --backtitle "$1" --fselect "$2" 0 0
	else
		dlg --stdout --backtitle "$1" --fselect "$2" 0 0
	fi
}

progressbox() {
	if [ "$DISPLAY" ]; then
		dlg --backtitle "$1" --no-cancel --tailbox - "$2" "$3" 
	else
		dlg --progressbox "$@" 18 76
	fi
}

# $1-text, returns true/false
yesno() {
	if [ "$DISPLAY" ]; then
		dlg --yesno "$1" 0 0
	else
		dlg --yesno "$1" 15 60
	fi	
}


############################################################
############# UPDATE BY	INSTALLING PACKAGES ################
############################################################

# NOTE:
# There are two update methods
# 1. Update package with same name (but newer date)
# 2. Update package with newer version
# We don't bother (2), we assume newer date => better one, 
# unless if pkgdates are identical, then check version

############# functional helpers ################

# output: $1-pkgname, $2-pkgver, $3-pkgfullname, $4-epoch
get_repo_pkgs() {
	{ 
		links -dump "$OFFICIAL_URL" -width 320
		links -dump "$CONTRIB_URL" -width 320
	} |
	#<a awk -v tzoffset=$SRC_TZ_OFFSET '
	awk -v tzoffset=$SRC_TZ_OFFSET '
BEGIN {
	# init months array
	split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", mon, " ");
	for (p in mon) {
		months[mon[p]]=p
	}
	
	# convert timezone to offset in seconds
	split(tzoffset,x,":")
	tzsecs = (x[1]*60+x[2])*60
	#print tzsecs
}

function time_to_epoch(date, time,       dmy,cmd,ret) {
	# date is yyyy-mon-date
	split(date,dmy,"-");
	split(time,hms,":");
#	cmd = "date -d\"" dmy[1] "-" months[dmy[2]] "-" dmy[3] "T" time tzoffset "\" +%s"
#	cmd | getline ret
#	close(cmd)
	return (mktime(dmy[1] " " months[dmy[2]] " " dmy[3] " " hms[1] " " hms[2] " " hms[3]) + tzsecs)
}

function get_pkgname(pkg,     z, zlen,i,ret) {
	ret=""
	zlen=split(pkg,z,"-")-3;
	for (i=1; i<=zlen; i++) {
		ret=ret "-" z[i]
	}
	return substr(ret,2)
}

function get_pkgversion(pkg,     z, zlen) {
	ret=""
	zlen=split(pkg,z,"-")
	return z[zlen-2]
}

# result is ver1 - ver2;
# if ver1 > ver2 --> 1, if ver1 < ver2 --> -1, ver1==ver2 --> 0
function compare_pkg_ver(ver1, ver2,             v1, v2, v1len, v2len) {
	v1len = split(ver1,v1,".")
	v2len = split(ver2,v2,".")
	vmin = v1len 
	if (vmin > v2len) vmin = v2len
		
	# compare matching versions
	for (i=1; i<=vmin; i++) {
		if (v1[i] < v2[i]) return -1
		if (v1[i] > v2[i]) return +1
	}
	
	# compare number of components
	if (v1len > v2len) return 1
	if (v1len < v2len) return -1
	return 0
}

$1 ~ /.txz$/ { 
	sub(/.txz$/,"",$1)
	pkgname = get_pkgname($1)
	pkgdate = time_to_epoch($2, $3) 
	pkgv = get_pkgversion($1)
#	if (!pkgs[pkgname] || (pkgdate >= pkgs[pkgname]))
#	if ((compare_pkg_ver(pkgv, pkgver[pkgname])>0))	
	if ((pkgdate > pkgs[pkgname]) || (compare_pkg_ver(pkgv, pkgver[pkgname])>0))
	{
		pkgs[pkgname] = pkgdate
		pkgver[pkgname] = pkgv
		pkgfile[pkgname] = $1
	} 
	#print pkgname "|" pkgv "| "$1 "|" pkgdate 
}

END {
	for (p in pkgs) {
		print p "|" pkgver[p] "|" pkgfile[p] "|" pkgs[p]
	}
	#print compare_pkg_ver("1.2.2abc", "1.2.3")
}' | sort

	#date -d "2015-2-18 08:04:16 -0400" +"%F %T %z"	
}


# output: $1-pkgname, $2-pkgver, $3-pkgfullname, $4-epoch
get_local_pkgs() {
	stat -c "%n|%Y" ${ROOT}/var/log/packages/* |
	awk -F"|" -v tzoffset="$(date +"%:z")" '
BEGIN {
	# convert timezone to offset in seconds
	split(tzoffset,x,":")
	tzsecs = (x[1]*60+x[2])*60
	#print tzsecs
}

function get_pkgname(pkg,     z, zlen,i,ret) {
	ret=""
	zlen=split(pkg,z,"-")-3;
	for (i=1; i<=zlen; i++) {
		ret=ret "-" z[i]
	}
	return substr(ret,2)
}

function get_pkgversion(pkg,     z, zlen) {
	ret=""
	zlen=split(pkg,z,"-")
	return z[zlen-2]
}

# result is ver1 - ver2;
# if ver1 > ver2 --> 1, if ver1 < ver2 --> -1, ver1==ver2 --> 0
function compare_pkg_ver(ver1, ver2,             v1, v2, v1len, v2len) {
	v1len = split(ver1,v1,".")
	v2len = split(ver2,v2,".")
	vmin = v1len 
	if (vmin > v2len) vmin = v2len
		
	# compare matching versions
	for (i=1; i<=vmin; i++) {
		if (v1[i] < v2[i]) return -1
		if (v1[i] > v2[i]) return +1
	}
	
	# compare number of components
	if (v1len > v2len) return 1
	if (v1len < v2len) return -1
	return 0
}

{ 
	sub(/.*\//,"",$1)
	pkgname = get_pkgname($1)
	pkgdate = $2 - tzsecs
	pkgv = get_pkgversion($1)
#	if (!pkgs[pkgname] || (pkgdate >= pkgs[pkgname]))
#	if ((compare_pkg_ver(pkgv, pkgver[pkgname])>0))	
	if ((pkgdate > pkgs[pkgname]) || (compare_pkg_ver(pkgv, pkgver[pkgname])>0))
	{
		pkgs[pkgname] = pkgdate
		pkgver[pkgname] = pkgv
		pkgfile[pkgname] = $1
	} 
	#print pkgname "|" pkgv "| "$1 "|" pkgdate 
}

END {
	for (p in pkgs) {
		print p "|" pkgver[p] "|" pkgfile[p] "|" pkgs[p]
	}
	#print compare_pkg_ver("1.2.2abc", "1.2.3")
}' | sort

	#date -d "2015-2-18 08:04:16 -0400" +"%F %T %z"	
}

# $1-repopkgs $2-localpkgs
# input: pkgname|pkgver|pkgfullname|epoch
# output: $1-pkgname, $2-old-pkgfullname, $3-new-pkgfullname, $4-date/time
find_updates() {
	printf "%s\n===\n%s\n" "$1" "$2" |
	awk -F"|" -v excludes="$EXCLUDES" '
BEGIN { slurp_repo=1 }
/===/ { slurp_repo=0; next; }
{
	if (slurp_repo) {
		# read repo pkgs
		pkgs[$1]=$4
		pkgver[$1]=$2		
		pkgfile[$1]=$3
	} else {
		# check for update
		if (match($1,excludes)) { next; }		
		if (pkgs[$1] > $4) {
			#print $1 "|" $2 "|" $3 "|" $4
			#print $1 "|" pkgfile[$1] "|" strftime("%F %R", pkgs[$1])
			print $1 "|" $3 "|" pkgfile[$1] "|" strftime("%F %R", pkgs[$1])
		}
	}
}' | sort
}

# $1-updatedpkgs
convert_to_checklist() {
	echo "$1" | awk	-F"|" '
	{
		if ($2 == $3) {
			print $1 " \"(REBUILD) " $3"\" off"
		} else {
			print $1 " \"(UPDATE) " $2 "  =>  " $3"\" off"
		}
	}'
}

############# MAIN PKG UPDATER ###############
update_all_packages() {
	info "Checking for updates, please wait..."
	repopkgs="$(get_repo_pkgs)"
	localpkgs="$(get_local_pkgs)"
	updatedpkgs="$(find_updates "$repopkgs" "$localpkgs")"
	info

	# see if there is anything to update
	numupdates=$(echo $updatedpkgs | wc -l)
	if [ $numupdates -eq 0 ] || [ -z "$updatedpkgs" ]; then
		msg "System is up-to-date."
		exit 0
	fi

	# list updated pkgs and ask to choose which package to update
	if ! selected=$(eval checklist \
	"'Found $(echo "$updatedpkgs" | wc -l) updated packages. 
	Select the ones that you want to update, then click OK to continue'" \
		30 80 11 $(convert_to_checklist "$updatedpkgs")); then
		msg "Cancelled, nothing is changed."
		exit 0
	fi

	# leave if none selected
	numselected=$(echo $selected | wc -w)
	if [ $numselected -eq 0 ] || [ -z "$selected" ]; then
		msg "Nothing is selected, nothing to update."
		exit 0
	fi

	# update repo database first
	{ 
		stdbuf -oL slapt-get --update 2>&1
		echo "Done. Please click OK to continue."
	} |  progressbox "Updating repository database, please wait ..." 

	# then install the packages 
	{
		slapt-get --install --reinstall --no-prompt --no-dep $selected 2>&1 
		echo "Done. Please click OK to continue."
	} |
		
	progressbox "Installing $numselected updates ..." 

	# done!
	msg "Update completed. $numselected packages were installed."
}


############################################################
################# UPDATE BY INSTALLING SFS #################
############################################################

### helper to select/download iso/sfs
# $1-prompt, $2-source URL, $3-filter $4-target-file
select_sfs_iso_for_update() {	
	# find and select ISO to update
	local selections="$(links -dump "$2" | sort -r | awk "$3"' {print $1 " " $1}')"
	if [ -z "$selections" ]; then
		msg "Unable to connect. Check your internet connections."
		return 1
	fi
	if selected=$(menu_notags "$1" 19 0 19 $selections); then
		# download updated iso/sfs
		trap "rm -rf $WORKDIR; exit" 0 HUP TERM INT
		mkdir -p $WORKDIR
		info "Downloading $selected from ${2}, please wait ..."
		wget --no-check-certificate -O "$WORKDIR/$4" "${2}/$selected"
		#cp /Fatdog64-701.iso "$WORKDIR/$4" ## DEBUG
	else
		msg "Cancelled. Nothing is changed."
		return 2
	fi	
}


############# MAIN SFS UPDATER ###############
# $1-sfs file to use (already exist in $WORKDIR)
update_sfs() {
	local sfsfile=$1
	if [ -z "$sfsfile" ]; then
		sfsfile=$BASESFS
		if select_sfs_iso_for_update \
			"Choose the SFS you want to use for updating:" "$SFS_URL" \
			'$1 ~ /^fd.*sfs$/' "$sfsfile"; 
		then
			info
			sfsfile=$(ls $WORKDIR/*.sfs)
		else
			if [ $2 == 1 ]; then
				info
				msg "Downloading of $selected failed. Nothing is changed."
			fi
			return 1	
		fi
	fi
	#echo $sfsfile
	
	# find where the basesfs is used. Fish it from /etc/BOOTSTATE
	[ -r $BOOTSTATE_PATH ] && . $BOOTSTATE_PATH # BASE_SFS_DEV_MOUNT
	if [ "$BASE_SFS_DEV_MOUNT" ]; then
		mv $sfsfile $BASE_SFS_DEV_MOUNT/${sfsfile##*/}-new
		msg "New base SFS installed in $BASE_SFS_DEV_MOUNT/${sfsfile##*/}-new
Move it over to your boot basesfs as needed."
	else
		# no BASE_DEV, so it must be in initrd (at least assume so)
		local initrd
		if initrd=$(fselect "Please select the initrd you use for booting.\n\
This initrd will be updated." "/initrd" 0 0); then
			if [ -e $initrd ]; then
				mkdir -p $WORKDIR/initrd
				( 
					cd $WORKDIR/initrd
					cpio -i < "$initrd"
					mv $sfsfile .
					find . | cpio -o -H newc > $initrd
				)
				msg "initrd updated with new basesfs."
			else
				msg "Cannot find initrd at $initrd."
			fi
		else
			msg "Cancelled. Nothing is changed."
		fi
	fi
}

############################################################
################# UPDATE BY INSTALLING ISO #################
############################################################

############# MAIN ISO UPDATER ###############
update_iso() {
	local isofile="fatdog.iso"
	if select_sfs_iso_for_update \
		"Choose the ISO you want to use for updating:" "$ISO_URL" \
		'$1 ~ /.iso$/' "$isofile"; 
	then	
		# extract initrd, extract sfs
		info
		mkdir $WORKDIR/mnt
		mount -o ro,loop "$WORKDIR/$isofile" $WORKDIR/mnt
		( cd $WORKDIR; cpio -i < mnt/initrd )
		umount $WORKDIR/mnt
		update_sfs $WORKDIR/*.sfs
	else
		if [ $2 == 1 ]; then
			info
			msg "Downloading of $selected failed. Nothing is changed."
		fi
	fi
}


############################################################
##################### UPDATE KERNEL ########################
############################################################

### helper to select/download iso/sfs
# $1-prompt, $2-source URL, $3-filter $4-kernel $5-modules
select_kernel_for_update() {	
	# find and select kernel to update
	local selections="$(links -dump "$2" | sort -r | awk "$3"' {print $1 " " $1}')"
	if [ -z "$selections" ]; then
		msg "Unable to connect. Check your internet connections."
		return 1
	fi
	if selected=$(menu_notags "$1" 19 0 19 $selections); then
		# download selected kernel/modules	
		selected=${selected#vmlinuz-}
		trap "rm -rf $WORKDIR; exit" 0 HUP TERM INT
		mkdir -p $WORKDIR
		info "Downloading kernel $selected from ${2}, please wait ..."
		wget --no-check-certificate -O "$WORKDIR/$4" "${2}/vmlinuz-$selected" &&
		#cp /root/vmlinuz "$WORKDIR/$4" && ### DEBUG
		wget --no-check-certificate -O "$WORKDIR/$5" "${2}/kernel-modules.sfs-$selected"
		#cp /root/kernel-modules.sfs "$WORKDIR/$5" ### DEBUG
	else
		msg "Cancelled. Nothing is changed."
		return 2
	fi	
}

############# MAIN KERNEL UPDATER ###############

update_kernel() {
	kernel=vmlinuz modules=kernel-modules.sfs
	if select_kernel_for_update \
		"Choose the kernel you want to use for updating:" "$KERNEL_URL" \
		'$1 ~ /^vmlinuz/' "$kernel" "$modules"; 
	then
		info
		# select kernel/initrd
		if oldkernel=$(fselect "Please select the kernel (vmlinuz) you use for booting.\n\
This kernel will be updated." "/vmlinuz" 0 0); then
			mv -v $oldkernel ${oldkernel}-old # backup
			mv -v $WORKDIR/$kernel $oldkernel
			
			if initrd=$(fselect "Please select the initrd you use for booting.\n\
This initrd will be updated." "/initrd" 0 0); then
				if [ -e $initrd ]; then
					mkdir -p $WORKDIR/initrd
					( 
						cd $WORKDIR/initrd
						cpio -i < "$initrd"
						mv -v $WORKDIR/$modules . # no backup
						find . | cpio -o -H newc > "$initrd"
					)
					msg "Kernel updated. It will be used after reboot."
				else
					msg "Cannot find initrd at $initrd."
				fi				
			else
				# revert kernel update
				mv -v ${oldkernel}-old $oldkernel
				msg "Cancelled. Nothing changed."
			fi
		else
			msg "Cancelled. Nothing changed."
		fi			
	else
		if [ $2 == 1 ]; then
			info
			msg "Downloading of $selected kernel failed. Nothing is changed."
		fi
	fi
}


########## main entry ##########
check_root_and_terminal
# determine what to do
if  [ -z "$METHOD" ] && 
	! METHOD=$(menu_notags "Choose update method to use:" 0 0 5 \
		pkg "Install updated packages" \
		sfs "Replace base sfs from given SFS" \
		iso "Replace base sfs from given ISO" \
		kernel "Update kernel"); 
then
	msg "Cancelled. Nothing is changed."
	exit 1
fi
# and now do it
case "$METHOD" in
	sfs) update_sfs ;;
	pkg) update_all_packages ;;
	iso) update_iso ;;
	kernel) update_kernel ;;
	*) echo "Usage: ${0##*/} [sfs|pkg|iso|kernel] [iso/sfs/kernel-mirror-url]"
	   return 1 ;;
esac
