# Copyright 2004-present Facebook. All Rights Reserved.
#
# Usage:
#
# To enable tab-completion for buck commands in bash, run the following
# command:
#
#     source path/to/this/file/buck-completion.bash
#
#
# Debugging:
#
# This script will output a bunch of information about what it is doing and
# considering to $DEBUG_BUCK_COMPLETION_TTY.  This enables debugging via
# the following steps.  Also consider 'set -x', though this is a bit more
# painful.
#
# To enable debugging for this script, open a bash window and run:
#
#     tty; cat >/dev/null
#
# Note the device output by the tty command for the next command.
#
# In a second bash window, run:
#
#     export DEBUG_BUCK_COMPLETION_TTY=/dev/other-window-tty
#     source path/to/this/file/buck-completion.bash
#
# replacing '/dev/other-window-tty' with the tty output from the first command.
#
# At this point, you can cd to a buck project, type a partial buck command, hit
# tab, and you should see logs in the original window of what this script is
# doing.

if [ -n "$ZSH_VERSION" ]; then
  autoload -U bashcompinit
  bashcompinit
fi

function _buck_completion_run() {
  COMPREPLY=()

  local -a words; words=( "${COMP_WORDS[@]}" )
  local cword=$COMP_CWORD
  local word="${words[$cword]}"
  local prev="$3"
  local log=_buck_completion_log

  $log "==============================="
  $log "word=$word"
  $log "prev=$prev"

  $log "words:"
  for w in "${words[@]}"; do
    $log "  $w"
  done
  $log "COMP_TYPE=$COMP_TYPE"

  case "$cword" in
    0)
      _buck_completion_internal_error "cword=0 in _buck_completion_run"
      ;;

    1)
      local commands="$(_buck_completion_echo_buck_commands)"
      COMPREPLY=( $(compgen -W "$commands" -- "$word") )
      ;;

    *)
      if [[ "${word:0:1}" == "@" ]]; then
        _buck_completion_try_arg_file "${word:1}"
      else
        case "${words[1]}" in
          audit)      _buck_completion_try_audit      "$@";;
          build)      _buck_completion_try_build      "$@";;
          cache)      _buck_completion_try_cache      "$@";;
          clean)      _buck_completion_try_clean      "$@";;
          install)    _buck_completion_try_install    "$@";;
          project)    _buck_completion_try_project    "$@";;
          run)        _buck_completion_try_run        "$@";;
          targets)    _buck_completion_try_targets    "$@";;
          test)       _buck_completion_try_test       "$@";;
          uninstall)  _buck_completion_try_uninstall  "$@";;
        esac
      fi
      ;;
  esac

  if [[ ${#COMPREPLY[@]} != 0 ]]; then
    $log "=========="
    $log "COMPREPLY:"
    for r in "${COMPREPLY[@]}"; do
      $log "  $r"
    done
  fi

  # Set return status
  [[ ${#COMPREPLY[@]} > 0 ]]
}

function _buck_completion_try_build() {
  _buck_completion_try_long_arg "--help --no-cache --num-threads --verbose --populate-cache" \
    || _buck_completion_try_target "$@"
}

function _buck_completion_try_audit() {
  case "$cword" in
    0 | 1)
      _buck_completion_internal_error "cword=$cword in _buck_completion_try_audit"
      return 1
      ;;

    2)
      COMPREPLY=( $(compgen -W "input classpath owner rules" -- "$word") )
      ;;

    *)
      if [[ "$word" == --* ]]; then
        local extra
        case "${words[2]}" in
          input)     extra="--dot --json";;
          classpath) extra="--dot --json";;
          owner)     extra="--dot --full --guess-for-missing";;
          rules)     extra="--type";;
        esac
        _buck_completion_try_long_arg "--help --no-cache --verbose $extra"
      else
        _buck_completion_try_file "$@"
      fi
      ;;
  esac
}

function _buck_completion_try_cache() {
  _buck_completion_try_long_arg "--help --no-cache --verbose"
}

function _buck_completion_try_clean() {
  _buck_completion_try_long_arg "--help --no-cache --verbose --project"
}

function _buck_completion_try_install() {
  _buck_completion_try_long_arg "
      --activity
      --help
      --no-cache
      --num-threads
      --run
      --uninstall
      --verbose
      --via-sd
      --keep
      --adb-threads
      --emulator
      --device
      --serial" \
    || _buck_completion_try_short_arg "-all" \
    || _buck_completion_try_serial "$@" \
    || _buck_completion_try_target "$@"
}

function _buck_completion_try_project() {
  _buck_completion_try_long_arg "
      --combined-project
      --deprecated-ij-generation
      --help
      --ide
      --no-cache
      --process-annotations
      --verbose
      --without-tests"
}

