#!/bin/sh

VERSION=1.2

#
# Simple git based voting system
#
# if $VOTE_DIR is no defined the checkout happens in the shell-script directory
#
# To setup a new shared VOTE_DIR, use:
# freefall> git init --bare /path/to/votes.git
# local>    git clone freefall:/path/to/votes.git votes
# local> cd votes
# local> git checkout -b main
#    Create the voters file (username:Name <email>)
# local> git add voters
# local> git commit -m "Initialize votes directory"
# local> git push -u origin main:main
#
#
# The mark_* and color_* values can be overriden in ~/.voterc 
#
col_reset="\033[0m"
# Color used to to show the output of shelled-out git calls.
col_git="\033[32m"
# Color for highlights within the script
col_highlight="\033[33m"

mark_ys="👍"
mark_no="👎"
mark_ab="💁"
mark_dn="💤"

mark_op="🔓"
mark_cl="🔒"

###
base_dir="$(dirname $(realpath $0))"
vote_dir="${VOTE_DIR:-${base_dir}/votes}"

###
text_ys="y"
text_no="n"
text_ab="a"
text_dn="-"

text_op="o"
text_op="c"

###
if [ -f ~/.voterc ] ; then
	. ~/.voterc ;
fi

###
__message () {
	echo -e "$@"
}

__error () {
	echo -e "$@" >&2
	exit 1
}
__usage() {
	__error "No command given. Available commands are:\n\n" \
		"- help\t\tShow this message:\n" \
		"\t\t${col_highlight}% vote help${col_reset}\n\n" \
		"- init\t\tInitialize a repository:\n" \
		"\t\t${col_highlight}% vote init <url>${col_reset}\n" \
		"- update\tUpdate the repository:\n" \
		"\t\t${col_highlight}% vote update${col_reset}\n\n" \
		"- list\t\tList current ongoing votes:\n" \
		"\t\t${col_highlight}% vote list${col_reset}\n" \
		"- all\t\tList all votes:\n" \
		"\t\t${col_highlight}% vote all${col_reset}\n" \
		"- info\t\tShow information on a vote:\n" \
		"\t\t${col_highlight}% vote info <vid>${col_reset}\n\n" \
		"- create\tCreate a new vote:\n" \
		"\t\t${col_highlight}% vote create <subject> <duedate> <further description>${col_reset}\n" \
		"- close\tMark a vote as closed:\n" \
		"\t\t${col_highlight}% vote close <vid>${col_reset}\n" \
		"- reopen\tReopen a vote:\n" \
		"\t\t${col_highlight}% vote reopen <vid>${col_reset}\n\n" \
		"- changedate\tChange the Due Date to something new\n" \
		"\t\t${col_highlight}% vote changedate <vid> <date>${col_reset}\n" \
		"- changesubject\tChange the Subject to something new\n" \
		"\t\t${col_highlight}% vote changesubject <vid> <subject>${col_reset}\n\n" \
		"- vote\t\tPlace a vote:\n" \
		"\t\t${col_highlight}% vote vote <vid> <yes/no/abstain>${col_reset}\n" \
		"- votefor\tPlace a vote for another person:\n" \
		"\t\t${col_highlight}% vote votefor <vid> <yes/no/abstain> <voter>${col_reset}\n\n" \
		"The checkout directory can be overridden using then ${col_highlight}VOTE_DIR${col_reset}\n" \
		"\n" \
		"Vote version ${VERSION}."
}

