#!/usr/bin/env bash

# Copyright (c) 2021 Skyward Experimental Rocketry
# Author: Damiano Amatruda
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

print_banner() {
    cat <<EOF
+---------------------------------------------------------------+
|   ____  _                                _                    |
|  / ___|| | ___   ___      ____ _ _ __ __| |                   |
|  \\___ \\| |/ / | | \\ \\ /\\ / / _\` | '__/ _\` |                   |
|   ___) |   <| |_| |\\ V  V / (_| | | | (_| |                   |
|  |____/|_|\\_\\\\__, | \\_/\\_/ \\__,_|_|  \\__,_|                   |
|   ____       |___/    _   ____            _                   |
|  | __ ) _   _(_) | __| | / ___| _   _ ___| |_ ___ _ __ ___    |
|  |  _ \\| | | | | |/ _\` | \\___ \\| | | / __| __/ _ \\ '_ \` _ \\   |
|  | |_) | |_| | | | (_| |  ___) | |_| \\__ \\ ||  __/ | | | | |  |
|  |____/ \\__,_|_|_|\\__,_| |____/ \\__, |___/\\__\\___|_| |_| |_|  |
+----------------------------------|___/-------------------v3.3-+
EOF
}

ohai() {
    printf "\n${TTY_BLUE}==>${TTY_RESET}${TTY_BOLD} %s${TTY_RESET}\n" "$@"
}

init_dirs() {
    sbs_base="$(cd -- "$(dirname "$0")" > /dev/null 2>&1 && pwd -P)"
    source_dir="$PWD"
    build_default_dir="$source_dir/$BUILD_DEFAULT_DIRNAME"
    build_host_dir="$source_dir/$BUILD_HOST_DIRNAME"
    toolchain_file="$sbs_base/libs/miosix-kernel/miosix/_tools/toolchain.cmake"
}

find_deps() {
    ohai "Find dependencies"

    command -v cmake               > /dev/null 2>&1 && found_cmake=true
    command -v arm-miosix-eabi-g++ > /dev/null 2>&1 && found_miosixgpp=true
    command -v ccache              > /dev/null 2>&1 && found_ccache=true
    command -v ninja               > /dev/null 2>&1 && found_ninja=true
    command -v python              > /dev/null 2>&1 && found_python=true
    command -v cppcheck            > /dev/null 2>&1 && found_cppcheck=true
    command -v clang-tidy          > /dev/null 2>&1 && found_clangtidy=true
    command -v clang-format        > /dev/null 2>&1 && found_clangformat=true
    command -v st-flash            > /dev/null 2>&1 && found_stflash=true
    command -v ST-LINK_CLI.exe     > /dev/null 2>&1 && found_stlink=true

    printf "Found CMake: ";               [ "$found_cmake" = true ]       && echo "yes" || echo "no"
    printf "Found arm-miosix-eabi-g++: "; [ "$found_miosixgpp" = true ]   && echo "yes" || echo "no"
    printf "Found Ccache: ";              [ "$found_ccache" = true ]      && echo "yes" || echo "no"
    printf "Found Ninja: ";               [ "$found_ninja" = true ]       && echo "yes" || echo "no"
    printf "Found Python: ";              [ "$found_python" = true ]      && echo "yes" || echo "no"
    printf "Found Cppcheck: ";            [ "$found_cppcheck" = true ]    && echo "yes" || echo "no"
    printf "Found clang-tidy: ";          [ "$found_clangtidy" = true ]   && echo "yes" || echo "no"
    printf "Found clang-format: ";        [ "$found_clangformat" = true ] && echo "yes" || echo "no"
    printf "Found flasher: ";             [ "$found_stflash" = true ]     && echo "st-flash" \
                                            || { [ "$found_stlink" = true ] && echo "st-link" || echo "no"; }

    [ "$found_cmake" = true ]     || { echo "Error: CMake must be installed"; return 1; }
    [ "$found_miosixgpp" = true ] || { echo "Error: arm-miosix-eabi-g++ must be installed"; return 1; }
}

# Workaround: Disable tests in excluded subdirectories
# See: https://gitlab.kitware.com/cmake/cmake/-/issues/20212
cmake_disable_excluded_tests() {
    declare build_dir="$1"

    [ ! -f "$build_dir/$CTEST_FILENAME" ] || sed -i.bak 's/^subdirs/# subdirs/' "$build_dir/$CTEST_FILENAME"
}

configure() {
    declare build_dir="$1"

    ohai "Configure"

    [ -f "$toolchain_file" ] || { echo "Error: CMake Toolchain File for Miosix was not found"; return 1; }

    declare -a defs=(-DCMAKE_EXPORT_COMPILE_COMMANDS=ON)
    defs+=(-DCMAKE_C_FLAGS=-fdiagnostics-color=always -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always)
    [ "$config_host" = false ]   && defs+=(-DCMAKE_TOOLCHAIN_FILE="$toolchain_file" -DBUILD_TESTING=OFF)
    [ "$found_ccache" = true ]   && defs+=(-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache)
    [ "$config_debug" = true ]   && defs+=(-DCMAKE_BUILD_TYPE=Debug) || defs+=(-DCMAKE_BUILD_TYPE=Release)
    [ "$config_verbose" = true ] && defs+=(-DCMAKE_VERBOSE_MAKEFILE=ON)

    declare gen
    [ "$found_ninja" = true ] && gen=-GNinja || gen=-G"Unix Makefiles"

    cmake -B"$build_dir" "${defs[@]}" "$gen" "$source_dir" || return

    { [ "$config_debug" = true ]   && touch "$build_dir/$DEBUG_FILENAME";   } || rm -f "$build_dir/$DEBUG_FILENAME"
    { [ "$config_verbose" = true ] && touch "$build_dir/$VERBOSE_FILENAME"; } || rm -f "$build_dir/$VERBOSE_FILENAME"
}

check_configured() {
    declare build_dir="$1"

    declare to_reconfigure=false
    if [ ! -d "$build_dir" ]; then
        to_reconfigure=true
    elif [ ! -f "$build_dir/$CMAKE_FILENAME" ]; then
        rm -rf "$build_dir"
        to_reconfigure=true
    else
        [ -f "$build_dir/$DEBUG_FILENAME" ]   && found_debug=true   || found_debug=false
        [ -f "$build_dir/$VERBOSE_FILENAME" ] && found_verbose=true || found_verbose=false
        if [ "$config_debug" != "$found_debug" ] \
                || [ "$config_verbose" != "$found_verbose" ]; then
            to_reconfigure=true
        fi
    fi

    if [ "$to_reconfigure" = true ]; then
        configure "$build_dir"
    fi
}

build() {
    declare build_dir="$1"
    declare target="$2"

    check_configured "$build_dir" || return

    ohai "Build"

    declare -a opts
    get_build_opts opts

    cmake --build "$build_dir" "${opts[@]}" --target "$target"
}

build_all() {
    declare build_dir="$build_default_dir"

    build "$build_dir" all
}

clean() {
    declare build_desc="$1"
    declare build_dir="$2"

    ohai "Clean ($build_desc)"

    if [ -f "$build_dir/$CMAKE_FILENAME" ]; then
        declare -a opts
        get_build_opts opts

        cmake --build "$build_dir" "${opts[@]}" --target clean
    fi

    echo "Removing build folder..."
    rm -rf "$build_dir"
}

clean_all() {
    clean "Default" "$build_default_dir"
    clean "Host" "$build_host_dir"
}

flash() {
    declare target="$1"
    declare build_dir="$build_default_dir"

    build "$build_dir" "$target" || return

    ohai "Flash"

    [ -f "$build_dir/$target.bin" ] || { echo "Error: target '$target' is not flashable"; return 1; }

    if [ "$found_stflash" = true ]; then
        st-flash --reset write "$build_dir/$target.bin" 0x8000000
    elif [ "$found_stlink" = true ]; then
        ST-LINK_CLI.exe -P "$build_dir/$target.bin" 0x8000000 -V -Rst
    else
        echo "Error: No flashing software found!"
        return 1
    fi
}

run_tests() {
    declare target="$1"
    declare build_dir="$build_host_dir"

    config_host=true

    build "$build_dir" "$target" || return

    ohai "Test"

    cmake_disable_excluded_tests "$build_dir"
    ( cd "$build_dir" || return; ctest )
}

list() {
    declare build_dir="$build_default_dir"

    check_configured "$build_dir" || return

    ohai "List targets"

    declare -a opts
    get_build_opts opts

    echo "[1/1] All SBS targets available:"
    cmake --build "$build_dir" "${opts[@]}" --target help \
        | grep -o '^[^/]*\.bin' | cut -f 1 -d '.'
}

boards() {
    declare build_dir="$build_default_dir"

    check_configured "$build_dir" || return

    ohai "List boards"

    declare -a opts
    get_build_opts opts

    cmake --build "$build_dir" "${opts[@]}" --target help-boards
}

lint_copyright() {
    ohai "Lint (Copyright)"

    "$sbs_base/scripts/linter.py" --copyright "$source_dir/src"
}

lint_find() {
    ohai "Lint (Find)"

    "$sbs_base/scripts/linter.py" --find "$source_dir/src"
}

lint_clangtidy() {
    declare build_dir="$1"

    check_configured "$build_dir" || return

    ohai "Lint (clang-tidy)"

    defs=(--extra-arg=-D_MIOSIX=1 --extra-arg=-D_MIOSIX_GCC_PATCH_MINOR=1 \
        --extra-arg=-D_MIOSIX_GCC_PATCH_MAJOR=3 --extra-arg=-D__LINT__)
    IFS=$'\n' read -rd '' -a incs < \
        <(arm-miosix-eabi-g++ -E -Wp,-v -xc++ /dev/null 2>&1 \
            | sed -n "s/^ /--extra-arg=-isystem/p")

    declare opts=()
    [ "$to_edit" = true ] && opts+=(--fix-notes --fix-errors)

    find "$source_dir/src" \
        -type f \( -iname "*.cpp" -o -iname "*.h" -o -iname "*.c" \) \
        -exec clang-tidy --header-filter=".*" -p="$build_dir" "${defs[@]}" \
        "${incs[@]}" "${opts[@]}" {} \;
}

lint_cppcheck() {
    ohai "Lint (Cppcheck)"

    declare -a opts=()
    [ -n "$jobs" ] && opts+=("-j $jobs")

    cppcheck --language=c++ --std=c++11 --enable=all --inline-suppr \
        --suppress=unmatchedSuppression --suppress=unusedFunction \
        --suppress=missingInclude --error-exitcode=1 "${opts[@]}" \
        "$source_dir/src"
}

lint_clangformat() {
    declare to_edit="$1"

    ohai "Lint (clang-format)"

    declare -a opts=(--style=file --Werror)
    [ "$to_edit" = true ] && opts+=(-i) || opts+=(--dry-run)

    find "$source_dir/src" \
        -type f \( -iname "*.cpp" -o -iname "*.h" -o -iname "*.c" \) \
        -exec clang-format "${opts[@]}" {} \;
}

lint() {
    declare to_edit="$1"

    if [ "$found_python" = true ]; then
        lint_copyright
        lint_find
    fi

    if [ "$found_clangtidy" = true ] && [ "$lint_clangtidy" = true ]; then
        lint_clangtidy "$build_default_dir"
    fi

    if [ "$found_cppcheck" = true ]; then
        lint_cppcheck
    fi

    if [ "$found_clangformat" = true ]; then
        lint_clangformat "$to_edit"
    fi
}

set_debug() {
    config_debug=true
}

set_verbose() {
    config_verbose=true
}

set_jobs() {
    jobs="$1"
}

get_build_opts() {
    declare -n build_opts=$1
    [ -n "$jobs" ] && build_opts=("-j $jobs")
}

set_clangtidy() {
    lint_clangtidy=true
}

usage() {
    echo
    cat <<EOF
Usage: $(basename "$0") [OPTIONS]

OPTIONS:
  General Options:
    -h, --help            Show this help message and exit
    -j JOBS, --jobs JOBS  Build or lint in parallel using a specific number of jobs
    -l, --list            List all targets available
    -r, --boards          List all boards available

  Build Options:
    -b TARGET, --build TARGET
                          Build a specific target
    -f TARGET, --flash TARGET
                          Build and flash a specific target
    -t TEST, --test TEST  Build a specific test natively and run it
    -c, --clean           Clean the working tree
    -u, --configure       Force configure and do not build
    -d, --debug           Enable debug
    -v, --verbose         Print a verbose output

  Lint Options:
    -n, --lint            Lint the code
    -e, --edit            Lint and edit the code
    --clang-tidy          Lint using also clang-tidy
EOF
}

CMAKE_FILENAME="CMakeCache.txt"
CTEST_FILENAME="CTestTestfile.cmake"
DEBUG_FILENAME=".sbs_debug"
VERBOSE_FILENAME=".sbs_verbose"
BUILD_DEFAULT_DIRNAME="build"
BUILD_HOST_DIRNAME="cmake-build-host"
TTY_BLUE="\033[34m"
TTY_BOLD="\033[1m"
TTY_RESET="\033[0m"

sbs_base=
source_dir=
build_default_dir=
build_host_dir=
toolchain_file=
found_cmake=false
found_miosixgpp=false
found_ccache=false
found_ninja=false
found_python=false
found_cppcheck=false
found_clangtidy=false
found_clangformat=false
found_stflash=false
found_stlink=false
config_debug=false
config_verbose=false
config_host=false
jobs=
lint_clangtidy=false

print_banner
init_dirs

for arg in "$@"; do
    shift
    case "$arg" in
        --boards)     set -- "$@" "-r";;
        --build)      set -- "$@" "-b";;
        --clang-tidy) set_clangtidy;;
        --clean)      set -- "$@" "-c";;
        --configure)  set -- "$@" "-u";;
        --debug)      set -- "$@" "-d";;
        --edit)       set -- "$@" "-e";;
        --flash)      set -- "$@" "-f";;
        --help)       set -- "$@" "-h";;
        --jobs)       set -- "$@" "-j";;
        --lint)       set -- "$@" "-n";;
        --list)       set -- "$@" "-l";;
        --test)       set -- "$@" "-t";;
        --verbose)    set -- "$@" "-v";;
        *)            set -- "$@" "$arg"
    esac
done

while getopts b:cdef:hj:lnrt:uv opt; do
    case "$opt" in
        b) find_deps && build "$build_default_dir" "$OPTARG"; exit;;
        c) find_deps && clean_all; exit;;
        d) set_debug;;
        e) find_deps && lint true; exit;;
        f) find_deps && flash "$OPTARG"; exit;;
        h) usage; exit 0;;
        j) set_jobs "$OPTARG";;
        l) find_deps && list; exit;;
        n) find_deps && lint false; exit;;
        r) find_deps && boards; exit;;
        t) find_deps && run_tests "$OPTARG"; exit;;
        u) find_deps && configure "$build_default_dir"; exit;;
        v) set_verbose;;
        ?) usage; exit 2;;
    esac
done
shift $((OPTIND - 1))

find_deps && build_all