#!/bin/sh

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"; : "${_sanitizer:?}"
	if echo 'int main(void) { return 0; }' |
	   compile "-fsanitize=${_sanitizer}"
	then
		echo "$@"
	fi
}

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}"
}

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

# pedantic
#
# Pedantic flags supported by most compilers.
pedantic() {
	cat <<-EOF | xargs
	-O2
	-Wall
	-Werror
	-Wextra
	-Wmissing-prototypes
	-Wpedantic
	-Wshadow
	-Wsign-conversion
	-Wwrite-strings
	EOF
}

# 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_reallocarray() {
	compile <<-EOF
	#include <stdlib.h>

	int main(void) {
		return !(reallocarray(NULL, 1, 1) != NULL);
	}
	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=0

while [ $# -gt 0 ]; do
	case "$1" in
	--fuzz)		shift; _fuzz="$1"; _sanitize=1;;
	--pedantic)	_pedantic=1;;
	--sanitize)	_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_REALLOCARRAY=0
HAVE_STAT_TIM=0
HAVE_STRLCPY=0
HAVE_WARNC=0
HAVE_YYSTYPE=0

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

CC=$(makevar CC || fatal "CC: not defined")
CFLAGS="$(unset CFLAGS DEBUG; makevar CFLAGS || :) ${CFLAGS:-} ${DEBUG}"
CFLAGS="${CFLAGS} -MD -MP"
CPPFLAGS="$(makevar CPPFLAGS || :)"
LDFLAGS="$(unset DEBUG; makevar LDFLAGS || :)"
YACC=$(makevar YACC || fatal "YACC: not defined")
YFLAGS="$(makevar YFLAGS || :)"

PREFIX="$(makevar PREFIX || echo /usr/local)"
BINDIR="$(makevar BINDIR || echo "${PREFIX}/bin")"
MANDIR="$(makevar MANDIR || echo "${PREFIX}/man")"
INSTALL="$(makevar INSTALL || echo install)"
INSTALL_MAN="$(makevar INSTALL_MAN || echo "${INSTALL}")"

# Following chunks must happen after CC is defined.
if [ "${_pedantic}" -eq 1 ]; then
	CFLAGS="$(cc_has_option -Wformat-signedness) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wimplicit-fallthrough) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wunreachable-code-aggressive) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wused-but-marked-unused) ${CFLAGS}"
	CFLAGS="-g $(pedantic) ${CFLAGS}"
	DEBUG="-g ${DEBUG}"
	LDFLAGS="$(cc_has_option -Wl,--fatal-warnings) ${LDFLAGS}"
fi
if [ "${_sanitize}" -eq 1 ]; then
	{
		cc_has_sanitizer address
		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)"
		CFLAGS="${_sanitize} ${CFLAGS}"
		DEBUG="${_sanitize} ${DEBUG}"
	fi
fi

check_arc4random && HAVE_ARC4RANDOM=1
check_errc && HAVE_ERRC=1
check_gnu_source && HAVE_GNU_SOURCE=1
check_pledge && HAVE_PLEDGE=1
check_reallocarray && HAVE_REALLOCARRAY=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_REALLOCARRAY}" -eq 0 ] && echo stddef.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_REALLOCARRAY}" -eq 1 ] && printf '#define HAVE_REALLOCARRAY\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_REALLOCARRAY}" -eq 0 ] &&
	printf 'void *reallocarray(void *, size_t, size_t);\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})

.PATH:	\${.CURDIR}/libks
VPATH=	\${.CURDIR}/libks
EOF
