#!/usr/bin/env bash
#          _                       __
#   ____ _(_)________  ____  _____/ /_
#  / __ `/ / ___/ __ \/ __ \/ ___/ __/
# / /_/ / / /  / /_/ / /_/ / /  / /_
# \__,_/_/_/  / .___/\____/_/   \__/
#            /_/
#
# A command line interface for Wi-Fi on macOS / OS X.
#
# https://github.com/xwmx/airport
#
# Based on Bash Boilerplate: https://github.com/xwmx/bash-boilerplate
#
# Copyright (c) 2015 William Melody • hi@williammelody.com

###############################################################################
# Strict Mode
###############################################################################

set -o nounset
set -o errexit
set -o pipefail
IFS=$'\n\t'

###############################################################################
# Environment
###############################################################################

# $_VERSION
#
# The current program version.
_VERSION="0.7.4"

# $_ME
#
# Set to the program's basename.
_ME=$(basename "${0}")

# $_AIRPORT_FRAMEWORK_PATH
#
# The path to Apple's AirPort Wi-Fi framework on the local system.
_AIRPORT_FRAMEWORK_PATH="/System/Library/PrivateFrameworks/Apple80211.framework"

# $_AIRPORT_CMD
#
# The path to the `airport` command line tool.
_AIRPORT_CMD="${_AIRPORT_FRAMEWORK_PATH}/Versions/A/Resources/airport"

# $_NETWORKSETUP_CMD
#
# shellcheck disable=SC2230
# https://github.com/koalaman/shellcheck/wiki/SC2230
# See: "SC2230 - command -v is not a direct replacement for which #1162"
#   https://github.com/koalaman/shellcheck/issues/1162
_NETWORKSETUP_CMD="$(which networksetup)"

# $_WIFI_INTERFACE
#
# Command references:
# - http://apple.stackexchange.com/a/90516
# - https://jamfnation.jamfsoftware.com/discussion.html?id=4849
_WIFI_INTERFACE="$(
  "${_NETWORKSETUP_CMD}" -listallhardwareports \
    | awk '/Wi-Fi|AirPort/ {getline; print $NF}'
)"

###############################################################################
# Debug
###############################################################################

# _debug()
#
# Usage:
#   _debug printf "Debug info. Variable: %s\n" "$0"
#
# A simple function for executing a specified command if the `$_USE_DEBUG`
# variable has been set. The command is expected to print a message and
# should typically be either `echo`, `printf`, or `cat`.
__DEBUG_COUNTER=0
_debug() {
  if [[ "${_USE_DEBUG:-"0"}" -eq 1 ]]
  then
    __DEBUG_COUNTER=$((__DEBUG_COUNTER+1))
    # Prefix debug message with "bug (U+1F41B)"
    printf "🐛  %s " "${__DEBUG_COUNTER}"
    "${@}"
    printf "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n"
  fi
}
# debug()
#
# Usage:
#   debug "Debug info. Variable: $0"
#
# Print the specified message if the `$_USE_DEBUG` variable has been set.
#
# This is a shortcut for the _debug() function that simply echos the message.
debug() {
  _debug echo "${@}"
}

###############################################################################
# Warn
###############################################################################

# _warn()
#
# Usage:
#   _warn <command>
#
# Description:
#   Print the specified command with output redirected to standard error.
#   The command is expected to print a message and should typically be either
#   `echo`, `printf`, or `cat`.
_warn() {
  {
    printf "%s " "$(tput setaf 1)!$(tput sgr0)"

    "${@}"
  } 1>&2
}

###############################################################################
# Version Check
###############################################################################

_MACOS_VERSION=("$(sw_vers -productVersion | awk -F. '{print $1 $2}')")

if [[ "${_MACOS_VERSION[0]}" -ge "14" ]] &&
   [[ "${_MACOS_VERSION[0]}" -ge "4"  ]]
