#compdef catkin
# Copyright 2016 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This is the completion script for the catkin command provided by catkin_tools
#
# TODO: Enable better completion for options after package names, look at the completion code for _beep as an example
# Also consider https://github.com/git/git/blob/c2c5f6b1e479f2c38e0e01345350620944e3527f/contrib/completion/git-completion.zsh

zstyle ':completion:*' sort no

local _workspace_root_hint
local _workspace_root
local _workspace_profiles
local _workspace_source_space
local _enclosing_package

_workspace_root_hint="$PWD"

# Logging function for development debugging
function _debug_log() {
  if [[ -n "$CATKIN_COMPLETION_DEBUG" ]]; then
      echo $1 >> "$HOME/.config/catkin/completion.log"
  fi
}

# Get the workspace root by following the specified path
_catkin_find_enclosing_workspace_for_path() {
  local _path
  _path="$1"

  while [[ "$_path" != "/" ]]; do
    _debug_log "Checking $_path/.catkin_tools"

    if [[ -e "$_path/.catkin_tools" ]]; then
      _debug_log "Found workspace root $_path"
      _workspace_root=$_path
      return 0
    else
      _path="$(dirname "$_path")"
    fi
  done

  return 1
}

# Get the workspace root (if available)
_catkin_get_enclosing_workspace() {
  if [[ -n "$_workspace_root" ]]; then
    return 0
  fi

  _debug_log "Search for workspace root in resolved path:"
  _catkin_find_enclosing_workspace_for_path $(readlink -f "$1")

  if [[ -n "$_workspace_root" ]]; then
    return 0
  fi

  _debug_log "Search for workspace root in real path:"
  _catkin_find_enclosing_workspace_for_path $1

  if [[ -n "$_workspace_root" ]]; then
    return 0
  fi

  return 1
}

# Get the name of the enclosing package (if available)
_catkin_get_enclosing_package() {
  if [[ -n "$_enclosing_package" ]]; then
    return 0
  fi

  local _path
  _path="$1"

  while [[ "$_path" != "/" ]]; do
    _debug_log "Checking $_path/package.xml"

    if [[ -e "$_path/package.xml" ]]; then
      _debug_log "Found package root $_path"
      # Get the package name
      _enclosing_package="$(xmllint --xpath '/package/name/text()' "$_path/package.xml")"
      return 0
    else
      _path="$(dirname "$_path")"
    fi
  done

  return 1
}

# Cache is invalidated if the source space has been changed since the
# package list was cached
_catkin_packages_caching_policy() {
  # Get the source space path
  if [[ -z "$_workspace_source_space" ]]; then
    _debug_log "Searching for source space..."

    _workspace_source_space="$(catkin locate -s --quiet)"

    # If the source space can't be found, regenerate cache
    if [[ "$?" -ne "0" ]]; then
      _debug_log "Source space can't be found"
      return 1
    fi
  fi

  # If the cache file is older than the source space
  if [[ "$1" -ot "$_workspace_source_space" ]]; then
    _debug_log "Cache is older than source space"
    return 0
  else
    _debug_log "Cache is newer than source space"
    return 1
  fi
}

