#!/bin/sh
#
# Copyright (c) 2008, 2010, 2016, 2018, 2021  Peter Pentchev
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.

set -e

: "${S5_CONFIG:=s5.conf}"
: "${S5_DIR:=/usr/local/share/s5}"
: "${S5_ETCDIR:=/usr/local/etc}"
: "${S5_TEMPLATE:=s5-blank}"

CKSUM='cksum'
CKSUM_ARGS=''

SHADOWDIR=''

s5_version='0.1.0'

# s5 blank path
# Create a new S5 presentation at the specified path
#
s5_blank()
{
	[ -z "$v" ] || echo "s5_blank: path $1" 1>&2
	if [ -z "$1" ]; then
		echo 'Usage: s5 blank path' 1>&2
		exit 1
	fi

	if [ -e "$1" ] && ! [ -d "$1/" ]; then
		echo "Not a directory: $1" 1>&2
		exit 1
	fi

	[ -z "$v" ] || echo "- creating the directory" 1>&2
	[ -e "$1" ] || $noop mkdir -p -- "$1"
	[ -z "$v" ] || echo "- copying files from $S5_TEMPLATE" 1>&2
	# shellcheck disable=SC2086
	$noop cp -Rp $v -- "$S5_TEMPLATE"/ "$1"/
	[ -z "$v" ] || echo "- s5_mksum for the initial checksums" 1>&2
	s5_mksum "$1"
}

# s5_cksum path
# Calculate the checksums of the actual files in path and compare them
# to the ones recorded when the presentation was created
#
s5_cksum()
{
	[ -z "$v" ] || echo "s5_cksum: path $1" 1>&2
	if [ -z "$1" ]; then
		echo 'Usage: s5 cksum path' 1>&2
		exit 1
	fi

	if ! [ -d "$1/" ]; then
		echo "Not a directory: $1" 1>&2
		exit 1
	fi
	cksumname='s5-checksums.txt'
	cksumfile=''
	for f in "$1/$cksumname" "$1/../$cksumname" "$cksumname"; do
		if [ -f "$f" ] && [ -r "$f" ]; then
			cksumfile="$f"
			break
		fi
	done
	if [ -z "$cksumfile" ]; then
		echo "Could not find a suitable checksum file $cksumname" 1>&2
		exit 1
	fi

	file=''
	nofile=''
	cksumbad=''
	while read -r cmd args; do
		[ -z "$v" ] || echo "- read cmd $cmd args $args" 1>&2
		case "$cmd" in
			CKSUM_CMD)
				[ -z "$v" ] || echo "  - comparing current command '$CKSUM' with '$args'" 1>&2
				if [ "$CKSUM" != "$args" ]; then
					echo "Checksum command mismatch: current '$CKSUM', file recorded with '$args'" 1>&2
					exit 1
				fi
				;;

			CKSUM_ARGS)
				[ -z "$v" ] || echo "  - comparing current args '$CKSUM_ARGS' with '$args'" 1>&2
				if [ "$CKSUM_ARGS" != "$args" ]; then
					echo "Checksum arguments mismatch: current '$CKSUM_ARGS', file recorded with '$args'" 1>&2
					exit 1
				fi
				;;

			FILE)
				[ -z "$v" ] || echo "  - handling file $args" 1>&2
				if [ ! -f "$1/$args" ]; then
					echo "Missing file described in the checksum records: $args" 1>&2
					nofile=1
					cksumbad=1
				else
					file="$args"
					nofile=''
				fi
				;;

			CKSUM)
				if [ -z "$nofile" ]; then
					if [ -z "$file" ]; then
						echo "Checksum without filename" 1>&2
						exit 1
					fi
					[ -z "$v" ] || echo "  - about to check $file for $args" 1>&2
					# shellcheck disable=SC2086
					o="$(cd "$1" && $CKSUM $CKSUM_ARGS -- "$file")"
					[ -z "$v" ] || echo "  - got checksum $o" 1>&2
					if [ "$o" != "$args" ]; then
						echo "Bad checksum for file $file" 1>&2
						cksumbad=1
					else
						if [ -n "$SHADOWDIR" ]; then
							d="$(dirname -- "$file")"
							mkdir -p -- "$SHADOWDIR"/t/"$d"
							touch -- "$SHADOWDIR"/t/"$file"
						fi
					fi
				else
					[ -z "$v" ] || echo "  - skipping checksum $args" 1>&2
				fi
				;;

			*)
				echo "Unsupported checksum keyword $cmd" 1>&2
				exit 1
				;;
		esac
	done < "$cksumfile"

	if [ -n "$cksumbad" ]; then
		echo "Some files failed the checksum check" 1>&2
		exit 1
	fi
	[ -z "$v" ] || echo "- all fine!" 1>&2
}

