#!/bin/bash

set -o errexit

cd $(dirname $0)

CMD=$(pwd)/$(basename $0)
mode=$1
shift || mode="no_action"

ROOT=../..
MYPWD=$( pwd )
IMAGE=$( readlink -f $ROOT/minix_x86.img )
DISK=$( readlink -f $ROOT/minix_x86.disk )
RC=$( readlink -f $ROOT/minix_x86.rc )
CONF=$( readlink -f $ROOT/minix_x86.conf )
MKFS=$( readlink -f $ROOT/../obj.i386/tooldir*/bin/nbmkfs.mfs )
MAKE=$( readlink -f $ROOT/../obj.i386/tooldir*/bin/nbmake-i386 )
ADDR2LINE=$( readlink -f $ROOT/../obj.i386/tooldir*/bin/i586-elf32-minix-addr2line )

BATCH=${BATCH:-0}
HYPER=${HYPER:-}
DISK_SIZE=${DISK_SIZE:-1024}
DISK_MNT=${DISK_MNT:-/media/minix-disk}
OUT=${OUT:-S} # S=STDOUT, F=FILE, C=CONSOLE, P=PTY
APPEND=${APPEND:-}

TIMEOUT=${TIMEOUT:-60}
BOOT_TIMEOUT=${BOOT_TIMEOUT:-45}

function rc_create {
    cat <<EOF
#!/bin/sh

set -o errexit

[ -d /ext ] || mkdir /ext
mount /dev/c0d1 /ext

if [ -f /ext/etc/rc ]; then
    sh /ext/etc/rc
fi

EOF
}

function rc_ext_default_create {
    cat <<EOF
#!/bin/sh

echo "/ext/etc/rc: /ext mounted correctly."

EOF
}

function rc_ext_runcmd_create {
    cat <<EOF
#!/bin/sh

echo "____runcmd_start"
$*
RET=\$?
echo "____runcmd_end \$RET status"

EOF
}

function minix_conf_create {
    cat <<EOF
#!/bin/sh

GUEST_NIC=$guest_nic
HOST_FTP_USER=$host_ftp_user
HOST_FTP_PASS=$host_ftp_pass
HOST_FTP_PORT=$host_ftp_port
HOST_FTP_DIR=$host_ftp_dir
HOST_IP_ADDR=$host_ip_addr

EOF
}

#
# Configuration variables
#
CONF_VARS="guest_nic host_ftp_user host_ftp_pass host_ftp_port host_ftp_dir host_ip_addr"

function conf_init
{
    for v in $CONF_VARS
    do
        if [ -z ${!v} ]; then
            eval ${v}="NULL"
        fi
    done
}

function get_conf_value
{
    local conf_var=$1
    local msg="$2"
    local default=$3
    local values=""
    [ ! -z $VALUES ] && values="$VALUES, "
    line="$msg [${values}Default=$default]: "
    (
        echo -n "$line"
    ) 2>&1
    if [ "${!conf_var}" == "NULL" ]; then
        if [ $BATCH -eq 0 ]; then
            read -r $conf_var
            echo -en "\e[1A"
        else
            eval $conf_var=""
        fi
    fi
    if [ "${!conf_var}" == "" ]; then
        eval $conf_var=$default
    fi
    (
        echo -en "\e[0K\r"
        echo -n "$line"
        echo ${!conf_var}
    ) 2>&1
    RET=${!conf_var}
}

function get_conf_value_repeat
{
    local check=$1
    shift
    local conf_var=$1
    while true
    do
        get_conf_value "$@"
        eval $check $RET || break
        eval $conf_var="NULL"
    done
}

function __get_conf_value_check_list
{
    for v in $( echo $VALUES | sed "s/|/ /g" )
    do
        [ "$1" == "$v" ] && return 1
    done
    return 0
}

function get_conf_value_list
{
    get_conf_value_repeat __get_conf_value_check_list "$@"
}

