#!/bin/sh
set -e

tests=$(ls -d */)
languages=""
skip=""
runs="10"
warmup="3"
export_format=""
while getopts r:w:l:s:t:e: name
do
    case $name in
    r)    runs="$OPTARG";;
    w)    warmup="$OPTARG";;
    l)    languages="$OPTARG";;
    s)    skip="$OPTARG";;
    t)    tests="$OPTARG";;
    e)    export_format="$OPTARG";;
    ?)    printf "usage: %s: [-r runs] [-w warmup] [-l languages...] [-s languages...] [-t tests...] [-e format]\n" "$0"
          printf "  -r: number of benchmark runs (default: 10)\n"
          printf "  -w: number of warmup runs (default: 3)\n"
          printf "  -l: run specific languages (e.g., 'c go rust')\n"
          printf "  -s: skip specific languages (e.g., 'lua python3')\n"
          printf "  -t: run specific tests (e.g., 'minimal/ record/')\n"
          printf "  -e: export format (json, csv, markdown, asciidoc, orgmode)\n"
          exit 2;;
    esac
done
shift $(($OPTIND - 1))
unset name

# Check if hyperfine is available
if ! command -v hyperfine > /dev/null; then
    echo "Error: hyperfine is required but not installed."
    echo "Install with: brew install hyperfine (macOS) or cargo install hyperfine"
    exit 1
fi

clean() {
    # C/C++
    if [ -f makefile ]
    then
      make -s clean
    else
      rm -f a.out
    fi

    # Go
    rm -f $1

    # C#
    rm -f *.dll $1.csproj
    rm -rf bin obj

    # Java
    rm -f "$(classname $1)".class

    # Rust
    rm -f ${1}
    if [ -f Cargo.toml ]; then
        rm -rf target
    fi

    # Zig
    rm -f ${1}
    rm -f ${1}.o
}

# backup - backup files as *.orig
# usage: backup source_file...
backup() {
    for source_file in "$@"
    do
        target_file=${source_file}.orig
        if [ -f "$source_file" ]
        then
            mv -f "$source_file" "$target_file"
        fi
        unset target_file
    done
    unset source_file
}

# restore - restore files from *.orig and ~
# usage: restore source_file...
restore() {
    for source_file in "$@"
    do
        if [ -f "$source_file" ]
        then
            target_file=${source_file%.orig}
            if [ "$source_file" != "$target_file" ]
            then
                mv -f "$source_file" "$target_file"
            fi
        fi

        if [ -f "$source_file" ]
        then
            target_file=${source_file%~}
            if [ "$source_file" != "$target_file" ]
            then
                mv -f "$source_file" "$target_file"
            fi
        fi

        unset target_file
    done
    unset source_file
}

capitalize() {
    read input
    printf "%s" "$input" | awk '{ print toupper(substr($0, 1, 1)) substr($0, 2) }'
    unset input
}

classname() {
    echo ${1}App | capitalize
}

should_run_language() {
    lang="$1"

    if [ -n "$languages" ]; then
        case " $languages " in
            *" $lang "*) return 0 ;;
            *) return 1 ;;
        esac
    fi

    if [ -n "$skip" ]; then
        case " $skip " in
            *" $lang "*) return 1 ;;
            *) return 0 ;;
        esac
    fi

    return 0
}

run_benchmark() {
    cmd="$1"
    name="$2"
    
    hyperfine_opts="-w $warmup -r $runs --style basic -u microsecond"
    
    if [ -n "$export_format" ]; then
        output_file="${file}_${name}_benchmark.${export_format}"
        hyperfine_opts="$hyperfine_opts --export-${export_format} $output_file"
    fi
    
    printf " %s" "$name"
    
    # Run hyperfine and capture the mean time
    # Handle both single run format "Time (abs ≡)" and multiple run format "Time (mean ± σ)"
    result=$(hyperfine $hyperfine_opts --shell=sh "$cmd" 2>&1 | grep -E "Time \((abs|mean)" | sed -E 's/.*Time \([^:]*\): *([0-9.]+) *(.*s).*/\1 \2/' | awk '{print $1 " " $2}')
    
    if [ -n "$result" ]; then
        printf "mean %s" "$result"
    else
        printf "failed"
    fi
    
    printf '\t\n'
}

