#!/bin/bash

# shellcheck disable=SC2016,SC2059
# SC2016: Expressions don't expand in single quotes, use double quotes for that.
# SC2059 Don't use variables in the printf format string.


# DEFINE_SCRIPT_DIR()
# ARG_POSITIONAL_SINGLE([input],[The input template file (pass '-' for stdin)])
# ARG_OPTIONAL_SINGLE([output],[o],[Name of the output file (pass '-' for stdout)],[-])
# ARG_OPTIONAL_SINGLE([type],[t],[Output type to generate],[bash-script])
# ARG_OPTIONAL_BOOLEAN([library],[],[Whether the input file if the pure parsing library.])
# ARG_OPTIONAL_SINGLE([strip],[],[Determines what to have in the output.],[none])
# ARG_OPTIONAL_BOOLEAN([check-typos],[],[Whether to check for possible argbash macro typos],[on])
# ARG_OPTIONAL_BOOLEAN([commented],[c],[Commented mode - include explanatory comments with the parsing code],[off])
# ARG_OPTIONAL_REPEATED([search],[I],[Directories to search for the wrapped scripts (directory of the template will be added to the end of the list)],["."])
# ARG_OPTIONAL_SINGLE([debug],[],[(developer option) Tell autom4te to trace a macro])
# ARG_TYPE_GROUP_SET([content],[content],[strip],[none,user-content,all])
# ARG_TYPE_GROUP_SET([type],[type],[type],[bash-script,completion,docopt])
# ARG_DEFAULTS_POS([])
# ARG_VERSION([echo "argbash v$version"])
# ARG_HELP([Argbash is an argument parser generator for Bash.])

# ARGBASH_GO()
# needed because of Argbash --> m4_ignore([
### START OF CODE GENERATED BY Argbash v2.6.1 one line above ###
# Argbash is a bash code generator used to get arguments parsing right.
# Argbash is FREE SOFTWARE, see https://argbash.io for more info

die()
{
	local _ret=$2
	test -n "$_ret" || _ret=1
	test "$_PRINT_HELP" = yes && print_help >&2
	echo "$1" >&2
	exit ${_ret}
}

begins_with_short_option()
{
	local first_option all_short_options
	all_short_options='otcIvh'
	first_option="${1:0:1}"
	test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0
}


# validators
content()
{
	local _allowed=("none" "user-content" "all")
	local _seeking="$1"
	for element in "${_allowed[@]}"
	do
		test "$element" = "$_seeking" && echo "$element" && return 0
	done
	die "Value '$_seeking' (of argument '$2') doesn't match the list of allowed values: 'none', 'user-content' and 'all'" 4
}
type()
{
	local _allowed=("bash-script" "completion" "docopt")
	local _seeking="$1"
	for element in "${_allowed[@]}"
	do
		test "$element" = "$_seeking" && echo "$element" && return 0
	done
	die "Value '$_seeking' (of argument '$2') doesn't match the list of allowed values: 'bash-script', 'completion' and 'docopt'" 4
}

# THE DEFAULTS INITIALIZATION - POSITIONALS
_positionals=()
_arg_input=
# THE DEFAULTS INITIALIZATION - OPTIONALS
_arg_output="-"
_arg_type="bash-script"
_arg_library="off"
_arg_strip="none"
_arg_check_typos="on"
_arg_commented="off"
_arg_search=(".")
_arg_debug=

