#!/bin/sh
# gengetoptions is released under CC0
# https://creativecommons.org/publicdomain/zero/1.0/

set -euf

VERSION="v3.3.2"
URL="https://github.com/ko1nksm/getoptions"
LICENSE="Creative Commons Zero v1.0 Universal"
PROG=${0##*/}

SHELLCHECK="shell=sh disable=SC2004,SC2034,SC2145,SC2194"

[ "${ZSH_VERSION:-}" ] && setopt shwordsplit

parser_definition_setup() {
	setup REST help:usage abbr:true width:25 error -- \
		"Usage: $PROG $1" '' 'Options:'
}

parser_definition() {
	parser_definition_setup \
		"[options]... <command> [arguments]..."

	disp :info --info -- 'Show detected getoptions info'
	disp :usage -h --help -- "Display this help and exit"
	disp VERSION -v --version -- "Display the version and exit"

	msg -- '' 'Commands:'

	cmd library -- 'Generate custom library'
	cmd parser -- 'Generate option parser'
	cmd embed -- 'Embed the generated library or parser'
	cmd example -- 'Generate parser definition example'
}

parser_definition_library() {
	parser_definition_setup \
		"library [options]..."

	flag SHELLCHECK --shellcheck -- \
		"Embed the shellcheck directives"

	flag BASE --no-base init:=getoptions -- \
		"Do not include base library"

	flag ABBR --no-abbr init:=getoptions_abbr -- \
		"Do not include abbr library"

	flag HELP --no-help init:=getoptions_help -- \
		"Do not include help library"

	parser_definition_style

	disp :usage -h --help -- "Display this help and exit"
}

parser_definition_parser() {
	parser_definition_setup \
		"parser [options]... <definition> <parser> [arguments]..."

	param FILE -f --file -- \
		"Read from a file instead of stdin"

	param DEFINITION -d --definition var:NAME hidden:true -- \
		"Specify the parser definition name (deprecated)"

	option SHELLCHECK --shellcheck var:DIRECTIVES on:"$SHELLCHECK" -- \
		"Embed the shellcheck directives" "  [default: '$SHELLCHECK']"

	parser_definition_style

	disp :usage -h --help -- "Display this help and exit"
}

parser_definition_style() {
	option INDENT -i --indent on:2 validate:number var:N -- \
		"Use N spaces instead of tabs for indentation [default: 2]"

	flag COMMENT --no-comments init:@on -- \
		"Do not embed comments"

	param ARG --optarg -- \
		"Use ARG instead of the variable name OPTARG"

	param IND --optind -- \
		"Use IND instead of the variable name OPTIND"
}

parser_definition_embed() {
	parser_definition_setup \
		"embed [options]... <file>"

	flag OVERWRITE -w --overwrite -- \
		"Overwrite the specified file"

	flag ERASE -e --erase -- \
		"Erase the embedded code"

	disp :usage -h --help -- "Display this help and exit"
}


number() {
	case $OPTARG in (*[!0-9]*)
		return 1
	esac
	return 0
}

abort() {
	echo "gengetoptions:" "$@" >&2
	exit 1
}

error() {
	case $2 in
		number:*) echo "Not a number: $3" ;;
		*) return 0 ;;
	esac
	return 1
}

parser_definition_not_found() {
	abort "The parser definition '$1' not found"
}

is_valid_identifier() {
	case $1 in (*[!a-zA-Z0-9_]*) return 1; esac
	return 0
}

parse_shellcheck_directives() {
	shellcheck_shell='' shellcheck_disable=''
	# shellcheck disable=SC2086
	set -- $1
	while [ $# -gt 0 ]; do
		case $1 in
			shell=*) shellcheck_shell=${1#*\=} ;;
			disable=*) shellcheck_disable=${1#*\=} ;;
		esac
		shift
	done
}

comment() {
	[ "$COMMENT" ] || return 0
	echo "#" "$@"
}

print_shellcheck() {
	[ "$2" ] || return 0
	printf '# shellcheck %s=%s\n' "$1" "$2"
}

replace_all() {
	if [ $# -lt 4 ]; then
		eval "set -- \"\$1\" \"\${$1}\" \"\$2\" \"\$3\" \"\""
	else
		set -- "$1" "$2" "$3" "$4" ""
	fi
	while :; do
		case $2 in
			*"$3"*) set -- "$1" "${2#*"$3"}" "$3" "$4" "$5${2%%"$3"*}$4" ;;
			*) break ;;
		esac
	done
	eval "$1=\$5\$2"
}

chvarname() {
	while IFS= read -r line; do
		[ "$ARG" ] && replace_all line OPTARG "$ARG"
		[ "$IND" ] && replace_all line OPTIND "$IND"
		printf '%s\n' "$line"
	done
}