# Get the packages in the workspace containing the CWD
_catkin_get_packages() {

  local cache_policy
  local cache_file_path

  # Get the workspace root
  _catkin_get_enclosing_workspace $_workspace_root_hint

  if [[ "$?" -ne 0 ]]; then
    _debug_log "Couldn't get workspace root!"
    return
  fi

  # Construct the path for the cache file
  cache_file_path="$_workspace_root/.catkin_tools/cache"

  _debug_log "Workspace path: $_workspace_root"
  _debug_log "Cache file path: $cache_file_path"

  zstyle ":completion:${curcontext}:" use-cache on
  zstyle ":completion:${curcontext}:" cache-path "$cache_file_path"
  zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
  if [[ -z "$cache_policy" ]]; then
    zstyle ":completion:${curcontext}:" cache-policy _catkin_packages_caching_policy
  fi

  # If cache is invalid, or if the package list or workspace cache file path are not set yet or this path differ from
  # its located path (we switched to different workspace) and variables can't be retrieved from cache regenerate cache
  # ( Remember that _retrieve_cache returns 0 if everything's fine, 1 if not. _cache_invalid returns 0 if cache needs rebuilding, 1 otherwise)
  # (see https://unix.stackexchange.com/q/588662 for the use of parentheses)
  if _cache_invalid workspace_packages || \
    {{ [[ ${+_workspace_packages} -eq 0 ]] ||
      [[ -z "$_workspace_cache_file_path" ]] ||
      [[ "$_workspace_cache_file_path" != "$cache_file_path" ]] } &&
      ! _retrieve_cache workspace_packages } ; then

    _debug_log "Regenerating package cache because retrieval failed ..."
    _workspace_packages=(${${(f)"$(catkin list -u --quiet)"}})
    _workspace_cache_file_path=$cache_file_path
    _store_cache workspace_packages _workspace_packages _workspace_cache_file_path
  fi

  local expl
  _wanted workspace_packages expl 'workspace packages' compadd -a _workspace_packages && return 0
  return 1
}

# Get the profiles in the workspace containing the CWD
_catkin_get_profiles() {
  if [[ -z "$_workspace_profiles" ]]; then

    _debug_log "Getting profiles... ($_workspace_profiles)"

    _workspace_profiles=(${${(f)"$(catkin profile list -u)"}})
    if [[ "$?" -ne 0 ]]; then
      return
    fi
  fi

  local expl
  _wanted workspace_profiles expl 'workspace profiles' compadd -a _workspace_profiles
}

_catkin_verbs_complete() {
  local verbs_array;
  verbs_array=('build:Build packages')
  [[ -f $(which catkin) ]] || verbs_array+=('cd:Change directory to a package')
  verbs_array+=(
    'clean:Clean workspace components'
    'config:Configure workspace'
    'create:Create workspace components'
    'env:Run commands in a modified environment'
    'init:Initialize workspace'
    'list:List workspace components'
    'locate:Locate workspace components'
    'profile:Switch between configurations'
  )
  [[ -f $(which catkin) ]] || verbs_array+=('source:Source a workspace')
  verbs_array+=('test:Test packages')
  _describe -t verbs 'catkin verb' verbs_array -V verbs && return 0
}