function _buck_completion_try_run() {
  if [ "${#words[@]}" -gt 8 ]; then
    _buck_completion_try_file_or_folder "$@"
  else
    _buck_completion_try_long_arg "--help --no-cache --verbose" \
      || _buck_completion_try_target "$@"
  fi
}

function _buck_completion_try_targets() {
  _buck_completion_try_long_arg "
      --help
      --json
      --no-cache
      --num-threads
      --referenced-file
      --resolve-alias
      --show-output
      --show-rulekey
      --type
      --verbose" \
    || _buck_completion_try_resolve_alias "$@"
  # TODO _buck_completion_try_referenced_file_set
}

function _buck_completion_try_test() {
  _buck_completion_try_long_arg "
      --all
      --code-coverage
      --debug
      --help
      --ignore-when-dependencies-fail
      --jacoco
      --no-cache
      --num-threads
      --verbose
      --xml
      --emulator
      --device
      --serial
      --test-selectors
      --explain-test-selectors
      --exclude
      --always_exclude" \
    || _buck_completion_try_serial "$@" \
    || _buck_completion_try_target "$@"
}

function _buck_completion_try_uninstall() {
  _buck_completion_try_long_arg "
      --help
      --no-cache
      --verbose
      --keep
      --adb-threads
      --emulator
      --device
      --serial" \
    || _buck_completion_try_short_arg "-all" \
    || _buck_completion_try_serial "$@" \
    || _buck_completion_try_target "$@"
}