tab2space() {
	[ ! "${1:-}" ] && cat && return 0
	TAB=$(printf '\t')

	prefix='' i=0
	# shellcheck disable=SC2004
	while [ "$i" -lt "$1" ] && i=$(($i + 1)); do
		prefix="$prefix "
	done
	i=0

	while IFS= read -r line; do
		while [ "$line" ]; do
			case $line in ([!${TAB}]*) break; esac
			# shellcheck disable=SC2004
			i=$(($i + 1)) line=${line#?}
		done
		# shellcheck disable=SC2004
		while [ "$i" -gt 0 ] && i=$(($i - 1)); do
			line="${prefix}${line}"
		done
		printf '%s\n' "$line"
	done
}

embed() {
	args=''
	while IFS= read -r line; do
		case $line in
			\#\ @end)
				if [ "$args" ]; then
					if [ ! "$ERASE" ]; then
						embed_gengetoptions "$1" "$args"
					fi
					args=''
				fi
				;;
			\#\ @gengetoptions\ *)
				[ "$args" ] && abort 'Missing @end directive'
				args=${line#*\ *\ }
				;;
			*) [ "$args" ] && continue
		esac
		printf '%s\n' "$line"
	done
	[ "$args" ] && abort 'Missing @end directive'
	printf '%s' "$line"
}

fetch() {
	in_section=''
	while IFS= read -r line; do
		case $line in
			\#\ @getoptions) in_section=1 && continue ;;
			\#\ @end) in_section='' && continue ;;
		esac
		if [ "$in_section" ]; then
			printf '%s\n' "$line"
		fi
	done
}

embed_gengetoptions() {
	eval "fetch < \"\$1\" | gengetoptions $2" | {
		while IFS= read -r line; do
			case $line in
				\#\ shellcheck\ shell=*) continue
			esac
			printf '%s\n' "$line"
		done
	}
}

do_library() {
	eval "$(getoptions parser_definition_library)"

	lib=$("$GETOPTIONS" -)
	printf '%s\n' "$lib" | {
		# shellcheck disable=SC2030
		funcname='' shellcheck_disable=""
		while IFS= read -r line; do
			case $line in
				\#\ shellcheck*)
					shellcheck_disable=""
					[ "$SHELLCHECK" ] || continue
					case $line in (*disable*)
						shellcheck_disable=${line#*disable=}
					esac
					continue ;;
				\#*) continue ;;
				getoptions*)
					funcname=${line%%\(\)*}
					case $funcname in ("$BASE" | "$ABBR" | "$HELP")
						comment "[$funcname] License: $LICENSE"
						comment "$URL ($VERSION)"
						print_shellcheck disable "$shellcheck_disable"
					esac
			esac
			case $funcname in ("$BASE" | "$ABBR" | "$HELP")
				printf '%s\n' "$line"
			esac
		done
	 } | tab2space "$INDENT" | chvarname
}

do_parser() {
	eval "$(getoptions parser_definition_parser)"
	[ $# -lt 2 ] && usage && exit 1

	if [ "$DEFINITION" ] || { [ ! "$FILE" ] && [ -e "$1" ]; } then
		echo "Deprecated. Use the --file option instead." >&2
		if [ ! "$DEFINITION" ]; then
			DEFINITION=${1##*/} && DEFINITION=${DEFINITION%%.*}
		fi
		FILE=$1 parser=$2 definition=$DEFINITION
		shift
		set -- "$definition" "$@"
	else
		definition=$1 parser=$2
	fi

	if [ "$definition" != "-" ]; then
		if is_valid_identifier "$definition"; then
			eval "$definition() { parser_definition_not_found \"\$definition\"; }"
		else
			abort "The parser definition name '$definition' is not a valid name"
		fi
	fi
	if ! is_valid_identifier "$parser"; then
		abort "The parser name '$parser' is not a valid name"
	fi

	{
		# shellcheck disable=SC1090
		case $FILE in
			'' | - ) eval "$(cat)" ;;
			/* | ./* | ../*) . "$FILE" ;;
			*) . "./$FILE" ;;
		esac
		getoptions "$@"
	} | {
		parse_shellcheck_directives "$SHELLCHECK"
		print_shellcheck shell "$shellcheck_shell"
		comment "Generated by getoptions (BEGIN)"
		comment "URL: $URL ($VERSION)"
		while IFS= read -r line; do
			[ "$line" = "# Do not execute" ] && continue
			if [ "$line" = "$parser() {" ]; then
				# shellcheck disable=SC2031
				print_shellcheck disable "$shellcheck_disable"
			fi
			printf '%s\n' "$line"
		done
		comment "Generated by getoptions (END)"
	} | tab2space "$INDENT" | chvarname
}

do_embed() {
	eval "$(getoptions parser_definition_embed)"
	[ $# -lt 1 ] && usage && exit 1

	[ -e "$1" ] || abort "No such file: $1"

	if [ "$OVERWRITE" ]; then
		# shellcheck disable=SC2094
		contents=$(embed "$1" < "$1")
		printf '%s\n' "$contents" > "$1"
	else
		# shellcheck disable=SC2094
		embed "$1" < "$1"
	fi
}

do_example() {
cat<<'HERE'
parser_definition() {
	setup   REST plus:true help:usage abbr:true \
		-- "Usage: ${2##*/} [options...] [arguments...]" ''
	msg -- 'Options:'
	flag    FLAG    -f +f --{no-}flag
	flag    VERBOSE -v    --verbose   counter:true init:=0
	param   PARAM   -p    --param     pattern:"foo | bar"
	option  OPTION  -o    --option    on:"default" validate:number
	disp    :usage  -h    --help
	disp    VERSION       --version
}
eval "$(getoptions parser_definition - "$0") exit 1"
HERE
}

info() {
	"$GETOPTIONS"
	echo "Path: $GETOPTIONS"
}

if [ -e "${0%/*}/getoptions" ]; then
	GETOPTIONS="$(cd "${0%/*}" && pwd)/getoptions"
elif which getoptions >/dev/null 2>&1; then
	GETOPTIONS=$(which getoptions)
else
	abort "getoptions not found"
fi
getoptions() { "$GETOPTIONS" "$@"; }

eval "$(getoptions parser_definition)"

[ "${1:---}" = "--" ] && usage && exit 1
command=$1
shift
"do_$command" "$@"
