# commacd - a faster way to move around (Bash 3+).
# https://github.com/shyiko/commacd
#
# ENV variables that can be used to control commacd:
#   COMMACD_CD - function to change the directory (by default commacd uses builtin cd and pwd)
#   COMMACD_NOTTY - set it to "on" when you want to suppress user input (= print multiple matches and exit)
#   COMMACD_NOFUZZYFALLBACK - set it to "on" if you don't want commacd to use "fuzzy matching" as a fallback for
#     "no matches by prefix" (introduced in 0.2.0)
#   COMMACD_SEQSTART - set it to 1 if you want "multiple choices" to start from 1 instead of 0
#
# @version 0.3.1
# @author Stanley Shyiko <stanley.shyiko@gmail.com>
# @license MIT

if [ -z "${COMMACD_CD}" ]; then
    [ -f "$(command -v "wcd")" ] && COMMACD_CD='. wcd'
fi

# turn on case-insensitive search by default
shopt -s nocaseglob

#shellcheck disable=SC2001
_commacd_split()  { printf "%s\\n" "$1" | sed $'s|/|\\\n/|g';   }
_commacd_join()   { local IFS="$1"; shift; printf "%s\\n" "$*"; }
_commacd_expand() ( shopt -s extglob nullglob; local ex=($1); printf "%s\n" "${ex[@]}"; )

_command_cd() {
  local dir=$1
  if [ -z "$COMMACD_CD" ]; then
    builtin cd "$dir" && pwd
  else
    $COMMACD_CD "$dir"
  fi
}

# show match selection menu
_commacd_choose_match() {
  local matches=("$@")
  for i in "${!matches[@]}"; do
    printf "%s\t%s\n" "$((i+${COMMACD_SEQSTART:-0}))" "${matches[$i]}" >&2
  done
  local selection;
  read -e -p ': ' selection >&2
  if [ -n "$selection" ]; then
    printf "%s" "${matches[$((selection-${COMMACD_SEQSTART:-0}))]}"
  else
    printf "%s" "$PWD"
  fi
}

_commacd_prefix_glob() (
  set -f
  local path="${*%/}/" IFS=$'\n'
  # shellcheck disable=SC2046
  printf "%s" "$(_commacd_join \* $(_commacd_split "$path"))"
)

_commacd_glob() (
  set -f
  local path="${*%/}" IFS=$'\n'
  if [ "${path/\/}" = "$path" ]; then
    path="*$path*/"
  else
    # shellcheck disable=SC2046
    path="$(_commacd_join \* $(_commacd_split "$path") | rev | sed 's/\//*\//' | rev)*/"
  fi
  printf "%s\\n" "$path"
)

_commacd_forward_by_prefix() {
  local matches=($(_commacd_expand "$(_commacd_prefix_glob "$*")"))
  if [ -z "$COMMACD_NOFUZZYFALLBACK" ] && [ ${#matches[@]} -eq 0 ]; then
    matches=($(_commacd_expand "$(_commacd_glob "$*")"))
  fi
  case ${#matches[@]} in
    0) printf "%s" "$PWD";;
    *) printf "%s\n" "${matches[@]}"
  esac
}

# jump forward (`,`)
_commacd_forward() {
  if [ -z "$*" ]; then return 1; fi
  OLDIFS="${IFS}"
  local IFS=$'\n'
  local dir=($(_commacd_forward_by_prefix "$@"))
  if [ "$COMMACD_NOTTY" = "on" ]; then
    printf "%s\n" "${dir[@]}"
    return
  fi
  if [ ${#dir[@]} -gt 1 ]; then
    dir=$(_commacd_choose_match "${dir[@]}")
  fi
  IFS="${OLDIFS}"
  _command_cd "$dir"
}

# search backward for the vcs root (`,,`)
_commacd_backward_vcs_root() {
  local dir="${PWD%/*}"
  while [ ! -d "$dir/.git" ] && [ ! -d "$dir/.hg" ] && [ ! -d "$dir/.svn" ]; do
    dir="${dir%/*}"
    if [ -z "$dir" ]; then
      printf "%s" "$PWD"
      return
    fi
  done
  printf "%s" "$dir"
}

# search backward for the directory whose name begins with $1 (`,, $1`)
_commacd_backward_by_prefix() {
  local prev_dir dir="${PWD%/*}" matches match IFS=$'\n'
  while [ -n "$dir" ]; do
    prev_dir="$dir"
    dir="${dir%/*}"
    matches=($(_commacd_expand "$dir/${1}*/"))
    for match in "${matches[@]}"; do
        if [ "$match" = "$prev_dir/" ]; then
          printf "%s" "$prev_dir"
          return
        fi
    done
  done
  # at this point there is still a possibility that $1 is an actual path (passed in
  # by completion or whatever), so let's check that one out
  [ -d "$1" ] && printf "%s" "$1" && return
  # otherwise fallback to pwd
  printf "%s" "$PWD"
}

# replace $1 with $2 in $PWD (`,, $1 $2`)
_commacd_backward_substitute() {
  printf "%s" "${PWD/$1/$2}"
}

# choose `,,` strategy based on a number of arguments
_commacd_backward() {
  local dir=
  case $# in
    0) dir=$(_commacd_backward_vcs_root);;
    1) dir=$(_commacd_backward_by_prefix "$*")
       if [ -z "$COMMACD_NOFUZZYFALLBACK" ] && [ "$dir" = "$PWD" ]; then
         dir=$(_commacd_backward_by_prefix "*$*")
       fi;;
    2) dir=$(_commacd_backward_substitute "$@");;
    *) return 1
  esac
  if [ "$COMMACD_NOTTY" = "on" ]; then
    printf "%s" "${dir}"
    return
  fi
  _command_cd "$dir"
}