print_help ()
{
	printf '%s\n' "Argbash is an argument parser generator for Bash."
	printf 'Usage: %s [-o|--output <arg>] [-t|--type <type>] [--(no-)library] [--strip <content>] [--(no-)check-typos] [-c|--(no-)commented] [-I|--search <arg>] [--debug <arg>] [-v|--version] [-h|--help] <input>\n' "$0"
	printf '\t%s\n' "<input>: The input template file (pass '-' for stdin)"
	printf '\t%s\n' "-o,--output: Name of the output file (pass '-' for stdout) (default: '-')"
	printf '\t%s\n' "-t,--type: Output type to generate (one of 'bash-script', 'completion' and 'docopt'; default: 'bash-script')"
	printf '\t%s\n' "--library,--no-library: Whether the input file if the pure parsing library. (off by default)"
	printf '\t%s\n' "--strip: Determines what to have in the output. (one of 'none', 'user-content' and 'all'; default: 'none')"
	printf '\t%s\n' "--check-typos,--no-check-typos: Whether to check for possible argbash macro typos (on by default)"
	printf '\t%s\n' "-c,--commented,--no-commented: Commented mode - include explanatory comments with the parsing code (off by default)"
	printf '\t%s\n' "-I,--search: Directories to search for the wrapped scripts (directory of the template will be added to the end of the list) (default array: (\".\") )"
	printf '\t%s\n' "--debug: (developer option) Tell autom4te to trace a macro (no default)"
	printf '\t%s\n' "-v,--version: Prints version"
	printf '\t%s\n' "-h,--help: Prints help"
}

parse_commandline ()
{
	while test $# -gt 0
	do
		_key="$1"
		case "$_key" in
			-o|--output)
				test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
				_arg_output="$2"
				shift
				;;
			--output=*)
				_arg_output="${_key##--output=}"
				;;
			-o*)
				_arg_output="${_key##-o}"
				;;
			-t|--type)
				test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
				_arg_type="$(type "$2" "type")" || exit 1
				shift
				;;
			--type=*)
				_arg_type="$(type "${_key##--type=}" "type")" || exit 1
				;;
			-t*)
				_arg_type="$(type "${_key##-t}" "type")" || exit 1
				;;
			--no-library|--library)
				_arg_library="on"
				test "${1:0:5}" = "--no-" && _arg_library="off"
				;;
			--strip)
				test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
				_arg_strip="$(content "$2" "strip")" || exit 1
				shift
				;;
			--strip=*)
				_arg_strip="$(content "${_key##--strip=}" "strip")" || exit 1
				;;
			--no-check-typos|--check-typos)
				_arg_check_typos="on"
				test "${1:0:5}" = "--no-" && _arg_check_typos="off"
				;;
			-c|--no-commented|--commented)
				_arg_commented="on"
				test "${1:0:5}" = "--no-" && _arg_commented="off"
				;;
			-c*)
				_arg_commented="on"
				_next="${_key##-c}"
				if test -n "$_next" -a "$_next" != "$_key"
				then
					begins_with_short_option "$_next" && shift && set -- "-c" "-${_next}" "$@" || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option."
				fi
				;;
			-I|--search)
				test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
				_arg_search+=("$2")
				shift
				;;
			--search=*)
				_arg_search+=("${_key##--search=}")
				;;
			-I*)
				_arg_search+=("${_key##-I}")
				;;
			--debug)
				test $# -lt 2 && die "Missing value for the optional argument '$_key'." 1
				_arg_debug="$2"
				shift
				;;
			--debug=*)
				_arg_debug="${_key##--debug=}"
				;;
			-v|--version)
				echo "argbash v$version"
				exit 0
				;;
			-v*)
				echo "argbash v$version"
				exit 0
				;;
			-h|--help)
				print_help
				exit 0
				;;
			-h*)
				print_help
				exit 0
				;;
			*)
				_positionals+=("$1")
				;;
		esac
		shift
	done
}


