#!/usr/bin/env bash

# fail hard
set -o pipefail
# fail harder
set -eu
# for patterns further down
shopt -s extglob

php_passthrough() {
    local dir=$(dirname $1)
    local file=$(basename $1)
    local out=$(basename $file .php)
    if [[ "$out" != "$file" ]]; then
        echo "Interpreting ${1#$HEROKU_APP_DIR/} to $out" >&2
        out="$dir/$out"
        php $1 > $out
        echo $out
    else
        echo $1
    fi
}

check_exists() {
    if [[ ! -f "$HEROKU_APP_DIR/$1" ]]; then
        echo "Cannot read -$2 '$1' (relative to '$HEROKU_APP_DIR')" >&2
        exit 1
    else
        echo "$HEROKU_APP_DIR/$1"
    fi
}

print_help() {
echo "\
Boots PHP-FPM together with Nginx on Heroku and for local development.

Usage:
  heroku-php-nginx [options] [<DOCUMENT_ROOT>]

Options:
  -C <nginx.inc.conf>     The path to the configuration file to include inside
                          the Nginx server config (see option -c below). Will
                          be included inside the 'server { ... }' block just
                          after the 'listen', 'root' etc directives.
                          Recommended approach when customizing Nginx's config
                          in most cases, unless you need to set http or
                          fundamental server level options.
                          [default: $COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/default_include.conf]
  -c <nginx.conf>         The path to the full configuration file that is
                          included after Heroku's (or your local) Nginx config
                          is loaded. It must contain an 'http { ... }' block
                          with a 'server { ... }' inside that contains 'listen'
                          and 'root' (see option -C above), but no global,
                          directives (globals are read from the system's default
                          Nginx configuration files).
                          [default: $COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/heroku.conf.php]
  -F <php-fpm.inc.conf>   The path to the configuration file to include at the  
                          end of php-fpm.conf (see option -f below), in the
                          '[www]' pool section. Recommended approach when
                          customizing PHP-FPM's configuration in most cases,
                          unless you need to set global options.
  -f <php-fpm.conf>       The path to the full PHP-FPM configuration file.
                          [default: $COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php-fpm.conf]
  -h                      Display this help screen and exit.
  -i <php.ini>            The path to the php.ini file to use.
                          [default: $COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php.ini]

Any file name that ends in '.php' will be run through the PHP interpreter first.
You may use this for templating; this is, for instance, necessary for Nginx,
where environment variables cannot be referenced in configuration files.
All file paths must be relative to '$HEROKU_APP_DIR'.

If you would like to use the -C and -c or -F and -f options together, make sure
you retain the appropriate include mechanisms (see default configs for details).
" >&2
}

# we need this in configs
export HEROKU_APP_DIR=$(pwd)
export DOCUMENT_ROOT="$HEROKU_APP_DIR"
# set a default port if none is given
export PORT=${PORT:-$(( $RANDOM+1024 ))}
COMPOSER_VENDOR_DIR=$(composer config vendor-dir)

while getopts ":C:c:F:f:i:h" opt; do
    case $opt in
        C)
            nginx_config_include=$(check_exists "$OPTARG" "C")
            ;;
        c)
            nginx_config=$(check_exists "$OPTARG" "c")
            ;;
        F)
            fpm_config_include=$(check_exists "$OPTARG" "F")
            ;;
        f)
            fpm_config=$(check_exists "$OPTARG" "f")
            ;;
        i)
            php_config=$(check_exists "$OPTARG" "i")
            ;;
        h)
            print_help 2>&1
            exit
            ;;
        \?)
            echo "Invalid option: -$OPTARG" >&2
            exit 2
            ;;
        :)
            echo "Option -$OPTARG requires an argument" >&2
            exit 2
            ;;
    esac
done
# clear processed arguments
shift $((OPTIND-1))

if [[ "$#" -gt "1" ]]; then
    print_help
    exit 2
fi

php -r 'exit((int)version_compare(PHP_VERSION, "5.5.11", "<"));' || { echo "This program requires PHP 5.5.11 or newer" >&2; exit 1; }

echo "Booting on port $PORT..." >&2

if [[ "$#" == "1" ]]; then
    DOCUMENT_ROOT="$HEROKU_APP_DIR/$1"
    if [[ ! -d "$DOCUMENT_ROOT" ]]; then
        echo "DOCUMENT_ROOT '$1' does not exist"
        exit 1
    else
        # strip trailing slashes if present
        DOCUMENT_ROOT=${DOCUMENT_ROOT%%*(/)}
        echo "DOCUMENT_ROOT changed to '$DOCUMENT_ROOT'" >&2
    fi
