#!/bin/bash
set -e

shopt -s nullglob

PROGRAM="$0"
MODULES=(tock libtock-rs)

success() {
  echo -e "\r\e[1;32mDone:\e[m $1"
  exit 0
}

fail() {
  echo -e "\r\e[1;31mError:\e[m $1"
  exit 1
}

commit() {
  local message="$1"
  git add .
  git commit -qm"${message}"
}

get_root() {
  local module="$1"
  git ls-tree HEAD:third_party | sed -En 's/^.* ([^ ]+)\t'"${module}"'$/\1/p'
}

get_head() {
  git rev-parse HEAD
}

help() {
  local root="$(get_root)"
  cat <<EOF
Usage: ${PROGRAM} {apply|save|restore|check}

  apply       Applies the patches to the submodules regardless of their state.
              As a consequence this can always be called to get to a clean state
              but may result in data loss if there are unsaved changes.

  save        Saves the submodules to the patches.
              This should only be called after apply and when all changes have
              been added to a commit. After saving, you can run ./setup.sh to
              return to normal state. Otherwise you can continue editing the
              submodules and calling save.

  restore     Restores the submodules to its normal state regardless of their
              state. As a consequence this can always be called to get to a
              clean state but may result in data loss if there are unsaved
              changes.

  check       Checks whether the submodules and the patches are in sync. This
              may fail in two cases:
              - when the patches were updated but not restored/applied, and
              - when the submodules have been modified but not saved.

Example:

  1. Enter the edit state from the normal state:

    ${PROGRAM} apply

  2. Edit files in the submodules:

    cd third_party/<submodule>
    edit <files>

  3. For each edited submodule, create a fix commit per affected patch by
  repeating the following commands until there are no more files to add:

    git add -p
    git commit -m'fix <patch#>'

  4. For each edited submodule, merge the fixes into their patches by moving
  their line below their patch and changing their "edit" into "fixup":

    git rebase -i ${root}

  5. Save the changes:

    cd ../..
    ${PROGRAM} save

  6. Either continue repeating steps 2 to 5, or return to the normal state:

    ${PROGRAM} restore
EOF
  exit 0
}

apply_module() {
  local root="$1" module="$2" file
  git checkout -q "${root}"
  if [[ "${module}" == tock ]]; then
    cp -a ../../boards .
    commit '00-boards'
  fi
  for file in ../../patches/"${module}"/*; do
    git apply "${file}"
    commit "$(basename "${file}" .patch)"
    echo -n .
  done
}

apply() {
  for module in "${MODULES[@]}"; do
    local root="$(get_root "${module}")"
    ( set -e
      cd third_party/"${module}"
      git reset -q --hard
      git clean -qfxd
      apply_module "${root}" "${module}"
    )
  done
}

save() {
  for module in "${MODULES[@]}"; do
    local root="$(get_root "${module}")"
    ( set -e
      cd third_party/"${module}"
      [[ -z "$(git status -s)" ]] \
        || fail "The ${module} submodule is not clean."
      rm -rf ../../patches/"${module}"
      mkdir ../../patches/"${module}"
      for file in $(git format-patch "${root}"); do
        sed -n '/^diff/,$p' "${file}" \
          | sed '/^-- $/,$d' > "../../patches/${module}/${file#*-}"
      done
      git clean -qfxd
      local top="$(get_head)"
      git checkout -q "${root}"
      if [[ "${module}" == tock ]]; then
        rm -r boards
        git apply --whitespace=nowarn ../../patches/"${module}"/00-boards.patch
        rm ../../patches/tock/00-boards.patch
        rm -r ../../boards
        cp -a boards ../..
      fi
      git reset -q --hard
      git clean -qfxd
      git checkout -q "${top}"
    )
  done
}

check() {
  # Overwrite the commit function to do nothing.
  commit() { true; }
  for module in "${MODULES[@]}"; do
    local root="$(get_root "${module}")"
    ( set -e
      cd third_party/"${module}"
      git commit --allow-empty -qmi
      git add .
      git commit --allow-empty -qmx
      local top="$(get_head)"
      apply_module "${root}" "${module}"
      git add .
      git commit --allow-empty -qmy
      # We need to cleanup (and not exit) regardless of a diff.
      local r; if git diff "${top}" --quiet; then r=0; else r=1; fi
      git checkout -q "${top}"
      git reset -q HEAD~
      git reset -q --soft HEAD~
      [[ "${r}" -eq 0 ]] \
        || fail "The ${module} submodule differs from its patches."
    )
  done
}

grep -q third_party/tock .gitmodules 2>/dev/null \
  || fail 'Not running from OpenSK directory.'
[[ $# -eq 1 ]] || help
case $1 in
  apply)
    apply
    success 'Applied the patches to the submodules.'
    ;;
  save)
    save
    success 'Saved the submodules to the patches.'
    ;;
  restore)
    # Overwrite the commit function to do nothing.
    commit() { true; }
    apply
    success 'Restored the submodules.'
    ;;
  check)
    check
    success 'The submodules and patches are in sync.'
    ;;
  *) fail 'Unexpected argument. Run without argument for help.' ;;
esac
