# Automatically update the environment when the current working directory
# changes, this is a reimplementation of the ideas found in the repository
# https://github.com/Tarrasch/zsh-autoenv stripped down to bare essentials.
#
# The secret sauce can be found at the bottom of this file, where the chpwd
# hook function _autoenv_chpwd is added.

# The autoenv command provides a convenient way to create, edit, and remove
# enter and exit scripts in the current directory.
autoenv() {
  local cmd=$1
  case "$cmd" in
    -h|--help)  # Display help.
      echo "\
usage: autoenv [-h] {init,edit,deinit}

options:
        -h, --help  show this help message and exit

commands:
        init        add .enter and .exit scripts in current directory
        edit        edit .enter and .exit scripts in current directory
        deinit      remove .enter and .exit scripts in current directory"
      ;;

    init)  # Create enter and exit scripts in current directory.
      if [ -f $PWD/.enter ] || [ -f $PWD/.exit ]; then
        echo '.enter or .exit already exists'; return 1
      fi
      # Create then edit enter and exit scripts.
      touch .enter .exit && autoenv edit
      # If enter script exists, authorize it.
      [ -f $PWD/.enter ] && _autoenv_authorized $PWD/.enter yes
      # If exit script exists, authorize it.
      [ -f $PWD/.exit ] && _autoenv_authorized $PWD/.exit yes
      # Enter the new autoenv.
      _autoenv_enter $PWD ;;

    edit)  # Edit enter and exit scripts in current directory.
      if ! [ -f $PWD/.enter ] || ! [ -f $PWD/.exit ]; then
        echo '.enter or .exit not found'; return 1
      fi
      # If vim exists, edit enter and exit scripts.
      if which vim &> /dev/null; then
        vim -p $PWD/.enter $PWD/.exit
      else
        echo 'vim not found'; return 1
      fi ;;

    deinit)  # Remove enter and exit scripts in current directory.
      if [ -f $PWD/.enter ] || [ -f $PWD/.exit ]; then
        # Prompt user to confirm removal of enter and exit scripts.
        while true; do
          read "answer?Are you sure [y/N]? "
          case "$answer" in
            y|Y|yes)
              # Remove enter and exit scripts if they exist.
              [ -f $PWD/.enter ] && rm $PWD/.enter
              [ -f $PWD/.exit ] && rm $PWD/.exit
              break ;;
            *) break ;;
          esac
        done
      else
        echo '.enter and .exit not found'; return 1
      fi ;;

    *)  # Invalid arguments, show help then error.
      echo "invalid arguments: $@"
      autoenv --help
      return 1 ;;
  esac
}

# Global entered directories array.
_autoenv_entered=()

# Load zstat from stat module for inspecting modified time.
zmodload -F zsh/stat b:zstat

# Check if the given file is authorized, if not prompt the user to authorize,
# ignore, or view the file. Authorized files and their modified times are
# stored in the ~/.cache/autoenv/authorized file to make authorization
# persistent.
_autoenv_authorized() {
  local file=$1 yes=$2
  # If autoenv cache directory does not exist, create it.
  ! [ -d ~/.cache/autoenv ] && mkdir -p ~/.cache/autoenv
  # If the authorized file does not exist, create it.
  ! [ -f ~/.cache/autoenv/authorized ] && touch ~/.cache/autoenv/authorized
  # Load the authorized file into a map of authorized key value pairs.
  typeset -A authorized=(`cat ~/.cache/autoenv/authorized`)
  # If the file has been removed, return.
  ! [ -f $file ] && return 1
  # If the given file has been authorized, i.e. the modified time matches that
  # held in the authorized file, return.
  local modified_time=`zstat +mtime $file`
  [ "$authorized[$file]" = "$modified_time" ] && return
  # If yes, don't prompt for user confirmation.
  if [ "$yes" != "yes" ]; then
    # Prompt to authorize file.
    while true; do
      read "answer?Authorize $file [Y/n/v]? "
      case "$answer" in
        y|Y|yes|'') break ;;    # Authorize the file.
        n|N|no) return 1 ;;     # Do not authorize the file.
        v|V|view) cat $file ;;  # View the file.
      esac
    done
  fi
  # Add file to the authorized map.
  authorized[$file]=$modified_time
  # Store authorized map in authorized file.
  echo ${(kv)authorized} > ~/.cache/autoenv/authorized
}

# Source an enter script and add its directory to the global entered
# directories array.
_autoenv_enter() {
  local entered=$1
  # If entered exists in the entered directories array, return.
  (( ${+_autoenv_entered[${_autoenv_entered[(i)$entered]}]} )) && return
  # If the enter script is not authorized, return.
  _autoenv_authorized $entered/.enter || return
  # Source the enter script.
  source $entered/.enter
  # Add the entered directory to the global entered array.
  _autoenv_entered+=$entered
}

# Source an exit script and remove its directory from the global entered
# directories array.
_autoenv_exit() {
  local entered=$1
  # If the exit script is not authorized, return.
  _autoenv_authorized $entered/.exit || return
  # Source the exit script.
  source $entered/.exit
  # Remove the entered directory from the global entered array.
  _autoenv_entered[${_autoenv_entered[(i)$entered]}]=()
}

# Find all directories containing a .enter file by searching up the directory
# tree starting in the current directory.
_autoenv_find_enter_directories() {
  local current=$PWD
  # If an enter script is found in the current directory, return it.
  [ -f $current/.enter ] && echo $current
  # Loop until an enter script or the root directory is found.
  while true; do
    # Go up one directory and make the path absolute.
    local next=$current/..; local next=${next:A}
    # If an enter script is found in the current directory, return it.
    [ -f $next/.enter ] && echo $next
    # If the current directory equals the next directory we are done, otherwise
    # update the current directory.
    [[ $current == $next ]] && return || local current=$next
  done
}

# A chpwd hook function which automatically sources enter and exit scripts to
# setup local environments for directory and its subdirectories.
_autoenv_chpwd() {
  local entered
  # Loop over the reversed entered directory array.
  for entered in ${(aO)_autoenv_entered}; do
    # If the the current directory was previously entered then exit.
    ! [[ $PWD/ == $entered/* ]] && _autoenv_exit $entered
  done
  # Find all enter script directories, store them in an array.
  local enter_dirs=(`_autoenv_find_enter_directories`)
  # Loop over reversed enter script directories array, so enter scripts found
  # last are sourced first, then source all enter scripts.
  for enter in ${(aO)enter_dirs}; do _autoenv_enter $enter; done
}

# Register the autoenv chpwd hook.
autoload -U add-zsh-hook
add-zsh-hook chpwd _autoenv_chpwd

# Ensure autoenv is activated in the current directory on first load.
_autoenv_chpwd