#!/usr/bin/env bash

# Currently generates:
#   config/config.h
#   config/config.var/VAR
#   config/config.vars
#
# The existence of config/config.vars indicates successful
# configuration.

set -euo pipefail

script_home="$(cd "$(dirname "$0")" && pwd -P)"

# fds representing the terminal stdout/stdout, allowing us to redirect
# the overall output (and set -x) to config.log and still write to the
# terminal.
out=1
err=2

info() { echo "$@" 1>&"$err"; }
die() { info "$@"; exit 2; }
infop() { printf "$@" 1>&"$err"; }
diep() { infop "$@"; exit 2; }

usage()
{
    cat 1>&"$out" <<EOF
Usage:
    configure --help
    configure [--with-pylint[=yes|no|maybe]]
Options:
    --with-pylint  require and run pylint (maybe)
EOF
}

outputs=(config/config.vars config/config.h config/config.var)

success='' # when empty, assume state may be inconsistent
rm_on_exit=("$script_home/config/tmp") # always removed
rm_on_err_exit=("${outputs[@]}") # only removed on failures

misuse() { success=1; usage 1>&"$err"; exit 2; }
exit-ok() { success=1; exit 0; }

on-exit()
{
    rm -rf "${rm_on_exit[@]}"
    if test -z "$success"; then
        rm -rf "${rm_on_err_exit[@]}"
        set +x
        info -e '\nerror: configuration failed (see config.log)'
    fi
}
trap on-exit EXIT