__want_args() {
	local count=$1
	shift

	if [ $# -ne ${count} ] ; then
		__error "Expected ${count} arguments, but got $#."
	fi
}

__username () {
	__want_args 1 "$@"
	local vote_dir="$1"

	username=$(git -C ${vote_dir} config user.email | awk -F '@' '{print $1}')
	if [ $? -eq 0 ] ; then
		echo "${username}"
		return 0
	fi

	__error "Could not evaluate git user name"
}

__setup_voters() {
	__want_args 2 "$@"
	local votes_dir="$1"
	local voters="$2"

	if [ -d ${votes_dir} ] ; then
		if [ -f ${voters} ] ; then
			for voter in $(awk -F ':' '{print $1}' ${voters})  ; do
				ln -s zzz ${votes_dir}/${voter}
			done
		fi
	fi
}

# Managing votes
create_vote () {
	__want_args 4 "$@"
	local vote_dir="$1"
	update_votes "${vote_dir}"

	local subject="$2"
	local duedate="$3"
	local description="$4"

	if [ $? -ne 0 ] ; then
		__error "Could not update votes prior to creation"
	fi

	local vid=$(__next_vid "${vote_dir}")
	local voters=$(awk -F ':' 'BEGIN{OFS=":"}{print " ",$0}' ${vote_dir}/voters | column -t -s ':')
	local commit_message=$(printf "<${vid}> [CREATE] ${subject} (${duedate})\n\n${description}\n\nVoters:\n${voters}\n")
	echo -e "${col_git}"
	cd ${vote_dir} && \
		mkdir -p ${vid} && \
		cd ${vid} && \
		echo "${subject}" > subject && \
		echo "${duedate}" > duedate && \
		echo "${description} " > description && \
		mkdir votes && \
		__setup_voters $(realpath votes) $(realpath ${vote_dir}/voters) && \
		git add subject duedate description votes && \
		git commit -m "${commit_message}" && \
		git push
	echo -e "${col_reset}"

	__message ""
	__pretty_print ${vote_dir} ${vid}
}

change_date () {
	__want_args 3 "$@"
	local vote_dir="$1"
	update_votes "${vote_dir}"
	local vid=$(__make_vid "$2")
	local newdate="$3"

	open=$(__is_open_vote ${vote_dir} ${vid})
	if [ $? -ne 0 ] ; then
		__error "Cannot modify a closed vote. ${vote_dir} ${vid}"
	fi
	local olddate=$(cat ${vote_dir}/${vid}/duedate)

	local commit_message=$(printf "<${vid}> [CHANGE DATE] ${subject} (${olddate}->${newdate})\n")
	echo -e "${col_git}"
	cd ${vote_dir} && \
		cd ${vid} && \
		echo "${newdate}" > duedate && \
		git add duedate && \
		git commit -m "${commit_message}" && \
		git push
	echo -e "${col_reset}"

	list_votes ${vote_dir}
}

change_subject () {
	__want_args 3 "$@"
	local vote_dir="$1"
	update_votes "${vote_dir}"
	local vid=$(__make_vid "$2")
	local newsubject="$3"

	open=$(__is_open_vote ${vote_dir} ${vid})
	if [ $? -ne 0 ] ; then
		__error "Cannot modify a closed vote. ${vote_dir} ${vid}"
	fi
	local oldsubject=$(cat ${vote_dir}/${vid}/subject)

	local commit_message=$(printf "<${vid}> [CHANGE DATE] ${oldsubject} -> ${newsubject}\n")
	echo -e "${col_git}"
	cd ${vote_dir} && \
		cd ${vid} && \
		echo "${newsubject}" > subject && \
		git add subject && \
		git commit -m "${commit_message}" && \
		git push
	echo -e "${col_reset}"

	list_votes ${vote_dir}
}


info_vote () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	__pretty_print ${vote_dir} ${vid}
}

__is_open_vote () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	if [ -f ${vote_dir}/${vid}/closeddate ] ; then
		return 1
	fi
	return 0
}

__set_vote() {
	__want_args 4 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")
	local username="$3"
	local value="$4"

	open=$(__is_open_vote ${vote_dir} ${vid})
	if [ $? -ne 0 ] ; then
		__error "Cannot vote on a closed vote. ${vote_dir} ${vid}"
	fi

	if [ ! -L ${vote_dir}/${vid}/votes/${username} ] ; then
		__error "Invalid voter ${username}"
	fi

	local target=""
	case "${value}" in
		"yes")		target="yes"     ;;
		"no")		target="no"      ;;
		"abstain")	target="abstain" ;;
		*)		__error "Invalid vote '${value}' (valid: yes, no, abstain)" ;;
	esac

	result=$(cd ${vote_dir}/${vid}/votes && ln -sf ${value} ${username})
	return $?
}