handle_passed_args_count ()
{
	_required_args_string="'input'"
	test ${#_positionals[@]} -ge 1 || _PRINT_HELP=yes die "FATAL ERROR: Not enough positional arguments - we require exactly 1 (namely: $_required_args_string), but got only ${#_positionals[@]}." 1
	test ${#_positionals[@]} -le 1 || _PRINT_HELP=yes die "FATAL ERROR: There were spurious positional arguments --- we expect exactly 1 (namely: $_required_args_string), but got ${#_positionals[@]} (the last one was: '${_positionals[*]: -1}')." 1
}

assign_positional_args ()
{
	_positional_names=('_arg_input' )

	for (( ii = 0; ii < ${#_positionals[@]}; ii++))
	do
		eval "${_positional_names[ii]}=\${_positionals[ii]}" || die "Error during argument parsing, possibly an Argbash bug." 1
	done
}

parse_commandline "$@"
handle_passed_args_count
assign_positional_args

# OTHER STUFF GENERATED BY Argbash
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || die "Couldn't determine the script's running directory, which probably matters, bailing out" 2
# Validation of values



### END OF CODE GENERATED BY Argbash (sortof) ### ])
# [ <-- needed because of Argbash

version=2.6.1

#

_files_to_clean=()
cleanup()
{
	test "${#_files_to_clean[*]}" != 0 && rm -f "${_files_to_clean[@]}"
}

# $1: What (string) to pipe to autom4te
run_autom4te()
{
	printf '%s\n' "$1" \
		| autom4te "${DEBUG[@]}" -l m4sugar -I "$m4dir"
	return $?
}


# TODO: Refactor to associative arrays as soon as the argbash online is sufficiently reliable
# We don't use associative arrays due to old bash compatibility reasons
autom4te_error_messages=(
	'end of file in string'
	'end of file in argument list'
)
# the %.s means "null format"
argbash_error_response_stem=(
	'You seem to have an unmatched square bracket on line %d:\n\t%s\n'
	"You seem to have a '(' open parenthesis unmatched by a closing one somewhere above the line %d (%s)\\n"
)

# $1: The error string
# $2: The input (that caused the error)
# $3: The number of the first line of the actual file input by the user
interpret_error()
{
	# print the error, smart stuff may follow
	printf '%s\n' "$1"
	local eof_lineno line_mentioned
	for idx in "${!autom4te_error_messages[@]}"
	do
		eof_lineno="$(printf "%s" "$1" | grep -e "${autom4te_error_messages[$idx]}" | sed -e 's/.*:\([0-9]\+\).*/\1/')"
		if test -n "$eof_lineno"
		then
			line_mentioned="$(printf '%s' "$2" | sed -n "$eof_lineno"p | tr -d '\n\r')"
			printf "${argbash_error_response_stem[$idx]}" "$((eof_lineno - $3))" "$line_mentioned"
		fi
		eof_lineno=''
	done
}

# $1: The input file
# $2: The original intended output file
define_file_metadata()
{
	local _defines='' _input_dirname _output_dirname

	_input_dirname="$(dirname "$1")"
	test "$1" != '-' && _defines="${_defines}m4_define([INPUT_BASENAME], [[$(basename "$1")]])"
	_defines="${_defines}m4_define([INPUT_ABS_DIRNAME], [[$(cd "$_input_dirname" && pwd)]])"

	_output_dirname="$(dirname "$2")"
	test "$2" != '-' && _defines="${_defines}m4_define([OUTPUT_BASENAME], [[$(basename "$2")]])"
	_defines="${_defines}m4_define([OUTPUT_ABS_DIRNAME], [[$(cd "$_output_dirname" && pwd)]])"
	printf "%s" "$_defines"
}


assert_m4_files_are_readable()
{
	local _argbash_lib="$m4dir/argbash-lib.m4"
	test -d "$m4dir" && test -x "$m4dir" || die "The directory '$m4dir' with files needed by Argbash is not browsable. Check whether it exists and review it's permissions."
	test -r "$_argbash_lib" && test -f "$_argbash_lib" || die "The '$_argbash_lib' file needed by Argbash is not readable. Check whether it exists and review it's permissions."
	test -r "$output_m4" && test -f "$output_m4" || die "The '$output_m4' file needed by Argbash is not readable. Check whether it exists and review it's permissions."
}


# The main function that generates the parsing script body
# $1: The input file
# $2: The output file
# $3: The argument type
do_stuff()
{
	local _pass_also="$_wrapped_defns" input prefix_len _ret
	test "$_arg_commented" = on && _pass_also="${_pass_also}m4_define([COMMENT_OUTPUT])"
	_pass_also="${_pass_also}m4_define([_OUTPUT_TYPE], [[$3]])"
	_pass_also="${_pass_also}$(define_file_metadata "$_arg_input" "$2")"
	input="$(printf '%s\n' "$_pass_also" | cat - "$m4dir/argbash-lib.m4" "$output_m4")"
	prefix_len=$(printf '%s\n' "$input" | wc -l)
	input="$(printf '%s\n' "$input" | cat - "$1")"
	run_autom4te "$input" 2> "$discard" \
		| grep -v '^#\s*needed because of Argbash -->\s*$' \
		| grep -v '^#\s*<-- needed because of Argbash\s*$'
	_ret=$?
	if test $_ret != 0
	then
		local errstr
		errstr="$(run_autom4te "$input" 2>&1 > "$discard")"
		interpret_error "$errstr" "$input" "$prefix_len" >&2
		echo "Error during autom4te run, aborting!" >&2;
		exit $_ret;
	fi
	return "$_ret"
}

# Fills content to variable _wrapped_defns --- where are scripts of given stems
settle_wrapped_fname()
{
	# Get arguments to ARGBASH_WRAP
	# Based on http://stackoverflow.com/a/19772067/592892
	_srcfiles=()
	while read -r line; do
		_srcfiles+=("$line")
	done < <(echo 'm4_changecom()m4_define([ARGBASH_WRAP])' "$(cat "$infile")" \
			| autom4te -l m4sugar -t 'ARGBASH_WRAP:$1' 2> "$discard")

	test "${#_srcfiles[@]}" -gt 0 || return
	for srcstem in "${_srcfiles[@]}"
	do
		_found=no
		for searchdir in "${_arg_search[@]}"
		do
			test -f "$searchdir/$srcstem.m4" && { _found=yes; ext='.m4'; break; }
			test -f "$searchdir/$srcstem.sh" && { _found=yes; ext='.sh'; break; }
			test -f "$searchdir/$srcstem" && { _found=yes; ext=''; break; }
		done
		# The last searchdir is a correct one
		test $_found = yes || { echo "Couldn't find wrapped file of stem '$srcstem' in any of directories: ${_arg_search[*]}" >&2; exit 2; }
		_wrapped_defns="${_wrapped_defns}m4_define([_SCRIPT_$srcstem], [[$searchdir/$srcstem$ext]])"
	done
}

# If we want to have the parsing code in a separate file,
# 1. Find out the (possible) filename
# 2. If the file exists, finish (OK).
# 3. If the .m4 file exists, finish (OK)
# 4. Something is wrong
get_parsing_code()
{
	local _shfile _m4file _newerfile
	# Get the argument of INCLUDE_PARSING_CODE
	_srcfile="$(echo 'm4_changecom()m4_define([INCLUDE_PARSING_CODE])' "$(cat "$infile")" \
			| autom4te -l m4sugar -t 'INCLUDE_PARSING_CODE:$1' 2> "$discard" \
			| tail -n 1)"
	test -n "$_srcfile" || return 1
	_thatfile="$(dirname "$infile")/$_srcfile"
	test -f "$_thatfile" && _shfile="$_thatfile"
	# Take out everything after last dot (http://stackoverflow.com/questions/125281/how-do-i-remove-the-file-suffix-and-path-portion-from-a-path-string-in-bash)
	_thatfile="${_thatfile%.*}.m4"
	test -f "$_thatfile" && _m4file="$_thatfile"

	# if have neither of files
	test -z "$_shfile" && test -z "$_m4file" && echo "Strange, we think that there was a source file '$_srcfile' that should be included, but we haven't found it in directory '$(dirname "$_thatfile")'" >&2 && return 1
	# We have one file, but not the other one => decision of what to pick is easy.
	test -z "$_shfile" && test -n "$_m4file" && echo "$_m4file" && return
	test -n "$_shfile" && test -z "$_m4file" && echo "$_shfile" && return

	# We have both, so we pick up the newer one
	_newerfile="$_shfile"
	test "$_m4file" -nt "$_shfile" && _newerfile="$_m4file"
	echo "$_newerfile"
}

# MS Windows compatibility fix
discard=/dev/null
test -e $discard || discard=NUL

set -o pipefail

infile="$_arg_input"

trap cleanup EXIT
# If we are reading from stdout, then create a temp file
if test "$infile" = '-'
then
	infile=temp_in_$$
	_files_to_clean+=("$infile")
	cat > "$infile"
fi

m4dir=//usr/share/argbash
test -n "$_arg_debug" && DEBUG=('-t' "$_arg_debug")

output_m4="$m4dir/output-strip-none.m4"
test "$_arg_library" = "on" && { echo "The --library option is deprecated, use --strip user-content" next time >&2; _arg_strip="user-content"; }
if test "$_arg_strip" = "user-content"
then
	output_m4="$m4dir/output-strip-user-content.m4"
elif test "$_arg_strip" = "all"
then
	output_m4="$m4dir/output-strip-all.m4"
fi

test -f "$infile" || _PRINT_HELP=yes die "argument '$infile' is supposed to be a file!" 1
test -n "$_arg_output" || { echo "The output can't be blank - it is not a legal filename!" >&2; exit 1; }
outfname="$_arg_output"
autom4te --version > "$discard" 2>&1 || { echo "You need the 'autom4te' utility (it comes with 'autoconf'), if you have bash, that one is an easy one to get." 2>&1; exit 1; }
_arg_search+=("$(dirname "$infile")")
_wrapped_defns=""

# So let's settle the parsing code first. Hopefully we won't create a loop.
parsing_code="$(get_parsing_code)"
# Just if the original was m4, we replace .m4 with .sh
test -n "$parsing_code" && parsing_code_out="${parsing_code:0:-2}sh"
test "$_arg_library" = off && test -n "$parsing_code" && ($0 --library "$parsing_code" -o "$parsing_code_out")

# We may use some of the wrapping stuff, so let's fill the _wrapped_defns
settle_wrapped_fname

assert_m4_files_are_readable
output="$(do_stuff "$infile" "$outfname" "$_arg_type")" || die "" "$?"
if test "$_arg_check_typos" = on
then
	# match against suspicious, then inverse match against correct stuff:
	# #<optional whitespace>\(allowed\|another allowed\|...\)<optional whitespace><opening bracket <or> end of line>
	# Then, extract all matches (assumed to be alnum chars + '_') from grep and put them in the error msg.
	grep_output="$(printf "%s" "$output" | grep '^#\s*\(ARG_\|ARGBASH\)' | grep -v '^#\s*\(ARGBASH_SET_INDENT\|ARG_OPTIONAL_SINGLE\|ARG_VERSION\|ARG_HELP\|ARG_OPTIONAL_INCREMENTAL\|ARG_OPTIONAL_REPEATED\|ARG_VERBOSE\|ARG_OPTIONAL_BOOLEAN\|ARG_OPTIONAL_ACTION\|ARG_POSITIONAL_SINGLE\|ARG_POSITIONAL_INF\|ARG_POSITIONAL_MULTI\|ARG_POSITIONAL_DOUBLEDASH\|ARG_OPTION_STACKING\|ARG_RESTRICT_VALUES\|ARG_DEFAULTS_POS\|ARG_LEFTOVERS\|ARGBASH_WRAP\|INCLUDE_PARSING_CODE\|DEFINE_SCRIPT_DIR\|ARGBASH_SET_DELIM\|ARGBASH_GO\|ARGBASH_PREPARE\|ARG_TYPE_GROUP\|ARG_TYPE_GROUP_SET\|ARG_USE_ENV\|ARG_USE_PROG\)\s*\((\|$\)' | sed -e 's/#\s*\([[:alnum:]_]*\).*/\1 /' | tr -d '\n\r')"
	test -n "$grep_output" && die "Your script contains possible misspelled Argbash macros: $grep_output" 1
fi
if test "$outfname" != '-'
then
	printf "%s\\n" "$output" > "$outfname"
	chmod a+x "$outfname"
else
	printf "%s\\n" "$output"
fi

# # ] <-- needed because of Argbash
