#!/bin/bash

SCRIPT_DIR=$(dirname $(realpath ${BASH_SOURCE[0]}))
GIT_DIR=$(git rev-parse --git-dir)
GIT_ROOT=$(git rev-parse --show-toplevel)

usage() {
    cat >&$1 <<EOF
Usage: git commit-subsystems [OPTIONS]

Ardupilot's git extension.

Create a different commit for each ardupilot's subsystem (vehicles, libraries
and folders in the project's root). The items in OPTIONS are passed down to the
original git commit command with exception of options --message and --file (and
their short versions). Boths are related to the commit message:
commit-subsystems will treat the commit message from one of those two options
as a template such that occurrences of \$subsystem are replaced with the
subsystem being currently committed.

If neither --message or --file are passed, then the first commit's message will
be used as a template for the other messages and option --edit will be used for
the next commits. To avoid that behavior use --naive.

Another custom option is --raw: don't use commit message as a template.
EOF
}

MSG_FILE="$GIT_DIR/COMMIT_SUBSYSTEM_MSG"
extra_options=

process_msg() {
    local subsystem=$1
    local prev_subsystem=$2
    if [[ -n "$option_m" || -n "$option_F" ]]; then
        if [[ $option_F == - ]]; then
            echo "Please, type the commits message template:"
            option_F="$GIT_DIR/COMMIT_SUBSYSTEM_TEMPLATE"
            cat - > "$option_F"
        fi

        if [[ -n "$option_m" ]]; then
            echo "$option_m"
        else
            cat "$option_F"
        fi | if $option_raw; then
                cat
             else
                sed "s,\$subsystem,$cur_subsystem,g"
             fi > "$MSG_FILE"


        extra_options=(-F "$MSG_FILE")
    elif [[ -n $prev_subsystem ]] && ! $option_naive; then # try to be "smart"
        cat $GIT_DIR/COMMIT_EDITMSG \
            | sed -e "/^\s*#/d" \
                  -e "s/.*\<$prev_subsystem\>.*/\0\n#\0/" \
            | sed "/^[^#]/ s/\<$prev_subsystem\>/$subsystem/g" > "$MSG_FILE"
        echo >> "$MSG_FILE"
        echo "# This commit message was adapted by commit-subsystems" >> "$MSG_FILE"
        extra_options=(-F "$MSG_FILE" --edit)
    fi
}

commit_subsystem() {
    local subsystem=$1
    local prev_subsystem=$2
    shift 2
    process_msg $subsystem $prev_subsystem
    if ! git commit ${extra_options[*]} "$@"; then
        echo "Couldn't commit subsystem $subsystem, aborting..." >&2
        exit 1
    fi
}

args=()

option_m=
option_F=
option_naive=false
option_raw=false

while [[ -n "$1" ]]; do
    opt="$1"
    case "$opt" in
    -h|--help)
        usage 1
        exit 0
        ;;
    -m|--message)
        shift
        if [[ -z "$1" ]]; then
            echo "Option $opt requires a commit message." >&2
            exit 1
        fi
        option_m="$1"
        ;;
    -F|--file)
        shift
        if [[ -z "$1" ]]; then
            echo "Option $opt requires a file name." >&2
            exit 1
        fi
        option_F="$1"
        ;;
    --naive)
        option_naive=true
        ;;
    --raw)
        option_raw=true
        ;;
    *)
        args+=("$1")
        ;;
    esac
    shift
done

if [[ -n $option_m && -n $option_F ]]; then
    echo "Options -m and -F can't be combined." >&2
    exit 1
fi

set -- "${args[@]}"

LIST=$GIT_DIR/COMMIT_SUBSYSTEMS_LIST

git diff --name-only --staged | $SCRIPT_DIR/path-libraries.sh -p > $LIST
git diff --name-only --staged | $SCRIPT_DIR/path-nonlibraries.sh -p >> $LIST

if [[ $(cat "$LIST" | wc -l) -eq 0 ]]; then
    echo "Nothing to commit." >&2
    exit 1
fi

echo "Reseting changes in order to add files separately..."
git reset >/dev/null

# head before commits - for recovery
RECOVERY_HEAD=$(git log -n 1 --format=%H)
exit_hook() {
    local last_error=$?

    set +e

    [[ -a /dev/fd/3 ]] && exec 3<&-

    [[ $last_error -eq 0 ]] && return 0

    echo
    echo "Program interrupted or finished with error(s), reseting head..." >&2
    git reset $RECOVERY_HEAD >/dev/null
    echo "Trying to re-add files..." >&2
    if [[ ! -f $LIST ]]; then
        echo "File with list of added files not found..." >&2
    else
        error=false
        cat $LIST | while read subsystem path; do
            if ! git add -- "$GIT_ROOT/$path"; then
                echo "Couldn't add \"$path\"..." >&2
                error=true
            fi
        done

        if $error; then
            echo "This is embarrassing, couldn't re-add all files. Sorry." >&2
        else
            echo "Files re-added." >&2
        fi
    fi

    return 1
}

set -e
trap "exit 1" SIGINT
trap exit_hook EXIT

echo "Adding and committing subsystems..."
exec 3< $LIST
cur_subsystem=
prev_subsystem=
empty=true
while read -u 3 subsystem path; do
    empty=false
    if [[ $cur_subsystem != $subsystem ]]; then
        if [[ -n $cur_subsystem ]]; then
            commit_subsystem "$cur_subsystem" "$prev_subsystem" "$@"
            echo
        fi
        prev_subsystem=$cur_subsystem
        cur_subsystem=$subsystem
    fi
    if ! git add -- "$GIT_ROOT/$path"; then
        echo "Couldn't add \"$path\", aborting..." >&2
        exit 1
    fi
done

# the last one
commit_subsystem "$cur_subsystem" "$prev_subsystem" "$@"
echo