__short_result () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	echo $(__tally_vote ${vote_dir} ${vid})
	return 0
}
__pretty_print () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	local subject=$(cat ${vote_dir}/${vid}/subject)

	local y_voters=""
	local n_voters=""
	local a_voters=""
	local d_voters=""
	for vote in $(find -s ${vote_dir}/${vid}/votes -type l) ; do
		value=$(readlink ${vote})
		voter=$(basename ${vote})
		case "${value}" in
			"yes")
				y_voters="${y_voters}${y_voters:+, }${voter}"
				;;
			"no")
				n_voters="${n_voters}${n_voters:+, }${voter}"
				;;
			"abstain")
				a_voters="${a_voters}${a_voters:+, }${voter}"
				;;
			"zzz")
				d_voters="${d_voters}${d_voters:+, }${voter}"
				;;
			*)	   __error "Invalid vote value ${value} in ${vote} (${vid})"
		esac
	done

	result=$(printf "Vote on ${subject}.\n\nYes:\t\t${y_voters}\nNo:\t\t${n_voters}\nAbstained:\t${a_voters}\n\nDid not vote:\t${d_voters}")

	echo "${result}"
	return 0
}

reopen_vote () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	open=$(__is_open_vote ${vote_dir} ${vid})
	if [ $? -eq 0 ] ; then
		__error "Vote already open"
	fi

	local closeddate=$(cat ${vote_dir}/${vid}/closeddate)
	rm ${vote_dir}/${vid}/closeddate

	local tally=$(__short_result ${vote_dir} ${vid})
	local pretty=$(__pretty_print ${vote_dir} ${vid})
	local commit_message=$(printf "<${vid}> [REOPEN] ${tally}\n\n${pretty}\nOld Closed Date:\t${closeddate}")

	echo -e "${col_git}"
	cd ${vote_dir} && \
		git add ${vote_dir}/${vid}/closeddate && \
		git commit -m "${commit_message}" && \
		git push
	echo -e "${col_reset}"

	__pretty_print ${vote_dir} ${vid}
	return 0
}

close_vote () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	open=$(__is_open_vote ${vote_dir} ${vid})
	if [ $? -ne 0 ] ; then
		__error "Vote already closed"
	fi

	echo $(date -u '+%Y%m%d') > ${vote_dir}/${vid}/closeddate

	local tally=$(__short_result ${vote_dir} ${vid})
	local pretty=$(__pretty_print ${vote_dir} ${vid})
	local commit_message=$(printf "<${vid}> [CLOSE] ${tally}\n\n${pretty}")

	echo -e "${col_git}"
	cd ${vote_dir} && \
		git add ${vote_dir}/${vid}/closeddate && \
		git commit -m "${commit_message}" && \
		git push
	echo -e "${col_reset}"

	__pretty_print ${vote_dir} ${vid}
	return 0
}

# Interacting with votes
init_votes () {
	__want_args 2 "$@"
	local vote_dir="$1"
	local url="$2"

	if [ -d "${vote_dir}" ] ; then
		__error "Directory '${vote_dir}' already exists."
	fi

	echo -e "${col_git}"
	git clone ${url} ${vote_dir} 
	if [ $? -ne 0 ] ; then
		echo -e "${col_reset}"
		__error "Could not clone ${url} to ${vote_dir}"
	fi
	echo -e "${col_reset}"

	list_all_votes ${vote_dir}
	return 0
}

update_votes () {
	__want_args 1 "$@"

	local vote_dir="$1"
	__check_git ${vote_dir}
	if [ $? -ne 0 ] ; then
		__error "Directory '${vote_dir}' is not a repository."
	fi

	echo -e "${col_git}"
	cd "${vote_dir}" && git pull --rebase --autostash
	if [ $? -ne 0 ] ; then
		echo -e "${col_reset}"
		__error "Failed to update '${vote_dir}' -- please try manual merge."
	fi
	echo -e "${col_reset}"

	list_votes ${vote_dir}

	return 0
}

__list_vids () {
	local vote_dir="$1"

	vids=$(find -E "${vote_dir}" -type d -regex ".*/[0-9][0-9][0-9][0-9]$" | sort | awk -F '/' '{print $NF}')

	if [ $? -ne 0 ] ; then
		__error "Failed to enumerate vids"
	fi

	echo "${vids}"
	return 0
}

__last_vid () {
	local vote_dir="$1"
	result=$(__list_vids "${vote_dir}" | tail -n1)
	echo ${result}
}

__make_vid () {
	__want_args 1 "$@"
	echo $(printf "%04d" $(expr $1 + 0))
	return 0
}

