#!/usr/bin/env bash

# Copyright (c) 2024 Skyward Experimental Rocketry
# Author: Davide Basso
#
# 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.

################################################################################
##############################      Constants     ##############################
################################################################################

# Terminal colors
TTY_RESET="\033[0m"
TTY_BOLD="\033[1m"
TTY_FOUND="\033[35m"
TTY_SUCCESS="\033[32m"
TTY_ERROR="\033[31m"
TTY_STEP="\033[94m"

TTY_LOGO_1="\033[38;5;200m"
TTY_LOGO_2="\033[38;5;164m"
TTY_LOGO_3="\033[38;5;93m"
TTY_LOGO_4="\033[38;5;57m"
TTY_LOGO_5="\033[38;5;27m"

# Error message
ERR="\n"$TTY_ERROR""$TTY_BOLD"ERROR"$TTY_RESET""

# Error codes
EPERM=1   # Operation not permitted
ENOENT=2  # No such file or directory
ENOEXEC=8 # Exec format error
ENODEV=19 # No such device
EINVAL=22 # Invalid argument
ENOPKG=65 # Package not installed

# Filenames/Dirnames
CMAKE_FILENAME="CMakeCache.txt"
CTEST_FILENAME="CTestTestfile.cmake"
DEBUG_FILENAME=".sbs_debug"
VERBOSE_FILENAME=".sbs_verbose"
BUILD_DEFAULT_DIRNAME="build"
BUILD_HOST_DIRNAME="build-host"

# Logical cores count, cross platform
N_PROC=$(
    nproc 2>/dev/null ||                   # Linux
    sysctl -n hw.logicalcpu 2>/dev/null || # macOS 
    getconf _NPROCESSORS_ONLN 2>/dev/null  # POSIX
)

################################################################################
##############################    Global States   ##############################
################################################################################

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

################################################################################
##############################       Banner       ##############################
################################################################################

banner="
$TTY_LOGO_1              _____ ____ _____
$TTY_LOGO_2 ___________ / ___// __ ) ___/  ________________________________
$TTY_LOGO_3 __________  \__ \/ __  \__ \  _________________________________
$TTY_LOGO_4 _________  ___/ / /_/ /__/ / ____________________________v4.0__
$TTY_LOGO_5           /____/_____/____/
$TTY_RESET
"

print_configuration() {
	step "Dependencies"

	yf=""$TTY_SUCCESS""$TTY_BOLD"Yes"$TTY_RESET""
	nf=""$TTY_ERROR""$TTY_BOLD"No"$TTY_RESET""

	printf "cmake:               "; [ "$found_cmake" = true       ] && printf "$yf\n" || printf "$nf\n"
	printf "arm-miosix-eabi-g++: "; [ "$found_miosixgpp" = true   ] && printf "$yf\n" || printf "$nf\n"
	printf "ccache:              "; [ "$found_ccache" = true      ] && printf "$yf\n" || printf "$nf\n"
	printf "ninja:               "; [ "$found_ninja" = true       ] && printf "$yf\n" || printf "$nf\n"
	printf "python:              "; [ "$found_python" = true      ] && printf "$yf\n" || printf "$nf\n"
	printf "cppcheck:            "; [ "$found_cppcheck" = true    ] && printf "$yf\n" || printf "$nf\n"
	printf "clang-tidy:          "; [ "$found_clangtidy" = true   ] && printf "$yf\n" || printf "$nf\n"
	printf "clang-format:        "; [ "$found_clangformat" = true ] && printf "$yf\n" || printf "$nf\n"

	printf "flasher:             ";
	if [ "$found_stflash" = true ]; then
		printf ""$TTY_FOUND""$TTY_BOLD"st-flash"$TTY_RESET"\n"
	elif [ "$found_stlink" = true ]; then
		printf ""$TTY_FOUND""$TTY_BOLD"st-link"$TTY_RESET"\n"
	else
		printf "$nf\n"
	fi
}

