# A collection of commands to make it easier to build projects.

# Default `build` alias to select a `build-dir` then invoke a build, using an
# alias means the configured build command's completion works out of the box.
alias build="build-dir --build"

# Detect installed debugger and set the `debug` alias to debug a program with
# command line arguments.
if [ `uname` = Linux ]; then
  autoload -U regexp-replace
  function vimdebug() {
    # For each item in $* replace * and \* and then replace \ with \\
    local args=()
    for arg in "$@"; do
      regexp-replace arg '\*' '\\*'
      args+=($arg)
    done
    nvim "+packadd termdebug" "+TermdebugCommand $args"
  }
  if command -v nvim &> /dev/null; then
    alias debug=vimdebug
  elif command -v gdb &> /dev/null; then
    alias debug='gdb --args'
  fi
elif [ `uname` = Darwin ]; then
  command -v lldb &> /dev/null && \
    alias debug='lldb --'
fi

# Interactively choose a `~build` directory for `build` to build.
build-dir() {
  local usage='usage: build-dir [-h] [--build] [<directory>]'
  local -a help show do_build
  zparseopts -D h=help -help=help s=show -show=show -build=do_build
  if [[ -n $help ]]; then
    cat << EOF
$usage

Find and select the current build directory interactively.

positional arguments:
  <directory> the build directory to select

optional arguments:
  -h, --help  show this help message and exit
  -s, --show  show the current build directory
  --build     invoke a build after selection
EOF
    return
  fi
  error() { echo "\e[31merror:\e[0m $1" }
  warning() { echo "\e[33mwarning:\e[0m $1" }
  if [[ -n $show ]]; then
    if [[ ! -n $build_dir ]]; then
      error "build directory not set"
      return 1
    else
      echo "$build_dir"
      return
    fi
  fi
  local local_build_dir
  if [[ ${#*} -gt 1 ]]; then
    echo $usage
    error "unexpected position arguments: ${*[2,${#*}]}"; return 1
  elif [[ ${#*} -eq 1 ]]; then
    if [[ ! -d ${*[1]} ]]; then
      warning "directory not found: ${*[1]}"
    else
      local_build_dir=${*[1]}
    fi
  fi

  # If <directory> was not set begin selection
  if [[ -z $local_build_dir ]]; then
    # Find build directories
    local -a local_build_dirs
    for entry in `ls -A`; do
      [ -d $entry ] && [[ $entry =~ build* ]] && \
        local_build_dirs+=${entry/\//}
    done

    # Interactively select a build directory if more than 1 found
    integer index=0
    if [[ ${#local_build_dirs} -eq 0 ]]; then
      error "no build directories found"; return 1
    elif [[ ${#local_build_dirs} -eq 1 ]]; then
      local_build_dir=${local_build_dirs[1]}
    elif [[ ${#local_build_dirs} -gt 1 ]]; then
      if command -v fzf &> /dev/null; then
        # Use fzf to select a build directory
        local max=$(( $( tput lines ) / 2 ))
        local best=$(( ${#local_build_dirs} + 3 ))
        local_build_dir=$(
          printf '%s\n' "${local_build_dirs[@]}" |
          fzf --layout=reverse --tac --info=hidden --border=rounded \
              --height=$(( $best < $max ? $best : $max ))
        )
        if [[ $? -ne 0 ]]; then
          return 1
        fi
      else
        # Fallback to zcurses selector when fzf is not available
        zmodload zsh/curses && {
          # Get the size of the terminal
          local size=`stty size`
          integer height=${size% *}
          integer width=${size#* }

          # Create the window and hide the cursor
          zcurses init
          zcurses addwin build-dir $height $width 0 0

          # Hide the cursor for zcurses, trap SIGINT to ensure cleanup in
          # always-list occurs below
          tput civis; trap 'return 130' INT

          # Enter display loop
          local key keypad
          while (( 1 )); do
            zcurses clear build-dir

            # Add the prompt text
            zcurses move build-dir 1 1
            zcurses string build-dir 'Select a build directory:'

            # Add the selections text
            for (( i = 0; i < ${#local_build_dirs}; i++ )); do
              integer line=$i+3
              zcurses move build-dir $line 1
              [[ $index -eq $i ]] &&
                zcurses string build-dir "* " ||
                zcurses string build-dir "  "
              zcurses string build-dir ${local_build_dirs[$i+1]}
            done

            # Display the text the and wait for input
            zcurses refresh build-dir
            zcurses input build-dir key keypad

            # Handle user input
            case $key in
              (UP|k|$'\C-P')
                [[ $index -gt 0 ]] && index=$index-1 ;;
              (DOWN|j|$'\C-N')
                [[ $index -lt ${#local_build_dirs}-1 ]] && index=$index+1 ;;
              (ENTER|$'\n')
                break ;;
            esac
          done
        } always {
          # Restore the cursor and cleanup zcurses
          tput cvvis; tput cnorm
          zcurses delwin build-dir
          zcurses end
        }

        # On success setup the build directory for use
        if [[ $? -eq 0 ]]; then
          # Set the build directory from selection if empty
          [[ -z $local_build_dir ]] && \
            local_build_dir=${local_build_dirs[$index+1]}
        fi
      fi
    fi
  fi

  # If `build.ninja` exists in alias `ninja`, return.
  local build
  [ -f $local_build_dir/build.ninja ] && \
    build="ninja -C $local_build_dir"

  # If `Makefile` exists in alias `make`, return.
  if [ -f $local_build_dir/Makefile ]; then
    [ `uname` = Darwin ] && \
      local cpu_count=`sysctl -n hw.ncpu` ||
      local cpu_count=`grep -c '^processor' /proc/cpuinfo`
    build="make -j $cpu_count -C $local_build_dir"
  fi

  # If the build variable is not defined the command could not be determined
  if [ -z $build ]; then
    warning "build command detection failed: $local_build_dir"
    # Prompt user to enter a build command
    vared -p 'enter comand: ' build
  fi

  # Redefine the `build` alias and update the `~build` hash directory
  alias build="$build"
  hash -d build=$local_build_dir
  export build_dir=$local_build_dir

  # If `--build` is specified then evaluate the command.
  if [[ -n $do_build ]]; then
    eval build
  fi
}

# Build then run a target residing in `~build/bin`.
build-run() {
  local target=$1; shift 1
  eval build $target && ~build/bin/$target "$@"
}

# Build then debug a target residing in `~build/bin`.
build-debug() {
  local target=$1; shift 1
  eval build $target && debug ~build/bin/$target "$@"
}