#!/bin/bash

# Copyright (c) 2010-2020, Lawrence Livermore National Security, LLC. Produced
# at the Lawrence Livermore National Laboratory. All Rights reserved. See files
# LICENSE and NOTICE for details. LLNL-CODE-806117.
#
# This file is part of the MFEM library. For more information and source code
# availability visit https://mfem.org.
#
# MFEM is free software; you can redistribute it and/or modify it under the
# terms of the BSD-3 license. We welcome feedback and contributions, see file
# CONTRIBUTING.md for details.

make="${MAKE:-make}"
mpiexec="${MPIEXEC:-mpirun}"
mpiexec_np="${MPIEXEC_NP:--np}"
run_prefix=""
run_vg="valgrind --leak-check=full --show-reachable=yes --track-origins=yes"
run_suffix="-no-vis"
skip_gen_meshes="yes"
# filter-out device runs ("no") or non-device runs ("yes"):
device_runs="no"
cur_dir="${PWD}"
mfem_dir="$(cd "$(dirname "$0")"/.. && pwd)"
mfem_build_dir=""
build_log=""
output_dir=""
output_sfx=".out"
# The group format is: '"group-name" "group-summary-title" "group-directory"
# "group-source-patterns"'
groups_serial=(
'"examples"
   "Examples:"
   "examples"
   "ex{,1,2}[0-9].cpp"'
#   "ex1.cpp"'
'"sundials"
   "SUNDIALS examples:"
   "examples/sundials"
   "ex{9,10,16}.cpp"'
'"performance"
   "Performance miniapps:"
   "miniapps/performance"
   "ex1.cpp"'
#   ""'
'"meshing"
   "Meshing miniapps:"
   "miniapps/meshing"
   "mobius-strip.cpp klein-bottle.cpp extruder.cpp toroid.cpp
    mesh-optimizer.cpp"'
)
# Parallel groups
groups_parallel=(
'"examples"
   "Examples:"
   "examples"
   "ex{,1,2}[0-9]p.cpp"'
#   "ex1p.cpp"'
'"sundials"
   "SUNDIALS examples:"
   "examples/sundials"
   "ex{9,10,16}p.cpp"'
'"petsc"
   "PETSc examples:"
   "examples/petsc"
   "ex{,1}[0-9]p.cpp"'
'"performance"
   "Performance miniapps:"
   "miniapps/performance"
   "ex1p.cpp"'
#   ""'
'"meshing"
   "Meshing miniapps:"
   "miniapps/meshing"
   "pmesh-optimizer.cpp"'
'"electromagnetics"
   "Electromagnetics miniapps:"
   "miniapps/electromagnetics"
   "joule.cpp"'
#   "{volta,tesla,joule}.cpp"' # todo: multiline sample runs
)
# All groups serial + parallel runs mixed in the same group:
groups_all=(
'"examples"
   "Examples:"
   "examples"
   "ex\"{,1,2}[0-9]\"{,p}.cpp"'
'"sundials"
   "SUNDIALS examples:"
   "examples/sundials"
   "ex\"{9,10,16}\"{,p}.cpp"'
'"petsc"
   "PETSc examples:"
   "examples/petsc"
   "ex{,1}[0-9]p.cpp"'
'"performance"
   "Performance miniapps:"
   "miniapps/performance"
   "ex1{,p}.cpp"'
'"meshing"
   "Meshing miniapps:"
   "miniapps/meshing"
   "mobius-strip.cpp klein-bottle.cpp extruder.cpp toroid.cpp
    {,p}mesh-optimizer.cpp"'
'"electromagnetics"
   "Electromagnetics miniapps:"
   "miniapps/electromagnetics"
   "joule.cpp"'
#   "{volta,tesla,joule}.cpp"' # todo: multiline sample runs
)
make_all="all"
base_timeformat=$'real: %3Rs  user: %3Us  sys: %3Ss  %%cpu: %P'
# separator
sep='----------------------------------------------------------------'