check_build() {
    declare host_build=$1

    [ "$found_cmake" = true ] || { printf ""$ERR": CMake must be installed\n"; return $ENOPKG; }

    if [ "$host_build" = false ]; then
        [ "$found_miosixgpp" = true ] || { printf ""$ERR": arm-miosix-eabi-g++ must be installed\n"; return $ENOPKG; }
    fi
}

################################################################################
##############################         CLI        ##############################
################################################################################

# Filter all the arguments that are flags
# The syntax is: filter_flags "arg1" "arg2" ... "argN"
# Returns a list of flags in the form "--flag=value" or "--flag"
filter_flags() {
    flags=()
    prev_is_flag=false

    for arg in "$@"; do
        if [[ "$arg" == -* ]]; then
            flags+=("$arg")
            prev_is_flag=true
        else
            if [ "$prev_is_flag" = true ]; then
                # if the previous argument was a flag, then this is a flag value
                flags[-1]="${flags[-1]}=$arg"
            fi

            prev_is_flag=false
        fi
    done

    echo "${flags[@]}"
}


# Filter all the arguments that are not flags
filter_args() {
    args=()
    prev_is_flag=false

    for arg in "$@"; do
        if [[ "$arg" == -* ]]; then
            prev_is_flag=true
        elif [ "$prev_is_flag" = true ]; then
            prev_is_flag=false
        else
            args+=("$arg")
        fi
    done

    echo "${args[@]}"
}

# Checks that no unknown flags are passed
# The syntax is: cmd_flags "known_flags" "flags"
cmd_flags() {
    known_flags=("${!1}")
    flags=("${!2}")

    for flag in "${flags[@]}"; do
        # ignore ''
        if [ -z "$flag" ]; then
            continue
        fi

        found=false
        for known_flag in "${known_flags[@]}"; do
            # if the know_flag is of type --flag=, allow flags with value
            if [[ "$known_flag" == *= ]]; then
                if [[ "$flag" == "$known_flag"* ]]; then
                    found=true
                    break
                fi
            else
                if [ "$flag" == "$known_flag" ]; then
                    found=true
                    break
                fi
            fi
        done

        if [ "$found" = false ]; then
            printf ""$ERR": Unknown flag $flag\n"
            return $EINVAL
        fi
    done
}

# Checks that no more than a certain number of arguments are passed
# The syntax is: cmd_args "max_args" "arg1" "arg2" ... "argN"
cmd_args() {
    max_args=$1
    shift
    args=("${!1}")
    if [ "${#args[@]}" -gt "$max_args" ]; then
        printf ""$ERR": Too many arguments\n"
        return $EINVAL
    fi
}

# Checks if a flag exists in the list of flags
# The syntax is: flag_exists "flag" "flag1" "flag2" ... "flagN"
flag_exists() {
    flag=$1
    shift
    flags=("${!1}")

    if [[ " ${flags[@]} " =~ " ${flag} " ]]; then
        echo "true"
    else
        echo "false"
    fi
}

# Get the value of a flag
# The syntax is: get_flag_value "possible_flags" "flags"
get_flag_value() {
    possible_flags=("${!1}")
    flags=("${!2}")

    for flag in "${flags[@]}"; do
        for possible_flag in "${possible_flags[@]}"; do
            if [[ "$flag" == "$possible_flag"* ]]; then
                # strip the value from the flag
                echo "${flag#*=}"
                return
            fi
        done
    done
}

