#! /bin/bash
#
# bootleg-stow
# Copyright (C) 2021 Vintage Salt <rehashedsalt@cock.li>
#
# Distributed under terms of the MIT license.
#
set -e

# Read-only set-once variables
declare -r _name="$(basename -- "$0")"
declare -i _opthelp
declare -i _optverbose
# Modes
declare -i _optstow=1
declare -i _optunstow
# Working variables
declare -a _args
declare _return
declare _files
declare _directories

# Helper functions
log() {
	# Print a line to the terminal if _optverbose is greater than $2
	# $2 defaults to 0
	# loglevel 0: Daily-use messages
	# loglevel 1: Detailed but not quite debugging
	# loglevel 2: Definitely debugging
	[ -z "$1" ] && return 1
	if (( _optverbose >= ${2:-0} )); then
		printf "%s\\n" "$1"
	fi
}
warn() {
	# Print a yellow line to the terminal, respecting _optverbose
	[ -z "$1" ] && return 1
	if (( _optverbose >= ${2:-0} )); then
		if [ -t 1 ]; then
			printf "\\e[33m%s\\e[0m\\n" "$1"
		else
			printf "WARN: %s\\n" "$1"
		fi
	fi
}
error() {
	# Print a red line to the terminal, exit if $2 is specified
	[ -z "$1" ] && return 1
	if [ -t 2 ]; then
		printf "\\e[31m%s\\e[0m\\n" "$1" 1>&2
	else
		printf "ERROR: %s\\n" "$1" 1>&2
	fi
	[ -z "$2" ] && return
	exit "${2:-1}"
}
has() {
	# Parse out all arguments and try to find them in path
	# If an argument cannot be found, set _return and fail
	for prog in "$@"; do
		if ! command -v "$prog" > /dev/null 2>&1; then
			_return="$prog"
			return 1
		fi
	done
	return 0
}
checkconflict() {
	# Take a directory as argument 1 and stow its contents in ..
	[ -z "$1" ] && return 1
	if ! [ -d "$1" ]; then
		error "Could not find directory: $1" 2
	fi
	if ! [ -r "$1" ]; then
		error "Could not read directory: $1" 2
	fi
	# Get our list of files
	local files="$(find "$1" -type f -o -type l)"
	local directories="$(find "$1" -type d)"
	local -a conflict
	# Iterate over them
	for file in $files; do
		# Get the basename of the file
		filename="${file#"$1"/}"
		if [ -f ../"$filename" ]; then
			if [ -h ../"$filename" ]; then
				continue
			else
				conflict+=("$filename")
			fi
		fi
	done
	if [ -n "${conflict[*]}" ]; then
		warn "The following non-symlinks would be touched by this operation:"
		for file in "${conflict[@]}"; do
			echo "$file"
		done
		error "Please resolve these conflicts manually." 3
	fi
	_files="$files"
	_directories="$directories"
}
stow() {
	# Stow all of _files and _directories in $1 as a package name
	# Note that you should checkconflict first
	[ -z "$1" ] && return 1
	stowdir="$(basename -- "$PWD")"
	log "Stowing package: $stowdir" 1
	pushd .. > /dev/null 2>&1
	for dir in $_directories; do
		dirname="${dir#"$1"/}"
		if [ "$dir" == "$1" ]; then
			log "Skipping package directory: $dir" 2
			continue
		fi
		if ! [ -d "$dirname" ]; then
			log "Creating directory: $dirname" 2
			mkdir -p "$dirname"
		fi
	done
	for file in $_files; do
		filename="${file#"$1"/}"
		if has realpath dirname; then
			path="$(realpath --relative-to="$(dirname "$filename")" "$PWD/$stowdir/$1/$filename")"
		else
			path="$PWD/$stowdir/$1/$filename"
		fi
		if [ -h "$filename" ]; then
			rm "$filename"
		fi
		if [ -h "$PWD/$stowdir/$1/$filename" ]; then
			log "Copying symlink: $filename" 2
			cp -d "$PWD/$stowdir/$1/$filename" "$filename"
		else
			log "Linking file: $filename to $path" 2
			ln -s "$path" "$filename"
		fi
	done
	log "Done stowing package: $1" 1
	popd > /dev/null 2>&1
}
unstow() {
	# Unstow all of _files and _directories
	# Takes a packagename as $1
	[ -z "$1" ] && return 1
	pushd .. > /dev/null 2>&1
	for file in $_files; do
		filename="${file#"$1"/}"
		if [ -h "$filename" ]; then
			rm "$filename"
		elif ! [ -e "$filename" ]; then
			warn "File does not exist, skipping: $filename" 1
		else
			warn "File is not a symlink, skipping: $filename"
		fi
	done
	_directories="$(echo "$_directories" | tac)"
	log "Removing empty directories" 2
	for dir in $_directories; do
		dirname="${dir#"$1"/}"
		# We silently ignore errors here so that rmdiring a directory with stuff in
		# it doesn't break our loop.
		rmdir -p "$dirname" > /dev/null 2>&1 || continue
	done
	popd > /dev/null 2>&1
}

# Core program functions
printhelp() {
	cat << EOF
Usage: $_name [OPTION]... PACKAGE...
Bootleg stow for the poor sods who can't get it.

  -h			Print this help text
  -R			Unstow a directory.
  -S			Stow a directory. Default operation.
  -v			Print more status messages. Stacks

Copyright (c) 2021 rehashedsalt@cock.li
Licensed under the MIT license
EOF
}

# Main
main() {
	# Parse out arguments
	while [ -n "$1" ]; do
		# Parse out flags
		while getopts ":hRSv" opt; do
			case $opt in
				h)
					_opthelp=1
					;;
				R)
					_optunstow=1
					;;
				S)
					_optstow=1
					;;
				v)
					_optverbose+=1
					;;
				:)
					error "Option requires argument: -$OPTARG" 2
					;;
				*)
					error "Invalid option: -$OPTARG" 2
					;;
			esac
		done
		# Store arguments
		shift $((OPTIND - 1))
		if [ -n "$1" ]; then
			_args+=("$1")
			shift
		fi
		unset OPTIND
	done
	# Early hook for help
	[ -n "$_opthelp" ] && printhelp && exit 0
	# Validate critical options
	# TODO: That
	# Validate core program dependencies
	log "Validating dependencies" 2
	if ! has basename find tac; then
		error "Failed to find program: $_return" 1
	fi

	# Do the do
	if [ -n "$_optunstow" ]; then
		if [ -n "${_args[*]}" ]; then
			for package in "${_args[@]}"; do
				checkconflict "$package"
				unstow "$package"
			done
		else
			error "No package specified" 1
		fi
	elif [ -n "$_optstow" ]; then
		if [ -n "${_args[*]}" ]; then
			for package in "${_args[@]}"; do
				checkconflict "$package"
				stow "$package"
			done
		else
			error "No package specified" 1
		fi
	fi
	exit 0
}

main "$@"