# s5 mksum path
# Calculate the checksums of the stock S5 template files and record
# them into the S5 presentation at path
#
s5_mksum()
{
	[ -z "$v" ] || echo "s5_mksum: path $1" 1>&2
	if [ -z "$1" ]; then
		echo 'Usage: s5 mksum path' 1>&2
		exit 1
	fi

	if [ -z "$noop" ] && ! [ -d "$1/" ]; then
		echo "Not a directory: $1" 1>&2
		exit 1
	fi

	cksumfile="$1/s5-checksums.txt"
	[ -z "$v" ] || echo "- from $S5_TEMPLATE to $cksumfile" 1>&2
	(cd -- "$S5_TEMPLATE" && \
	echo "CKSUM_CMD $CKSUM" && \
	echo "CKSUM_ARGS $CKSUM_ARGS" && \
	find . -type f | sed -e 's,^\./,,' | while read -r f; do
		s5_mksum_single "$f"
	done) | if [ -z "$noop" ]; then
		cat > "$cksumfile"
		if [ -n "$v" ]; then
			cnt="$(grep -Ece '^CKSUM ' -- "$cksumfile")"
			echo "- $cnt checksums recorded into $cksumfile" 1>&2
		fi
	else
		$noop "Would write checksums to $cksumfile"
		cnt=0
		while read -r line; do
			[ -z "$v" ] || $noop "$line"
			if expr "$line" : 'CKSUM ' > /dev/null; then
				cnt="$((cnt + 1))"
			fi
		done
		$noop "Would record checksums for $cnt files"
	fi
}

# Internal routine for calculating the checksum of a single file
s5_mksum_single()
{
	# shellcheck disable=SC2086
	o="$($CKSUM $CKSUM_ARGS -- "$1")"
	echo "FILE $1"
	echo "CKSUM $o"
}

# s5 help
# Display the help message and exit
#
s5_help()
{
	cat << EOF
Usage:
s5 [-Nv] [-d dir] [-f config] [-T full] [-t template] command [args...]
s5 -h | --help | -V | --version
s5 --features

The available command-line options are:
	-d	directory containing the template files, default $S5_DIR
	-f	configuration file, default $S5_ETCDIR/$S5_CONFIG and
		$HOME/.$S5_CONFIG
	-h	display this help message and exit
	-N	no-op mode, just display the commands without executing them
	-T	full path to the template directory to use,
		default $S5_DIR/$S5_TEMPLATE
	-t	template directory name, default $S5_TEMPLATE
	-v	verbose operation, display diagnostic information
	-V	display the version of the s5 tool and exit

The available commands are:
	blank path
		Create a new S5 presentation at the specified path

	check path
		Alias for cksum

	cksum path
		Verify the checksums of the S5 presentation at path

	create path
		Alias for blank

	help
		Display this help message and exit

	mksum path
		Store the template checksums at the specified path

	new path
		Alias for blank

	update path
		Update an S5 presentation with the new S5 template files

	usage
		Alias for help
	
	verify path
		Alias for cksum
EOF
}

# Internal routine: create a temporary directory for tracking checksum files
s5_shadowdir_create()
{
	d="$(mktemp -d -t s5tool.XXXXXX)"
	if [ ! -d "$d" ]; then
		echo "Could not create a shadow directory using mktemp" 1>&2
		exit 1
	fi
	chmod 700 -- "$d"
	trap s5_shadowdir_cleanup EXIT HUP INT QUIT TERM PIPE
	SHADOWDIR="$d"
}

# Internal routine: clean up the temporary directory at exit time
s5_shadowdir_cleanup()
{
	if [ -n "$SHADOWDIR" ] && [ -d "$SHADOWDIR" ] && [ -w "$SHADOWDIR" ]; then
		rm -rf -- "$SHADOWDIR"/t
		rmdir -- "$SHADOWDIR"
	fi
}