find-cmd()
{
    test $# -eq 1 || die "${FUNCNAME[0]}: invalid args" "$@"
    local which="$1" rc=0
    case "$which" in
        */*)
            type -p "$which" || true
            ;;
        *)
            path="$(type -p "$which")" || rc=$?
            if test "$rc" -eq 0; then
                echo "$(basename "$path")"
            else
                return 1
            fi
            ;;
    esac
}

find-prog()
{
    # Prints prog path to stdout or nothing.
    test $# -eq 2 || die "${FUNCNAME[0]}: invalid args" "$@"
    local name="$1" result="$2"
    infop "checking for program %q" "$name"
    if ! [ "$result" ]; then
        result="$(find-cmd "$name")" || true
    fi
    infop ' (%q)\n' "$result"
    echo "$result"
}

# Can not currently handle options that "printf %q" would change,
# i.e. options with spaces in them, etc. Should only include common
# cflags, e.g. not library/pkg-config specifc flags.
config_cflags=()

try-c-code()
{
    # Runs $CC will all the arguments before the first "--". All the
    # arguments after the first "--" are passed to echo -e to produce
    # the code to compile. For now -Wall -Werror are always included.
    local boundary='' opts=()
    while test "$#" -gt 0; do
        if test "$1" = '--'; then
            shift
            break
        fi
        opts+=("$1")
        shift
    done
    if test "$#" -lt 1; then
        die 'try-c-code: no code provided to test compile'
    fi
    local tmp rc=0
    tmp="$(mktemp config/tmp/bup-try-c-compile-XXXXXXX.c)"
    echo -e "$@" > "$tmp"
    "$CC" -Wall -Werror "${config_cflags[@]}" "${opts[@]}" -c \
          -o /dev/null "$tmp" || rc=$?
    rm "$tmp"
    return "$rc"
}

add-cflag-if-supported()
{
    test $# -eq 1 || die "${FUNCNAME[0]}: invalid args" "$@"
    local opt="$1"
    if test -z "$opt"; then
        die 'No option to check'
    fi
    infop "checking for %q %q support" "$CC" "$opt"
    if try-c-code "$opt" -- "int main(int argc, char**argv) { return 0; }"
    then
        config_cflags+=($opt)
        info ' (found)'
    else
        info ' (not found)'
    fi
}

with_pylint=maybe
while test $# -gt 0; do
    case "$1" in
        --help) usage; exit-ok; ;;
        --with-pylint=yes) with_pylint=yes; shift ;;
        --with-pylint=maybe) with_pylint=maybe; shift ;;
        --with-pylint=no) with_pylint=no; shift ;;
        *) misuse ;;
    esac
done

# Make fd 5 and fd 6 refer to the terminal stdout and stderr, and then
# redirect this scripts stdout and stderr to config.log.
exec 5>&1 6>&2 1>"$script_home/config.log" 2>&1
# Arrange for info and die to print to the terminal.
out=5
err=6

set -x # to config.log

mkdir -p config/tmp
rm -f config/config.vars # first - existence indicates successful completion
rm -rf "${outputs[@]}"

info -n 'checking for C compiler'
CC="${CC:-}"
if test -z "$CC"; then
    CC="$(find-cmd gcc)" \
        || CC="$(find-cmd clang)" \
        || CC="$(find-cmd cc)" \
        || true
fi
if test "$CC"; then
    infop " %q\n" "$CC"
else
    die -e '\nerror: unable to find C compiler as $CC, gcc, clang, or cc'
fi

if ! "$CC" -Wall -Werror -c -o /dev/null config/test/hello.c; then
    diep 'error: see config.log -- cannot compile config/test/hello.c with %q\n' "$CC"
fi

add-cflag-if-supported -Wno-unused-command-line-argument
add-cflag-if-supported -fno-strict-aliasing
add-cflag-if-supported -fwrapv

# Haven't seen a documented way to determine the python version via
# python-config right now, so we'll defer version checking until
# later.

if test "${BUP_PYTHON_CONFIG:-}"; then
    bup_python_config="$(type -p "$BUP_PYTHON_CONFIG")"
    if test -z "$bup_python_config"; then
        die $(printf "error: BUP_PYTHON_CONFIG value %q appears invalid" \
                     "$BUP_PYTHON_CONFIG")
    fi
else
    for py_min_ver in 14 13 12 11 10 9 8 7; do
        bup_python_config="$(find-prog "python3.$py_min_ver-config" '')"
        test -z "$bup_python_config" || break
    done
    test -z "$bup_python_config" \
        && bup_python_config="$(find-prog python3-config '')"
    if test -z "$bup_python_config"; then
        die "error: unable to find a suitable python-config"
    fi
fi

bup_python_cflags=$("$bup_python_config" --cflags)
bup_python_ldflags=$("$bup_python_config" --ldflags)
rc=0
bup_python_cflags_embed=$("$bup_python_config" --cflags --embed) || rc=$?
if test "$rc" -eq 0; then
    bup_python_ldflags_embed=$("$bup_python_config" --ldflags --embed)
else  # Earlier versions didn't support --embed
    bup_python_cflags_embed=$("$bup_python_config" --cflags)
    bup_python_ldflags_embed=$("$bup_python_config" --ldflags)
fi

config_ldflags_so=()
case "$OSTYPE" in
    # For at least 10.3+ (2003+)
    darwin*) config_ldflags_so=(-bundle -undefined dynamic_lookup) ;;
    *) config_ldflags_so=(-shared) ;;
esac

bup_git="$(find-prog git '')"
if test -z "$bup_git"; then
    die 'error: unable to find git'
fi


find-header()
{
    test $# -eq 1 || die "${FUNCNAME[0]}: invalid args" "$@"
    local header="$1"
    try-c-code -- \
               "#include <$header>\n" \
               "int main(int argc, char **argv) { return 0; }\n"
}

declare -A have_header # have_header[x/y.h]=1 when found
declare -A have_func # have_func[name]=1 when found

# Each c_define entry produces a #define in config.h, and c_define
# will have a c_define[HAVE_X_Y_H]=1 entry for each
# have_header[x/y.h]=1.
declare -A c_define

check_headers=(sys/time.h
               sys/stat.h sys/types.h # for stat
               unistd.h # for stat and mincore
               sys/mman.h # for mincore
               # For FS_IOC_GETFLAGS and FS_IOC_SETFLAGS.
               linux/fs.h
               sys/ioctl.h)
for header in "${check_headers[@]}"; do
    info -n "checking for header <$header>"
    if find-header "$header"; then
        info " (found)"
        have_header["$header"]=1
        def_name="${header//[^a-zA-Z0-9]/_}"
        def_name="HAVE_${def_name^^?}"
        c_define["$def_name"]=1
    else
        info " (found)"
    fi
done

info -n 'checking for mincore function'
mincore_test_code='
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    BUF_TYPE buf[32];
    const long sc_page_size = sysconf(_SC_PAGESIZE);
    return mincore(main, sc_page_size, buf);
}
'
if try-c-code -DBUF_TYPE=char -- "$mincore_test_code"; then
    info ' (found)'
    have_func[mincore]=1
    c_define[HAVE_MINCORE]=1
    c_define[BUP_MINCORE_BUF_TYPE]=char
elif try-c-code -DBUF_TYPE='unsigned char' -- "$mincore_test_code"; then
    info ' (found)'
    have_func[mincore]=1
    c_define[HAVE_MINCORE]=1
    c_define[BUP_MINCORE_BUF_TYPE]='unsigned char'
else
    info ' (not found)'
fi

info -n 'checking for MINCORE_INCORE definition'
mincore_test_code='
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    return (int) MINCORE_INCORE;
}
'
if try-c-code -- "$mincore_test_code"; then
    info ' (found)'
    c_define[BUP_HAVE_MINCORE_INCORE]=1
else
    info ' (not found)'
fi

info -n 'checking for readline'
# We test this specific thing because it should work everywhere and it was
# a particulary problem on macos (we'd get the wrong includes if we just
# tested that the includes work).
bup_readline_via_pkg_config=''
readline_test_code='
  static char *on_completion_entry(const char *text, int state) { return NULL; }
  void bup_test(void) { rl_completion_entry_function = on_completion_entry; }
'
if pkg-config readline; then
    bup_readline_cflags="$(pkg-config readline --cflags)"
    bup_readline_ldflags="$(pkg-config readline --libs)"
    # It looks like it's not uncommon for pkg-config to provide a -I
    # that doesn't support the documentation's specified #include
    # <readline/readline.h>.  See what's really going on.
    if try-c-code "$bup_readline_cflags" -- \
                  '#include <stdio.h> // required by unpatched readline\n' \
                  '#include <readline/readline.h>\n' \
                  "$readline_test_code"
    then
        c_define[BUP_HAVE_READLINE]=1
        c_define[BUP_READLINE_INCLUDES_IN_SUBDIR]=1
    elif try-c-code "$bup_readline_cflags" -- \
                    '#include <stdio.h> // required by unpatched readline\n' \
                    '#include <readline.h>\n' \
                    "$readline_test_code"
    then
        c_define[BUP_HAVE_READLINE]=1
    fi
    if test "${c_define[BUP_HAVE_READLINE]:-}"; then
        bup_readline_via_pkg_config=1
    else
        bup_readline_cflags=''
        bup_readline_ldflags=''
    fi
fi
if ! test "${c_define[BUP_HAVE_READLINE]:-}"; then
    if try-c-code -- "#include <readline/readline.h> $readline_test_code"; then
        bup_readline_ldflags=-lreadline
        c_define[BUP_HAVE_READLINE]=1
        c_define[BUP_READLINE_INCLUDES_IN_SUBDIR]=1
    elif try-c-code -- "#include <readline.h> $readline_test_code"; then
        bup_readline_ldflags=-lreadline
        c_define[BUP_HAVE_READLINE]=1
    fi
fi
if test "${c_define[BUP_HAVE_READLINE]:-}"; then
    if test "$bup_readline_via_pkg_config"; then
        info ' (yes, pkg-config)'
    else
        info ' (yes)'
    fi
fi

info -n 'checking for ns resolution stat times'
stat_code='
#include <sys/stat.h>

int main(int argc, char **argv)
{
    struct stat st;
    stat(argv[0], &st);
    return (int) st.BUP_TIME_FIELD;
}
'
if try-c-code -DBUP_TIME_FIELD=st_atim.tv_nsec -- "$stat_code"; then
    info ' (found, tim)'
    c_define[BUP_STAT_NS_FLAVOR_TIM]=1
elif try-c-code -DBUP_TIME_FIELD=st_atimensec.tv_nsec -- "$stat_code"; then
    info ' (found, timensec)'
    c_define[BUP_STAT_NS_FLAVOR_TIMENSEC]=1
elif try-c-code -DBUP_TIME_FIELD=st_atimespec.tv_nsec -- "$stat_code"; then
    info ' (found, timespec)'
    c_define[BUP_STAT_NS_FLAVOR_TIMESPEC]=1
else
    info ' (not found)'
    c_define[BUP_STAT_NS_FLAVOR_NONE]=1
fi

info -n 'checking for libacl'
if pkg-config libacl; then
    bup_libacl_cflags="$(pkg-config libacl --cflags)"
    bup_libacl_ldflags="$(pkg-config libacl --libs)"
    info ' (found, pkg-config)'
else
    bup_libacl_cflags=
    bup_libacl_ldflags='-lacl'
    info ' (not found)'
fi
info -n 'checking for complete acl support'
if "$CC" $bup_libacl_cflags -Wall -Werror -o /dev/null config/test/have-acls.c \
         $bup_libacl_ldflags
then
    c_define[BUP_HAVE_ACLS]=1
    info ' (found)'
else
    bup_libacl_cflags=
    bup_libacl_ldflags=
    info ' (not found)'
fi


## Generate config.h

(for def in "${!c_define[@]}"; do
     echo "#define $def ${c_define[$def]}"
 done) | dev/refresh config/config.h

rm -rf config/config.var config/tmp/config.var
mkdir -p config/tmp/config.var
echo -n "$bup_python_config" > config/tmp/config.var/bup-python-config
echo -n "$with_pylint" > config/tmp/config.var/with-pylint
mv config/tmp/config.var config/config.var

dev/refresh config/config.vars <<EOF
CC = $CC
CFLAGS = ${CFLAGS:-}
CPPFLAGS = ${CPPFLAGS:-}
LDFLAGS = ${LDFLAGS:-}

bup_config_detritus=${outputs[@]} config/config.log

bup_config_cflags = ${config_cflags[@]}
bup_config_ldflags_so = ${config_ldflags_so[@]}

bup_python_config = ${bup_python_config[@]}
bup_python_cflags = ${bup_python_cflags[@]}
bup_python_ldflags = ${bup_python_ldflags[@]}
bup_python_cflags_embed = ${bup_python_cflags_embed[@]}
bup_python_ldflags_embed = ${bup_python_ldflags_embed[@]}

bup_have_acls = ${c_define[BUP_HAVE_ACLS]:-}
bup_libacl_cflags = ${bup_libacl_cflags[@]}
bup_libacl_ldflags = ${bup_libacl_ldflags[@]}

bup_have_readline = ${c_define[BUP_HAVE_READLINE]:-}
bup_readline_cflags = ${bup_readline_cflags[@]}
bup_readline_ldflags = ${bup_readline_ldflags[@]}
EOF

infop "
found: python-config (%q)
found: git (%q, $("$bup_git" --version))
" \
       "$bup_python_config" \
       "$bup_git" \

summarize()
{
    local found="$1"
    shift
    if test "$found"; then
        info found: "$@"
    else
        info not found: "$@"
    fi
}
summarize "${c_define[BUP_HAVE_READLINE]:-}" 'readline support (e.g. bup ftp)'
summarize "${c_define[BUP_HAVE_ACLS]:-}" 'POSIX ACL support'
info

success=1
