#!/usr/bin/env bash

###############################################################################
# run is a utility for organizing your project’s CLI commands.
# https://run.jotaen.net
###############################################################################
# MIT License
# Copyright (c) 2022 Jan Heuermann, https://www.jotaen.net
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
###############################################################################

# Runtime configuration.
# These variables can be overriden while parsing the flags.
TASK_FILE='./run.sh'
ACTION=(run::action-list)

# Regex patterns for parsing the task file.
COMMENT_PATTERN='^#{1,}[[:blank:]]?(.*)$'
TASK_NAME_PATTERN='[a-zA-Z]+[a-zA-Z0-9:_-]*'
TASK_DEF_PATTERN_1='^[[:blank:]]*run::('"${TASK_NAME_PATTERN}"')[[:blank:]]*\([[:blank:]]*\)'
TASK_DEF_PATTERN_2='^[[:blank:]]*function[[:blank:]]*run::('"${TASK_NAME_PATTERN}"')'

# The --help procedure.
run::action-help() {
	echo 'Run tasks from a run.sh file.'
	echo
	echo 'Usage: run [OPTION] TASK [TASK_ARGS...]'
	echo
	echo 'Options:'
	echo '  -f, --file       Specify the task file (default: ./run.sh)'
	echo '  -i, --info task  Show task description'
	echo '  -l, --list       List all available tasks'
	echo '  -h, --help       Print this help'
	echo '      --version    Print version of this command'
}

# The --version procedure.
run::action-version() {
	echo 'Version 2.0'
	echo 'https://run.jotaen.net'
}

# The --list procedure.
run::action-list() {
	local title_candidate=""
	local tasks=() # All encountered task names.
	local titles=() # All encountered titles. (Same arity as $tasks.)
	local longest_task_name=0 # Needed for aligning the output.

	# Parse the task file.
	while IFS= read -r line; do
		# Process comment block.
		if [[ "${line}" =~ $COMMENT_PATTERN ]]; then
			if [[ -z "${title_candidate}" ]]; then
				local comment_text="${BASH_REMATCH[1]}"
				title_candidate="${comment_text}"
			fi
			continue
		fi

		# After a comment block, there must be the task definition.
		# Allow both definition styles: `foo() {...` and `function foo {`.
		if [[ "${line}" =~ $TASK_DEF_PATTERN_1 || "${line}" =~ $TASK_DEF_PATTERN_2 ]]; then
			local task="${BASH_REMATCH[1]}"
			tasks+=("${task}")
			titles+=("${title_candidate}")
			if [[ ${#task} -gt $longest_task_name ]]; then
				longest_task_name=${#task}
			fi
		fi

		# Reset parser for next iteration.
		title_candidate=""
	done < "${TASK_FILE}"

	# Print out the gathered information.
	local column_offset=3
	for (( i=0; i<${#tasks[@]}; i++ )); do
		local padding=0
		if [[ "${titles[$i]}" != "" ]]; then
			padding=$((longest_task_name+column_offset))
		fi
		printf "%-${padding}s%s\n" "${tasks[$i]}" "${titles[$i]}"
	done
}

# The --info procedure.
run::action-info() {
	local requested_task="$1"
	local comment_block_candidate=() # All comment lines for the task.

	# Abort if no task name was specified.
	if [[ "${requested_task}" == '' ]]; then
		echo 'No task specified' >&2
		exit 1
	fi

	# Parse the task file.
	while IFS= read -r line; do
		if [[ "${line}" =~ $COMMENT_PATTERN ]]; then
			comment_block_candidate+=("${BASH_REMATCH[1]}")
			continue
		fi
		if [[ "${line}" =~ $TASK_DEF_PATTERN_1 || "${line}" =~ $TASK_DEF_PATTERN_2 ]]; then
			if [[ "${BASH_REMATCH[1]}" == "${requested_task}" ]]; then
				# Print out the gathered comment lines.
				for i in "${comment_block_candidate[@]}"; do
					echo "${i}"
				done
				exit
			fi
		fi
		comment_block_candidate=()
	done < "${TASK_FILE}"
	echo "No such task: ${requested_task}" >&2
	exit 1
}

# The procedure when invoking a task.
run::action-execute() {
	readonly task_name="$1"
	readonly task_identifier="run::${task_name}"
	readonly input=( "${@:2}" )
	readonly task_file="${TASK_FILE}"

	# Check shebang of task file.
	shebang=$(head -n 1 "${task_file}")
	if [[ "${shebang}" == '#!'* &&
		    "${shebang}" != *'bash'* ]]; then
		echo "Unsupported shebang." >&2
		exit 1
	fi

	# Assemble final script.
	run_sh=''

	# Include task file.
	run_sh+='
source '"${task_file}"'
'

	# Add check, whether subcommand is defined.
	# shellcheck disable=SC2016
	run_sh+='
if [[ "$(type -t '"${task_identifier}"')" != "function" ]]; then
	echo "No such task: '"${task_name}"'" >&2
	exit 1
fi
'

	# Add task invocation.
	run_sh+='
'"${task_identifier} ${input[*]}"'
'

	# Run the final script.
	bash -c "$run_sh"
}

# Parse CLI arguments.
while [[ $# -gt 0 ]]; do
	case $1 in
		--help|-h)
			run::action-help
			exit
			;;
		--version)
			run::action-version
			exit
			;;
		--list|--ls|-l)
			ACTION=( run::action-list )
			shift
			continue
			;;
		--info=*|-i=*)
			ACTION=( run::action-info "${1#*=}" )
			shift
			continue
			;;
		--info|-i)
			ACTION=( run::action-info "$2" )
			shift
			shift
			continue
			;;
		--file=*|-f=*)
			TASK_FILE="${1#*=}"
			shift
			continue
			;;
		--file|-f)
			TASK_FILE="$2"
			shift
			shift
			continue
			;;
		-*)
			echo "Unknown option $1" >&2
			exit 1
			;;
		*)
			ACTION=( run::action-execute "$1" "${@:2}" )
			break
			;;
	esac
done

# If task file is given, check it.
if [[ -n "${TASK_FILE}" ]]; then
	# Check that it’s a regular file.
	if [[ -d "${TASK_FILE}" ]]; then
		echo "Not a file: ${TASK_FILE}" >&2
		exit 2
	fi
	# Check that it exists.
	if [[ ! -f "${TASK_FILE}" ]]; then
		echo "No such file: ${TASK_FILE}" >&2
		exit 2
	fi
fi

# Execute action.
"${ACTION[@]}"