# Command line parameters:
opt_help="no"
opt_show="no"
mfem_config="MFEM_USE_MPI=NO MFEM_DEBUG=NO"
groups=()
group_name=""
group_title=""
valgrind="no"
make_j="-j $(getconf _NPROCESSORS_ONLN)"
color="no"
built="no"
timing="no"

# Read the sample runs from the source "$1" and put them in the array variable
# "runs".
function extract_sample_runs()
{
   local old_IFS="${IFS}" sruns="" pruns=""
   local src="$1"
   if [ "${src}" == "" ]; then runs=(); return 1; fi
   local app=${src%.cpp}
   local vg_app="${app}"
   if [ "${valgrind}" == "yes" ]; then vg_app="${run_vg} ${app}"; fi
   # parallel sample runs are lines matching "^//.*  mpirun .* ${app}" with
   # everything in front of "mpirun" removed:
   pruns=`grep "^//.*  mpirun .* ${app}" "${src}" |
          sed -e "s/.*  mpirun \(.*\) ${app}/${mpiexec} \1 ${app}/g" \
              -e "s/ -np\(.*\) ${app}/ ${mpiexec_np}\1 ${vg_app}/g"`
   # serial sample runs are lines that are not parallel sample runs and matching
   # "^//.*  ${app}" with everything in front of "${app}" removed:
   sruns=`grep -v "^//.*  mpirun .* ${app}" "${src}" |
          grep "^//.*  ${app}" |
          sed -e "s/.*  ${app}/${vg_app}/g"`
   runs="${sruns}${pruns}"
   if [ "$skip_gen_meshes" == "yes" ]; then
      runs=`printf "%s" "$runs" | grep -v ".* -m .*\.gen"`
   fi
   if [ "$device_runs" == "yes" ]; then
      runs=`printf "%s" "$runs" | grep ".* -d .*"`
      if [ "$have_occa" == "no" ]; then
         runs=`printf "%s" "$runs" | grep -v ".* -d occa-.*"`
      fi
      if [ "$have_raja" == "no" ]; then
         runs=`printf "%s" "$runs" | grep -v ".* -d raja-.*"`
      fi
      if [ "$have_ceed" == "no" ]; then
         runs=`printf "%s" "$runs" | grep -v ".* -d ceed-.*"`
      fi
   else
      runs=`printf "%s" "$runs" | grep -v ".* -d .*"`
   fi
   IFS=$'\n'
   runs=(${runs})
   IFS="${old_IFS}"
}

# Echo usage information
function help_message()
{
   cat <<EOF

   $0 [options]

   Options:
      -h|-help    Print this usage information and exit
      -p|-par     Build the parallel MFEM library + examples + miniapps.
                  The default is to build the serial MFEM library + examples +
                  miniapps. The build can be customized by setting the variable
                  'mfem_config' or by building separately and using '-b'
      -g <dir> <pattern>
                  Specify explicitly a group (dir + file pattern) to run; This
                  option can be used multiple times to define multiple groups
      -dev        configure only sample runs using devices.
                  To test with a parallel build, the parallel (-p|-par) option
                  should be set first on the command line.
      -v          Enable valgrind
      -o <dir>    [${output_dir:-"<empty>: output goes to stdout"}]
                  If not empty, save output to files inside <dir>
      -d <dir>    [${mfem_build_dir}]
                  If <dir> is different from <mfem_dir> then use an
                  out-of-source build in <dir>
      -j <np>     [${make_j}] Specify the number of jobs to use for building
      -c|-color   Always use colors for the status messages: OK, FAILED, etc
      -b|-built   Do NOT rebuild the library and the executables
      -t|-time    Measure and print execution time for each sample run
      -s|-show    Show all configured sample runs and exit
      -n          Dry run: replace "\$sample_run" with "echo \$sample_run"
      <var>=<value>
                  Set a shell script variable; see below for valid variables
       *          Any other parameter is treated as <mfem_dir>
      <mfem_dir>  [${mfem_dir}] is the MFEM source directory

   This script tests all the sample runs listed in the beginning comments of
   MFEM's serial or parallel example and miniapp codes. The list of sample runs
   is auto-generated and can be viewed with the -s|-show option.

   The following shell script variables can be set with <var>=<value>:
      output_dir [${output_dir}]
         Same as '-o': if not empty, save output to files in that directory
      output_sfx [${output_sfx}]
         Suffix to append to the output files
      mfem_config [${mfem_config}]
         Set MFEM configuration options
      make [${make}], mpiexec [${mpiexec}], mpiexec_np [${mpiexec_np}]
         Their values can also set using the respective uppercase environment
         variable
      mfem_build_dir [${mfem_build_dir}]
         Same as '-d': set this variable to something different from <mfem_dir>
         to use an out-of-source build

   For other valid variables, see the script source.

   The following environment variables, if non-empty, are used:
      MAKE, MPIEXEC, MPIEXEC_NP

   Example usage:
       $0 -s                [ Show all configured sample runs ]
       $0 -o baseline       [ Serial build; run and save all built sample runs ]
       $0 -p -o baseline    [ Parallel build; run and save all sample runs ]
       $0 -b -g examples ex8.cpp [ Use the existing build; run ex8 sample runs ]

EOF
}

