#!/usr/bin/env bash
## source: https://github.com/kdabir/has

## To add more tools, search for "__detect" function in this file

## Important so that version is not extracted for failed commands (not found)
set -o pipefail

readonly BINARY_NAME="has"
readonly VERSION="v1.4.0"

## constants - symbols for success failure
readonly txtreset="$(tput sgr0)"
readonly txtbold="$(tput bold)"
readonly txtblack="$(tput setaf 0)"
readonly txtred="$(tput setaf 1)"
readonly txtgreen="$(tput setaf 2)"
readonly txtyellow="$(tput setaf 3)"
readonly txtblue="$(tput setaf 4)"
readonly txtpurple="$(tput setaf 5)"
readonly txtcyan="$(tput setaf 6)"
readonly txtwhite="$(tput setaf 7)"
# unicode "✗"
readonly fancyx='\342\234\227'
# unicode "✓"
readonly checkmark='\342\234\223'
# PASS="\e[1m\e[38;5;2m✔\e[m"
# FAIL="\e[1m\e[38;5;1m✘\e[m"
readonly PASS="${txtbold}${txtgreen}${checkmark}${txtreset}"
readonly FAIL="${txtbold}${txtred}${fancyx}${txtreset}"

## These variables are used to keep track of passed and failed commands
OK=0
KO=0

## Regex to extract simple version - extracts numeric sem-ver style versions
REGEX_SIMPLE_VERSION="([[:digit:]]+\.?){2,3}"

## name of RC file that can contain list of tools to be checked
RC_FILE=".hasrc"

# try to extract version by executing "${1}" with "${2}" arg
# command name to be executed is dynamically passed to this function as ${1}
# arg to ${1} is passed in ${2}
__dynamic_detect(){
  cmd="${1}"
  params="${2}"
  version=$( eval "${cmd}" "${params}" "2>&1" | grep -Eo "${REGEX_SIMPLE_VERSION}" | head -1)
  status=$?
}

# commands that use `--version` flag
__dynamic_detect--version(){
  __dynamic_detect "${1}" "--version"
}

# commands that use `-version` flag
__dynamic_detect-version(){
  __dynamic_detect "${1}" "-version"
}

# commands that use `-v` flag
__dynamic_detect-v(){
  __dynamic_detect "${1}" "-v"
}

# commands that use `-V` flag
__dynamic_detect-V(){
  __dynamic_detect "${1}" "-V"
}

# commands that use `version` argument
__dynamic_detect-arg_version(){
  __dynamic_detect "${1}" "version"
}

