#!/bin/sh
# shellcheck disable=SC2030,SC2031

set -e

atexit() {
	local _err="$?"

	# Dump contents of generated files to config.log.
	exec 1>>config.log 2>&1
	set -x
	[ -e config.h ] && cat config.h
	[ -e config.mk ] && cat config.mk
	rm -rf "$@" || :
	[ "${_err}" -ne 0 ] && fatal
	exit 0
}

compile() {
	# shellcheck disable=SC2086
	"${CC}" ${CPPFLAGS} -Werror -o /dev/null -x c - "$@"
}

cc_has_option() {
	if echo 'int main(void) { return 0; }' | compile "$@"; then
		echo "$@"
	fi
}

cc_has_sanitizer() {
	local _sanitizer

	_sanitizer="$1"; shift; : "${_sanitizer:?}"
	if echo 'int main(void) { return 0; }' |
	   compile "-fsanitize=${_sanitizer}" "$@"
	then
		echo "${_sanitizer}"
	fi
}

cc_sanitizer_runtime() {
	case "$(uname -s)" in
	OpenBSD)	echo "-fsanitize-minimal-runtime";;
	*)		;;
	esac
}

fatal() {
	[ $# -gt 0 ] && echo "fatal: ${*}"
	exec 1>&3 2>&4
	cat config.log
	exit 1
}

headers() {
	local _tmp="${WRKDIR}/headers"

	cat >"${_tmp}"
	[ -s "${_tmp}" ] || return 0

	xargs printf '#include <%s>\n' <"${_tmp}"
}

is_openbsd() {
	case "$(uname -s)" in
	OpenBSD)	return 0;;
	*)		return 1;;
	esac
}

make_has_object_dir() (
	cd "${WRKDIR}"
	# shellcheck disable=SC2016
	printf 'all:\n\t[ ${PWD} = ${.OBJDIR} ]\n' >Makefile
	printf 'all:\n\tfalse\n' >GNUMakefile
	mkdir -p obj
	make >/dev/null 1>&2
)

make_variable() {
	# shellcheck disable=SC2016
	var="$(printf 'all:\n\t@echo ${%s}\n' "$1" | make -sf -)"
	if [ -n "${var}" ]; then
		echo "${var}"
	else
		return 1
	fi
}

# rmdup arg ...
#
# Remove duplicates from the given arguments while preserving the order.
rmdup() {
	echo "$@" |
	xargs printf '%s\n' |
	awk '!x[$1]++' |
	xargs
}

check_arc4random() {
	compile <<-EOF
	#include <stdlib.h>

	int main(void) {
		return !(arc4random() <= 0xffffffff);
	}
	EOF
}

check_errc() {
	compile <<-EOF
	#include <err.h>

	int main(void) {
		errc(1, 0, "");
		return 0;
	}
	EOF
}

# Check if strptime(3) is hidden behind _GNU_SOURCE.
check_gnu_source() {
	local _tmp="${WRKDIR}/gnu"

	cat <<-EOF >"${_tmp}"
	#include <time.h>

	int main(void) {
		struct tm tm;
		return !(strptime("0", "%s", &tm) != NULL);
	}
	EOF

	compile <"${_tmp}" && return 1

	{ echo "#define _GNU_SOURCE"; cat "${_tmp}"; } | compile
}

check_pledge() {
	compile <<-EOF
	#include <unistd.h>

	int main(void) {
		return !(pledge("stdio", NULL) == 0);
	}
	EOF
}

check_stat_tim() {
	compile <<-EOF
	#include <sys/stat.h>

	int main(void) {
		struct stat st;

		if (stat("/var/empty", &st) == -1)
			return 1;
		return !(st.st_mtim.tv_sec > 0);
	}
	EOF
}

check_strlcpy() {
	compile <<-EOF
	#include <string.h>

	int main(void) {
		char buf[128];

		return !(strlcpy(buf, "strlcpy", sizeof(buf)) < sizeof(buf));
	}
	EOF
}

check_warnc() {
	compile <<-EOF
	#include <err.h>

	int main(void) {
		warnc(1, "");
		return 0;
	}
	EOF
}

# Quirk for GNU Bison 2.X present on macOS which declares YYSTYPE unless
# YYSTYPE_IS_DECLARED is defined.
check_yystype() {
	local _c="${WRKDIR}/c"
	local _yacc="${WRKDIR}/yacc"

	cat <<-EOF >"${_c}"
	%{
	void yyerror(const char *, ...);
	int yylex(void);

	typedef double YYSTYPE;
	%}

	%%
	grammar:
	%%

	int main(void) {
		return 0;
	}

	void yyerror(const char *fmt, ...)
	{
	}

	int yylex(void) {
		return 0;
	}
	EOF

	${YACC} -o "${_yacc}" "${_c}" || fatal "${YACC}: fatal error"
	! compile <"${_yacc}"
}