function show_runs()
{
   echo "${sep}"
   for group_params in "${groups[@]}"; do
      eval params=(${group_params})
      name="${params[0]}"
      title="${params[1]}"
      group_dir="${mfem_dir}/${params[2]}"
      pattern="${params[3]}"
      printf "group name:    [%s]\n" "${name}"
      printf "summary title: [%s]\n" "${title}"
      printf "directory:     [%s]\n" "${group_dir}"
      printf "pattern:       [%s]\n" "${pattern}"
      cd "${cur_dir}"; cd "${group_dir}" || exit 1
      eval sources=(${pattern})
      eval sources=("${sources[@]}")
      printf "sources:       (%s)\n" "${sources[*]}"
      printf "sample runs:\n"
      for src in "${sources[@]}"; do
         extract_sample_runs "${src}"
         for run in "${runs[@]}"; do
            printf "   %s\n" "${run}"
         done
      done
      echo "${sep}"
   done
}

# Process command line parameters
while [ $# -gt 0 ]; do

case "$1" in
   -h|-help)
      opt_help="yes"
      ;;
   -p|-par)
      mfem_config="MFEM_USE_MPI=YES MFEM_DEBUG=NO"
      ;;
   -g)
      gbasename="$(basename "$2")"
      gname="${group_name:-${gbasename}}"
      gtitle="${group_title:-"Group <${gbasename}>:"}"
      test_group="\"${gname}\" \"${gtitle}\" \"$2\" \"$3\""
      groups=("${groups[@]}" "${test_group}")
      shift 2
      ;;
   -dev)
       device_runs="yes"
       mfem_config+=" MFEM_USE_CUDA=YES MFEM_USE_OPENMP=YES"
       # OCCA, RAJA, libCEED are enabled below, if available
      ;;
   -v)
      valgrind="yes"
      ;;
   -o)
      shift
      output_dir="$1"
      ;;
   -d)
      shift
      mfem_build_dir="$1"
      ;;
   -j)
      shift
      make_j="-j $1"
      ;;
   -c|-color)
      color="yes"
      ;;
   -b|-built)
      built="yes"
      ;;
   -t|-time)
      timing="yes"
      ;;
   -s|-show)
      opt_show="yes"
      ;;
   -n)
      run_prefix="echo"
      ;;
   -*)
      echo "unknown option: '$1'"
      exit 1
      ;;
   *=*)
      eval $1
      ;;
   *)
      mfem_dir="$1"
      ;;
esac

shift
done # while ...

mfem_build_dir="${mfem_build_dir:-${mfem_dir}}"

