#!/usr/bin/env bash

GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)

# awk script that takes 2 input files. The first one is newline deilmeted
# string of patterns to look for, second one is the file to remove them from.
# outfile has to be passed with -v upon call and is the file result is
# written to.
read -r -d '' AWK_SCRIPT <<'EOF'
# process the first "file" (newline split argument list)
NR==FNR { a[b++]=$0; next; }
# process file
{
   found=0;
   for (i=0; i != b; i++) {
      if (a[i] == $0) { found=1; }
   }
   if (found == 1) { print "... removing '"$0"'"; }
   else { print $0 > outfile }
}
EOF



show_contents() {
  local file="${2/#~/$HOME}"
  if [ -f "$file" ]; then
    echo "$1 gitignore: $2" && cat "$file"
  else
    echo "There is no $1 .gitignore yet"
  fi
}

cd_to_git_root() {
  local error_level="$1"

  if ! git rev-parse --git-dir &>/dev/null; then
    if [ "$error_level" = '--warn' ]; then
      echo "Warning: Not currently in a Git repository" >&2
    elif [ "$error_level" = '--error' ]; then
      echo "Error: Not currently in a Git repository" >&2
      exit 1
    fi
  fi

  local result=
  if result=$(git rev-parse --show-toplevel 2>/dev/null); then
    cd "$result" || exit
  fi
}

global_ignore() {
  if ! git config --global core.excludesFile 2>/dev/null; then
    if [ -f "$HOME/.gitignore" ]; then
      echo "$HOME/.gitignore"
    else
      echo "${XDG_CONFIG_HOME:-$HOME/.config}/git/ignore"
    fi
  fi
}

show_global() {
  show_contents Global "$(global_ignore)"
}

add_global() {
  local global_gitignore
  global_gitignore="$(global_ignore)"
  if [ -z "$global_gitignore" ]; then
    echo "Can't find global .gitignore."
    echo ""
    echo "Use 'git config --global --add core.excludesfile ~/.gitignore-global' to set the path to your global gitignore file to '~/.gitignore-global'."
    echo ""
  else
    add_patterns "$global_gitignore" "$@"
  fi
}

show_local() {
  cd_to_git_root --warn
  show_contents Local .gitignore
}

add_local() {
  cd_to_git_root --warn
  add_patterns .gitignore "$@"
}

show_private() {
  cd_to_git_root --error
  show_contents Private "${GIT_DIR}/info/exclude"
}

add_private() {
  cd_to_git_root --error
  test -d "${GIT_DIR}/info" || mkdir -p "${GIT_DIR}/info"
  add_patterns "${GIT_DIR}/info/exclude" "$@"
}

add_patterns() {
  echo "Adding pattern(s) to: $1"
  local file="${1/#~/$HOME}"
  dir_name=$(dirname "$file")
  if [ ! -d "$dir_name" ]; then
      mkdir -p "$dir_name"
  fi
  if [ -s "$file" ]; then
      # If the content of $file doesn't end with a newline, add one
      test "$(tail -c 1 "$file")" != "" && echo "" >> "$file"
  fi
  for pattern in "${@:2}"; do
    echo "... adding '$pattern'"
    (test -f "$file" && test "$pattern" && grep -q -F -x -- "$pattern" "$file") || echo "$pattern" >> "$file"
  done
}

edit_file() {
  local file="${2/#~/$HOME}"
  if [ -f "$file" ]; then
    echo "Editing $1 gitignore: ($2)" && eval "$(git var GIT_EDITOR) $(printf '%q' "$file")"
    echo "Done."
  else
    echo "There is no $1 .gitignore yet." >&2
    exit 1
  fi
}