function get_conf_value_yn
{
    VALUES="y|n" get_conf_value_list "$@"
}

function __get_conf_value_check_int_range
{
    min=$( echo $VALUES | sed "s/|/ /g" | cut -d' ' -f 1 )
    max=$( echo $VALUES | sed "s/|/ /g" | cut -d' ' -f 2 )
    case $min in
        ''|*[!0-9]*) return 0 ;;
    esac
    case $max in
        ''|*[!0-9]*) return 0 ;;
    esac
    [ $1 -ge $min ] && [ $1 -le $max ] && return 1
    return 0
}

function get_conf_value_int_range
{
    get_conf_value_repeat __get_conf_value_check_int_range "$@"
}

function get_conf_value_nic
{
    VALUES="0|13" get_conf_value_int_range "$@"
}

function __get_conf_value_check_string
{
    return 1
}

function get_conf_value_string
{
    get_conf_value_repeat __get_conf_value_check_string "$@"
}

function disk_mount {
    local mnt=$1
    echo "* Mounting ${DISK} to $mnt..."
    [ -d $mnt ] || sudo mkdir -p $mnt
    disk_umount &> /dev/null || true
    sudo mount -o loop $DISK $mnt || true
}

function disk_umount {
    echo "* Unmounting ${DISK}..."
    sudo umount $DISK
}

function disk_build {
    local size=$1
    local tmp_mnt=$( mktemp -d /tmp/clientctl-XXXXX )
    echo "* Building ${size} MB disk image..."
    dd if=/dev/zero of=$DISK bs=1M count=$size
    $MKFS $DISK
    echo "* Setting up /ext/etc/rc file executed at startup..."
    rc_create > $RC
    disk_mount $tmp_mnt
    [ -d $tmp_mnt/etc ] || mkdir -p $tmp_mnt/etc
    rc_ext_default_create > $tmp_mnt/etc/rc
    disk_umount
    rm -rf $tmp_mnt
}

function minix_conf {
    conf_init
    nics="virbr0 virbr1 eth0 eth1 wlan0 wlan1"
    for n in $nics
    do
        host_ip_addr_default=$( ifconfig $n 2> /dev/null | perl -nle'/dr:(\S+)/ && print $1' )
        [ -z "$host_ip_addr_default" ] || break
    done
    get_conf_value_nic guest_nic "[NIC] Which nic do you want to use on MINIX (see MINIX' \`netconf -c\`)?" 7
    get_conf_value_string host_ftp_user "[FTP] Username for FTP server running on the host?" ftptest
    get_conf_value_string host_ftp_pass "[FTP] Password for FTP server running on the host?" ftptest
    get_conf_value_string host_ftp_port "[FTP] Port for FTP server running on the host?" 21
    get_conf_value_string host_ftp_dir  "[FTP] Base directory for FTP server running on the host?" /home/$host_ftp_user
    get_conf_value_string host_ip_addr "[HOSTS] Host ip address?" $host_ip_addr_default

    echo ""
    echo "* Setting up configuration settings in $CONF..."
    minix_conf_create > $CONF
}

function wait_output {
    RET=0
    date
    timeout $1 watch -n 1 -e \! grep -q "$2" $3 &> /dev/null  < /dev/null || RET=$?
    if [ $RET -eq 124 ]; then
        echo "* Timed out after $1 seconds!" 1>&2
        return 1
    fi
    echo "* Done." 1>&2
    date
    return 0
}

function run_cmd_exec {
    echo "* Waiting for MINIX to boot..." 1>&2
    wait_output $BOOT_TIMEOUT "__runcmd_start" runcmd.log || return 33
    echo "* Waiting for command \"$*\"..." 1>&2
    wait_output $TIMEOUT "__runcmd_end" runcmd.log || return 66
    RET=$( grep __runcmd_end runcmd.log | cut -d' ' -f 2 )
    return $RET
}

