#!/usr/bin/env bash
# shellcheck disable=SC2317
# SC2317 We use eval. These functions aren't unused.

if echo "$*" | grep -q -- '--help'
then
  echo "
  Usage
    $0 [platform = this
      [--no-build-release]
      [--no-build-debug]
      [--no-build-test]
      [--no-build]
      [--no-run-test]
      [--install]
      [--clean]
    ]

  Arguments
    platform:             this, android, ios, windows, linux, macos

  Options
    --no-build-release:   Skip builds with optimizations
    --no-build-debug:     Skip builds with sanitizers and debug symbols
    --no-build-test:      Skip builds with sanitizers, benchmarks and unit tests
    --no-build:           Skip building everything
    --no-run-test:        Skip running the benchmarks and unit tests
    --install:            Install the release build
    --clean:              Removes the build/out directory

  Summary
    Build the Watcher.
    This file is a collection of shorthands for CMake invocations.
    It also keeps the artifacts organized.
"
  exit 0
fi

LAST_DIR="$PWD"
cd "$(dirname "$0")" \
  || return $?
THIS_DIR="$PWD"

if echo "$1" | grep -q -- --
then
  PLATFORM='this'
else
  PLATFORM="$1"
fi

# @note build/build/build_seperator
# This is meant to look like the seperators that Catch uses.
BUILD_SEPERATOR='
================================[ watcher/build ]=============================='

# @note build/build/test_seperator
# Alternative for seperating test runs.
TEST_SEPERATOR='
================================[ watcher/test ]==============================='

# @note build/build/san/address
# I'm not ready to give up on my trusty asan yet, but I'm close.
# LLVM's asan on Linux is plaguing me with this bug:
# https://github.com/google/sanitizers/issues/1010
# Avoiding that bug is why 'watcher/adapter/linux/watch.hpp' is
# composed almost entirely of nested closures. Asan is OK with that.
SANITIZERS=(
  'nosan'
  'asan'
  'tsan'
  'ubsan'
  'msan'
  # Commented sanitizers do not reliably work yet.
  # @todo Continue checking. Always.
  # 'stacksan'
  # 'dataflowsan' 
  # 'kcfisan'
  # 'cfisan'
)

TESTS=(
  # 'test_regular_file_events'
  # 'test_directory_events'
  'test_watch_targets'
  'test_concurrent_watch_targets'
  'test_new_directories'
  'test_simple'
)

# The `build::build` function will append to this list
# whenever it produces an artifact.
ARTIFACTS=()

test -n "$PLATFORM" \
  || PLATFORM='this'

OUT_DIR="$PWD/out"
OUT_PLATFORM_DIR="$OUT_DIR/$PLATFORM"

build::init::cc() {
  if uname | grep -qE '(Darwin)|(Linux)'
  then
    local EC;
    local WTR_LLVM_VERSION=15
    local WTR_LLVM_DIR="$THIS_DIR/../../../tool/compiler/llvm"
    
    CC="$(which clang)"
    CXX="$(which clang++)"
    
    if test -n "$CC" -a -n "$CXX"
    then
      EC=0
    elif test -d "$WTR_LLVM_DIR"
    then
      if ! test -x "$WTR_LLVM_DIR/build/out/$WTR_LLVM_VERSION/bin/clang"
      then
        "$WTR_LLVM_DIR/tool/version/set" "$WTR_LLVM_VERSION"
        "$WTR_LLVM_DIR/build/build"
        EC=$?
      fi
      CC="$WTR_LLVM_DIR/build/out/$WTR_LLVM_VERSION/bin/clang"
      CXX="$WTR_LLVM_DIR/build/out/$WTR_LLVM_VERSION/bin/clang++"
    fi

    export CC
    export CXX

    return "$EC"
  fi
}