then
  _warn printf \
    "Airport command line access is no longer available as of macOS Sonoma 14.4.\\n\\n"
  "${_AIRPORT_CMD}"

  exit 1
fi
###############################################################################
# Options
###############################################################################

# Parse Options ###############################################################

# Initialize program option variables.
_PRINT_HELP=0
_PRINT_VERSION=0
_USE_DEBUG=0

# Initialize additional expected option variables.
_SUBCOMMAND="info"
_ARGUMENTS=("${0}")

# getopts and getopts have inconsistent behavior, so using a simple home-brewed
# while loop. This isn't perfectly compliant with POSIX, but it's close enough
# and this appears to be a widely used approach.
#
# More info:
#   http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html
#   http://stackoverflow.com/a/14203146
#   http://stackoverflow.com/a/7948533
while [[ ${#} -gt 0 ]]
do
  opt="${1}"
  shift
  case "${opt}" in
    -h|--help|help)
      _PRINT_HELP=1
      ;;
    --debug)
      _USE_DEBUG=1
      ;;
    --version)
      _PRINT_VERSION=1
      ;;
    on|off|info|quality|scan|ssid|join|tool)
      _SUBCOMMAND="${opt}"
      ;;
    --endopts)
      # Terminate option parsing.
      break
      ;;
    *)
      _ARGUMENTS+=("${opt}")
      ;;
  esac
done

_debug printf "\${_SUBCOMMAND}: %s\\n" "${_SUBCOMMAND}"
_debug printf "\${_ARGUMENTS[*]:-}: %s\\n" "${_ARGUMENTS[*]:-}"

###############################################################################
# Program Functions
###############################################################################

