# greenos bash operational mode completion
# **** License ****
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
# 
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# This code was originally developed by GreenOS, Inc.
# Portions created by GreenOS are Copyright (C) 2006, 2007 GreenOS, Inc.
# All Rights Reserved.
# 
# Author: Tom Grennan
# Date: 2007
# Description: setup bash completion for GreenOS operational commands
# 
# **** End License ****

# Provide compatibility shim for _init_completion when running under
# vbash (bash 4.1) where bash-completion 2.11+ (requires bash 4.2+)
# fails to load. This prevents "Invalid command: [_init_completion]" errors.
if ! declare -F _init_completion >/dev/null 2>&1; then
  _init_completion()
  {
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"
    words=("${COMP_WORDS[@]}")
    cword=$COMP_CWORD
    return 0
  }
fi
if ! declare -F _get_comp_words_by_ref >/dev/null 2>&1; then
  _get_comp_words_by_ref()
  {
    while [ $# -gt 0 ]; do
      case "$1" in
        cur) cur="${COMP_WORDS[COMP_CWORD]}" ;;
        prev) prev="${COMP_WORDS[COMP_CWORD-1]}" ;;
        words) words=("${COMP_WORDS[@]}") ;;
        cword) cword=$COMP_CWORD ;;
        -n) shift ;;
      esac
      shift
    done
  }