__next_vid () {
	__want_args 1 "$@"

	local vote_dir="$1"
	last=$(__last_vid "${vote_dir}")
	if [ -z ${last} ] ; then
		last=0
	fi

	echo $(__make_vid $(expr ${last} + 1))
	return 0
}

__list_vote_entry () {
	__want_args 3 "$@"

	local vote_dir="$1"
	local vid=$(__make_vid "$2")
	local include_closed="$3"

	if [ ! -d ${vote_dir}/${vid} ] ; then
		__error "Failed to read info on ${vid} from ${vote_dir}"
	fi

	local state=${mark_cl}
	__is_open_vote ${vote_dir} ${vid}
	if [ $? -eq 0 ] ; then
		state=${mark_op}
	fi
	local duedate=$(cat         ${vote_dir}/${vid}/duedate)
	local subject=$(cut -c 1-50 ${vote_dir}/${vid}/subject)
	local votes=$(__tally_vote  ${vote_dir} ${vid})
	local own_vote=$(__own_vote ${vote_dir} ${vid})
	local closed_info=""
	if [ "${include_closed}" = "yes" ] ; then
		if [ -f ${vote_dir}/${vid}/closeddate ] ; then
			closed_info="|$(cat ${vote_dir}/${vid}/closeddate)"
		else
			closed_info="|"
		fi
	fi
	echo "${state}|${duedate}|${vid}|${subject}|${votes}|${own_vote}${closed_info}"
}

__vote_mark () {
	__want_args 1 "$@"

	local result=${mark_dn}
	case "$1" in
		"yes")     result=${mark_ys} ;;
		"no")      result=${mark_no} ;;
		"abstain") result=${mark_ab} ;;
	esac
	echo "${result}"
}

__vote_text() {
	__want_args 1 "$@"

	local result=${text_dn}
	case "$1" in
		"yes")     result=${text_ys} ;;
		"no")      result=${text_no} ;;
		"abstain") result=${text_ab} ;;
	esac
	echo "${result}"
}

__get_vote () {
	__want_args 3 "$@"

	local vote_dir="$1"
	local vid=$(__make_vid "$2")
	local username="$3"

	if [ ! -d ${vote_dir}/${vid} ] ; then
		__error "Failed to read info on ${vid} from ${vote_dir}"
	fi

	local uservote=""

	local vote=${vote_dir}/${vid}/votes/${username}
	voted=1

	local result=${mark_dn}
	if [ -L ${vote} ] ; then
		vote_value=$(readlink ${vote})
		if [ "${vote_value}" != "zzz" ] ; then
			voted=0
		fi
		result=$(__vote_mark ${vote_value})
	fi

	echo "${result}"
	return ${voted}
}

__own_vote () {
	__want_args 2 "$@"

	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	local username=$(__username ${vote_dir})
	local result=$(__get_vote ${vote_dir} ${vid} ${username})

	echo "${result}"
	return 0
}


__tally_vote () {
	__want_args 2 "$@"

	local vote_dir="$1"
	local vid=$(__make_vid "$2")

	if [ ! -d ${vote_dir}/${vid} ] ; then
		__error "Failed to read info on ${vid} from ${vote_dir}"
	fi

	local count_yes=0
	local count_no=0
	local count_abstain=0
	local count_dnv=0

	for vote in $(find ${vote_dir}/${vid}/votes -type l) ; do
		value=$(readlink ${vote})
		voter=$(basename ${vote})
		case "${value}" in
			"yes")     count_yes=$(expr ${count_yes} + 1)         ;;
			"no")      count_no=$(expr ${count_no} + 1)           ;;
			"abstain") count_abstain=$(expr ${count_abstain} + 1) ;;
			"zzz")     count_dnv=$(expr ${count_dnv} + 1)         ;;
			*)	   __error "Invalid vote value ${value} in ${vote} (${vid})"
		esac
	done

	result=$(printf "${mark_ys}%2d, ${mark_no}%2d, ${mark_ab}%2d, ${mark_dn}%2d" ${count_yes} ${count_no} ${count_abstain} ${count_dnv})
	echo "${result}"

	return 0
}