for d in $tests
do
    file=$(basename "$d")
    cd "$file"
    echo "$file"
    clean "$file"

    # C++
    if [ -f ${file}.cpp ] && should_run_language "cpp"
    then
        if [ -f makefile ]
        then
            make -s c++
        else
            # CXX
            g++ -O2 ${file}.cpp
        fi
        run_benchmark "./a.out" "cpp            "
    fi

    # C
    if [ -f ${file}.c ] && should_run_language "c"
    then
        restore *.c* *.cpp*
        if [ -f makefile ]
        then
            make -s
        else
            # ISO POSIX-2:1993
            c89 -O2 ${file}.c
        fi
        run_benchmark "./a.out" "c              "
    fi

    # Zig
    if [ -f ${file}.zig ] \
    && command -v zig > /dev/null \
    && should_run_language "zig"
    then
        zig build-exe -O ReleaseFast ${file}.zig > /dev/null 2>&1
        run_benchmark "./${file}" "zig            "
    fi

    # Rust
    if [ -f ${file}.rs ] \
    && command -v rustc > /dev/null \
    && should_run_language "rust"
    then
        if [ -f Cargo.toml ]; then
            if command -v cargo > /dev/null; then
                cargo build --release > /dev/null 2>&1
                run_benchmark "./target/release/${file}" "rust           "
            fi
        else
            rustc -O ${file}.rs -o ${file}
            run_benchmark "./${file}" "rust           "
        fi
    fi

    # Go
    # C/C++ source files not allowed when not using cgo or SWIG
    # Avoid setting CGO_ENABLED=0, backup and restore instead
    if [ -f ${file}.go ] \
    && command -v go > /dev/null \
    && should_run_language "go"
    then
        backup *.c *.cpp
        go build ./...
        restore *.c* *.cpp*
        run_benchmark "./${file}" "go             "
    fi

    # Java
    if [ -f ${file}.java ] \
    && command -v javac > /dev/null \
    && command -v java  > /dev/null \
    && should_run_language "java"
    then
        javac -cp '.:*' ${file}.java
        class=$(classname $file)
        run_benchmark "java -cp '.:*' $class" "java           "
        unset class
    fi

    # C#
    if [ -f ${file}.cs ] \
    && command -v dotnet > /dev/null \
    && should_run_language "cs"
    then
        dotnet new console > /dev/null 2>&1
        mv -f ${file}.cs Program.cs
        dotnet build -c Release > /dev/null 2>&1
        mv -f Program.cs ${file}.cs
        run_benchmark "dotnet ./bin/Release/net*/${file}.dll" "cs             "
    fi

    # Node
    if [ -f ${file}.js ] \
    && command -v node > /dev/null \
    && should_run_language "js"
    then
        # nvm
        # sudo ln -s $(which node) /usr/local/bin/node
        run_benchmark "node --max-old-space-size=8192 ${file}.js" "js             "
    fi

    # LuaJIT
    if [ -f ${file}.lua ]
    then
        if command -v luajit > /dev/null \
        && should_run_language "luajit"
        then
            if [ -f makefile ]
            then
                make -s luajit
            fi

            run_benchmark "luajit ${file}.lua" "luajit         "
        fi
    fi

    # PHP
    if [ -f ${file}.php ] \
    && command -v php > /dev/null \
    && should_run_language "php"
    then
        run_benchmark "php -d memory_limit=8G ${file}.php" "php            "
    fi

    # Lua
    if [ -f ${file}.lua ]
    then
        if command -v lua > /dev/null \
        && should_run_language "lua"
        then
            if [ -f makefile ]
            then
                make -s clean lua
            fi

            # macOS
            # cd /opt/homebrew/bin/
            # ln -s ../Cellar/lua@5.1/5.1.5_8/bin/lua-5.1 lua
            run_benchmark "lua ${file}.lua" "lua            "
        fi
    fi

    # Ruby
    if [ -f ${file}.rb ] \
    && command -v ruby > /dev/null \
    && should_run_language "ruby"
    then
        run_benchmark "ruby ${file}.rb" "ruby           "
    fi

    # Python
    if [ -f ${file}.py ] \
    && command -v python3 > /dev/null \
    && should_run_language "python3"
    then
        run_benchmark "python3 ${file}.py" "python3        "
    fi

    cd ..
    unset file
done
unset d