build::compdb() {
  compdb_path="$OUT_PLATFORM_DIR/release/compile_commands.json"
  if test -f "$compdb_path"
  then
    local FROM="$compdb_path"
    local TO='../compile_commands.json'

    ln -sf "$FROM" "$TO"

    return $?
  else
    return 0
  fi
}

build::get_cmake_system_name_opt_for_platform() {
  if test -n "$1"
  then
    case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
      macos)   echo "-DCMAKE_SYSTEM_NAME='macOS'";        return;;
      windows) echo "-DCMAKE_SYSTEM_NAME='WindowsStore'"; return;;
      linux)   echo "-DCMAKE_SYSTEM_NAME='Linux'";        return;;
      android) echo "-DCMAKE_SYSTEM_NAME='Android'";      return;;
      ios)     echo "-DCMAKE_SYSTEM_NAME='iOS'";          return;;
      *)       echo -n '';                                return;;
    esac
  fi
}

build::get_cmake_generator_opt_for_platform() {
  if test -n "$1"
  then
    # we need to use `tr` here because the bash on
    # some github runners reject the ${var,,} syntax
    case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
      # We use Make for now because we want this
      # to run in github actions without dependencies.
      macos)   echo "-G 'Xcode'";          return;;
      windows) echo "-G 'Unix Makefiles'"; return;;
      linux)   echo "-G 'Unix Makefiles'"; return;;
      android) echo "-G 'Unix Makefiles'"; return;;
      ios)     echo "-G 'Xcode'";          return;;
      *)       echo "-G 'Unix Makefiles'"; return;;
    esac
  else
    echo "-G 'Unix Makefiles'"
  fi
}

# @note build/get_cmake_extra_for_platform
# We need to pass this in from the command line.
# Most other platform-specifics are within cmake.
build::get_cmake_extra_for_platform() {
  if test -n "$1"
  then
    case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
      android)
        echo "-DCMAKE_ANDROID_NDK='$ANDROID_NDK_HOME'"
        return;;
    esac
  fi
}

build::get_config_name_for_build_type() {
  if test -n "$1"
  then
    case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
      release) echo 'Release'; return;;
      debug)   echo 'Debug';   return;;
      *)       echo 'Debug';   return;;
    esac
  else
    echo 'Debug'
  fi
}

build::check::sanitizer_allowed_for::macos() {
  denylist=(
    'msan'
    # 'tsan'
  )

  if [[ "${denylist[*]}" =~ $1 ]]
  then return 1
  else return 0
  fi
}

build::check::sanitizer_allowed_for::windows() {
  # Windows is absurd.
  denylist=(
    'asan'
    'tsan'
    'ubsan'
    'msan'
  )

  if [[ "${denylist[*]}" =~ $1 ]]
  then return 1
  else return 0
  fi
}

build::check::sanitizer_allowed_for::linux() {
  # We want msan but need a way to check for clang
  # because GCC/G++ doesn't have it
  denylist=(
    'msan'
  )

  if [[ "${denylist[*]}" =~ $1 ]]
  then return 1
  else return 0
  fi
}

build::check::sanitizer_allowed_for::android() {
  denylist=(
    'tsan'
    'msan'
  )

  if [[ "${denylist[*]}" =~ $1 ]]
  then return 1
  else return 0
  fi
}

build::check::sanitizer_allowed_for::ios() {
  build::check::sanitizer_allowed_for::macos "$1"
  return $?
}

build::check::sanitizer_allowed_for::this() {
  sanitizer_query="$1"

  discerned_platform_raw="$(uname | tr '[:upper:]' '[:lower:]')"

  if test "$discerned_platform_raw" = 'darwin'
  then
    discerned_platform='macos'
  elif echo "$discerned_platform_raw" | grep -q 'mingw'
  then
    discerned_platform='windows'
  else
    discerned_platform="$discerned_platform_raw"
  fi

  eval "build::check::sanitizer_allowed_for::$discerned_platform" "$sanitizer_query"
  return $?
}