list_votes () {
	__want_args 1 "$@"
	local vote_dir="$1"

	result=""
	for vid in $(__list_vids "$@") ; do
		__is_open_vote ${vote_dir} ${vid}
		if [ $? -eq 0 ] ; then
			result="${result}\n$(__list_vote_entry ${vote_dir} ${vid} no)"
		fi
	done

	result=$(echo -e "${result}" | sort -r)
	result=$(printf "State|Due Date|VID|Subject|Tally|Own Vote\n${result}")
	echo "${result}" | column -t -s '|'
}


list_all_votes () {
	__want_args 1 "$@"
	local vote_dir="$1"

	result=""
	for vid in $(__list_vids "$@") ; do
		result="${result}\n$(__list_vote_entry ${vote_dir} ${vid} yes)"
	done

	result=$(echo -e "${result}" | sort -r)
	result=$(printf "State|Due Date|VID|Subject|Tally|Own Vote|Closed On\n${result}")
	echo "${result}" | column -t -s '|'
}

place_vote () {
	__vote "$@"
}

place_vote_for () {
	__vote_for "$@"
}

__vote_for () {
	__want_args 4 "$@"
	local vote_dir="$1"
	update_votes "${vote_dir}"

	local vid=$(__make_vid "$2")
	local value="$3"
	local username="$4"

	if [ ! -L ${vote_dir}/${vid}/votes/${username} ] ; then
		__error "Invalid voter ${username}"
	fi

	local author=$(awk -F : "/${username}/{print \$NF}" ${vote_dir}/voters)
	if [ -z "${author}" ] ; then
		__error "Could not read author for ${username}"
	fi

	local action=""
	local msg=""
	oldvote=$(__get_vote ${vote_dir} ${vid} ${username})
	if [ $? -eq 0 ] ; then
		action="[CHANGE VOTE]"
		message="${oldvote} -> $(__vote_text ${value})"
	else
		action="[SET VOTE]"
		message="$(__vote_text ${value})"
	fi

	local foreignvote=""
	gituser=$(__username ${vote_dir})
	if [ ${username} != ${gituser} ] ; then
		foreignvote=" (vote placed by ${gituser})"
	fi

	__set_vote "${vote_dir}" "${vid}" "${username}" "${value}"
	if [ $? -ne 0 ] ; then
		__error "Could not place vote ${value} on ${vid}"
	fi

	local subject=$(cut -c 1-50 ${vote_dir}/${vid}/subject)

	local commit_message=$(printf "<${vid}> ${action} ${subject} :: ${username} ${message}${foreignvote}\n\n$(__pretty_print ${vote_dir} ${vid})")

	echo -e "${col_git}"
	cd ${vote_dir} && \
		git add ${vid}/votes/${username} && \
		git commit -m "${commit_message}" --author="${author}" && \
		git push
	echo -e "${col_reset}"

	__message ""
	__pretty_print ${vote_dir} ${vid}
}


__vote () {
	__want_args 3 "$@"
	local username=$(__username ${vote_dir})
	if [ -z "${username}" ] ; then
		__error "Could not read your git username"
	fi
	__vote_for "$@" ${username}
}

# utils
__check_git () {
	local git_dir=$(realpath "$1")
	if [ ! -d "${git_dir}" ] ; then
		return 1
	fi
	(
		cd "${git_dir}"
		git rev-parse --is-inside-work-tree > /dev/null 2>&1
		if [ $? -ne 0 ] ; then
			return 1
		fi
	)
	return 0
}

if [ -z $1 ] ; then
	__usage
fi

command=$1
shift
case "${command}" in
	"help")    __usage ;;
	"init")    init_votes      ${vote_dir} "$@" ;;
	"update")  update_votes    ${vote_dir}      ;;
	"list")    list_votes      ${vote_dir}      ;;
	"all")     list_all_votes  ${vote_dir}      ;;
	"create")  create_vote     ${vote_dir} "$@" ;;
	"info")    info_vote       ${vote_dir} "$@" ;;
	"close")   close_vote      ${vote_dir} "$@" ;;
	"reopen")  reopen_vote     ${vote_dir} "$@" ;;
	"changedate")    change_date     ${vote_dir} "$@" ;;
	"changesubject") change_subject  ${vote_dir} "$@" ;;
	"vote")    place_vote      ${vote_dir} "$@" ;;
	"votefor") place_vote_for  ${vote_dir} "$@" ;;
	*)         __usage ;;
esac
