#!/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
	-Wcast-qual
	EOF
}

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

# Check if the compiler implicitly supports GNU C99.
check_gnu99() {
	compile <<-EOF
	int main(void) {
		int n = __extension__ ({ int _n = 0; _n + 1; });
		for (int i = 0; i < n; i++)
			return 0;
		return 1;
	}
	EOF
}

# check_gnu_source path
#
# Check if the given header rooted in /usr/include depends on _GNU_SOURCE.
check_gnu_source() {
	local _header
	local _path
	local _tmp="${WRKDIR}/gnu"

	_header="$1"; : "${_header:?}"
	_path="/usr/include/${_header}"
	# shellcheck disable=SC2086
	"${CC}" ${CPPFLAGS} -E "${_path}" >"${_tmp}"
	# shellcheck disable=SC2086
	! "${CC}" ${CPPFLAGS} -E -D_GNU_SOURCE "${_path}" | cmp -s "${_tmp}" -
}

check_machine() {
	compile <<-EOF
	#include <sys/param.h>

	int main(void) {
		static const char m[] = MACHINE;
		return 0;
	}
	EOF
}

check_machine_arch() {
	compile <<-EOF
	#include <sys/param.h>

	int main(void) {
		static const char m[] = MACHINE_ARCH;
		return 0;
	}
	EOF
}

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

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

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

	int main(void) {
		return !(strtonum("1", 1, 2, NULL) != 0);
	}
	EOF
}

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

	int main(void) {
		return !(unveil("/", "r") == 0);
	}
	EOF
}

_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_GNU_SOURCE=0
HAVE_MACHINE=0
HAVE_MACHINE_ARCH=0
HAVE_PLEDGE=0
HAVE_STRTONUM=0
HAVE_UNVEIL=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 || :)"

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

# Following chunks must happen after CC is defined.
if ! check_gnu99; then
	CFLAGS="-std=gnu99 ${CFLAGS}"
fi
if [ "${_pedantic}" -eq 1 ]; then
	CFLAGS="$(cc_has_option -Waddress-of-packed-member) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wextra-semi-stmt) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wformat-signedness) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wimplicit-fallthrough) ${CFLAGS}"
	CFLAGS="$(cc_has_option -Wmissing-variable-declarations) ${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_gnu_source time.h && HAVE_GNU_SOURCE=1
check_machine && HAVE_MACHINE=1
check_machine_arch && HAVE_MACHINE_ARCH=1
check_pledge && HAVE_PLEDGE=1
check_strtonum && HAVE_STRTONUM=1
check_unveil && HAVE_UNVEIL=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.
{
	:
} | sort | uniq | headers

[ "${HAVE_MACHINE}" -eq 1 ] || printf '#define MACHINE\t"%s"\n' "$(arch)"
[ "${HAVE_MACHINE_ARCH}" -eq 1 ] || printf '#define MACHINE_ARCH\t"%s"\n' "$(arch)"
[ "${HAVE_PLEDGE}" -eq 1 ] && printf '#define HAVE_PLEDGE\t1\n'
[ "${HAVE_STRTONUM}" -eq 1 ] && printf '#define HAVE_STRTONUM\t1\n'
[ "${HAVE_UNVEIL}" -eq 1 ] && printf '#define HAVE_UNVEIL\t1\n'

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

[ "${HAVE_PLEDGE}" -eq 0 ] &&
	printf 'int pledge(const char *, const char *);\n'
[ "${HAVE_STRTONUM}" -eq 0 ] && \
	printf 'long long strtonum(const char *, long long, long long, const char **);\n'
[ "${HAVE_UNVEIL}" -eq 0 ] &&
	printf 'int unveil(const char *, const char *);\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})

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

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