_commacd_backward_forward_by_prefix() {
  local dir="$PWD" path="${*%/}/" matches match IFS=$'\n'
  if [ "${path:0:1}" = "/" ]; then
    # assume that we've been brought here by the completion
    dir=(${path%/}*)
    printf "%s\n" "${dir[@]}"
    return
  fi
  while [ -n "$dir" ]; do
    dir="${dir%/*}"
    matches=($(_commacd_expand "$dir/$(_commacd_prefix_glob "$*")"))
    if [ -z "$COMMACD_NOFUZZYFALLBACK" ] && [ ${#matches[@]} -eq 0 ]; then
      matches=($(_commacd_expand "$dir/$(_commacd_glob "$*")"))
    fi
    case ${#matches[@]} in
      0) ;;
      *) printf "%s\n" "${matches[@]}"
         return;;
    esac
  done
  printf "%s" "$PWD"
}

# combine backtracking with `, $1` (`,,, $1`)
_commacd_backward_forward() {
  if [ -z "$*" ]; then return 1; fi
  OLDIFS="${IFS}"
  local IFS=$'\n'
  local dir=($(_commacd_backward_forward_by_prefix "$@"))
  if [ "$COMMACD_NOTTY" = "on" ]; then
    printf "%s\n" "${dir[@]}"
    return
  fi
  if [ ${#dir[@]} -gt 1 ]; then
    dir=$(_commacd_choose_match "${dir[@]}")
  fi
  IFS="${OLDIFS}"
  _command_cd "$dir"
}

_commacd_completion_invalid() {
  if [ "$2" = "$PWD" ] || [ "${2// /\\ }" = "$1" ]; then return 0; else return 1; fi
}

_commacd_completion() {
  local pattern=${COMP_WORDS[COMP_CWORD]} IFS=$'\n'
  # shellcheck disable=SC2088
  if [ "${pattern:0:2}" = "~/" ]; then
    # shellcheck disable=SC2116
    pattern=$(printf "%s\\n" ~/"${pattern:2}")
  fi
  local completion=($(COMMACD_NOTTY=on $1 "$pattern"))
  if _commacd_completion_invalid "$pattern" "$completion"; then
    pattern="$pattern?"
    # retry with ? matching
    completion=($(COMMACD_NOTTY=on $1 "$pattern"))
    if _commacd_completion_invalid "$pattern" "$completion"; then
      return
    fi
  fi
  # remove trailing / (if any)
  for i in "${!completion[@]}"; do
    completion[$i]="${completion[$i]%/}";
  done
  COMPREPLY=($(compgen -W "$(printf "%s\n" "${completion[@]}")" -- ''))
}

_commacd_forward_completion() {
  _commacd_completion _commacd_forward
}

_commacd_backward_completion() {
  _commacd_completion _commacd_backward
}

_commacd_backward_forward_completion() {
  _commacd_completion _commacd_backward_forward
}

alias ,=_commacd_forward
alias ,,=_commacd_backward
alias ,,,=_commacd_backward_forward

complete -o filenames -F _commacd_forward_completion ,
complete -o filenames -F _commacd_backward_completion ,,
complete -o filenames -F _commacd_backward_forward_completion ,,,