#!/bin/zsh
# vim: ft=zsh noet ts=4 sts=4 sw=4

autoload -Uz colors; colors

setopt extended_glob warn_create_global no_unset
zmodload zsh/zutil

typeset -gA vars vars_prev
typeset -ga do_command actions_performed
typeset -g confz_indent check_only verbose fail_reason quiet

: ${check_only:=0}
: ${verbose:=0}
: ${quiet:=0}

# helper that prints out error message and exits
die() {
	print -r - "$fg_bold[red]*$reset_color $@" >&2
	exit 1
}

confz_print_usage() {
	echo >&2 "usage: $0:h [-f <fpath>] <requirement> [<args>] [; <requirement> ...]"
	exit 2
}

# option parsing
typeset -ga confz_opt_f
zparseopts -D - f+:=confz_opt_f || die "Unable to parse options"
() {
	local opt val
	for opt val in "$confz_opt_f[@]"; do
		[[ $opt == "-"* ]] || die "Something went wrong"
		fpath=( $val "$fpath[@]" )
	done
}

# trace-printing helpers
confz_check_start() {
	(($quiet)) || print -r - "${confz_indent}checking $fg_bold[default]$1$reset_color ${(@q)argv[2,-1]} ${reset_color}[" >&2
	confz_indent+=" "
}

confz_check_ok() {
	confz_indent=${confz_indent% }
	(($quiet)) || print -r - "$confz_indent] $fg[green]$1$reset_color OK" >&2
}

confz_check_fail() {
	confz_indent=${confz_indent% }
	if (($check_only)); then
		print -P "$confz_indent] $fg[red]$1$reset_color FAIL" >&2
	else
		(($quiet)) || print -P "$confz_indent] $fg[yellow]$1$reset_color NEED" >&2
	fi
}

confz_do() {
	(($quiet)) || print -r - "$confz_indent$fg[yellow]*$fg[default] ${(q)@}" >&2
	actions_performed+=( "$*" )
	"$@" || die "command failed with error $?: ${(q)@}"
}

# trace-printing helper for setting $vars values
setvar() {
	if (($verbose)); then
		print -r - "$confz_indent$fg[cyan]$1$fg[default]=${(qqq)2}" >&2
	fi
	vars[$1]=$2
}

# set $vars parameter if it's empty (ie. set default value)
defvar() {
	if ! (($+vars[$1])); then
		setvar "$1" "$2"
	fi
}