build_log="${build_log:-${mfem_build_dir}/config/sample-runs-build.log}"

if [ 0 -eq ${#groups[*]} ]; then
   groups=("${groups_all[@]}")
   # These can be used as command line arguments:
   #   'groups=("${groups_serial[@]}")'
   #   'groups=("${groups_parallel[@]}")'
fi

if [ "${opt_help}" == "yes" ]; then
   help_message
   exit
fi

if [ "${opt_show}" == "yes" ]; then
   show_runs
   exit
fi

# Setup colors
if [ -t 1 ] && [ -z "${output_dir}" ] || [ "${color}" == "yes" ]; then
   red='\033[0;31m'
   green='\033[0;32m'
   yellow='\033[0;33m'
   magenta='\033[0;35m'
   cyan='\033[0;36m'
   none='\033[0m'
else
   red=
   green=
   yellow=
   magenta=
   cyan=
   none=
fi

# Run the given command, saving the rune time in the variable "timer".
function timed_run()
{
   timer="$({ time "$@" 1>&3 2>&4; } 2>&1)"
} 3>&1 4>&2

# This function is used to execute the sample runs
function go()
{
   local cmd=("$@")
   local res=""
   echo $sep
   echo "<${group}>" "${cmd[@]}"
   echo $sep
   if [ "${timing}" == "yes" ]; then
      timed_run "${cmd[@]}"
   else
      "${cmd[@]}"
   fi
   if [ "$?" -eq 0 ]; then
      res="${green}  OK  ${none}"
   else
      res="${red}FAILED${none}"
   fi
   printf "[${res}] <${group}> ${cmd[*]}\n"
   if [ "${timing}" == "yes" ]; then
      printf "Run time: %s\n" "${timer}"
      timer=(${timer})
      timer="${timer[1]}"
      printf -v line "[$res](%8s) ${cmd[*]}" "$timer"
      summary=("${summary[@]}" "$line")
   else
      summary=("${summary[@]}" "[${res}] ${cmd[*]}")
   fi
   echo $sep
}

# This function is used to run a group of sample runs (in the same directory)
function go_group()
{
   local res=""
   if [ $# -eq 0 ]; then return 0; fi
   local group_output_dir="" output_file="" output=""
   if [ ! -z "$output_dir" ]; then
      group_output_dir="${output_dir}/${group_dir}"
      mkdir -p "${group_output_dir}" || exit 1
   fi
   for src in "$@"; do
      cd "${mfem_dir}/${group_dir}" || exit 1
      extract_sample_runs "${src}" || continue
      [ "${#runs[@]}" -eq 0 ] && continue
      cd "${mfem_build_dir}/${group_dir}" || exit 1
      if [ ! -x "${src%.cpp}" ]; then
         res="${magenta} SKIP ${none}"
         echo $sep
         printf "[${res}] <${group}> <${src}>\n"
         echo $sep
         summary=("${summary[@]}" "[${res}] <${src}>")
         continue
      fi
      if [ ! -z "$output_dir" ]; then
         output_file="${group_output_dir}/${src}${output_sfx}"
         : > "${output_file}"
         output=">> \"${output_file}\" 2>&1"
      fi
      for run in "${runs[@]}"; do
         if [ "${run}" == "" ]; then continue; fi
         eval go \${run_prefix} \${run} \${run_suffix} $output
      done
   done
   ${make} clean-exec
}

# Make sure $mfem_dir exists and we can cd into it
cd "$mfem_dir" || exit 1
# Make sure $mfem_dir is an absolute path
mfem_dir="$PWD"
cd "${cur_dir}"
if [ "${built}" == "no" ]; then
   mkdir -p "${mfem_build_dir}" || exit 1
fi
# Make sure $mfem_build_dir exists and we can cd into it
cd "${mfem_build_dir}" || exit 1
# Make sure $mfem_build_dir is an absolute path
mfem_build_dir="$PWD"
# Setup 'output_dir'
if [ ! -z "$output_dir" ]; then
   cd "${cur_dir}"
   mkdir -p "${output_dir}" && cd "${output_dir}" || exit 1
   output_dir="$PWD"
   echo "Sending output to files in: [${output_dir}]"
   echo "Using suffix: [${output_sfx}]"
fi

TIMEFORMAT="${base_timeformat}"

# Setup optional libraries when not using externally built MFEM:
if [ "${built}" == "no" ]; then
   have_occa="no"
   have_raja="no"
   have_ceed="no"
   if [ "${device_runs}" == "yes" ]; then
      if [ -n "${CUDA_ARCH}" ]; then
         mfem_config+=" CUDA_ARCH=${CUDA_ARCH}"
      fi
      if [ -d "${mfem_dir}/../occa" ]; then
         mfem_config+=" MFEM_USE_OCCA=YES"
         have_occa="yes"
      fi
      if [ -d "${mfem_dir}/../raja" ]; then
         mfem_config+=" MFEM_USE_RAJA=YES"
         have_raja="yes"
      fi
      if [ -d "${mfem_dir}/../libCEED" ]; then
         mfem_config+=" MFEM_USE_CEED=YES"
         have_ceed="yes"
      fi
   fi
fi

function set_echo_log()
{
   local dirname=`dirname "$1"`
   cd "${cur_dir}"
   mkdir -p "${dirname}" && cd "${dirname}" || exit 1
   echo_log="$PWD"/`basename "$1"`
}

# Echo the given command line; then run it sending all output to $echo_log
function echo_run()
{
   echo "   $@"
   { echo "   $@"; echo "$sep";
     "$@"
     echo "$sep"; } >> "$echo_log" 2>&1
}

# Function that builds the mfem library, examples and miniapps
function build_all()
{
   printf "Building MFEM with all examples and miniapps:\n"
   set_echo_log "${build_log}"
   echo "   ### build log: [$echo_log]"
   { echo "$sep"; echo "   MFEM build log"; echo "$sep"; } > "$echo_log"
   echo_run cd "${mfem_build_dir}"
   if [ "${mfem_dir}" != "${mfem_build_dir}" ]; then
      echo_run ${make} -f "${mfem_dir}"/makefile config
   fi
   # Don't use 'make distclean' as it will delete the default $build_log
   echo_run ${make} clean || exit 1
   echo_run ${make} config ${mfem_config} || exit 1
   echo_run ${make} ${make_j} || exit 1
   echo_run ${make} ${make_all} ${make_j} || exit 1
}

# Function that runs all sample runs, given by the array variable "groups".
function all_go()
{
   for group_params in "${groups[@]}"; do
      eval params=(${group_params})
      group="${params[0]}"
      group_dir="${params[2]}"
      cd "${mfem_dir}/${group_dir}" || exit 1
      eval sources=(${params[3]})
      eval sources=("${sources[@]}")
      summary=("${summary[@]}" "${params[1]}")
      go_group "${sources[@]}"
   done

   printf "Summary:\n--------\n"
   for line in "${summary[@]}"; do
      printf "${line}\n"
   done
}

function main()
{
   # Build all mfem examples and miniapps
   if [ "${built}" == "no" ]; then
      if [ "${timing}" == "yes" ]; then
         timed_run build_all
         printf "Build time: %s\n" "${timer}"
      else
         build_all
      fi
   fi

   summary=()
   PATH=.:$PATH

   # Print the MFEM configuration info
   cd "${mfem_build_dir}"
   echo "$sep"
   echo "MFEM configuration"
   echo "$sep"
   ${make} info
   echo "$sep"

   # Run all sample runs.
   if [ "${timing}" == "yes" ]; then
      timed_run all_go
      printf "Total run time: %s\n" "${timer}"
   else
      all_go
   fi
   echo
}

output=""
if [ ! -z "$output_dir" ]; then
   output=">> \"${output_dir}/main${output_sfx}\" 2>&1"
fi
eval main $output