_catkin_build_complete() {
  # Determine if we're in a package, and should autocomplete context-aware options
  local build_opts

  _catkin_get_enclosing_package "$PWD"

  if [[ -n $_enclosing_package ]]; then
    _debug_log "In package $_enclosing_package"
    build_opts=(\
      "(--unbuilt)--this[Build '$_enclosing_package']"\
      "(--start-with)--start--with--this[Skip all packages on which '$_enclosing_package' depends]"\
      )
  else
    _debug_log "Not in a package"
    build_opts=()
  fi

  # Add all other options
  build_opts+=(
    {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[The workspace to build]:workspace:_files'\
    '--profile[Which configuration profile to use]:profile:_catkin_get_profiles'\
    {-n,--dry-run}'[Show build process without actually building]'\
    {-v,--verbose}'[Print all output from build commands]'\
    {-i,--interleave-output}"[Print output from build commands as it's generated]"\
    {-c,--continue-on-failure}'[Build all selected packages even if some fail]'\
    '--get-env[Print the environment for a given workspace]:package:_catkin_get_packages'\
    '--force-cmake[Force CMake to run for each package]'\
    '--pre-clean[Execute clean target before building]'\
    '(--this)--unbuilt[Build packages which have not been built]'\
    '(--start-with-this)--start-with[Skip all packages on which a package depends]:package:_catkin_get_packages'\
    '--save-config[Save configuration options]'\
    '--no-deps[Only build specified packages]'\
    '*:package:_catkin_get_packages')

  _arguments -C $build_opts && return 0
}

_catkin_cd_complete() {
  _arguments -C ':package:_catkin_get_packages' && return 0
}

_catkin_clean_complete() {
  # Determine if we're in a package, and should autocomplete context-aware options
  local clean_opts

  _catkin_get_enclosing_package "$PWD"

  if [[ -n $_enclosing_package ]]; then
    _debug_log "In package $_enclosing_package"
    clean_opts=(\
      "--this[Clean '$_enclosing_package']"\
      )
  else
    _debug_log "Not in a package"
    clean_opts=()
  fi

  # Add all other options
  clean_opts+=(
  {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[Workspace path]:workspace:_files -/'\
    '--profile[Which configuration profile to use]:profile:_catkin_get_profiles'\
    {-v,--verbose}'[Print all output from build commands]'\
    {-n,--dry-run}'[Show build process without actually building]'\
    {-y,--yes}'[Assume "yes" to all interactive checks]'\
    {-f,--force}'[Allow cleaning files outside of the workspace root]'\
    '--all-profiles[Apply the clean operation to all profiles]'\
    '--deinit[De-initialize the workspace]'\
    {-l,--logs}'[Remove the log space]'\
    {-b,--build}'[Remove the build space]'\
    {-d,--devel}'[Remove the devel space]'\
    {-i,--install}'[Remove the install space]'\
    '--dependents[Clean all packages which depend on a specified package]'\
    '--orphans[Clean any packages which are no longer in the source space]'\
    '*:package:_catkin_get_packages')

  _arguments -C $clean_opts && return 0
}

_catkin_config_complete() {
  local config_opts
  config_opts=(\
    {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[Workspace path]:workspace:_files -/'\
    '--profile[Which configuration profile to use]:profile:_catkin_get_profiles'\
    '(-r --remove-args)'{-a,--append-args}'[Append elements for list-type arguments]'\
    '(-a --append-args)'{-r,--remove-args}'[Remove elements for list-type arguments]'\
    '--init[Initialize a workspace]'\
    '(--no-extend)'{-e,--extend}'[Explicitly extend another workspace]:workspace:_files'\
    '(-e --extend)--no-extend[Unset the explicit extension of another workspace]'\
    '--mkdirs[Create directories required by the configuration]'\
    '(--no-buildlist)--buildlist[Set the packages on the buildlist]:package:->packages'\
    '(--no-skiplist)--skiplist[Set the packages on the skiplist]:package:->packages'\
    '(--buildlist)--no-buildlist[Clear the buildlist]'\
    '(--skiplist)--no-skiplist[Clear the skiplist]'\
    '(--default-source-space)'{-s,--source-space}'[Source space path]:source space:_files'\
    '(-s --source-space)--default-source-space[Use the default path to the source space]'\
    '(--default-devel-space)'{-d,--devel-space}'[Devel space path]'\
    '(-d --devel-space)--default-devel-space[Use the default path to the devel space]'\
    '(--default-build-space)'{-b,--build-space}'[Build space path]'\
    '(-b --build-space)--default-build-space[Use the default path to the build space]'\
    '(--default-install-space)'{-i,--install-space}'[Install space path]'\
    '(-i --install-space)--default-install-space[Use the default path to the install space]'\
    '(--default-log-space)'{-L,--log-space}'[Log space path]'\
    '(-L --log-space)--default-log-space[Use the default path to the log space]'\
    {-x,--space-suffix}'[Suffix for build, devel, and install spaces]:suffix:()'\
    '(--link-devel --isolate-devel)--merge-devel[Build products directly into a merged devel space]'\
    '(--merge-devel --isolate-devel)--link-devel[Symbolically link products into a merged devel space]'\
    '(--merge-devel --link-devel)--isolate-devel[Isolate devel speaces for each package]'\
    '(--no-install)--install[Causes each package to be installed to the install space]'\
    '(--install)--no-install[Disables installing each package into the install space]'\
    '(--merge-install)--isolate-install[Install each catkin package into a separate install space]'\
    '(--isolate-install)--merge-install[Install each catkin package into a single merged install space]'\
    {-j,--jobs}'[Maximum number of build jobs to be distributed across active packages]:JOBS:()'\
    {-p,--parallel-packages}'[Maximum number of packages allowed to be built in parallel]:PACKAGE_JOBS:()'\
    {-l,--load-average}'[Maximum load average before no new build jobs are scheduled]'\
    '(--no-jobserver)--jobserver[Use the internal GNU Make job server]'\
    '(--jobserver)--no-jobserver[Disable the internal GNU Make job server]'\
    '(--no-env-cache)--env-cache[Re-use cached environment variables]'\
    "(--env-cache)--no-env-cache[Don't cache environment variables]"\
    '(--no-cmake-args)--cmake-args[Arbitrary arguments which are passed to CMake]'\
    '(--cmake-args)--no-cmake-args[Pass no additional arguments to CMake]'\
    '(--no-make-args)--make-args[Arbitrary arguments which are passed to make]'\
    '(--make-args)--no-make-args[Pass no additional arguments to make]'
    '(--no-catkin-make-args)--catkin-make-args[Arbitrary arguments which are passed to make for catkin packages]'\
    '(--catkin-make-args)--no-catkin-make-args[Pass no additional arguments to make for catkin packages]')

  _arguments -C $config_opts && return 0
  return 1
}

_catkin_profile_complete() {
  local profile_verbs;

  _arguments -C \
    {-h,--help}'[Show usage help]'\
    ':profile_verbs:->profile_verbs'\
    '*:: :->args'\
    && return 0

  case "$state" in
    (profile_verbs)
      profile_verbs=(
      'list:List the profiles'
      'set:Set the active profile'
      'rename:Rename a profile'
      'remove:Remove a profile'
      )
      _describe -t profile_verbs 'profile_verb' profile_verbs && return 0
      ;;
    (args)
      case $line[1] in
        (set|rename|remove)
          local expl
          _wanted workspace_profiles expl "workspace profiles" _catkin_get_profiles && return 0
          ;;
      esac
      ;;
  esac;

  return 1
}

_catkin_create_complete() {
  local create_verbs;

  _arguments -C \
    {-h,--help}'[Show usage help]'\
    ':create_verbs:->create_verbs'\
    '*:: :->args'\
    && return 0

  case "$state" in
    (create_verbs)
      create_verbs=(
      'pkg:Create a new catkin package'
      )
      _describe -t create_verbs 'create_verbs' create_verbs && return 0
      ;;
    (args)
      case $line[1] in
        (pkg)
          local pkg_opts
          pkg_opts=(\
            {-h,--help}'[Show usage help]'\
            {-p,--path}'[The path into which the package should be generated]:path:_files'\
            '--rosdistro[Which ROS distro to use for the template]:rosdistro:(melodic noetic)'\
            {-v,--version}'[Version MAJOR.MINOR.PATCH]:version'\
            {-l,--license}'[Software license for distribution]:license:(BSD MIT GPLV3)'\
            {-d,--description}'[Package description]:description'\
            '*'{-m,--maintainer}'[Maintainer NAME EMAIL]:m_name:( ):m_email:( )'
            '*'{-c,--catkin-deps}'[Catkin packages]:dep:( )'\
            '*'{-s,--system-packages}'[System packages]:dep:( )'\
            '*--boost-components[Boost C++ components]:dep:( )'\
              )

          _arguments -C $pkg_opts && return 0
          ;;
      esac
      ;;
  esac;

  return 1
}

_catkin_env_complete() {
  local env_opts
  env_opts=(\
    {-h,--help}'[Show usage help]'\
    {-i,--ignore-environment}'[Start with an empty environment]'\
    {-s,--stdin}'[Read environment definitions from stdin]')

  _arguments -C $env_opts && return 0
  return 1
}

_catkin_init_complete() {
  local init_opts
  init_opts=(\
    {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[Workspace path]:workspace:_files -/'\
    '--reset[Reset all metadata in the workspace]')

  _arguments -C $init_opts && return 0
  return 1
}

_catkin_list_complete() {
  local list_opts
  list_opts=(\
    {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[Workspace path]:workspace:_files -/'\
    '--this[Get the name of the package enclosing the current working directory]'\
    '--deps[Show direct dependencies of each package]'\
    '--rdeps[Show recursive dependencies of each package]'\
    '--quiet[Suppress warnings]'\
    {-u,--unformatted}'[Suppress warnings]'\
    )

  _arguments -C $list_opts && return 0
  return 1
}

_catkin_locate_complete() {
  local locate_opts
  locate_opts=(\
    {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[Workspace path]:workspace:_files -/'\
    {-e,--existing-only}'[Only print existing paths]'\
    {-r,--relative}'[Format paths relative to the working directory]'\
    {-q,--quiet}'[Suppress warnings]'\
    {-s,--src}'[Get the path to the source space]'\
    {-b,--build}'[Get the path to the build space]'\
    {-d,--devel}'[Get the path to the devel space]'\
    {-i,--install}'[Get the path to the install space]'\
    )

  _arguments -C $locate_opts && return 0
  return 1
}

_catkin_source_complete() {
  return 0
}

_catkin_test_complete() {
  local test_opts

  test_opts=(
    {-h,--help}'[Show usage help]'\
    {-w,--workspace}'[The workspace to test]:workspace:_files'\
    '--profile[Which configuration profile to use]:profile:_catkin_get_profiles'\
    {-v,--verbose}'[Print all output from test commands]'\
    {-i,--interleave-output}"[Print output from test commands as it's generated]"\
    '--this[Test the current package]'\
    {-c,--continue-on-failure}"[Continue testing after failed tests]"\
    {-p,--parallel-packages}"[Number of packages to build in parallel]:number"\
    {-t,--test-target}"[make target to run for tests]:target"\
    '--catkin-test-target[make target to run for tests in catkin packages]:target'\
    '--no-status[Suppress status line]'\
    {-s,--summarize}"[Add a summary at the end]"\
    '*:package:_catkin_get_packages')

  _arguments -C $test_opts && return 0
}


local curcontext="$curcontext" state line ret

ret=1

# Define the root arguments for the catkin command
_arguments -C \
  {-h,--help}'[Show usage help]'\
  '--version[Show catkin_tools version]'\
  '--force-color[Force colored output]'\
  '--no-color[Force non-colored output]'\
  ':verb:->verb'\
  '*::options:->options' && ret=0

_debug_log "---"
_debug_log "state: $state"
_debug_log "line: $line"
_debug_log "words: $words"
_debug_log "CURRENT: $CURRENT"
_debug_log "PREFIX: $PREFIX"
_debug_log "IPREFIX: $IPREFIX"
_debug_log "SUFFIX: $SUFFIX"
_debug_log "ISUFFIX: $ISUFFIX"

# Switch to getting packages
case "$state" in
  (verb)
    _alternative _catkin_verbs_complete && ret=0
    ;;
  (options)
    case $line[1] in
      (build) _catkin_build_complete && ret=0
        ;;
      (cd) _catkin_cd_complete && ret=0
        ;;
      (clean) _catkin_clean_complete && ret=0
        ;;
      (config)
        local terminating_opts
        terminating_opts=(\
          '--buildlist:Set the packages on the buildlist'\
          '--skiplist:Set the packages on the skiplist'\
          '--cmake-args:Arbitrary arguments which are passed to CMake'\
          '--make-args:Arbitrary arguments which are passed to make'\
          '--catkin-make-args:Arbitrary arguments which are passed to make for catkin packages'\
          '--:Continue with other catkin config arguments')
        _debug_log "last considered -- is ${words[1,$CURRENT-1][(I)--]}"
        _debug_log "last considered --list is ${words[1,$CURRENT-1][(I)--(black|white)list]}"
        _debug_log "last considered --make is ${words[1,$CURRENT-1][(I)--*make-args]}"
        if ((${words[1,$CURRENT-1][(I)--(black|white)list]} > ${words[1,$CURRENT-1][(I)--]} && ${words[1,$CURRENT-1][(I)--(black|white)list]} > ${words[1,$CURRENT-1][(I)--*make-args]})); then
          _debug_log "lists"
          _describe -t terminators "terminate multiword argument" terminating_opts
          _catkin_get_packages && ret=0
        elif ((${words[1,$CURRENT-1][(I)--cmake-args]} > ${words[1,$CURRENT-1][(I)--]} && ${words[1,$CURRENT-1][(I)--cmake-args]} >= ${words[1,$CURRENT-1][(I)--*make-args]} && ${words[1,$CURRENT-1][(I)--cmake-args]} > ${words[1,$CURRENT-1][(I)--(black|white)list]})); then
          _debug_log "cmake"
          # a block of --cmake-args has been started
          # but not yet ended with --
          # not ended with --*make-args (--make-args, --catkin-make-args, or --cmake-args)
          # not ended with --skiplist or --buildlist
          # ignoring the current word for the end-range (such that --cmake-args --<TAB> gives cmake completion)
          _dispatch $service cmake
          _describe -t terminators "terminate multiword argument" terminating_opts
        elif ((${words[1,$CURRENT-1][(I)--make-args]} > ${words[1,$CURRENT-1][(I)--]} && ${words[1,$CURRENT-1][(I)--make-args]} >= ${words[1,$CURRENT-1][(I)--*make-args]} && ${words[1,$CURRENT-1][(I)--make-args]} > ${words[1,$CURRENT-1][(I)--(black|white)list]})); then
          # same as above, just for make
          _debug_log "make"
          _dispatch $service make
          _describe -t terminators "terminate multiword argument" terminating_opts
        elif ((${words[1,$CURRENT-1][(I)--catkin-make-args]} > ${words[1,$CURRENT-1][(I)--]} && ${words[1,$CURRENT-1][(I)--catkin-make-args]} >= ${words[1,$CURRENT-1][(I)--*make-args]} && ${words[1,$CURRENT-1][(I)--catkin-make-args]} > ${words[1,$CURRENT-1][(I)--(black|white)list]})); then
          # same as above, just for catkin_make
          _debug_log "catkin_make"
          _dispatch $service catkin_make
          _describe -t terminators "terminate multiword argument" terminating_opts
        else
          _debug_log "normal"
          # when returning from a --*-args sequence `catkin config --cmake-args ...... -- -<TAB>`
          # the cmake arguments are visible to _catkin_config_complete and cause failure there
          # might want to remove them from $words.
          # adjust $CURRENT accordingly, esp. when CURRENT is not the last word.
          local -a bodgedwords
          local -i bodgedcurrent=0
          local inignore=false
          for (( i=1 ; i <= ${#words} ; i++ )) ; do
            if [[ $inignore != true ]]; then
              bodgedwords+=($words[i])
              if (( i <= $CURRENT )); then
                (( bodgedcurrent++ ))
                _debug_log "incrementing bodgedcurrent at $words[$i]"
              else
                _debug_log "not incrementing bodgedcurrent at $words[$i] because passed the interesting part (i = $i, current = $CURRENT)"
              fi
            else
              _debug_log "not incrementing bodgedcurrent at $words[$i] because in ignore"
            fi
            if [[ $words[$i] == '--' ]]; then inignore=false; fi
            if [[ $words[$i] == --*make-args ]]; then
              inignore=true
            fi
          done
          words=($bodgedwords)
          CURRENT=$bodgedcurrent
          _catkin_config_complete && ret=0
        fi
        ;;
      (create) _catkin_create_complete && ret=0
        ;;
      (env) _catkin_env_complete && ret=0
        ;;
      (init) _catkin_init_complete && ret=0
        ;;
      (list) _catkin_list_complete && ret=0
        ;;
      (locate) _catkin_locate_complete && ret=0
        ;;
      (profile) _catkin_profile_complete && ret=0
        ;;
      (source) _catkin_source_complete && ret=0
        ;;
      (test) _catkin_test_complete && ret=0
        ;;
    esac
    ret=0
    ;;
esac

return ret
