#!/bin/bash

##
## binaryDiv's dotfiles management script
## Version 0.4.5
##

usage() {
	cat <<- END_OF_HELPTEXT
	Usage: $(basename $0) COMMAND [...]
	Manage dotfiles via git.

	General commands:
	  help          print this help message

	Installation:
	  install       run this on first start: install dotfiles script in ~/bin, configure git, ...
	                (this will NOT create links for all dotfiles, though)

	Dotfile management:
	  ls [DIR]            list contents of .dotfiles directory (or DIR relative to it)
	  edit FILE           open ~/.dotfiles/FILE in vim
	  diff FILE           use vimdiff to compare ~/FILE to ~/.dotfiles/FILE
	  link FILE...        create a symlink from ~/FILE to ~/.dotfiles/FILE (multiple files possible)
	                      (-q: be quiet, only print something if an error occurs)
	  linkstatus [DIR]    list contents of .dotfiles with information about whether there are links to
	                      them in ~/, whether they differ, etc...
	  createfrom FILE...  copy ~/FILE to ~/.dotfiles/FILE and symlink the file
	                      (-n: only copy, leave the original, do not create symlink)

	Synchronization:
	  git ARGS...   wrapper for git (all arguments are passed on to git)
	  pull          shortcut for git pull
	END_OF_HELPTEXT
}


# Define root dir for dotfiles management (defaults to $HOME, can be overridden
# with environment variables for debugging)
if [[ -z $DOTFILESROOT ]]; then
	DOTFILESROOT="$HOME"
fi

# Set variables
homedir="$DOTFILESROOT"
dotfilesdir="$DOTFILESROOT/.dotfiles"