# Print a step message
step() {
	printf "\n"$TTY_STEP""$TTY_BOLD"$1"$TTY_RESET"\n"
	# Print - for every letter in the pre string
	for (( i=0; i<${#1}; i++ )); do printf -- "-"; done;
	printf "\n";
}

################################################################################
##############################      Internals     ##############################
################################################################################

# Find all the dependencies
find_deps() {
    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
}

# Initialize all the directories
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/cmake/toolchain.cmake"
}

# Get the build options for cmake
# The syntax is: get_build_opts "opts" "jobs"
get_build_opts() {
    build_opts=()
    jobs=$2

    if [ -n "$jobs" ]; then
        echo "- Building with $jobs jobs"
        build_opts+=("-j $jobs")
    fi
}

# workaround: disable tests in excluded subdirectories
# see: https://gitlab.kitware.com/cmake/cmake/-/issues/20212
cmake_disable_excluded_tests() {
    build_dir="$build_host_dir"

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

################################################################################
##############################     Subcommands    ##############################
################################################################################

# Check if the project is configured
# The syntax is: check_configured "config_debug" "config_verbose" "config_host"
check_configured() {
    build_dir="$1"
    config_debug="$2"
    config_verbose="$3"
    config_host="$4"

    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
        step "Configure"

        printf "Reconfiguring project with the following options:\n"
        printf "  - Debug:   %s\n" "$config_debug"
        printf "  - Verbose: %s\n" "$config_verbose"
        printf "  - Host:    %s\n" "$config_host"

        [ -f "$toolchain_file" ] || { printf ""$ERR": CMake Toolchain File for Miosix was not found\n"; return $ENOPKG1; }

        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"
    fi
}

build_impl() {
    target="$1"
    build_dir="$2"
    config_debug="$3"
    config_verbose="$4"
    config_host="$5"

    check_build "$config_host" || return

    check_configured "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return

    step "Build"

    declare opts
    get_build_opts opts $jobs

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

flash_impl() {
    target="$1"
    build_dir="$2"
    reset="$3"

    # check if the target is flashable
    [ -f "$build_dir/$target.bin" ] || { printf ""$ERR": target '$target' is not flashable"; return $ENOEXEC; }

    # flash the target
    step "Flash"

    declare -a flash_opts
    [ "$reset" = true ] && flash_opts+=("--connect-under-reset")

    if [ "$found_stflash" = true ]; then
        st-flash --reset "${flash_opts[@]}" write "$build_dir/$target.bin" 0x8000000
    elif [ "$found_stlink" = true ]; then
        ST-LINK_CLI.exe -P "$build_dir/$target.bin" 0x8000000 -V -Rst
    else
        printf ""$ERR": No flashing software found!\n"
        return $ENOPKG
    fi
}

lint_copyright() {
    step "Lint copyright"

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

lint_find() {
    step "Lint find"

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

lint_clangtidy() {
    build_dir="$1"

    step "Lint clang-tidy"

    config_debug=false
    config_verbose=false

    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() {
    step "Lint cppcheck"

    echo "Running cppcheck..."

    cppcheck --language=c++ --std=c++14 --enable=all --inline-suppr \
        --suppress=unmatchedSuppression --suppress=unusedFunction \
        --suppress=missingInclude --error-exitcode=1 -q \
        "$source_dir/src"
}

lint_clangformat() {
    step "Lint clang-format"

    echo "Running clang-format..."

    opts=(--style=file --Werror --dry-run)

    # find all the source files
    declare files=$(find "$source_dir/src" \
    -type f \( -iname "*.cpp" -o -iname "*.h" -o -iname "*.c" \))

    # count files and spread them evenly across cores: files / nproc + 1
    declare files_per_proc=$(
        echo "$files" | wc -l | \
        xargs -I {} bash -c "expr \( {} / $N_PROC \) + 1"
    )

    echo "$files" | xargs -n $files_per_proc -P 0 clang-format "${opts[@]}"
}

################################################################################
##############################   Native Commands  ##############################
################################################################################

build() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    jobs_flag=("-j" "--jobs")
    known_flags=("-d" "-v" "-j=" "--debug" "--verbose" "--jobs=")

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 1 args[@] || return

    jobs=$(get_flag_value jobs_flag[@] flags[@])

    [ "$(flag_exists "-d" flags[@])" = true ] || [ "$(flag_exists "--debug"   flags[@])" = true ] && config_debug=true   || config_debug=false
    [ "$(flag_exists "-v" flags[@])" = true ] || [ "$(flag_exists "--verbose" flags[@])" = true ] && config_verbose=true || config_verbose=false
    config_host=false

    build_dir="$build_default_dir"

    target=${args[0]:-all}

    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host"
}

clean() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    known_flags=()

    step "Clean"
    cmd_flags known_flags[@] flags[@] || return
    cmd_args 0 args[@] || return

    echo "Cleaning build directories..."
    rm -rf "$build_default_dir"
    rm -rf "$build_host_dir"
}

flash() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    jobs_flag=("-j" "--jobs")
    known_flags=("-d" "-v" "-r" "-j=" "--debug" "--verbose" "--reset" "--jobs=")

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 1 args[@] || return

    build_dir="$build_default_dir"

    jobs=$(get_flag_value jobs_flag[@] flags[@])

    [ "$(flag_exists "-d" flags[@])" = true ] || [ "$(flag_exists "--debug"   flags[@])" = true ] && config_debug=true   || config_debug=false
    [ "$(flag_exists "-v" flags[@])" = true ] || [ "$(flag_exists "--verbose" flags[@])" = true ] && config_verbose=true || config_verbose=false
    [ "$(flag_exists "-r" flags[@])" = true ] || [ "$(flag_exists "--reset"   flags[@])" = true ] && reset=true          || reset=false
    config_host=false

    target=${args[0]}

    if [ -z "$target" ]; then
        printf ""$ERR": No target specified\n"
        return $ENOENT
    fi

    # build the target
    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return

    # flash the target
    flash_impl "$target" "$build_dir" "$reset"
}

run() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    jobs_flag=("-j" "--jobs")
    device_flag=("-D" "--device")
    baudrate_flag=("-B" "--baudrate")
    known_flags=("-d" "-v" "-r" "-D=" "-B=" "-j=" "--debug" "--verbose" "--reset" "--device=" "--baudrate=" "--jobs=")

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 1 args[@] || return

    build_dir="$build_default_dir"

    jobs=$(get_flag_value jobs_flag[@] flags[@])

    [ "$(flag_exists "-d" flags[@])" = true ] || [ "$(flag_exists "--debug"   flags[@])" = true ] && config_debug=true   || config_debug=false
    [ "$(flag_exists "-v" flags[@])" = true ] || [ "$(flag_exists "--verbose" flags[@])" = true ] && config_verbose=true || config_verbose=false
    [ "$(flag_exists "-r" flags[@])" = true ] || [ "$(flag_exists "--reset"   flags[@])" = true ] && reset=true          || reset=false
    config_host=false

    target=${args[0]}

    if [ -z "$target" ]; then
        printf ""$ERR": No target specified\n"
        return $ENOENT
    fi

    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return

    flash_impl "$target" "$build_dir" "$reset" || return

    device=$(get_flag_value device_flag[@] flags[@])
    baudrate=$(get_flag_value baudrate_flag[@] flags[@])

    # If no device was specified, try to find it based on what we commonly use
    if [ -z "$device" ]; then
        if [ -e "/dev/ttyACM0" ]; then
            device="/dev/ttyACM0"
        elif [ -e "/dev/ttyUSB0" ]; then
            device="/dev/ttyUSB0"
        else
            printf ""$ERR": No device specified and no default device found\n"
            return $ENODEV
        fi
    fi

    # Set Skyward's default baudrate, if none was specified
    if [ -z "$baudrate" ]; then
        baudrate="115200"
    fi

    step "Run - $device @ $baudrate"

    # tty devices use \r\n for newlines by default
    # disable newlines on \r with `-icrnl` to avoid double newlines
    stty --file $device $baudrate -icrnl || return

    # connect to the device
    cat $device
}

test() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    jobs_flag=("-j" "--jobs")
    known_flags=("-d" "-v" "-j=" "--debug" "--verbose" "--jobs=")

    cmd_flags know_flags[@] flags[@] || return
    cmd_args 1 args[@] || return

    build_dir="$build_host_dir"

    jobs=$(get_flag_value jobs_flag[@] flags[@])

    [ "$(flag_exists "-d" flags[@])" = true ] || [ "$(flag_exists "--debug"   flags[@])" = true ] && config_debug=true   || config_debug=false
    [ "$(flag_exists "-v" flags[@])" = true ] || [ "$(flag_exists "--verbose" flags[@])" = true ] && config_verbose=true || config_verbose=false
    config_host=true

    target=${args[0]:-catch-tests-boardcore}

    if [ -z "$target" ]; then
        printf ""$ERR": No target specified\n"
        return $ENOENT
    fi

    # build the target
    build_impl "$target" "$build_dir" "$config_debug" "$config_verbose" "$config_host" || return

    # run the tests
    step "Test"

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

list() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    known_flags=()

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 1 args[@] || return

    config_debug=false
    config_verbose=false
    config_host=false
    build_dir="$build_default_dir"

    list_type=${args[0]}

    if [ -z "$list_type" ]; then
        printf ""$ERR": No list type specified\n"
        return $EINVAL
    fi

    check_build "$config_host" || return

    check_configured "$build_dir" "$config_debug" "$config_verbose" "$config_host" &> /dev/null || return

    if [ "$list_type" = "targets" ]; then
        cmake --build "$build_dir" --target help \
            | grep -o '^[^/]*\.bin' | cut -f 1 -d '.'
    elif [ "$list_type" = "boards" ]; then
        cmake --build "$build_dir" --target help | awk -F '[-:]' '/^boardcore/ {print $2}'
    else
        printf ""$ERR": Unknown list type $list_type\n"
        return $EINVAL
    fi
}

lint() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    known_flags=()

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 0 args[@] || return

    declare ret=0

    if [ "$found_python" = true ]; then
        lint_copyright
        [ $? -ne 0 ] && ret=-1
        lint_find
        [ $? -ne 0 ] && ret=-1
    else
        echo "Python not found, skipping copyright and find..."
    fi

    # Disable clang-tidy for now, as too many false positives
    # if [ "$found_clangtidy" = true ]; then
    #     lint_clangtidy "$build_default_dir"
    # else
    #     echo "Clang-tidy not found, skipping clang-tidy..."
    # fi

    if [ "$found_cppcheck" = true ]; then
        lint_cppcheck
        [ $? -ne 0 ] && ret=-1
    else
        echo "Cppcheck not found, skipping cppcheck..."
    fi

    if [ "$found_clangformat" = true ]; then
        lint_clangformat
        [ $? -ne 0 ] && ret=-1
    else
        echo "Clang-format not found, skipping clang-format..."
    fi

    return $ret
}

format() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    known_flags=()

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 0 args[@] || return

    if [ "$found_clangformat" = false ]; then
        printf ""$ERR": clang-format must be installed\n"
        return $ENOPKG
    fi

    step "Format"

    echo "Running clang-format..."

    opts=(--style=file --Werror -i)

    # find all the source files
    declare files=$(find "$source_dir/src" \
    -type f \( -iname "*.cpp" -o -iname "*.h" -o -iname "*.c" \))

    # count files and spread them evenly across cores: files / nproc + 1
    declare files_per_proc=$(
        echo "$files" | wc -l | \
        xargs -I {} bash -c "expr \( {} / $N_PROC \) + 1"
    )

    echo "$files" | xargs -n $files_per_proc -P 0 clang-format "${opts[@]}"
}