function run_cmd {
    echo "* Setting up configuration..." 1>&2
    [ -f $DISK ] || $CMD builddisk
    [ -f $IMAGE ] || $CMD buildimage

    $CMD mountdisk
    [ ! -f $DISK_MNT/etc/rc ] || mv $DISK_MNT/etc/rc $DISK_MNT/etc/rc.bak
    rc_ext_runcmd_create $*  > $DISK_MNT/etc/rc
    $CMD umountdisk
    echo "* Starting up..." 1>&2
    rm -f runcmd.*
    OUT=S $CMD run &> runcmd.log &
    PID=$!
    RUNCMD_RET=0
    run_cmd_exec $* || RUNCMD_RET=$?
    cat runcmd.log | awk '/__runcmd_end/ {N=0;} { if (N==1) print; } /__runcmd_start/ {N=1;}' > runcmd.out
    echo "* Shutting down..." 1>&2
    for pid in $(ps ax -o pid,args --sort start_time | grep -e "$CMD run[^a-zA-Z]" -e qemu -e kvm | grep -v grep | cut -d' ' -f 1 | head -3 )
    do
        kill $pid &> /dev/null || true
    done
    $CMD mountdisk
    [ ! -f $DISK_MNT/etc/rc.bak ] || mv $DISK_MNT/etc/rc.bak $DISK_MNT/etc/rc
    echo "* COMMAND: \"$*\", RET: $RUNCMD_RET, OUTPUT:"
    cat runcmd.out
    return $RUNCMD_RET
}

function run {
    if [ "$HYPER" == "" ]; then
        if [ -e /dev/kvm ]; then
            HYPER="qemu-system-i386 --enable-kvm"
        else
            HYPER=qemu-system-i386
        fi
    fi
    opts="-m 256 -hda $IMAGE"
    append="$APPEND rootdevname=c0d0p1"
    [ ! -f $DISK ] || opts="$opts -hdb $DISK"
    if [ "$OUT" == "F" ]; then
        opts="$opts -curses -serial file:$MYPWD/serial.out"
        append="$append cttyline=0"
        echo "tail -f $MYPWD/serial.out" > $MYPWD/connect.cmd
    elif [ "$OUT" == "C" ]; then
	opts="$opts -curses -chardev socket,id=serial0,path=$MYPWD/serial.sock,server,nowait -serial chardev:serial0"
	append="$append cttyline=0"
	echo "(cd $MYPWD && minicom -D unix\#serial.sock)" > $MYPWD/connect.cmd
    else
        opts="$opts -nographic"
        append="$append console=tty00"
        if [ "$OUT" == "P" ]; then
            opts="$opts -serial pty"
            [ -z $PTS ] && PTS=$( $HYPER -serial pty 2>&1 | grep pts | sed "s/.* \([^ ]*pts[^ ]*\) .*/\1/g" )
            echo "minicom -D $PTS" > $MYPWD/connect.cmd
        else
            echo "echo Cannot connect with OUT=S option." > $MYPWD/connect.cmd
        fi
    fi
    (cd ../../../obj.i386/destdir.i386/boot/minix/.temp && $HYPER -kernel kernel -append "$append" $opts -initrd "mod01_ds,mod02_rs,mod03_pm,mod04_sched,mod05_vfs,mod06_memory,mod07_tty,mod08_mfs,mod09_vm,mod10_pfs,mod11_init")
}

function minix_connect {
    echo "*****"
    echo "***** Running: $( cat $MYPWD/connect.cmd )"
    echo "*****"
    eval $MYPWD/connect.cmd
}

function minix_unstack {
    local service=$1
    shift
    if [ ! -f $ROOT/minix.mods.map ]; then
        echo "$ROOT/minix.mods.map does not exist, run relink.llvm first!"
        return 1
    fi
    path=$(grep "^$service=" $ROOT/minix.mods.map | cut -d= -f 2)
    path=$ROOT/../obj.i386/$path/$service
    $ADDR2LINE -p -f -e $path $*
}

