#!/bin/sh -e
#
#    dotdee - convert a flat file into one dynamically generated from a .d directory
#    Copyright (C) 2010 Dustin Kirkland <kirkland@ubuntu.com>
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, version 3 of the License.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.

PKG="dotdee"
DIR="/etc/${PKG}"
ORIGINAL="50-original"
COMMENT=".comment"

FORCE=

info() {
	echo "INFO: $@" 1>&2
}

error() {
	echo "ERROR: $@" 1>&2
	exit 1
}

usage() {
	echo
	echo "dotdee --dir|--original|--setup|--update|--undo FILENAME"
	echo
	echo "  -c|--comment FILENAME COMMENT_DELIM set the file comment delimiter"
	echo "  -d|--dir FILENAME	print the dotdee directory where snippets live"
	echo "  --handle-inotify	inotify handler procedures"
	echo "  -l|--list [FILENAME]	list dotdee managed paths"
	echo "  -o|--original FILENAME	print the current location of the original file"
	echo "  -s|--setup FILENAME	convert FILENAME to a .d directory structure"
	echo "  -u|--update FILENAME	update the generated FILENAME immediately"
	echo "  --update-link FILENAME	only update the generated FILENAME symlink"
	echo "  --update-contents FILENAME only update the contents of FILENAME"
	echo "  --undo FILENAME		undo/revert a dotdee setup"
	echo
}

u_a_name() {
	# Ensure a unique name in update-alternatives
	f=$(managed_path "${1}")
	echo "${f}" | sed -e "s%/%:%g" -e "s%^:\+%%"
}

managed_path() {
	# Best to keep this code in one place; strip:
	#  - duplicate "/"
	#  - leading $DIR
	#  - trailing ".d"
	#  - trailing ".d/*"
	echo "${1}" | sed -e "s:/\+:/:g" -e "s:^${DIR}/:/:" -e "s:\.d$::" -e "s:\.d/[^/]*$::g"
}

is_dotdee() {
	# Is this file managed by dotdee?
	update-alternatives --list "${HANDLE}" 2>/dev/null | grep -qs "^${DIR}/${1}$"
}

sync_attrs() {
	chown --reference "${1}" "${2}"
	chmod --reference "${1}" "${2}"
}

comment() {
	local tmp=$(mktemp /tmp/${PKG}-XXXXXXXX)
	echo "${2}" > "${tmp}"
	[ -n "${3}" ] && echo "${3}" >> "${tmp}"
	mv -f "${tmp}" "${DIR}/${1}.d/${COMMENT}"
	sleep 0.1	# Ugly, but somehow this isn't getting written to disk?
}

directory() {
	if is_dotdee "${1}"; then
		echo "${DIR}/${1}.d"
	else
		error "[${1}] is not managed by dotdee"
	fi
}

original() {
	if is_dotdee "${1}"; then
		echo "${DIR}/${1}.d/${ORIGINAL}"
	else
		error "[${1}] is not managed by dotdee"
	fi
}

restart_dotdee() {
	/sbin/restart "${PKG}" || /sbin/start "${PKG}"
}

setup() {
	# Setup a file for dotdee management
	if is_dotdee "${1}" && [ "${FORCE}" != "1" ]; then
		error "[${1}] is already managed by dotdee"
	else
		# Test for applicability
		[ -f "${1}" ] || error "Not a regular file [${1}]"
		# Create the directory and move the file
		mkdir -p "${DIR}/${1}.d"
		mv -f "${1}" "${DIR}/${1}.d/${ORIGINAL}"
		[ -n "${2}" ] && comment "${1}" "${2}" "${3}"
		NEW=1
		update "${1}"
		# Re-establish links to file
		# Establish update-alternatives link at a higher priority to the generated one
		update-alternatives --install "${1}" "${HANDLE}" "$DIR/${1}" 50
		# Establish update-alternatives link at a lower priority to the original one
		update-alternatives --install "${1}" "${HANDLE}" "$DIR/${1}.d/${ORIGINAL}" 20
		t=$(mktemp /tmp/${PKG}-XXXXXXXX)
		d=$(dirname "${1}")
		b=$(basename "${1}")
		sed -e "s@___DIRNAME___@${d}@g" -e "s@___BASENAME___@${b}@g" /usr/share/dotdee/watch_template > "${t}"
		mv -f "${t}" "${DIR}/etc/dotdee.xml.d/60-${HANDLE}"
		restart_dotdee
	fi
}

link_ok() {
	f=$(readlink -f "${1}")
	case "${f}" in
		${DIR}${1}|${DIR}/${1})
			# No need to update; already correct
			info "[${1}] -> [${f}] is correct"
			return 0
		;;
		*)
			info "[${1}] -> [${f}] is incorrect"
			return 1
		;;
	esac
}

update_link() {
	update-alternatives --install "${1}" "${HANDLE}" "${DIR}/${1}" 50 >/dev/null 2>&1
}

contents_ok() {
	md5sum -c "/var/lib/${PKG}/${HANDLE}"
}