function _buck_completion_try_long_arg() {
  if [[ "$word" == --* ]]; then
    COMPREPLY=( $(compgen -W "$@" -- "$word") )
  fi

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_short_arg() {
  if [[ "$word" == -* ]]; then
    COMPREPLY=( $(compgen -W "$@" -- "$word") )
  fi

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_dest_dir() {
  if [[ "$prev" == "--dest-dir" ]]; then
    COMPREPLY=( $(compgen -A directory -- "$word") )
  fi

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_file() {
  COMPREPLY=( $(compgen -A file -- "$word") )

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_file_or_folder() {
  local -a raw_files; raw_files=( $(compgen -A file -- "$word") )
  for p in "${raw_files[@]}"; do
    if [[ -d "$p" ]]; then
      _buck_completion_add_reply "$p/"
    else
      _buck_completion_add_reply "$p"
    fi
  done

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_arg_file() {
  path="$1"  
  local -a raw_files; raw_files=( $(compgen -A file -- "$path") )
  for p in "${raw_files[@]}"; do
    if [[ -d "$p" ]]; then
      _buck_completion_add_reply "@$p/"
    else
      _buck_completion_add_reply "@$p"
    fi
  done
}

function _buck_completion_try_serial() {
  case "$prev" in
    -s | --serial)
      for d in "$(adb devices | tail -n+2 | awk '/./ { print $1 }')"; do
        if [[ "$d" == "$word"* ]]; then
          _buck_completion_add_reply "$d"
        fi
      done
      ;;
  esac

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_resolve_alias() {
  # Make sure we are in a buck root
  local root=$(_buck_completion_get_root)
  $log "root=$root"
  if [[ -z "$root" ]]; then
    return 1
  fi

  case "$prev" in
    --resolvealias | --resolve-alias)
      _buck_completion_add_target_alias "$@"
      ;;
  esac

  # Set return status
  [[ "${#COMPREPLY[@]}" > 0 ]]
}

function _buck_completion_try_target() {
  # Make sure we are in a buck root
  local root=$(_buck_completion_get_root)
  $log "root=$root"
  if [[ -z "$root" ]]; then
    return 1
  fi

  if [[ "$word" == //*:* ]]; then
    _buck_completion_add_explicit_target_name_unsplit "$@"
  elif [[ "$prev" == ":" || "$word" == ":" ]]; then
    if [[ "$word" == ":" ]]; then
      # Patch up word arrays to pretend we are looking at '' after the ':'
      word=''
      words[${#words[@]}]=''
      cword=$(($cword + 1))
    fi
    _buck_completion_add_explicit_target_name "$@"
  elif [[ "$word" == *:* ]]; then
    _buck_completion_add_relative_target_name_unsplit "$@"
  elif [[ "$word" == //* ]]; then
    _buck_completion_add_explicit_target_path "$@"
  else
    _buck_completion_add_target_alias_or_relative_path "$@"
  fi

  # Set return status
  [[ ${#COMPREPLY[@]} > 0 ]]
}

function _buck_completion_add_explicit_target_path() {
  local dir="${root}/${word#//}"
  $log "dir=$dir"

  _buck_completion_add_relative_path_with_prefix "$dir" "//"
}

function _buck_completion_add_explicit_target_name() {
  # TODO: verify previous words
  local dir="${root}/${words[$(($cword - 2))]#//}"
  local buck_file="$dir/BUCK"
  local name_prefix="$word"

  _buck_completion_add_target_names "$buck_file" "$name_prefix"
}

function _buck_completion_add_explicit_target_name_unsplit() {
  local dir="${root}/$(echo $word | sed -E -e 's|//(.*):.*|\1|')"
  local buck_file="$dir/BUCK"
  local name_prefix="${word#//*:}"

  _buck_completion_add_target_names "$buck_file" "$name_prefix"
}

function _buck_completion_add_relative_target_name_unsplit() {
  local dir="${word%:*}"
  local buck_file="$dir/BUCK"
  local name_prefix="${word#*:}"

  _buck_completion_add_target_names "$buck_file" "$name_prefix"
}

function _buck_completion_add_target_alias_or_relative_path() {
  local dir="${word}"
  $log "dir=$dir"

  _buck_completion_add_target_alias "$@"
  _buck_completion_add_relative_path_with_prefix "$dir" ""
}

function _buck_completion_add_target_alias() {
  local prog=$(cat <<EOF
    /^\[/ {
      p=0
    }
    /^\[alias]/ {
      p=1
    }
    p && \$2 == "=" && substr(\$1, 0, length("$word")) == "$word" {
      print \$1
    }
EOF
)
  local -a aliases; aliases=($(awk "$prog" < "$root/.buckconfig"))

  for a in "${aliases[@]}"; do
    _buck_completion_add_reply "$a"
  done
}

function _buck_completion_add_relative_path() {
  local dir="${root}/${word}"
  _buck_completion_add_relative_path_with_prefix "$dir" ""
}

function _buck_completion_add_relative_path_with_prefix() {
  local dir="$1"
  local prefix="$2"

  # Complete directory containing BUCK file
  local -a raw_dirs; raw_dirs=( $(compgen -A directory -- "$dir") )

  for d in "${raw_dirs[@]}"; do
    local suffix
    if [[ -f "$d/BUCK" ]]; then
      suffix=':'
    else
      suffix='/'
    fi
    _buck_completion_add_reply "${prefix}${d#${root}/}${suffix}"
  done
}

function _buck_completion_add_target_names() {
  local buck_file="$1"
  $log "buck_file=$buck_file"

  local name_prefix="$2"
  $log "name_prefix=$name_prefix"

  if [[ ! -f "$buck_file" ]]; then
    $log "No such file: '$buck_file'"
    return 1
  fi

  local pattern="^ *name *= *[\'\"]\([^\'\"]*\)[\'\"] *, *$"
  local target_names
  if [[ -n "${BUCK_COMPLETION_HARDTARGETRESOLUTION-}" || -n "${BUCK_COMPLETION_USE_BUCK}" ]]; then
    target_names=( $(buck audit rules "$buck_file" 2>/dev/null | sed -n -e "s/$pattern/\\1/ p") )
  else
    target_names=( $(grep --color=none "$pattern" "$buck_file" | sed -n -e "s/$pattern/\\1/ p") )
  fi

  $log "target_names='${target_names[@]}'"
  for name in "${target_names[@]}"; do
    $log "  considering target '$name'"
    if [[ "$name" == "$name_prefix"* ]]; then
      $log "    adding '$name' because it has prefix '$name_prefix'"
      _buck_completion_add_reply "${name}"
    else
      $log "    not adding '$name' because it does not have prefix '$name_prefix'"
    fi
  done
}

function _buck_completion_get_root() {
  local root=$PWD
  while [[ -n "$root" ]]; do
    if [[ -e "$root"/.buckconfig ]]; then
      echo "$root" && return 0
    fi
    root=${root%/*}
  done
}

function _buck_completion_add_reply() {
  local completion="$1"

  $log "Adding completion: '$completion'"
  COMPREPLY[${#COMPREPLY[@]}]="$completion"
}

function _buck_completion_log() {
  [[ -n "$DEBUG_BUCK_COMPLETION_TTY" ]] && echo "$@" >"$DEBUG_BUCK_COMPLETION_TTY"
}

function _buck_completion_echo_buck_commands() {
  echo "audit build cache clean install project run targets test uninstall --version --help -V"
}

function _buck_completion_internal_error() {
    $log ""
    $log "Internal error in _buck_completion_echo_target_names:"
    $log "  $@"
    $log "at:"
    for f in "${FUNCNAME[@]}"; do
      $log "  $f"
    done
}

complete -o nospace -F _buck_completion_run buck