_fuzz=''
_pedantic=0
_sanitize=''

while [ $# -gt 0 ]; do
	case "$1" in
	--fuzz)		shift; _fuzz="$1"; _sanitize="address";;
	--pedantic)	_pedantic=1;;
	--sanitize)	shift; _sanitize="${1}";;
	*)		;;
	esac
	shift
done

WRKDIR=$(mktemp -dt configure.XXXXXX)
trap 'atexit ${WRKDIR}' EXIT

exec 3>&1 4>&2
exec 1>config.log 2>&1

# At this point, all variables used must be defined.
set -u
# Enable tracing, will end up in config.log.
set -x

HAVE_ARC4RANDOM=0
HAVE_ERRC=0
HAVE_GNU_SOURCE=0
HAVE_PLEDGE=0
HAVE_STAT_TIM=0
HAVE_STRLCPY=0
HAVE_WARNC=0
HAVE_YYSTYPE=0

# Order is important, must come first if not defined.
DEBUG="$(make_variable DEBUG || :)"

CC=$(make_variable CC || fatal "CC: not defined")
CFLAGS="$(unset CFLAGS DEBUG; make_variable CFLAGS || :) ${CFLAGS:-} ${DEBUG}"
CFLAGS="${CFLAGS} -MD -MP"
CPPFLAGS="$(make_variable CPPFLAGS || :)"
LDFLAGS="$(unset DEBUG; make_variable LDFLAGS || :)"
YACC=$(make_variable YACC || fatal "YACC: not defined")
YFLAGS="$(make_variable YFLAGS || :)"
PREFIX="$(make_variable PREFIX || echo /usr/local)"
BINDIR="$(make_variable BINDIR || echo "${PREFIX}/bin")"
MANDIR="$(make_variable MANDIR || echo "${PREFIX}/man")"
INSTALL="$(make_variable INSTALL || echo install)"
INSTALL_MAN="$(make_variable INSTALL_MAN || echo "${INSTALL}")"

# Following chunks must happen after CC is defined.

if [ "${_pedantic}" -eq 1 ]; then
	while read -r _o; do
		CFLAGS="${CFLAGS} ${_o}"
	done <<-CC-PEDANTIC
	-g
	-O2
	-Wall
	-Werror
	-Wextra
	-Wcast-qual
	-Wmissing-prototypes
	-Wpedantic
	-Wshadow
	-Wsign-conversion
	-Wwrite-strings
	CC-PEDANTIC

	while read -r _o; do
		CFLAGS="${CFLAGS} $(cc_has_option "${_o}")"
	done <<-CC-PEDANTIC-OPTIONAL
	-Waddress-of-packed-member
	-Wcovered-switch-default
	-Wextra-semi-stmt
	-Wformat-signedness
	-Wimplicit-fallthrough
	-Wmissing-format-attribute
	-Wmissing-variable-declarations
	-Wtautological-unsigned-enum-zero-compare
	-Wunreachable-code-aggressive
	-Wused-but-marked-unused
	-Wno-gnu-zero-variadic-macro-arguments
	CC-PEDANTIC-OPTIONAL

	while read -r _o; do
		LDFLAGS="${LDFLAGS} $(cc_has_option "${_o}")"
	done <<-LD-PEDANTIC-OPTIONAL
	-Wl,--fatal-warnings
	LD-PEDANTIC-OPTIONAL

	DEBUG="-g ${DEBUG}"
fi

if [ -n "${_sanitize}" ]; then
	(
		if is_openbsd; then
			# Abuse CPPFLAGS honored by compile.
			CPPFLAGS="${CPPFLAGS} -fsanitize-minimal-runtime"
			CPPFLAGS="${CPPFLAGS} -Wl,--no-execute-only"
		fi
		cc_has_sanitizer "${_sanitize}"
		cc_has_sanitizer undefined
		cc_has_sanitizer unsigned-integer-overflow
		{ [ "${_fuzz}" = "llvm" ] && echo fuzzer; } || :
	) >"${WRKDIR}/sanitize"
	if ! cmp -s /dev/null "${WRKDIR}/sanitize"; then
		_sanitize="-fsanitize=$(paste -sd , "${WRKDIR}/sanitize")"
		_sanitize="${_sanitize} $(cc_has_option -fno-sanitize-recover=all)"
		_sanitize="${_sanitize} $(cc_has_option -fno-omit-frame-pointer)"
		if is_openbsd; then
			_sanitize="${_sanitize} -fsanitize-minimal-runtime"
			LDFLAGS="${LDFLAGS} -Wl,--no-execute-only"
		fi
		CFLAGS="${_sanitize} ${CFLAGS}"
		DEBUG="${_sanitize} ${DEBUG}"
	fi