_usage() {
  cat << EOF
Usage: git ignore [OPTION]... [PATTERN]...
Modify or display local global or private gitignore files.

OPTIONs set context (local, global, private) and action (display, add,
remove, edit). In case of collision, last flag overwrites preceding ones.
Defaults are: local context, action is display (see man git ignore for
more details).
Context examples:
local:   .../repo/.gitignore
private: .../repo/.git/info/exclude
global:  \$HOME/.gitignore

PATTERNs are the gitignore patterns as specified in git documentation.

Options:
  -l, --local      Set context to local gitignore
  -g, --global     Set context to global gitignore
  -p, --private    Set context to private gitignore
  -e, --edit       Set action to edit
  -r, --remove     Set action to remove
  -h, --help       Print this message
  --               End of options delimiter
EOF
}

remove_patterns() {
  declare -a args
  local tmpfile
  args=( "${@:3}" )
  local file="${2/#~/$HOME}"
  if [ -f "$file" ]; then
    tmpfile="$(mktemp -q "${TMPDIR:-/tmp}"/git-extras-ignore.XXXXXX 2>/dev/null)"
    echo "Removing patterns from $1 gitignore: $2"
    awk -v outfile="$tmpfile" "${AWK_SCRIPT}"\
       <(printf '%s\n' "${args[@]}") "$file"
    cat "$tmpfile" > "$file"
    rm "$tmpfile"
  else
    echo "There is no $1 .gitignore yet"
    exit 1
  fi
}


# can have only 1 action and 1 level, if conflicting flags are passed
# latter ones overwrite former
# sanity of args not tested (e.g. there should be no args if -e is supplied)
if test $# -eq 0; then
   show_global
   echo "---------------------------------"
   show_local
   echo "---------------------------------"
   show_private
else
  # parse flags and args
  level="none"
  action="none"
  declare -a args
  while [ $# -gt 0 ]; do
    case "$1" in
      -h|--help)
        _usage
        exit 0
        ;;
      -l|--local)
        level="local"
        ;;
      -g|--global)
        level="global"
        ;;
      -p|--private)
        level="private"
        ;;
      -e|--edit)
        action="edit"
        ;;
      -r|--remove)
        action="remove"
        ;;
      --)
        shift
        args+=("$@")
        break
        ;;
      *)
        args+=("$1")
        ;;
    esac
    shift
  done
  # compatible with previous version
  if [ "$action" = "none" ]; then
    case "$level" in
      local)
        test ${#args[@]} -ne 0 && add_local "${args[@]}" && echo
        show_local
        ;;
      global)
        test ${#args[@]} -ne 0 && add_global "${args[@]}" && echo
        show_global
        ;;
      private)
        test ${#args[@]} -ne 0 && add_private "${args[@]}" && echo
        show_private
        ;;
      none)
        add_local "${args[@]}"
        # maybe show_local here too?
        ;;
    esac
    exit 0
  # open file in editor
  elif [ "$action" = "edit" ]; then
    case "$level" in
      local|none)
        cd_to_git_root --warn
        edit_file 'local' .gitignore && echo
        ;;
      global)
        global_gitignore="$(global_ignore)"
        edit_file global "$global_gitignore" && echo
        ;;
      private)
        cd_to_git_root --error
        test -d "${GIT_DIR}/info" || mkdir -p "${GIT_DIR}/info"
        edit_file private "${GIT_DIR}/info/exclude" && echo
        ;;
    esac
    exit 0
  # remove entries
  else
    if [ ${#args[@]} -eq 0 ]; then
      echo "Nothing to remove."
      exit 0
    fi
    case "$level" in
      local|none)
        cd_to_git_root --warn
        remove_patterns 'local' .gitignore "${args[@]}" && echo
        show_local
        ;;
      global)
        global_gitignore="$(global_ignore)"
        remove_patterns global "$global_gitignore" "${args[@]}" && echo
        show_global
        ;;
      private)
        cd_to_git_root --error
        remove_patterns private "${GIT_DIR}/info/exclude" "${args[@]}" && echo
        show_private
        ;;
    esac
    exit 0

  fi
fi