help() {
    echo "Usage: sbs (command) <args> [options] "
    echo ""
    echo "Commands:"
    echo " build     - Build the specified target. If none is specified, all targets are built"
    echo "           -> <target=all> [-d | --debug] [-v | --verbose] [-j | --jobs=<jobs>]"
    echo " flash     - Build and flash the specified target"
    echo "           -> <target> [-d | --debug] [-v | --verbose] [-r | --reset] [-j | --jobs=<jobs>]"
    echo " run       - Build and flash the specified target and connect to serial device"
    echo "           -> <target> [-d | --debug] [-v | --verbose] [-r | --reset] [-j | --jobs=<jobs>]"
    echo "                       [-D | --device=<device>] [-B | --baudrate=<baudrate>]"
    echo " test      - Run the specified test. If none is specified, catch tests are run"
    echo "           -> <target> [-d | --debug] [-v | --verbose] [-j | --jobs=<jobs>]"
    echo " list      - List the available targets or boards"
    echo "           -> <targets | boards>"
    echo " clean     - Clean the build directory"
    echo " lint      - Lint the source code"
    echo " format    - Format the source code"
    echo " install   - Install autocomplete"
    echo " uninstall - Uninstall autocomplete"
    echo ""
    echo "Options:"
    echo " -d, --debug:    Build in debug mode"
    echo " -v, --verbose:  Build in verbose mode"
    echo " -r, --reset:    Reset the target before flashing"
    echo " -j, --jobs:     Number of jobs to run in parallel"
    echo " -D, --device:   Serial device to connect to (default: /dev/ttyACM0 or /dev/ttyUSB0)"
    echo " -B, --baudrate: Baudrate for the serial device (default: 115200)"
    echo ""
}