fi

if make_has_object_dir; then
	mkdir -p obj/{,libks}
fi

check_arc4random && HAVE_ARC4RANDOM=1
check_errc && HAVE_ERRC=1
check_gnu_source && HAVE_GNU_SOURCE=1
check_pledge && HAVE_PLEDGE=1
check_stat_tim && HAVE_STAT_TIM=1
check_strlcpy && HAVE_STRLCPY=1
check_warnc && HAVE_WARNC=1
check_yystype && HAVE_YYSTYPE=1

# Redirect stdout to config.h.
exec 1>config.h

printf '#ifndef CONFIG_H\n#define CONFIG_H\n'

# Order is important, must be present before any includes.
[ "${HAVE_GNU_SOURCE}" -eq 1 ] && printf '#define _GNU_SOURCE\n'

# Headers needed for function prototypes.
{
[ "${HAVE_ARC4RANDOM}" -eq 0 ] && echo stdint.h
[ "${HAVE_STRLCPY}" -eq 0 ] && echo stddef.h
} | sort | uniq | headers

[ "${HAVE_ARC4RANDOM}" -eq 1 ] && printf '#define HAVE_ARC4RANDOM\t1\n'
[ "${HAVE_ERRC}" -eq 1 ] && printf '#define HAVE_ERRC\t1\n'
[ "${HAVE_PLEDGE}" -eq 1 ] && printf '#define HAVE_PLEDGE\t1\n'
[ "${HAVE_STRLCPY}" -eq 1 ] && printf '#define HAVE_STRLCPY\t1\n'
[ "${HAVE_WARNC}" -eq 1 ] && printf '#define HAVE_WARNC\t1\n'

if [ -n "${_fuzz}" ]; then
	printf '#define FUZZER_%s\t1\n' \
		"$(echo "${_fuzz}" | tr '[:lower:]' '[:upper:]')"
fi

if [ "${HAVE_STAT_TIM}" -eq 0 ]; then
	printf '#define st_atim\tst_atimespec\n'
	printf '#define st_ctim\tst_ctimespec\n'
	printf '#define st_mtim\tst_mtimespec\n'
fi

if [ "${HAVE_YYSTYPE}" -eq 1 ]; then
	printf '#define YYSTYPE_IS_DECLARED\t1\n'
fi

[ "${HAVE_ARC4RANDOM}" -eq 0 ] &&
	printf 'uint32_t arc4random(void);\n'
[ "${HAVE_ERRC}" -eq 0 ] && {
	printf 'void errc(int, int, const char *, ...)\n';
	printf '\t__attribute__((__noreturn__, __format__ (printf, 3, 4)));\n'; }
[ "${HAVE_PLEDGE}" -eq 0 ] &&
	printf 'int pledge(const char *, const char *);\n'
[ "${HAVE_STRLCPY}" -eq 0 ] &&
	printf 'size_t strlcpy(char *, const char *, size_t);\n'
[ "${HAVE_WARNC}" -eq 0 ] && {
	printf 'void warnc(int, const char *, ...)\n'
	printf '\t__attribute__((__format__ (printf, 2, 3)));\n'; }

printf '#endif\n'

# Redirect stdout to config.mk.
exec 1>config.mk

# Use echo to normalize whitespace.
# shellcheck disable=SC1083,SC2086,SC2116
cat <<EOF
CC=			$(echo ${CC})
CFLAGS=			$(rmdup ${CFLAGS})
CPPFLAGS=		$(rmdup ${CPPFLAGS} -I\${.CURDIR})
DEBUG=			$(rmdup ${DEBUG})
LDFLAGS=		$(rmdup ${LDFLAGS})
YACC=			$(echo ${YACC})
YFLAGS=			$(rmdup ${YFLAGS})

BINDIR?=		$(echo ${BINDIR})
MANDIR?=		$(echo ${MANDIR})
INSTALL?=		$(echo ${INSTALL})
INSTALL_MAN?=		$(echo ${INSTALL_MAN})
EOF

if make_has_object_dir; then
	cat <<EOF

.SUFFIXES: .c .o
.c.o:
	\${CC} \${CPPFLAGS} \${CFLAGS} \${DEBUG} -c -o \${<:\${.CURDIR}/%.c=%.o} \$<
EOF
else
	cat <<EOF

.SUFFIXES: .c .o
.c.o:
	\${CC} \${CPPFLAGS} \${CFLAGS} \${DEBUG} -c -o \$@ \$<
EOF
fi