# Get command
[[ $# -gt 0 ]] || { usage; exit 1; }
cmd=$1
shift

# Parse command
case "$cmd" in
	help|--help|-h)
		usage
		exit 1
	;;

	install)
		bin_source="$dotfilesdir/bin/dotfiles"
		bin_target="$homedir/bin/dotfiles"
		bin_dir="$homedir/bin"

		if [[ ! -e $bin_target ]]; then
			echo "* Installing dotfiles script in $bin_dir/"
			mkdir -pv "$bin_dir"
			ln -sr "$bin_source" "$bin_target" || exit 1
		elif [[ ! "$(realpath $bin_target)" -ef "$bin_source" ]]; then
			echo "! Warning: File $bin_target already exists but is NOT a link to $bin_source"
		fi

		if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
			echo "! Warning: $bin_dir is not in your PATH."
		fi

		show_bash_restart_info=

		# Enable user-defined bash completion per directory
		bashcompletion_filename=.bash_completion
		if [[ ! -e "$homedir/$bashcompletion_filename" ]] ||
		   [[ ! "$(realpath "$homedir/$bashcompletion_filename")" -ef "$dotfilesdir/$bashcompletion_filename" ]];
		then
			echo "* Installing user-defined bash completion (~/.bash_completion.d/)"
			"$0" link -q "$bashcompletion_filename" &&
				show_bash_restart_info=1
		fi
		mkdir -p "$homedir/.bash_completion.d"

		# Activate dotfiles bash completion
		bashcompletion_dotfiles_filename=.bash_completion.d/10-dotfiles.sh
		if [[ ! -e "$homedir/$bashcompletion_dotfiles_filename" ]] ||
		   [[ ! "$(realpath "$homedir/$bashcompletion_dotfiles_filename")" -ef "$dotfilesdir/$bashcompletion_dotfiles_filename" ]];
		then
			echo "* Installing bash completion for dotfiles script"
			"$0" link -q "$bashcompletion_dotfiles_filename" &&
				show_bash_restart_info=1
		fi

		if [[ -n $show_bash_restart_info ]]; then
			echo "  Restart bash or run 'source $homedir/.bash_completion' to activate it now"
		fi

		echo "* Configuring git settings"
		git -C "$dotfilesdir" config merge.ff only
	;;

	ls)
		lsdir="$dotfilesdir"
		[[ -n $1 ]] && lsdir="$lsdir/$1"

		echo "$lsdir:"
		ls -lAh --color=auto "$lsdir"
	;;

	linkstatus)
		subdir=""
		if [[ -n $1 ]]; then
			subdir="${1%/}"
			[[ -e $subdir ]] || { echo "$dotfilesdir/$subdir does not exist"; exit 1; }
			[[ -d $subdir ]] && subdir="$subdir/"
		fi

		shopt -s nullglob dotglob
		cd $dotfilesdir
		dotdirs=()

		for file in $subdir*; do
			[[ -e $file ]] || continue
			[[ $file = ".git" ]] && continue

			file_home="$homedir/$file"
			file_dotfiles="$dotfilesdir/$file"

			if [[ -e $file_home ]]; then
				if [[ "$(realpath "$file_home")" -ef "$file_dotfiles" ]]; then
					file_status="    LINKED  "
				elif [[ -d $file_home ]]; then
					dotdirs+=($file)
					continue
				elif [[ -f $file_home ]] && cmp --silent "$file_home" "$file_dotfiles"; then
					file_status="==  EQUAL   "
				else
					file_status="<>  DIFFERS "
				fi
			else
				if [[ -d $file_dotfiles ]]; then
					file_status="++  NEW DIR "
				else
					file_status="++  NEW FILE"
				fi
			fi

			[[ -d $file_dotfiles ]] && file="$file/"
			echo "$file_status    $file"
		done

		if [[ $dotdirs ]]; then
			echo
			for dir in ${dotdirs[@]}; do
				echo "[$dir]"
				dotfiles linkstatus "$dir" | grep '\(LINKED\|EQUAL\|DIFFERS\|NEW\)'
				echo
			done
		fi
	;;

	edit)
		[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
		vim "$dotfilesdir/$1"
	;;

	diff)
		[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }
		file_A="$homedir/$1"
		file_B="$dotfilesdir/$1"

		if [[ "$(realpath "$file_A")" -ef "$file_B" ]]; then
			echo "$file_A is a link to $file_B"
		else
			vimdiff "$file_A" "$file_B"
		fi
	;;

	link)
		if [[ $1 = '-q' ]]; then
			QUIET=1
			shift
		fi

		[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }

		for filename in "$@"; do
			file_home="$homedir/$filename"
			file_dotfiles="$dotfilesdir/$filename"

			if [[ ! -e $file_dotfiles ]]; then
				echo "! $file_dotfiles: target file does not exist"
				exit 1
			fi

			if [[ -e $file_home ]]; then
				if [[ "$(realpath "$file_home")" -ef "$file_dotfiles" ]]; then
					# Target is already a link to the correct file
					continue
				fi

				if cmp --silent "$file_home" "$file_dotfiles"; then
					echo "! $file_home already exists and is equal to $file_dotfiles"
					read -p "  Delete $file_home and create link? [y/N] " -r
					if [[ ! $REPLY =~ ^[Yy]$ ]]; then
						# Don't delete file
						continue
					fi
					rm "$file_home" || exit 1
				else
					echo "! $file_home already exists but is NOT a link to $file_dotfiles, please fix manually"
					exit 1
				fi
			fi

			[[ $QUIET ]] || echo "* Creating link $file_home -> $file_dotfiles"
			ln -sr "$file_dotfiles" "$file_home" || exit 1
		done
	;;

	createfrom)
		if [[ $1 = '-n' ]]; then
			NOLINK=1
			shift
		fi

		[[ -n $1 ]] || { echo "$(basename $0) $cmd: Missing argument."; exit 1; }

		for filename in "$@"; do
			file_home="$homedir/$filename"
			file_dotfiles="$dotfilesdir/$filename"

			if [[ ! -e $file_home ]]; then
				echo "! $file_home: source file does not exist"
				exit 1
			elif [[ -e $file_dotfiles ]]; then
				echo "! $file_dotfiles already exists"
				exit 1
			else
				mkdir -p "$(dirname "$file_dotfiles")"
				[[ $QUIET ]] || echo "* Copy $file_home -> $file_dotfiles"
				cp -n "$file_home" "$file_dotfiles" || exit 1
				if [[ ! $NOLINK ]]; then
					[[ $QUIET ]] || echo "* Remove original and link $file_home -> $file_dotfiles"
					rm "$file_home" || exit 1
					ln -sr "$file_dotfiles" "$file_home" || exit 1
				fi
			fi
		done
	;;
	
	git)
		git -C "$dotfilesdir" "$@"
	;;

	pull)
		git -C "$dotfilesdir" pull "$@"
	;;

	bash-completion)
		cat <<- 'END_OF_BASHCOMPLETION'
		_dotfiles_filenames() {
			compopt -o filenames
			files=($(compgen -f -X '*/@(.|..|.git)' .dotfiles/$cur))
			COMPREPLY=(${files[@]#.dotfiles/})
		}
		_dotfiles() {
			COMPREPLY=()
			local cur="${COMP_WORDS[COMP_CWORD]}"
			local commands="help install ls edit diff link linkstatus createfrom git pull"
			local gitcommands="add commit diff fetch log pull push rebase status"

			if [[ $COMP_CWORD -eq 1 ]]; then
				COMPREPLY=($(compgen -W "$commands" -- $cur))
			else
				case "${COMP_WORDS[1]}" in
					ls|edit|diff)
						[[ $COMP_CWORD -eq 2 ]] && _dotfiles_filenames
					;;

					link|linkstatus)
						_dotfiles_filenames
					;;

					createfrom)
						compopt -o filenames
						COMPREPLY=($(compgen -f -- ${cur}))
					;;

					git)
						if [[ $COMP_CWORD -eq 2 ]]; then
							COMPREPLY=($(compgen -W "$gitcommands" -- ${cur}))
						else
							_dotfiles_filenames
						fi
					;;
				esac
			fi
		}

		complete -F _dotfiles dotfiles
		END_OF_BASHCOMPLETION
	;;

	*)
		echo "Unknown command '$cmd', use 'help' to show all commands."
		exit 1
	;;
esac