# s5_update path
# Update the S5 presentation at the specified path with files from
# the template directory, but only if the S5 presentation has not
# been modified in the meantime
#
s5_update()
{
	[ -z "$v" ] || echo "s5_update: path $1" 1>&2
	s5_shadowdir_create
	s5_cksum "$1"

	[ -z "$v" ] || echo "s5_update: checking for new files" 1>&2
	(cd -- "$S5_TEMPLATE" && find . -type f) | (
	noupdate=''
	while read -r f; do
		[ -z "$v" ] || echo "- $f" 1>&2
		if [ -f "$1/$f" ]; then
			[ -z "$v" ] || echo "  - exists in the working directory" 1>&2
			if cmp -s -- "$S5_TEMPLATE/$f" "$1/$f"; then
				[ -z "$v" ] || echo "  - the files are the same :)" 1>&2
			else
				if [ ! -f "$SHADOWDIR"/t/"$f" ]; then
					echo "File in template and working dir, but not part of the previous template: $f" 1>&2
					noupdate=1
				fi
			fi
		fi
	done
	if [ -n "$noupdate" ]; then
		echo "Not proceeding with the update" 1>&2
		exit 1
	fi
	)

	[ -z "$v" ] || echo "s5_update: about to delete files" 1>&2
	(cd -- "$SHADOWDIR"/t && find . -type f) | while read -r f; do
		if [ ! -f "$S5_TEMPLATE/$f" ]; then
			# shellcheck disable=SC2086
			$noop rm $v -- "$1/$f"
		fi
	done

	[ -z "$v" ] || echo "s5_update: about to copy files" 1>&2
	(cd -- "$S5_TEMPLATE" && find . -mindepth 1 -type d) | while read -r d; do
		if [ ! -d "$1/$d" ]; then
			# shellcheck disable=SC2086
			$noop mkdir -p $v -- "$1/$d"
		else
			[ -z "$v" ] || echo "- existing dir $d" 1>&2
		fi
	done
	(cd -- "$S5_TEMPLATE" && find . -type f) | while read -r f; do
		# shellcheck disable=SC2086
		$noop cp -p $v -- "$S5_TEMPLATE/$f" "$1/$f"
	done

	[ -z "$v" ] || echo "s5_update: invoking s5_mksum" 1>&2
	s5_mksum "$1"
}

# Process the command-line options
unset -v noop opt_d opt_f opt_T opt_t v
while getopts 'd:f:hNT:t:Vv-:' o; do
	case "$o" in
		d)
			opt_d="$OPTARG"
			;;

		f)
			opt_f="$OPTARG"
			;;

		h)
			s5_help
			exit 0
			;;

		N)
			noop='echo'
			;;

		T)
			opt_T="$OPTARG"
			;;

		t)
			opt_t="$OPTARG"
			;;

		V)
			echo "s5 $s5_version"
			exit 0
			;;

		v)
			v='-v'
			;;

		-)
			case "$OPTARG" in
				features)
					echo "Features: s5=$s5_version"
					exit 0
					;;

				help)
					s5_help
					exit 0
					;;

				version)
					echo "s5 $s5_version"
					exit 0
					;;

				*)
					echo "Unrecognized option '--$OPTARG'" 1>&2
					s5_help 1>&2
					exit 1
					;;
			esac
			;;

		*)
			[ "$o" = '?' ] || echo "Unrecognized option '$o'" 1>&2
			s5_help 1>&2
			exit 1
			;;
	esac
done
shift "$((OPTIND - 1))"

# Treat a simple "s5" invocation as a usage request
if [ -z "$1" ]; then
	s5_help
	exit
fi

# Parse the config file
if [ -n "$opt_f" ]; then
	# shellcheck source=s5.conf
	. "$opt_f"
else
	if [ -e "$S5_ETCDIR/$S5_CONFIG" ]; then
		# shellcheck source=s5.conf
		. "$S5_ETCDIR/$S5_CONFIG" 
	fi
	if [ -e "$HOME/.$S5_CONFIG" ]; then
		# shellcheck source=s5.conf
		. "$HOME/.$S5_CONFIG" 
	fi
fi

# Let the command line override the config file settings
if [ -n "$opt_T" ]; then
	S5_TEMPLATE="$opt_T"
else
	if [ -n "$opt_d" ]; then
		S5_DIR="$opt_d"
	fi
	if [ -n "$opt_t" ]; then
		S5_TEMPLATE="$opt_t"
	fi
	S5_TEMPLATE="$S5_DIR/$S5_TEMPLATE"
fi

# Okay, we have some a command...
[ -z "$v" ] || echo "Parsing a command: $1" 1>&2
case "$1" in
	# b(lank), cr(eate), n(ew)
	b|bl|bla|blan|blank|cr|cre|crea|creat|create|n|ne|new)
		shift
		s5_blank "$1"
		;;
	
	# ch(eck), ck(sum), v(erify)
	ch|che|chec|check|ck|cks|cksu|cksum|v|ve|ver|verify)
		shift
		s5_cksum "$1"
		;;

	# h(elp), us(age)
	h|he|hel|help|us|usa|usag|usage)
		shift
		s5_help
		;;
	
	# m(ksum)
	m|mk|mks|mksu|mksum)
		shift
		s5_mksum "$1"
		;;
	
	# up(date)
	up|upd|upda|updat|update)
		shift
		s5_update "$1"
		;;

	*)
		echo "Unrecognized s5 command $1" 1>&2
		s5_help 1>&2
		exit 1
		;;
esac