fi

fpm_config_include=${fpm_config_include:-}
if [[ -n "$fpm_config_include" ]]; then
    echo "Using PHP-FPM configuration include '${fpm_config_include#$HEROKU_APP_DIR/}'" >&2
    fpm_config_include=$(php_passthrough "$fpm_config_include")
    export HEROKU_PHP_FPM_CONFIG_INCLUDE="$fpm_config_include"
fi

fpm_config=${fpm_config:-"$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php-fpm.conf"}
echo "Using PHP-FPM configuration file '${fpm_config#$HEROKU_APP_DIR/}'" >&2
fpm_config=$(php_passthrough "$fpm_config")

php_config=${php_config:-"$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/php/php.ini"}
echo "Using PHP configuration (php.ini) file '${php_config#$HEROKU_APP_DIR/}'" >&2
php_config=$(php_passthrough "$php_config")

nginx_config_include=${nginx_config_include:-"$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/default_include.conf"}
echo "Using Nginx server-level configuration include '${nginx_config_include#$HEROKU_APP_DIR/}'" >&2
nginx_config_include=$(php_passthrough "$nginx_config_include")
export HEROKU_PHP_NGINX_CONFIG_INCLUDE="$nginx_config_include"

nginx_config=${nginx_config:-"$HEROKU_APP_DIR/$COMPOSER_VENDOR_DIR/heroku/heroku-buildpack-php/conf/nginx/heroku.conf.php"}
echo "Using Nginx configuration file '${nginx_config#$HEROKU_APP_DIR/}'" >&2
nginx_config=$(php_passthrough "$nginx_config")

# make a shared pipe; we'll write the name of the process that exits to it once that happens, and wait for that event below
# this particular call works on Linux and Mac OS (will create a literal ".XXXXXX" on Mac, but that doesn't matter).
wait_pipe=$(mktemp -t "heroku.waitpipe-$PORT.XXXXXX" -u)
rm -rf $wait_pipe
mkfifo $wait_pipe

# trap SIGINT/SIGQUIT (ctrl+c or ctrl+\ on the console), SIGTERM, and EXIT (upon failure of any command due to set -e, or because of the exit 1 at the very end), kill subshell child processes, then subshells
# 1) restore EXIT trap immediately, or the exit at the end of the line will trigger this trap again
# 2) kill childrens' child processes (the stuff running inside the sub-shells) using xargs because this is easier (-P expects a comma separated list); the || true prevents premature exit (set -e) if one of those doesn't have children anymore (it's likely that's why we're hitting this bit of code in the first place)
# 3) kill child processes (that's the sub-shells); it's likely that some of them have already disappeared, so xarg || true it too and suppress "no such process" complaints by sending them to /dev/null
# FIXME: this doesn't currently fire when the subshells themselves are terminated, and apparently also not when killing the tail command
# TODO: for extra brownie points, move to a function and curry for each given signal, passing the signal in as an arg, so we can use different exit codes or messages
trap 'trap - EXIT; echo "Going down, terminating child processes..." >&2; jobs -p | xargs -n1 pkill -TERM -P || true; jobs -p | xargs -n1 kill -TERM 2> /dev/null || true; exit' SIGINT SIGQUIT SIGTERM EXIT

# redirect logs to STDERR; write "tail ..." to the shared pipe if it exits
echo "Starting log redirection..." >&2
( touch "/tmp/heroku.php-fpm.$PORT.log" "/tmp/heroku.nginx_access.$PORT.log"; tail -qF -n 0 /tmp/heroku.*.$PORT.log 1>&2; echo "tail heroku.*.$PORT.log"; ) > $wait_pipe &
# start FPM; write "php-fpm" to the shared pipe if it exits
echo "Starting php-fpm..." >&2
( php-fpm --nodaemonize -y "$fpm_config" -c "$php_config"; echo "php-fpm"; ) > $wait_pipe &
# wait a few seconds for FPM to finish initializing; otherwise an early request might break nginx with the FastCGI pipe not being ready
# start nginx; write "nginx" to the shared pipe if it exits
echo "Starting nginx..." >&2
( sleep 2; nginx -g "daemon off; include $nginx_config;"; echo "nginx"; ) > $wait_pipe &

# wait for something to come from the shared pipe, which means that the given process was killed or has failed
# we'll only reach this if one of the processes above has terminated
read exitproc < $wait_pipe
echo "Process exited unexpectedly: $exitproc"

# this will trigger the trap and kill all remaining children
exit 1