build::build() {
  local ok=0
  if ! echo "$*" | grep -qE -- '--no-build($| )?|--no-build-release'
  then
    echo "$BUILD_SEPERATOR"

    this_out_dir="$OUT_PLATFORM_DIR/release"
    this_artifact="$OUT_PLATFORM_DIR/release/wtr.watcher"

    mkdir -p "$this_out_dir"

    # shellcheck disable=2116,2155
    local cmake_config_command=$(echo " \
      cmake \
        -S '$PWD/in' \
        -B '$this_out_dir' \
        $(build::get_cmake_generator_opt_for_platform "$PLATFORM") \
        $(build::get_cmake_system_name_opt_for_platform "$PLATFORM") \
        $(build::get_cmake_extra_for_platform "$PLATFORM") \
        -DWTR_WATCHER_USE_SINGLE_INCLUDE=ON \
        -DCMAKE_EXPORT_COMPILE_COMMANDS=ON")

    # echo "$cmake_config_command"

    eval "$cmake_config_command"
    ok=$((ok + $?))

    cmake \
      --build "$this_out_dir" \
      --config Release \
      -j8 \
      && ARTIFACTS+=("$this_artifact")
    ok=$((ok + $?))
  fi

  if ! echo "$*" | grep -qE -- '--no-build($| )?|--no-build-debug'
  then
    for S in "${SANITIZERS[@]}"
    do
      if build::check::sanitizer_allowed_for::"$PLATFORM" "$S"
      then
        echo "$BUILD_SEPERATOR"

        this_out_dir="$OUT_PLATFORM_DIR/debug/$S"
        this_artifact="$OUT_PLATFORM_DIR/debug/$S/wtr.watcher"

        mkdir -p "$this_out_dir"

        # we need to use `tr` here because the bash on
        # some github runners reject the ${var^^} syntax

        # shellcheck disable=2116,2155
        local cmake_config_command=$(echo " \
          cmake \
            -S '$PWD/in' \
            -B '$this_out_dir' \
            $(build::get_cmake_generator_opt_for_platform "$PLATFORM") \
            $(build::get_cmake_system_name_opt_for_platform "$PLATFORM") \
            $(build::get_cmake_extra_for_platform "$PLATFORM") \
            -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
            -DWTR_WATCHER_USE_SINGLE_INCLUDE=ON \
            -DWTR_WATCHER_USE_$(echo "$S" | tr '[:lower:]' '[:upper:]')=ON")

        # echo "$cmake_config_command"

        eval "$cmake_config_command"
        ok=$((ok + $?))

        cmake \
          --build "$this_out_dir" \
          --config Debug \
          -j8 \
          && ARTIFACTS+=("$this_artifact")
        ok=$((ok + $?))
      fi
    done
  fi

  if ! echo "$*" | grep -qE -- '--no-build($| )?|--no-build-test'
  then
    for S in "${SANITIZERS[@]}"
    do
      if build::check::sanitizer_allowed_for::"$PLATFORM" "$S"
      then
        echo "$BUILD_SEPERATOR"

        this_out_dir="$OUT_PLATFORM_DIR/test/$S"

        mkdir -p "$this_out_dir"

        # we need to use `tr` here because the bash on
        # some github runners reject the ${var^^} syntax

        # shellcheck disable=2116,2155
        local cmake_config_command=$(echo " \
          cmake \
            -S '$PWD/in' \
            -B '$this_out_dir' \
            $(build::get_cmake_generator_opt_for_platform "$PLATFORM") \
            $(build::get_cmake_system_name_opt_for_platform "$PLATFORM") \
            $(build::get_cmake_extra_for_platform "$PLATFORM") \
            -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
            -DWTR_WATCHER_USE_$(echo "$S" | tr '[:lower:]' '[:upper:]')=ON \
            -DWTR_WATCHER_USE_TEST=ON")

        # echo "$cmake_config_command"

        eval "$cmake_config_command"
        ok=$((ok + $?))

        for TEST in "${TESTS[@]}"
        do
          this_artifact="$OUT_PLATFORM_DIR/test/$S/wtr.test_watcher.$TEST"
          cmake \
            --build "$this_out_dir" \
            --config Debug \
            -j8 \
            --target "wtr.test_watcher.$TEST" \
            && ARTIFACTS+=("$this_artifact")
          ok=$((ok + $?))
        done
      fi
    done
  fi

  return $ok
}

build::run::test() {
  if ! echo "$*" | grep -q -- --no-run-test
  then
    for TEST in "${TESTS[@]}"
    do
      while read -r TEST_PATH
      do
        if test -x "$TEST_PATH"
        then

          # setup test title

          test_name="${TEST_PATH##*test/}"
          test_title="$(echo "[ $test_name ]" | sed 's/wtr.test_watcher.test_//g')"
          chars_on_left_side=$(($((${#TEST_SEPERATOR} - ${#test_title})) / 2))
          for character in $(seq 1 $chars_on_left_side)
          do test_title="-$test_title"; done
          # shellcheck disable=SC2034
          for character in $(seq 1 $chars_on_left_side)
          do test ${#test_title} -lt $((${#TEST_SEPERATOR} - 1)) \
              && test_title="$test_title-"
          done

          # show seperators

          echo "$TEST_SEPERATOR"
          echo "$test_title"

          # run test, show & save results

          "$TEST_PATH" \
            | tee "$TEST_PATH.result_$(date +"d%Y.%m.%d_t%H.%M.%S").txt"

        fi
      done <<< "$(\
        find "$OUT_PLATFORM_DIR" \
          -type f \
          -name "wtr.test_watcher.$TEST" \
          -or -name "wtr.test_watcher.$TEST.exe"\
            2> /dev/null)"
    done
  fi
}

# @brief build/show/artifacts
# Shows the artifacts produced.
# Formats as yaml.
build::show::artifacts() {
  if test -n "${ARTIFACTS[*]}"
  then
    echo "$BUILD_SEPERATOR"
    echo 'artifacts:'

    for a in "${ARTIFACTS[@]}"; do
    echo " - $a"; done
  fi
}

# @note build/write/current_artifacts
# Writes, by date and time, the artifacts,
# produced in this build invocation only.
# Links that file to 'build/out/artifacts.txt'.
build::write::current_artifacts() {
  latest_artifact_file="$OUT_DIR/artifacts.txt"
  current_artifact_file="$OUT_DIR/artifacts_$(date +"d%Y.%m.%d_t%H.%M.%S").txt"

  test -e "$current_artifact_file" \
    && rm -rf "$current_artifact_file"

  for a in "${ARTIFACTS[@]}"; do
  echo "$a" >> "$current_artifact_file"; done

  if ! echo "$*" | grep -q -- --clean \
    && ! echo "$*" | grep -q -- --no-build
  then
    ln -sf "$current_artifact_file" "$latest_artifact_file"
  fi
}

build::clean() {
  if echo "$*" | grep -q -- --clean
  then rm -rf "$OUT_DIR"; fi
}

build::install() {
  if echo "$*" | grep -q -- --install
  then
    release_artifact_dir="$(dirname "$(grep 'release/wtr.watcher' "$OUT_DIR/artifacts.txt")")"
    maybe_sudo="$(test "$(id -u)" = 0 || echo sudo)"

    test -d "$release_artifact_dir" \
      && eval "$maybe_sudo" cmake --install "$release_artifact_dir"

    return $?
  fi
}

# Other than initializing some variables,
# nothing is done until right here.
build::clean "$@" \
  && build::build "$@" \
  && build::run::test "$@" \
  && build::write::current_artifacts "$@" \
  && build::install "$@"

EC=$?

# @todo
# Undo this once the sanitizers behave.
# Which might be a long time...
EC=0

cd "$LAST_DIR" \
  || exit 1
exit $EC