update_contents() {
	# Construct the file via concatenation
	if is_dotdee "${1}" || [ "${NEW}" = 1 ]; then
		tmp=$(mktemp /tmp/${PKG}-XXXXXXXX)
		# Conditionally add the comment header to the beginning of the generated file.
		# BUG: XML will need special handling, as comments cannot come before the doc type header
		if [ -f "${DIR}/${1}.d/${COMMENT}" ]; then
			lines=$(wc -l "${DIR}/${1}.d/${COMMENT}" | awk '{print $1}')
			if [ "$lines" = "1" ]; then
				c1=$(head -n1 "${DIR}/${1}.d/${COMMENT}")
				c2=
			elif [ "$lines" = "2" ]; then
				c1=$(head -n1 "${DIR}/${1}.d/${COMMENT}")
				c2=$(tail -n1 "${DIR}/${1}.d/${COMMENT}")
				c2=" ${c2}"
			fi
			if [ "$lines" = "1" ] || [ "$lines" = "2" ]; then
				printf "${c1} DOTDEE: DO NOT EDIT THIS FILE DIRECTLY!${c2}\n" >> "${tmp}"
				printf "${c1} DOTDEE: Rather, add, remove, or modify file(s) in [%s]${c2}\n" "${DIR}/${1}.d" >> "${tmp}"
				printf "${c1} DOTDEE: per the dotdee(8) manpage.${c2}\n" >> "${tmp}"
			fi
		fi
		for i in "${DIR}/${1}.d/"[0-9]*; do
			case "${i}" in
				"${DIR}/${1}.d/${ORIGINAL}")
					cat "${i}" >> "${tmp}"
				;;
				*.patch|*.diff)
					patch "${tmp}" "${i}"
				;;
				*)
					if [ -x "${i}" ]; then
						# file is not 50-original, and is executable,
						# so by convention, feed the current state
						# of the file as STDIN, and write STDOUT to
						# the file
						tmp2=$(mktemp /tmp/${PKG}-XXXXXXXX)
						${i} <"${tmp}" >"${tmp2}"
						mv -f "${tmp2}" "${tmp}"
					else
						cat "${i}" >> "${tmp}"
					fi
				;;
			esac
		done
		# Sync owner:group and permissions
		sync_attrs "${DIR}/${1}.d/${ORIGINAL}" "${tmp}"
		# But then remove all write access, to prevent inadvertent writes to this generated file
		chmod a-w "${tmp}"
		mv -f "${tmp}" "${DIR}/${1}"
		# Store a checksum of the contents, in order to detect changes
		mkdir -p "/var/lib/${PKG}"
		md5sum "${DIR}/${1}" > "/var/lib/${PKG}/${HANDLE}"
		info "[${1}] updated by dotdee"
	else
		error "[${1}] is not managed by dotdee"
	fi
}

update() {
	update_contents "${1}"
	update_link "${1}"
	[ "${1}" = "/etc/dotdee.xml" ] && restart_dotdee "${PKG}" || true
}

undo() {
	# Undo a dotdee setup
	if is_dotdee "${1}"; then
		rm -f "$DIR/etc/dotdee.xml.d/60-${HANDLE}"
		update-alternatives --remove "${HANDLE}" "${DIR}/${1}"
		update-alternatives --remove "${HANDLE}" "${DIR}/${1}.d/${ORIGINAL}"
		cp -af "${DIR}/${1}.d/${ORIGINAL}" "${1}"
		info "[${1}] has been restored"
		info "You may want to manually remove [${DIR}/${1} ${DIR}/${1}.d]"
	else
		error "[${1}] is not managed by dotdee"
	fi
}

list() {
	if [ -n "${1}" ]; then
		if is_dotdee "${1}"; then
			echo "${1}"
		else
			error "[${1}] is not managed by ${PKG}"
			exit 1
		fi
	else
		# List all dotdee managed paths
		for i in $(find "${DIR}" -type d -name "*.d"); do
			f=$(managed_path "${i}")
			case "$(readlink -f "${f}")" in
				${DIR}/*)
					echo "${f}"
				;;
			esac
		done
	fi
}

handle_inotify() {
	case "${1}" in
		# Files in an /etc/dotdee/*.d dir changed
		${DIR}/*.d/[0-9]*|${DIR}/*.d/.comment)
			f=$(managed_path "${1}")
			# Update the managed file
			update "${f}"
		;;
		${DIR}/*)
			# Dereferenced dotdee link; strip leading dir
			f=$(managed_path "${1}")
			if is_dotdee "${f}"; then
				# Link is correct; check/update contents
				contents_ok || update_contents "${f}"
			fi
		;;
		*)
			if is_dotdee "${1}"; then
				# Check/update link
				link_ok "${1}" || update_link "${1}"
				# Check/update contents
				contents_ok "${1}" || update_contents "${1}"
			fi
		;;
	esac
}

# Main
#set -x
HANDLE=$(u_a_name "${2}")
case "${1}" in
	-c|--comment)
		comment "${2}" "${3}" "${4}"
	;;
	-d|--dir|--directory)
		directory "${2}"
	;;
	--handle-inotify)
		handle_inotify "${2}"
	;;
	-l|--list)
		list "${2}"
	;;
	-o|--original)
		original "${2}"
	;;
	-s|--setup)
		setup "${2}" "${3}"
	;;
	--setup-force)
		FORCE=1
		setup "${2}" "${3}" "${4}"
	;;
	-u|--update)
		NEW=
		update "${2}"
	;;
	--update-contents)
		update_contents "${2}"
	;;
	--update-link)
		update_link "${2}"
	;;
	--undo)
		undo "${2}"
	;;
	--help)
		usage
	;;
	*)
		usage
		error "Invalid argument [${1}]"
	;;
esac