# --------------------------------------------------------------------- _help()
# _help()
#
# Usage:
#   _help
#
# Description:
#   Print the program help information.
_help() {
  cat <<HEREDOC
         _                       __
  ____ _(_)________  ____  _____/ /_
 / __ \`/ / ___/ __ \\/ __ \\/ ___/ __/
/ /_/ / / /  / /_/ / /_/ / /  / /_
\\__,_/_/_/  / .___/\\____/_/   \\__/
           /_/

A command line interface for Wi-Fi on macOS.

Usage:
  ${_ME} info [-l|--long]
  ${_ME} join <SSID>
  ${_ME} off
  ${_ME} on
  ${_ME} quality
  ${_ME} scan [<query>]
  ${_ME} ssid
  ${_ME} -h | --help | help
  ${_ME} --version

Subcommands:
  info     Print the current network SSID and quality. Use the -l or --long
           options to print detailed information.
  join     Join the specified network.
  quality  Show the wireless quality as a percentage.
  off      Turn wireless off.
  on       Turn wireless on.
  scan     Perform a scan for wireless networks.
  ssid     Print the current network's SSID.
  help     Display this help information.

Options:
  -h --help  Display this help information.
  --version  Display version information.

Home:
  https://github.com/xwmx/airport
HEREDOC
}

# --------------------------------------------------------------------- _info()
# _info()
#
# Usage:
#   _info
#
# Description:
#   Print the current network SSID and quality.
_info() {
  local _ssid
  _ssid="$(_ssid)"

  local _quality
  if [[ -n "${_ssid:-}" ]]
  then
    _quality="$(_quality)"
  fi

  if [[ "${_ARGUMENTS[1]:-}" =~ ^-l|--help$ ]]
  then
    "${_AIRPORT_CMD}" --getinfo \
      && [[ -n "${_quality:-}" ]] \
      && printf "        quality: %s\\n" "${_quality}"
  else
    if [[ -n "${_ssid}" ]]
    then
      printf "AirPort: On, Connected: %s (%s)\\n" "${_ssid}" "${_quality}"
    else
      printf "AirPort: On, Disconnected\\n"
    fi
  fi
}

# --------------------------------------------------------------------- _join()
# _join()
#
# Usage:
#   _join <ssid>
#
# Description:
#   Join the specified network.
_join() {
  local _ssid="${1:-}"
  local _password=

  # Request password without displaying it
  printf "Password for \"%s\": " "${_ssid}"
  read -r -s _password
  printf "\\n" # print newline to stop `read`.

  _debug printf "airport() \${_WIFI_INTERFACE}: %s\\n" "${_WIFI_INTERFACE}"
  _debug printf "airport() \${_ssid}: %s\\n" "${_ssid}"

  "${_NETWORKSETUP_CMD}" \
    -setairportnetwork \
    "${_WIFI_INTERFACE}" \
    "${_ssid}" \
    "${_password}"
}

# ----------------------------------------------------------------------- _on()
# _on()
#
# Usage:
#   _on
#
# Description:
#   Turn wireless on.
_on() {
  "${_NETWORKSETUP_CMD}" -setairportpower "${_WIFI_INTERFACE}" on && \
    printf "AirPort: On\\n"
}

# ---------------------------------------------------------------------- _off()
# _off()
#
# Usage:
#   _off
#
# Description:
#   Turn wireless off.
_off() {
  "${_NETWORKSETUP_CMD}" -setairportpower "${_WIFI_INTERFACE}" off && \
    printf "AirPort: Off\\n"
}

# ------------------------------------------------------------------ _quality()
# _quality()
#
# Usage:
#   _quality
#
# Description:
#   Print the wi-fi quality as a percent.
_quality() {
  local _info
  local _signal
  local _noise
  local _snr
  local _percent

  # Reference: http://apple.stackexchange.com/a/110884
  _info="$("${_AIRPORT_CMD}" --getinfo)"
  _signal="$(
    printf "%s\\n" "${_info}" | awk '/agrCtlRSSI/ {print $NF}'
  )"
  _noise="$(
    printf "%s\\n" "${_info}" | awk '/agrCtlNoise/ {print $NF}'
  )"
  _snr="$(printf "100*(%s -(%s))\\n" "${_signal}" "${_noise}" | bc -l)"
  _percent="$(printf "%s/50\\n" "${_snr}" | bc -l)"
  printf "%s%%\\n" "${_percent%.*}"
}

# --------------------------------------------------------------------- _scan()
# _scan()
#
# Usage:
#   _scan [<query>]
#
# Description:
#   Perform a scan for wireless networks, optionally filtering by <query>.
_scan() {
  local _query="${1:-}"
  local _result=

  if [[ -n "${_query}" ]]
  then
    _result="$("${_AIRPORT_CMD}" --scan | grep "${_query}")"
  else
    _result="$("${_AIRPORT_CMD}" --scan)"
  fi

  if [[ -n "${_result}" ]]
  then
    printf "%s\\n" "${_result}"
  else
    return 1
  fi
}

# --------------------------------------------------------------------- _ssid()
# _ssid()
#
# Usage:
#   _ssid
#
# Description:
#   Print the current network's SSID.
_ssid() {
  "${_AIRPORT_CMD}" --getinfo \
    | grep '[^B]SSID' \
    | sed 's/.*SSID: //'
}

# --------------------------------------------------------------------- _tool()
# _tool()
#
# Usage:
#   _tool
#
# Description:
#   Run a command using the `airport` framework command line tool directly.
_tool() {
  "${_AIRPORT_CMD}" "$@"
}

# ------------------------------------------------------------------ _version()
# _version()
#
# Usage:
#   _version
#
# Description:
#   Print the value of $_VERSION.
_version() {
  printf "%s\\n" "${_VERSION}"
}

###############################################################################
# Main
###############################################################################

_main() {
  if ((_PRINT_HELP))
  then
    _help
  elif ((_PRINT_VERSION))
  then
    _version
  else
    "_${_SUBCOMMAND}" "${_ARGUMENTS[@]:1}"
  fi
}

_main "${@:-}"