## the main function
__detect(){
  name="${1}"

  # setup aliases - maps commonly used name to exact command name
  case ${name} in
    golang                ) command="go"            ;;
    jre                   ) command="java"          ;;
    jdk                   ) command="javac"         ;;
    nodejs                ) command="node"          ;;
    goreplay              ) command="gor"           ;;
    httpie                ) command="http"          ;;
    homebrew              ) command="brew"          ;;
    awsebcli              ) command="eb"            ;;
    awscli                ) command="aws"           ;;
    postgresql            ) command="psql"          ;;
    *coreutils|linux*utils) command="gnu_coreutils" ;;
    *                     ) command=${name}         ;;
  esac

  case "${command}" in

    # commands that need --version flag

    ## Shells
    bash|zsh)               __dynamic_detect--version "${command}" ;;

    ## VCS
    git|hg|svn|bzr)         __dynamic_detect--version "${command}" ;;

    ## Http
    curl|wget|http)         __dynamic_detect--version "${command}" ;;

    ## Editors
    vim|emacs|nano)         __dynamic_detect--version "${command}" ;;
    subl|code)              __dynamic_detect--version "${command}" ;;

    ## File system search and navigation
    jq)                     __dynamic_detect--version "${command}" ;;
    ag|ack|rg)              __dynamic_detect--version "${command}" ;;
    tree|autojump)          __dynamic_detect--version "${command}" ;;

    ## package mangers
    apt|apt-get|aptitude)   __dynamic_detect--version "${command}" ;;
    brew)                   __dynamic_detect--version "${command}" ;;

    ## System tools
    sed|awk|grep|file|sudo) __dynamic_detect--version "${command}" ;;
    gzip|xz|unar|bzip2)     __dynamic_detect--version "${command}" ;;
    tar|pv)                 __dynamic_detect--version "${command}" ;;

    # Container runtimes
    docker|podman)          __dynamic_detect--version "${command}" ;;
    
    ## Database CLI
    psql)                   __dynamic_detect--version "${command}" ;;

    ## Scripting Language / runtime
    ruby|R|python|python3)  __dynamic_detect--version "${command}" ;;
    perl|perl6|php|php5)    __dynamic_detect--version "${command}" ;;
    groovy|node)            __dynamic_detect--version "${command}" ;;

    ## Compile
    gcc|make|bats)          __dynamic_detect--version "${command}" ;;

    ## Build tools
    lein|gradle|mvn)        __dynamic_detect--version "${command}" ;;
    grunt|brunch)           __dynamic_detect--version "${command}" ;;
    gem|rake|bundle)        __dynamic_detect--version "${command}" ;;
    npm|yarn)               __dynamic_detect--version "${command}" ;;


    ## Cloud Tools
    aws|eb|sls|gcloud)      __dynamic_detect--version "${command}" ;;
    heroku)                 __dynamic_detect--version "${command}" ;;
    netlify)                __dynamic_detect--version "${command}" ;;
    netlifyctl)             __dynamic_detect-arg_version "${command}" ;;


    # commands that need -v flag
    unzip)                  __dynamic_detect-v "${command}" ;;

    # commands that need -V flag
    ab)                     __dynamic_detect-V "${command}" ;;

    # commands that need -version flag
    ant)                               __dynamic_detect-version "${command}" ;;
    java|javac|scala|kotlin)           __dynamic_detect-version "${command}" ;;

    # commands that need version arg
    go|hugo)                __dynamic_detect-arg_version "${command}" ;;

    ## Example of commands that need custom processing

    ## TODO cleanup, currently need to add extra space in regex, otherwise the time gets selected
    gulp)
      version=$( gulp --version 2>&1| grep -Eo " ${REGEX_SIMPLE_VERSION}" | head -1)
      status=$?
      ;;

    ## gor returns version but does not return normal status code, hence needs custom processing
    gor)
      version=$( gor version 2>&1 | grep -Eo "${REGEX_SIMPLE_VERSION}" | head -1)
      if [ $? -eq 1 ]; then status=0; else status=127; fi
      ;;

    sbt)
      version=$( sbt about 2>&1 | grep -Eo "([[:digit:]]{1,4}\.){2}[[:digit:]]{1,4}" | head -1)
      status=$?
      ;;

    ## use 'readlink' to test for GNU coreutils
    # readlink (GNU coreutils) 8.28
    gnu_coreutils)    __dynamic_detect--version readlink ;;

    ## hub uses --version but version string is on second line, or third if HUB_VERBOSE set
    hub)
      version=$( HUB_VERBOSE='' hub --version 2>&1 | sed -n 2p | grep -Eo "${REGEX_SIMPLE_VERSION}" | head -1)
      status=$?
      ;;

    ## zip uses -v but version string is on second line
    zip)
      version=$( zip -v 2>&1 | sed -n 2p | grep -Eo "${REGEX_SIMPLE_VERSION}" | head -1)
      status=$?
      ;;

    has)
      version=$( has 2>&1 | grep -Eo "${REGEX_SIMPLE_VERSION}" | head -1)
      status=$?
      ;;

    *)
      ## Can allow dynamic checking here, i.e. checking commands that are not listed above
      if [[ "${HAS_ALLOW_UNSAFE}" == "y" ]]; then
        __dynamic_detect--version "${command}"
        ## fallback checking based on status!=127 (127 means command not found)
        ## TODO can check other type of supported version-checks if status was not 127
      else
        ## -1 is special way to tell command is not supported/whitelisted by `has`
        status="-1"
      fi
      ;;
  esac

  if [ "$status" -eq "-1" ]; then     ## When unsafe processing is not allowed, the -1 signifies
    printf '%b %s not understood\n' "${FAIL}" "${command}"
    KO=$(( KO+1 ))

  elif [ ${status} -eq 127 ]; then    ## command not installed
    printf '%b %s\n' "${FAIL}" "${command}"
    KO=$(( KO+1 ))

  elif [ ${status} -eq 0 ] || [ ${status} -eq 141 ]; then      ## successfully executed
    printf "%b %s %b\n" "${PASS}" "${command}" "${txtbold}${txtyellow}${version}${txtreset}"
    OK=$(( OK+1 ))

  else  ## as long as its not 127, command is there, but we might not have been able to extract version
    printf '%b %s\n' "${PASS}" "${command}"
    OK=$(( OK+1 ))
  fi
} #end __detect

if [ -s "${RC_FILE}" ];  then
  HASRC="true"
fi

# if HASRC is not set AND no arguments passed to script
if [[ -z "${HASRC}" ]] && [ "$#" -eq 0 ]; then
  # print help
  printf '%s %s\n' "${BINARY_NAME}" "${VERSION}"
  printf 'USAGE:\t %s <command-names>..\n' "${BINARY_NAME}"
  printf 'EXAMPLE: %s git curl node\n' "${BINARY_NAME}"

else
  # for each cli-arg
  for cmd in "$@"; do
    __detect "${cmd}"
  done

  ## display found / total
  #  echo  ${OK} / $(($OK+$KO))

  ## if HASRC has been set
  if [[ -n "${HASRC}" ]]; then
    ## for all
    while read -r line; do
      __detect "${line}"
    done <<<"$( grep -Ev "^\s*(#|$)" "${RC_FILE}" )"
  fi

  ## max status code that can be returned
  MAX_STATUS_CODE=126

  if [[ "${KO}" -gt "${MAX_STATUS_CODE}" ]]; then
    exit "${MAX_STATUS_CODE}"
  else
    exit "${KO}"
  fi

fi