fi
if ! declare -F _tilde >/dev/null 2>&1; then
  _tilde()
  {
    if [[ ${1-} == \~* && $1 != */* ]]; then
      COMPREPLY=( $(compgen -P '~' -u -- "${1#\~}") )
      [[ ${#COMPREPLY[@]} -gt 0 ]] && return 0
    fi
    return 1
  }
fi

test -z "$_greenos_less_options" && \
    declare -r _greenos_less_options="\
	--QUIT-AT-EOF\
	--quit-if-one-screen\
	--RAW-CONTROL-CHARS\
	--squeeze-blank-lines\
	--no-init"
test -z "$_greenos_default_pager" && \
    declare -r _greenos_default_pager="less \
	--buffers=64\
	--auto-buffers\
	--no-lessopen\
	$_greenos_less_options"
test -z "$GREENOS_PAGER" && \
    declare -x GREENOS_PAGER=$_greenos_default_pager

_greenos_op_do_key_bindings ()
{
  if [[ "$SHELL" != "/bin/vbash" && "$SHELL" != "/sbin/radius_shell" ]]; then
    # only do bindings if vbash and radius_shell
    return
  fi
  nullglob_save=$(shopt -p nullglob)
  shopt -u nullglob
  case "$-" in
    *i*)
      bind '"?": possible-completions' 
      bind 'set show-all-if-ambiguous on' 
      bind_cmds=$(grep '^bind .* # greenos key binding$' $HOME/.bashrc)
      eval $bind_cmds 
    ;;
  esac
  eval $nullglob_save
}

_greenos_op_do_key_bindings

test -f /etc/default/greenos && \
    source /etc/default/greenos

test ! -d "$greenos_op_templates" && \
    return 0

case "$-" in
  *i*)
    declare -r _greenos_op_last_comp_init='>>>>>>LASTCOMP<<<<<<'
  ;;
esac
declare _greenos_op_last_comp=${_greenos_op_last_comp_init}
declare _greenos_op_node_path
declare -a _greenos_op_noncompletions _greenos_op_completions
declare -x -a _greenos_pipe_noncompletions _greenos_pipe_completions
declare _greenos_comptype
declare -x -a reply
declare -a _greenos_operator_allowed

if [[ "$GREENOS_USER_LEVEL_DIR" != "/opt/greenos/etc/shell/level/admin" ]]; then
   _greenos_operator_allowed=( $(cat $GREENOS_USER_LEVEL_DIR/allowed-op) )
fi

declare -a functions
functions=( /opt/greenos/share/greenos-op/functions/interpreter/* )

for file in "${functions[@]}";do
  source $file;
done

# $1: label
# #2...: strings
_greenos_op_debug ()
{
    echo -ne \\n$1:
    shift
    for s ; do
      echo -ne " \"$s\""
    done
}

# this is needed to provide original "default completion" behavior.
# see "greenos-cfg" completion script for details.
_greenos_op_default_expand ()
{
  local wc=${#COMP_WORDS[@]}
  if [[ "${COMP_WORDS[0]}" =~ "/" ]]; then
    # if we are looking for a directory on the first completion then do directory completions
    _filedir_xspec_greenos
  elif (( wc < 2 )) ||
     [[ $COMP_CWORD -eq 0 ]] ||
     [[ $1 == $2 ]]; then
    _greenos_op_expand "$@"
  else
    # after the first word => cannot be greenos command so use original default
    _filedir_xspec_greenos
  fi
}

# $1: label
# $2...: help
_greenos_op_print_help ()
{
    local label=$1 help=$2
    if [ ${#label} -eq 0 ] ; then
     return
    elif [ ${#help} -eq 0 ] ; then
      echo -ne "\n  $label"
    elif [ ${#label} -lt 6 ] ; then
      echo -ne "\n  $label\t\t\t$help"
    elif [ ${#label} -lt 14 ] ; then
      echo -ne "\n  $label\t\t$help"
    elif [ ${#label} -lt 21 ] ; then
      echo -ne "\n  $label\t$help"
    else
      echo -ne "\n  $label\n\t\t\t$help"
    fi
}

# $1: $cur
# $2...: possible completions
_greenos_op_help ()
{
    local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
    shopt -u nullglob
    local cur=$1; shift
    local ndef node_tag_help node_run help last_help

    ndef=${_greenos_op_node_path}/node.tag/node.def
    [ -f $ndef ] && \
      node_tag_help=$( _greenos_op_get_node_def_field $ndef help )

    ndef=${_greenos_op_node_path}/node.def
    [ -f $ndef ] && \
      node_run=$( _greenos_op_get_node_def_field $ndef run )

    if [[ "$1" == "<nocomps>" ]]; then
       eval "$restore_shopts"
       return
    fi
    echo -en "\nPossible completions:"
    if [ -z "$cur" -a -n "$node_run" ]; then
       _greenos_op_print_help '<Enter>' "Execute the current command"
    fi
    if [ $# -eq 0 ];then
       _greenos_op_print_help '<text>' "$node_tag_help"
       eval "$restore_shopts"
       return
    fi
    for comp ; do
      if [[ "$comp" == "<Enter>" ]]; then
        continue
      fi
      if [ -z "$comp" ] ; then
        if [ "X$node_tag_help" == "X$last_help" ] ; then
          help=""
        else
          last_help=$node_tag_help
          help=$node_tag_help
        fi
        _greenos_op_print_help '*' "$help"
      elif [[ -z "$cur" || $comp == ${cur}* ]] ; then
        ndef=${_greenos_op_node_path}/$comp/node.def
        if [ -f $ndef ] ; then
          help=$( _greenos_op_get_node_def_field $ndef help )
        else
          help=$node_tag_help
        fi
        if [ "X$help" == "X$last_help" ] ; then
          help=""
        else
          last_help=$help
        fi
        _greenos_op_print_help "$comp" "$help"
      fi
    done
    eval "$restore_shopts"
}

_greenos_op_set_node_path ()
{
    local node
    _greenos_op_node_path=$greenos_op_templates
    for (( i=0 ; i<COMP_CWORD ; i++ )) ; do
        # expand the command so completion continues to work with short versions
        if [[ "${COMP_WORDS[i]}" == "*" ]]; then
          node="node.tag" # user defined wildcars are always tag nodes
        else
          node=$(_greenos_op_conv_node_path $_greenos_op_node_path ${COMP_WORDS[i]})
        fi
        if [ -f "${_greenos_op_node_path}/$node/node.def" ] ; then
          _greenos_op_node_path+=/$node
        elif [ -f ${_greenos_op_node_path}/node.tag/node.def ] ; then
          _greenos_op_node_path+=/node.tag
        else
          return 1
        fi
    done
}

_greenos_op_set_completions ()
{
    local -a allowed completions
    local cur=$1
    local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
    for ndef in ${_greenos_op_node_path}/*/node.def ; do
      if [[ $ndef == */node.tag/node.def ]] ; then
        local acmd=$( _greenos_op_get_node_def_field $ndef allowed )
        shopt -u extglob nullglob
        local -a a=($( eval "$acmd" ))
        eval "$restore_shopts"

        if [ ${#a[@]} -ne 0 ] ; then
          allowed+=( "${a[@]}" )
        else
          allowed+=( "<text>" )
        fi
      else
        local sdir=${ndef%/*}
        allowed+=( ${sdir##*/} )
      fi
    done

    # donot complete entries like <HOSTNAME> or <A.B.C.D>
    _greenos_op_noncompletions=( )
    completions=( )

    # make runable commands have a non-comp
    ndef=${_greenos_op_node_path}/node.def
    [ -f $ndef ] && \
      node_run=$( _greenos_op_get_node_def_field $ndef run )
    if [ -z "$cur" -a -n "$node_run" ]; then
      _greenos_op_noncompletions+=('<Enter>')
    fi

    for (( i=0 ; i<${#allowed[@]} ; i++ )) ; do
      if [[ "${allowed[i]}" == \<*\> ]] ; then
        _greenos_op_noncompletions+=( "${allowed[i]}" )
      else
        if [[ "$GREENOS_USER_LEVEL_DIR" == "/opt/greenos/etc/shell/level/admin" ]]; then
          completions+=( ${allowed[i]} )
        elif is_elem_of ${allowed[i]} _greenos_operator_allowed; then
          completions+=( ${allowed[i]} )
        elif [[ $_greenos_op_node_path == $greenos_op_templates ]];then
          continue
        else 
          completions+=( ${allowed[i]} )
        fi
      fi
    done
    
    # Prefix filter the non empty completions
    if [ -n "$cur" ]; then
      _greenos_op_completions=()
      get_prefix_filtered_list "$cur" completions _greenos_op_completions 
      _greenos_op_completions=($( printf "%s\n" ${_greenos_op_completions[@]} | sort -u ))
    else 
      _greenos_op_completions=($( printf "%s\n" ${completions[@]} | sort -u ))
    fi
    #shopt -s nullglob
}

_greenos_op_comprely_needs_ambiguity ()
{
    local -a uniq

    [ ${#COMPREPLY[@]} -eq 1 ] && return

    uniq=( `printf "%s\n" ${COMPREPLY[@]} | cut -c1 | sort -u` )

    [ ${#uniq[@]} -eq 1 ] && return
    false
}

_greenos_op_invalid_completion ()
{
      local tpath=$greenos_op_templates
      local -a args
      local i=1 
      for arg in "${COMP_WORDS[@]}"; do
        arg=( $(_greenos_op_conv_node_path $tpath $arg) )  # expand the arguments
        # output proper error message based on the above expansion
        if [[ "${arg[1]}" == "ambiguous" ]]; then
          echo -ne "\n\n  Ambiguous command: ${args[@]} [$arg]\n"
          local -a cmds=( $(compgen -d $tpath/$arg) )
          _greenos_op_node_path=$tpath
          local comps=$(_greenos_op_help $arg ${cmds[@]##*/})
          echo -ne "$comps" | sed -e 's/^P/  P/'
          break
        elif [[ "${arg[1]}" == "invalid" ]]; then
          echo -ne "\n\n  Invalid command: ${args[@]} [$arg]"
          break
        fi  

        if [ -f "$tpath/$arg/node.def" ] ; then
            tpath+=/$arg
        elif [ -f $tpath/node.tag/node.def ] ; then
            tpath+=/node.tag
        else
            echo -ne "\n\n  Invalid command: ${args[@]} [$arg]" >&2 
            break
        fi  
        args[$i]=$arg
        let "i+=1"
        if [ $[${#COMP_WORDS[@]}+1] -eq $i ];then
          _greenos_op_help "" \
            "${_greenos_op_noncompletions[@]}" \
            "${_greenos_op_completions[@]}" \
            | ${GREENOS_PAGER:-cat}
        fi
      done
}

_greenos_op_expand ()
{
    # We need nospace here and we have to append our own spaces
    compopt -o nospace

    local restore_shopts=$( shopt -p extglob nullglob | tr \\n \; )
    shopt -s extglob nullglob
    local cur=""
    local _has_comptype=0
    local current_prefix=$2
    local current_word=$3
    _greenos_comptype=""

    if (( ${#COMP_WORDS[@]} > 0 )); then
        cur=${COMP_WORDS[COMP_CWORD]}
    else
        (( COMP_CWORD = ${#COMP_WORDS[@]} ))
    fi

    if _greenos_pipe_completion "${COMP_WORDS[@]}"; then
      if [ "${COMP_WORDS[*]}" == "$_greenos_op_last_comp" ] ||
         [ ${#_greenos_pipe_completions[@]} -eq 0 ]; then
          _greenos_do_pipe_help
          COMPREPLY=( "" " " )
          _greenos_op_last_comp=${_greenos_op_last_comp_init}
      else
          COMPREPLY=( "${_greenos_pipe_completions[@]}" )
          _greenos_op_last_comp="${COMP_WORDS[*]}"
          if [ ${#COMPREPLY[@]} -eq 1 ]; then
             COMPREPLY=( "${COMPREPLY[0]} " )
          fi
      fi
      eval "$restore_shopts"
      return
    fi

    # this needs to be done on every completion even if it is the 'same' comp.
    # The cursor can be at different places in the string. 
    # this will lead to unexpected cases if setting the node path isn't attempted
    # each time.
    if ! _greenos_op_set_node_path ; then
      echo -ne \\a
      _greenos_op_invalid_completion
      COMPREPLY=( "" " " )
      eval "$restore_shopts"
      return 1
    fi
    
    if [ "${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}" != "$_greenos_op_last_comp" ] ; then
        _greenos_set_comptype
        case $_greenos_comptype in
          'imagefiles') 
              _has_comptype=1
              _greenos_image_file_complete
          ;;
          *)
              _has_comptype=0
              if [[ -z "$current_word" ]]; then
                _greenos_op_set_completions $cur
              else
                _greenos_op_set_completions $current_prefix
              fi
          ;;
        esac
    fi
    if [[ $_has_comptype == 1 ]]; then
      COMPREPLY=( "${_greenos_op_completions[@]}" )
    else
      COMPREPLY=($( compgen -W "${_greenos_op_completions[*]}" -- $current_prefix ))
    fi

    # if the last command line arg is empty and we have
    # an empty completion option (meaning wild card),
    # append a blank(s) to the completion array to force ambiguity
    if [ -z "$current_prefix" -a -n "$current_word" ] ||
       [[ "${COMPREPLY[0]}" =~ "$cur" ]]; then
      for comp ; do
        if [ -z "$comp" ] ; then
          if [ ${#COMPREPLY[@]} -eq 0 ] ; then
            COMPREPLY=( " " "" )
          elif _greenos_op_comprely_needs_ambiguity ; then
            COMPREPLY+=( " " )
          fi
        fi
      done
    fi
    # Set this environment to enable and disable debugging on the fly
    if [[ $DBG_OP_COMPS -eq 1 ]]; then
      echo -e "\nCurrent: '$cur'"
      echo -e "Current word: '$current_word'"
      echo -e "Current prefix: '$current_prefix'"
      echo "Number of comps: ${#_greenos_op_completions[*]}"
      echo "Number of non-comps: ${#_greenos_op_noncompletions[*]}"
      echo "_greenos_op_completions: '${_greenos_op_completions[*]}'"
      echo "COMPREPLY: '${COMPREPLY[@]}'"
      echo "CWORD: $COMP_CWORD"
      echo "Last comp: '$_greenos_op_last_comp'"
      echo -e "Current comp: '${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}'\n"
    fi

    # This is non obvious... 
    # To have completion continue to work when working with words that aren't the last word,
    # we have to set nospace at the beginning of this script and then append the spaces here.
    if [ ${#COMPREPLY[@]} -eq 1 ] && 
       [[ $_has_comptype -ne 1 ]]; then
       COMPREPLY=( "${COMPREPLY[0]} " )
    fi
    # if there are no completions then handle invalid commands
    if [ ${#_greenos_op_noncompletions[@]} -eq 0 ] &&
       [ ${#_greenos_op_completions[@]} -eq 0 ]; then
          _greenos_op_invalid_completion 
          COMPREPLY=( "" " " )
          _greenos_op_last_comp=${_greenos_op_last_comp_init}
    elif [ ${#COMPREPLY[@]} -eq 0 ] &&
         [ -n "$current_prefix" ]; then
          _greenos_op_invalid_completion 
          COMPREPLY=( "" " " )
          _greenos_op_last_comp=${_greenos_op_last_comp_init}
    # Stop completions from getting stuck
    elif [ ${#_greenos_op_completions[@]} -eq 1 ] &&
         [ -n "$cur" ] &&
         [[ "${COMPREPLY[0]}" =~ "$cur" ]]; then
          _greenos_op_last_comp=${_greenos_op_last_comp_init}
    elif [ ${#_greenos_op_completions[@]} -eq 1 ] &&
         [ -n "$current_prefix" ] &&
         [[ "${COMPREPLY[0]}" =~ "$current_prefix" ]]; then
          _greenos_op_last_comp=${_greenos_op_last_comp_init}
    # if there are no completions then always show the non-comps
    elif [ "${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}" == "$_greenos_op_last_comp" ] || 
         [ ${#_greenos_op_completions[@]} -eq 0 ] ||
         [ -z "$cur" ]; then
          _greenos_op_help "$current_prefix" \
            "${_greenos_op_noncompletions[@]}" \
            "${_greenos_op_completions[@]}" \
            | ${GREENOS_PAGER:-cat}
          COMPREPLY=( "" " " )
          _greenos_op_last_comp=${_greenos_op_last_comp_init}
    else
      _greenos_op_last_comp="${COMP_WORDS[*]:0:$[$COMP_CWORD+1]}"
    fi

    eval "$restore_shopts"
}

# "pipe" functions
count ()
{
  wc -l
}

match ()
{
  grep -E -e "$1"
}

no-match ()
{
  grep -E -v -e "$1"
}

no-more ()
{
  cat
}

strip-private ()
{
  ${greenos_libexec_dir}/strip-private.py
}

commands ()
{
  if [ "$_OFR_CONFIGURE" != "" ]; then
      if $(cli-shell-api sessionChanged); then
          echo "You have uncommited changes, please commit them before using the commands pipe"
      else
          greenos-config-to-commands
      fi
  else
      echo "commands pipe is not supported in operational mode"
  fi
}

json ()
{
  if [ "$_OFR_CONFIGURE" != "" ]; then
      if $(cli-shell-api sessionChanged); then
          echo "You have uncommited changes, please commit them before using the JSON pipe"
      else
          greenos-config-to-json
      fi
  else
      echo "JSON pipe is not supported in operational mode"
  fi
}

# pipe command help
# $1: command
_greenos_pipe_help ()
{
  local help="No help text available"
  case "$1" in
    count) help="Count the number of lines in the output";;
    match) help="Only output lines that match specified pattern";;
    no-match) help="Only output lines that do not match specified pattern";;
    more) help="Paginate the output";;
    no-more) help="Do not paginate the output";;
    strip-private) help="Remove private information from the config";;
    commands) help="Convert config to set commands";;
    json) help="Convert config to JSON";;
    '<pattern>') help="Pattern for matching";;
  esac
  echo -n "$help"
}

_greenos_do_pipe_help ()
{
  local help=''
  if (( ${#_greenos_pipe_completions[@]} + ${#_greenos_pipe_noncompletions[@]}
       == 0 )); then
    return
  fi
  echo -en "\nPossible completions:"
  for comp in "${_greenos_pipe_completions[@]}" \
              "${_greenos_pipe_noncompletions[@]}"; do
    _greenos_op_print_help "$comp" "$(_greenos_pipe_help "$comp")"
  done
}

# pipe completion
# $@: words
_greenos_pipe_completion ()
{
  local -a pipe_cmd=()
  local -a all_cmds=( 'count' 'match' 'no-match' 'more' 'no-more' 'strip-private' 'commands' 'json' )
  local found=0
  _greenos_pipe_completions=()
  _greenos_pipe_noncompletions=()

  for word in "$@"; do
    if [[ "$found" == "1" || "$word" == "|" ]]; then
      pipe_cmd+=( "$word" )
      found=1
    fi
  done
  if (( found == 0 )); then
    return 1
  fi
  if (( ${#pipe_cmd[@]} == 1 )); then
    # "|" only
    _greenos_pipe_completions=( "${all_cmds[@]}" )
    return 0
  fi
  if (( ${#pipe_cmd[@]} == 2 )); then
    # "|<space, chars, or space+chars>"
    _greenos_pipe_completions=($(compgen -W "${all_cmds[*]}" -- ${pipe_cmd[1]}))
    return 0
  fi
  if (( ${#pipe_cmd[@]} == 3 )); then
    # "|<chars or space+chars><space or space+chars>"
    case "${pipe_cmd[1]}" in
      match|no-match) _greenos_pipe_noncompletions=( '<pattern>' );;
    esac
    return 0
  fi
  return 0
}

# comptype
_greenos_set_comptype () 
{
  local comptype
  unset _greenos_comptype
  for ndef in ${_greenos_op_node_path}/*/node.def ; do
    if [[ $ndef == */node.tag/node.def ]] ; then
      local comptype=$( _greenos_op_get_node_def_field $ndef comptype )
      if [[ $comptype == "imagefiles" ]] ; then
        _greenos_comptype=$comptype
        return 0
      else
        _greenos_comptype=""
        return 1
      fi
    else
      _greenos_comptype=""
      return 1
    fi
  done
}

_filedir_xspec_greenos()
{
    local cur prev words cword
    _init_completion || return

    _tilde "$cur" || return 0

    local IFS=$'\n' xspec=${_xspec[${1##*/}]} tmp
    local -a toks

    toks=( $(
        compgen -d -- "$(quote_readline "$cur")" | {
        while read -r tmp; do
            printf '%s\n' $tmp
        done
        }
        ))

    # Munge xspec to contain uppercase version too
    # http://thread.gmane.org/gmane.comp.shells.bash.bugs/15294/focus=15306
    eval xspec="${xspec}"
    local matchop=!
    if [[ $xspec == !* ]]; then
        xspec=${xspec#!}
        matchop=@
    fi
    xspec="$matchop($xspec|${xspec^^})"

    toks+=( $(
        eval compgen -f -X "!$xspec" -- "\$(quote_readline "\$cur")" | {
        while read -r tmp; do
            [[ -n $tmp ]] && printf '%s\n' $tmp
        done
        }
        ))

    if [[ ${#toks[@]} -ne 0 ]]; then
        compopt -o filenames
        COMPREPLY=( "${toks[@]}" )
    fi
}

nullglob_save=$( shopt -p nullglob )
shopt -s nullglob
for f in ${greenos_datadir}/greenos-op/functions/allowed/* ; do
    source $f
done
eval $nullglob_save
unset nullglob_save

# don't initialize if we are in configure mode
if [ "$_OFR_CONFIGURE" == "ok" ]; then
  return 0
fi

if [[ "$GREENOS_USER_LEVEL_DIR" != "/opt/greenos/etc/shell/level/admin" ]]; then
  greenos_unpriv_init $@
else
  _greenos_op_init $@
fi

###  Local Variables:
###  mode: shell-script
###  End:
