#!/bin/bash

# Project: Salis
# Author:  Paul Oliver
# Email:   contact@pauloliver.dev

# Salis simulator launcher script. Builds salis binary (according to passed
# arguments) and launches it. Binary is purged on exit. This JIT compilation
# scheme allows to quickly switch between available architectures and UIs.

set -euo pipefail

headline="Salis: Simple A-Life Simulator."
help_msg="Shows help and exits"

usage() {
cat << EOF
${headline}
Usage: ${0} [-h|--help] COMMAND [args...]

Options:
  -h, --help    ${help_msg}

Commands:
  bench         Runs benchmark
  load          Loads saved simulation
  new           Creates a new simulation

Use '-h' to list arguments for each command.
Example: ${0} bench -h
EOF
}

case ${1:-} in
bench|load|new)
    ;;
-h|--help)
    usage
    exit 0
    ;;
"")
    echo "${0}: please specify command -- 'bench|load|new'"
    exit 1
    ;;
*)
    echo "${0}: invalid command -- '${1}'"
    exit 1
    ;;
esac

cmd=${1}

shift

falter() {
    find src/${1}/ -type f | sed 's|.*/||; s|\.c||' | paste -sd: -
}

arches=`falter arch`
uis=`falter ui`

anc_def_desc() {
    echo "Default ancestor file name without extension, "
    echo "to be compiled on all cores "
    echo "('ANC' points to file 'ancs/<ARCH>/<ANC>.asm')"
}

anc_spec_def() {
    echo "Core specific ancestor file names separated by commas, "
    echo "using same convention as with default ancestor (<ANC0>,<ANC1>,...). "
    echo "When provided will override default ancestor on specified cores."
}

options=(
    "A|anc-def|ANC|`anc_def_desc`|||bench:new"
    "a|arch|ARCH|VM architecture|${arches}|dummy|bench:new"
    "b|steps|N|Number of steps to run in benchmark||0x1000000|bench"
    "C|clones|N|Number of ancestor clones on each core||1|bench:new"
    "c|cores|N|Number of simulator cores||2|bench:new"
    "F|muta-flip||Cosmic rays flip bits instead of randomizing whole bytes||false|bench:new"
    "f|force||Overwrites existing simulation of given name||false|new"
    "H|half||Compiles ancestor at the middle of the memory buffer||false|bench:new"
    "h|help||${help_msg}|||bench:load:new"
    "M|muta-pow|POW|Mutator range exponent (range == 2^POW)||32|bench:new"
    "m|mvec-pow|POW|Memory vector size exponent (size == 2^POW)||20|bench:new"
    "n|name|NAME|Name of new or loaded simulation||def.sim|load:new"
    "o|optimized||Builds Salis binary with optimizations||false|bench:load:new"
    "p|pre-cmd|CMD|Shell command to wrap executable (e.g. gdb, valgrind, etc.)|||bench:load:new"
    "S|anc-spec|ANC0,ANC1,...|`anc_spec_def`|||bench:new"
    "s|seed|SEED|Seed value for new simulation||0|bench:new"
    "t|thread-gap|N|Memory gap between cores in bytes (could help reduce cache misses?)||0x100|bench:load:new"
    "u|ui|UI|User interface|${uis}|curses|load:new"
    "y|sync-pow|POW|Core sync interval exponent (interval == 2^POW)||20|bench:new"
    "z|auto-save-pow|POW|Auto-save interval exponent (interval == 2^POW)||36|new"
)

field() {
    echo ${1} | cut -d'|' -f${2}
}

flist() {
    sopt=`field "${1}" 1`
    lopt=`field "${1}" 2`
    meta=`field "${1}" 3`

    echo -n "[-${sopt}|--${lopt}`[[ -n ${meta} ]] && echo " ${meta}"`] "
}

fhelp() {
    sopt=`field "${1}" 1`
    lopt=`field "${1}" 2`
    meta=`field "${1}" 3`

    printf "%s\r" "  -${sopt}, --${lopt}`[[ -n ${meta} ]] && echo " ${meta}"`"

    help=`field "${1}" 4`
    choi=`field "${1}" 5`
    defv=`field "${1}" 6`
    copt=`[[ -n ${choi} ]] && echo " (choices: ${choi/:/, })"`
    dopt=`[[ -n ${defv} ]] && echo " (default: ${defv})"`

    echo -e "\t\t\t\t${help}${copt}${dopt}" | fmt -w120
}

fshort() {
    sopt=`field "${1}" 1`
    meta=`field "${1}" 3`

    echo -n "${sopt}`[[ -n ${meta} ]] && echo :`"
}

flong() {
    lopt=`field "${1}" 2`
    meta=`field "${1}" 3`

    echo -n "${lopt}`[[ -n ${meta} ]] && echo :`,"
}

fdefaults() {
    lopt=`field "${1}" 2`

    [[ ${lopt} == help ]] && return

    defv=`field "${1}" 6`
    nopt=opt_${lopt//-/_}

    eval ${nopt}=${defv}
}

fshow() {
    lopt=`field "${1}" 2`

    [[ ${lopt} == help ]] && return

    nopt=opt_${lopt//-/_}

    echo "${nopt}=${!nopt}"
}

fiter() {
    for ((i = 0; i < ${#options[@]}; i++)) ; do
        if [[ `field "${options[${i}]}" 7` =~ ${cmd} ]] ; then
            ${1} "${options[${i}]}" || true
        fi
    done
}

usage() {
cat << EOF
${headline}
Usage: ${0} ${cmd} `fiter flist | fmt -t -w120`

Options:
`fiter fhelp`
EOF
}

fiter fdefaults

sopts=`fiter fshort`
lopts=`fiter flong`
popts=`getopt -n "${0} ${cmd}" -o ${sopts} -l ${lopts::-1} -- "${@}"`

eval set -- ${popts}

parse_next() {
    for ((i = 0; i < ${#options[@]}; i++)) ; do
        vopt="${options[${i}]}"

        sopt=`field "${vopt}" 1`
        lopt=`field "${vopt}" 2`

        [[ ${1} != -${sopt} ]] && [[ ${1} != --${lopt} ]] && continue

        meta=`field "${vopt}" 3`
        nopt=opt_${lopt//-/_}

        if [[ -z ${meta} ]] ; then
            eval ${nopt}=true
            shift_next=1
        else
            eval ${nopt}=${2}
            shift_next=2
        fi
    done
}

while true ; do
    case ${1} in
    -h|--help)
        usage
        exit 0
        ;;
    --)
        shift
        break
        ;;
    *)
        parse_next ${@}
        shift ${shift_next}
        ;;
    esac
done

if [[ -n ${1:-} ]] ; then
    while [[ -n ${1:-} ]] ; do
        echo "${0} ${cmd}: unrecognized option -- '${1}'"
        shift
    done

    exit 1
fi

blue() {
    echo -e "\033[1;34m${1}\033[0m"
}

red() {
    echo -e "\033[1;31m${1}\033[0m"
}

blue "${headline}"
blue "Called '${cmd}' command with the following options:"
fiter fshow

case ${cmd} in
load|new)
    sim_dir=${HOME}/.salis/${opt_name}
    sim_path=${sim_dir}/${opt_name}
    sim_opts=${sim_dir}/opts
    ;;
esac

case ${cmd} in
load)
    if [[ ! -d ${sim_dir} ]] ; then
        red "Error: no saved simulation was found named '${opt_name}'."
        exit 1
    fi

    blue "Sourcing configurations from '${sim_opts}':"
    cat ${sim_opts}
    source ${sim_opts}
    ;;
esac

blue "Generating a temporary Salis directory:"
salis_tmp=/tmp/salis-tmp
salis_exe=${salis_tmp}/salis-bin
mkdir -pv ${salis_tmp}

act_bench=1
act_load=2
act_new=3

act_var="act_${cmd}"

gcc_flags="-Wall -Wextra -Werror -std=c11 -pedantic"

fquote() {
    echo "\\\"${1}\\\""
}

fpow() {
    printf '%#xul' $((1 << ${1}))
}

bcmd="gcc src/salis.c -o ${salis_exe} ${gcc_flags} -Isrc -lncursesw -pthread"
bcmd="${bcmd} `[[ ${opt_optimized} == true ]] && echo "-O3 -DNDEBUG" || echo "-ggdb"`"
bcmd="${bcmd} -DACTION=${!act_var}"
bcmd="${bcmd} -DARCHITECTURE=`fquote ${opt_arch}`"
bcmd="${bcmd} -DARCH_SOURCE=`fquote arch/${opt_arch}.c`"
bcmd="${bcmd} -DCORE_COUNT=${opt_cores}"
bcmd="${bcmd} -DMUTA_RANGE=`fpow ${opt_muta_pow}`"
bcmd="${bcmd} -DMVEC_SIZE=`fpow ${opt_mvec_pow}`"
bcmd="${bcmd} -DNCURSES_WIDECHAR=1"
bcmd="${bcmd} -DSEED=${opt_seed}ul"
bcmd="${bcmd} -DSYNC_INTERVAL=`fpow ${opt_sync_pow}`"
bcmd="${bcmd} -DTGAP_SIZE=${opt_thread_gap}ul"

case ${cmd} in
bench)
    bcmd="${bcmd} -DBENCH_STEPS=${opt_steps}ul"
    bcmd="${bcmd} -DUI=`fquote bench.c`"
    ;;
esac

case ${cmd} in
bench|new)
    anc_list=

    for cix in `seq 1 ${opt_cores}` ; do
        anc_spec=`echo ${opt_anc_spec}, | cut -s -d, -f${cix}`
        anc_spec=${anc_spec:-${opt_anc_def}}

        if [[ -n ${anc_spec} ]] ; then
            anc_src=ancs/${opt_arch}/${anc_spec}.asm
            anc_path=${salis_tmp}/${anc_spec}.asm
            sed -E '/(^$|^;)/d; s/ +/ /g' ${anc_src} > ${anc_path}
        else
            anc_path=_
        fi

        anc_list=${anc_list}${anc_path},
    done

    bcmd="${bcmd} -DANC_LIST=`fquote "${anc_list::-1}"`"
    bcmd="${bcmd} -DANC_HALF=`[[ ${opt_half} == true ]] && echo 1 || echo 0`"
    bcmd="${bcmd} -DANC_CLONES=${opt_clones}"
    ;;
esac

case ${cmd} in
load|new)
    bcmd="${bcmd} -DAUTO_SAVE_INTERVAL=`fpow ${opt_auto_save_pow}`"
    bcmd="${bcmd} -DAUTO_SAVE_NAME_LEN=$((${#sim_path} + 20))"
    bcmd="${bcmd} -DMUTA_FLIP_BIT=`[[ ${opt_muta_flip} == true ]] && echo 1 || echo 0`"
    bcmd="${bcmd} -DSIM_NAME=`fquote ${opt_name}`"
    bcmd="${bcmd} -DSIM_PATH=`fquote ${sim_path}`"
    bcmd="${bcmd} -DUI=`fquote ui/${opt_ui}.c`"
    ;;
esac

blue "Using build command:"
echo "${bcmd}"
eval "${bcmd}"

case ${cmd} in
new)
    if [[ -d ${sim_dir} ]] && [[ ${opt_force} == true ]] ; then
        red "Force flag used. Wiping old simulation at '${sim_dir}':"
        rm -rv ${sim_dir}
    fi

    if [[ -d ${sim_dir} ]] ; then
        red "Error: simulation directory found at '${sim_dir}'."
        red "Please, remove it or call 'load' instead."
        exit 1
    fi

    blue "Creating new simulation directory at '${sim_dir}':"
    mkdir -pv ${sim_dir}
    ;;
esac

rcmd="`[[ -z ${opt_pre_cmd} ]] || echo "${opt_pre_cmd} "`${salis_exe}"

blue "Using run command:"
echo "${rcmd}"

blue "Running Salis..."
eval "${rcmd}"

case ${cmd} in
new)
    blue "Saving new simulation configuration file at:"
    echo "${sim_opts}"

    for ((i = 0; i < ${#options[@]}; i++)) ; do
        oopt=`field "${options[${i}]}" 7`

        [[ ! ${oopt} =~ new ]] || [[ ${oopt} =~ load ]] && continue

        lopt=`field "${options[${i}]}" 2`
        nopt=opt_${lopt//-/_}

        echo "${nopt}=${!nopt}" >> ${sim_opts}
    done
    ;;
esac

blue "Removing temporary Salis directory and resources:"
rm -rv ${salis_tmp}