function minix_test {
    JOBS=${JOBS:-8}
    RUNCMD=${RUNCMD:-ls}
    C=${C:-full}
    LOG=$(pwd)/test.log
    if [ "$C" == "full" ]; then
        cd $ROOT
        JOBS=$JOBS BUILDVARS="-V MKBITCODE=yes" ./releasetools/x86_hdimage.sh -b 2>&1 | tee $LOG; test ${PIPESTATUS[0]} -eq 0 || exit 125
        cd -
    else
        C=$C ./relink.llvm 2>&1 | tee $LOG; test ${PIPESTATUS[0]} -eq 0 || exit 125
        ./clientctl buildimage 2>&1 | tee -a $LOG; test ${PIPESTATUS[0]} -eq 0 || exit 125
    fi
    if [ "$RUNCMD" != "" ]; then
        ./clientctl runcmd $RUNCMD 2>&1 | tee -a $LOG; test ${PIPESTATUS[0]} -eq 0 || exit 2
    fi
    exit 0
}

function minix_bisect {
    cd $ROOT
    git bisect reset
    echo " * Enter bad commit: "
    read bad
    echo " * Enter good commit: "
    read good
    git bisect start $bad $good
    git bisect run minix/llvm/clientctl test
    cd -
}

# Usage: [C=set] ./clientctl buildasr [num]
#
# Build 'num' sets of ASR-randomized service binaries for the 'set' set of
# services.  Defaults to one set (in addition to the set used to boot) for
# all services.  To be used after building the full system.
#
# The MINIX3 counterpart of the generation taking place here is the
# update_asr(8) command, which cycles through the generated binaries.
#
function minix_buildasr {
    MINIXLLVMDIR=$ROOT/minix/llvm
    ASRDIR=/usr/service/asr

    . $MINIXLLVMDIR/common.inc		# get DESTDIR
    DESTDIR="$DESTDIR/destdir.i386"	# correct DESTDIR

    ASRDESTDIR="$DESTDIR$ASRDIR"
    COUNT=${1:-1}	# take count from command line, default to 1
    C=${C:-"servers,fs,net,drivers"}

    # start by relinking everything against the magic library
    C=$C $MINIXLLVMDIR/relink.llvm magic

    # we are replacing any previously made ASR binaries
    rm -rf $ASRDESTDIR/*

    # generate $COUNT number of sets of ASR-randomized service binaries
    # TODO: do not use current time as random seed
    N=1
    while [ $N -le $COUNT ]; do
        mkdir $ASRDESTDIR/$N
        export BINDIR=$ASRDIR/$N
        C=$C $MINIXLLVMDIR/build.llvm magic asr
        sleep 1 # just to make sure they're guaranteed to be different
        N=$(($N + 1))
    done

    # generate the initial set of service binaries, different as well
    unset BINDIR
    C=$C $MINIXLLVMDIR/build.llvm magic asr

    # finally generate the image
    # x86_hdimage will automatically add the binaries to the image set
    $MINIXLLVMDIR/clientctl buildimage
}

case "$mode" in
  'buildimage')
    (cd $ROOT && CREATE_IMAGE_ONLY=1 releasetools/x86_hdimage.sh -b)
    ;;
  'buildboot')
    (cd $ROOT && $MAKE -C releasetools do-hdboot)
    ;;
  'conf')
    minix_conf
    ;;
  'builddisk')
    rm -f $DISK
    disk_build $DISK_SIZE
    ;;
  'mountdisk')
    disk_mount $DISK_MNT
    ;;
  'umountdisk')
    disk_umount
    ;;
  'runcmd')
    run_cmd $*
    ;;
  'run')
    run
    ;;
  'connect')
    minix_connect
    ;;
  'unstack')
    minix_unstack $*
    ;;
  'test')
    minix_test
    ;;
  'bisect')
    minix_bisect
    ;;
  'buildasr')
    minix_buildasr $*
    ;;
  *)
    echo "Invalid action: $mode"
    exit 1
    ;;
esac