# check if variables are nonempty
checkvars() {
	local var
	local -a empty
	for var in "$@"; do
		if ! (($+vars[$var])); then
			empty+=( $var )
		fi
	done
	(( $#empty )) && die "required parameters are empty: ${(@q)empty}"
}

# check if given arguments (as name value pairs) match current $vars
matchvars() {
	local name value
	for name value in "$@"; do
		if (($+vars[$name])); then
			[[ $value == $vars[$name] ]] || return 1
		fi
	done
	return 0
}

# unify given arguments (as name value pairs) with current $vars
unify() {
	local name value
	for name value in "$@"; do
		if (($+vars[$name])); then
			[[ $value == $vars[$name] ]] \
				|| die "Unified variable $name has value ${(qqq)vars[$name]} not matching ${(qqq)value}"
		else
			vars[$name]=$value
		fi
	done
}

# autoload all relevant functions and run confz_*_init
confz_load() {
	local func
	local -a match mbegin mend confz_functions func_files

	func_files=( $^fpath/confz_*(N) )

	for func in $func_files ; do
		if ! [[ -r "$func" ]]; then
			print -r >&2 - "$func is not readable"
			continue
		fi
		autoload -Uz $func:t
		confz_functions+=$func:t
	done

	for func in ${(o)confz_functions}; do
		if [[ $func == *_init ]]; then
			$func || die "Init function failed: $func"
		fi
	done
}

# check & run & check a dependency
require() {
	# usage: require <name> [<variables>] [-- <arguments>]
	# where:
	#   <name> is a dependency name as defined by function confz_<name>_check
	#   <variables> are variable assignments in the form of:
	#       foo=bar  -- sets ${vars[foo]} in calee to value "bar"
	#       :foo  -- passes ${vars[foo]} from caller to calee
	#       ?foo=bar -- sets ${vars[foo]} in calee to value of ${vars[bar]}
	#                   iff it is defined in the caller
	#       ?foo -- passes ${vars[foo]} from caller to calee iff it is defined
	#               in the caller
	#       %foo  -- passes ${vars[foo]} from callee to caller
	#       %foo=bar  -- passes ${vars[foo]} from callee
	#                    to variable ${vars[bar]} of caller
	#   <arguments> are arguments passed to the dependency function

	local name outer inner indent_prev check_only_prev check_ret
	local prev_name new_name
	local -a do_command_prev
	local -A vars_local lift

	name=$1
	shift

	# store and clear $vars
	vars_prev=( "${(@kv)vars}" )
	vars_local=( "${(@kv)vars}" )
	vars=( )

	# print check start
	confz_check_start $name "$@"

	# parse variable assignments
	while (( $# )); do
		case $1 in
			(:*)   ((${+vars_prev[${1#:}]})) || \
				die "variable ${(qqq)1#:} not set, passed as :argument"
				setvar ${1#:} "${vars_prev[${1#:}]}";;
			(\?*=*)
				prev_name=${${1#\?}#*=}
				new_name=${${1#\?}%%=*}
				(($+vars_prev[$prev_name])) && \
					setvar $new_name "$vars_prev[$prev_name]";;
			(\?*)  ((${+vars_prev[${1#\?}]})) && \
				setvar ${1#\?} "${vars_prev[${1#\?}]}";;
			(%*=*) lift[${${1#%}%%=*}]=${${1#%}#*=};;
			(%*)   lift[${1#%}]=${1#%};;
			(*=*)  setvar "${1%%=*}" "${1#*=}";;
			(--)   shift; break;;
			(*)    die "$name: unrecognised argument: ${(qqq)1}";;
		esac
		shift
	done

	# store old $do_command
	do_command_prev=( "${do_command[@]}" )
	do_command=( confz_${name}_do )

	# clear fail_reason
	fail_reason=''

	if ! (($+functions[confz_${name}_check])); then
		die "$name: Function confz_${name}_check not defined."
	fi
	# perform check - run - check
	if confz_${name}_check "$@"; then
		confz_check_ok $name
	else
		check_ret=$?
		confz_check_fail $name

		if (($check_only)); then
			# if (($verbose)); then
			# 	print "re-running check with xtrace"
			# 	set +x
			# 	confz_${name}_check
			# 	set -x
			# fi
			die "$name: check failed: ${fail_reason:-error $check_ret}"
		else
			if (($verbose)); then
				print -r - "$confz_indent$fg[cyan]*$fg[default] reason: ${fail_reason:-error $check_ret}"
				(($+functions[confz_${name}_do])) && \
					typeset -f -t confz_${name}_do
			fi
			confz_do "${do_command[@]}" "$@"

			# print check start
			confz_check_start $name "$@" again...

			# clear fail_reason
			fail_reason=''

			# perform check once again
			check_only_prev=$check_only
			check_only=1
			confz_${name}_check "$@" || \
				die "$name: check failed: ${fail_reason:-error $?}"
			confz_check_ok $name
			check_only=$check_only_prev
		fi
	fi

	# clear fail_reason
	fail_reason=''

	# restore $do_command
	do_command=( "${do_command_prev[@]}" )

	# restore old $vars and put calee's $vars into $vars_prev
	vars_prev=( "${(@kv)vars}" )
	vars=( "${(@kv)vars_local}" )

	# lift %variables from calee to caller
	for inner outer in ${(kv)lift}; do
		((${+vars_prev[$inner]})) || \
			die "variable ${(qqq)inner} not set, was requested by caller as ${(qqq)outer}"
		setvar $outer ${vars_prev[$inner]}
	done
}

# "dependency" that represents toplevel and gets it's deps out of argv
confz_main_check() {
	local arg
	local -a args
	local -a printout
	for arg in "$@"; do
		if [[ $arg == ';' ]]; then
			require $args
			for arg in $printout; do
				printf "%s=%s\n" $arg ${(qqq)vars[$arg]}
			done
			args=()
			printout=()
		else
			if [[ $arg == %* ]]; then
				printout+=( ${${arg#%}##*=} )
			fi
			args+=( $arg )
		fi
	done
	(( $#args )) && require $args
			for arg in $printout; do
				printf "%s=%s\n" $arg ${(qqq)vars[$arg]}
			done

	(($check_only)) && return 0
	fail_reason="performed $#actions_performed actions, recheck"
	return $#actions_performed
}

# noop for the toplevel, functionality is in the dependencies
confz_main_do() {
	true
}

# run the toplevel
confz_load
require main -- "$@"