################################################################################
##############################   Python Commands  ##############################
################################################################################


install() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    known_flags=()

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 0 args[@] || return

    step "Install autocomplete"

    if [ "$found_python" = false ]; then
        printf ""$ERR": Python is required to install autocomplete\n"
        return $ENOPKG
    fi

    echo "Retrieving targets..."

    targets=$(list "targets")
    declare ret=$?
    [ $ret -ne 0 ] && printf "$targets\n" && return $ret

    # split the targets on newlines
    IFS=$'\n' read -rd '' -a targets <<< "$targets"

    echo "Found ${#targets[@]} targets"

    echo "Installing completion files..."
    sudo python "$sbs_base/scripts/autocomplete.py" "--install" "${targets[@]}"
}

uninstall() {
    IFS=' ' read -r -a flags <<< "$(filter_flags "$@")"
    IFS=' ' read -r -a args <<< "$(filter_args "$@")"

    known_flags=()

    cmd_flags known_flags[@] flags[@] || return
    cmd_args 0 args[@] || return

    step "Uninstall autocomplete"

    if [ "$found_python" = false ]; then
        printf ""$ERR": Python is required to uninstall autocomplete\n"
        return $ENOPKG
    fi

    echo "Uninstalling completion files..."
    sudo python "$sbs_base/scripts/autocomplete.py" "--uninstall"
}

################################################################################
##############################        Main        ##############################
################################################################################

# exit immediately if no argument was provided
if [ "$#" -eq 0 ]; then
    help
    exit $EINVAL
fi

welcome() {
    printf "$banner"
    print_configuration
}

# populate global variables for all commands
find_deps
init_dirs

for arg in "$@"; do
    case $arg in
        build)     welcome; build "${@:2}"; exit ;;
        clean)     welcome; clean "${@:2}"; exit ;;
        flash)     welcome; flash "${@:2}"; exit ;;
        run)       welcome; run   "${@:2}"; exit ;;
        test)      welcome; test  "${@:2}"; exit ;;
        lint)      welcome; lint  "${@:2}"; exit ;;
        format)    welcome; format; exit ;;
        install)   welcome; install; exit ;;
        uninstall) welcome; uninstall; exit ;;
        list)      list  "${@:2}"; exit ;;
        *)         help; exit $EINVAL;;
    esac
done