#!/bin/bash
#
# candy - a small collection of mixed up linux candy scripts, unified behind
# subcommands. Run without arguments to list the available commands.

#==========================================================================================
##@ Network
#==========================================================================================
cmd_ip() { ## show the public IP and the addresses of the local network interfaces
  echo "Public IP:"
  local pub
  pub="$(wget -qO- --timeout=5 http://ipinfo.io/ip 2>/dev/null)"
  if [ -n "$pub" ]; then
    echo "  $pub"
  else
    echo "  (unavailable)"
  fi

  echo
  echo "Network interfaces:"
  if command -v ip >/dev/null 2>&1; then
    # -brief gives: "<iface> <state> <addr> [addr ...]"; skip loopback.
    ip -brief address show | while read -r iface state addrs; do
      [ "$iface" = "lo" ] && continue
      printf "  %-12s %-8s %s\n" "$iface" "$state" "${addrs:-—}"
    done
  else
    echo "  (the 'ip' command from iproute2 is not available)"
  fi
}

cmd_port() { ## list the processes listening on a given TCP/UDP port: candy port <port>
  local port="${1:-}"
  if [ -z "$port" ]; then
    echo >&2 "usage: candy port <port>"
    return 1
  fi
  if ! command -v ss >/dev/null 2>&1; then
    echo >&2 "candy port: the 'ss' command (iproute2) is required."
    return 1
  fi

  # -t tcp, -u udp, -l listening, -p process, -n numeric, -H no header;
  # the filter matches the local (source) port.
  local out
  out="$(ss -tulpnH "sport = :$port" 2>/dev/null)"
  if [ -z "$out" ]; then
    echo "nothing is listening on port $port"
    return 0
  fi

  printf "%-5s %-24s %s\n" "PROTO" "LOCAL ADDRESS" "PROCESS"
  # columns: Netid State Recv-Q Send-Q Local:Port Peer:Port Process
  echo "$out" | while read -r proto _ _ _ laddr _ proc; do
    local procfmt
    if [ -z "$proc" ]; then
      procfmt="(process hidden — run with sudo)"
    else
      # users:(("nginx",pid=1234,fd=6)) -> nginx pid 1234
      procfmt="$(printf '%s' "$proc" | sed -E 's/users:\(\(//; s/\)\)$//; s/"//g; s/,fd=[0-9]+//g; s/pid=/pid /g; s/\),\(/, /g; s/,/ /g')"
    fi
    printf "%-5s %-24s %s\n" "$proto" "$laddr" "$procfmt"
  done
}

#==========================================================================================
##@ Files
#==========================================================================================
cmd_topsize() { ## list the biggest directories (or files with -f) under a path
  local LINES=15 DIR="./" DEEP=1 FILE=false

  local -a args=("$@")
  local i e number re='^[0-9]+$'
  for ((i=0; i<${#args[@]}; i++)); do
    case "${args[i]}" in
      -h|--help)
        echo "parameters:"
        echo " -n <INT> amount of lines to print, default 15"
        echo " -d <INT> amount of subdirectory levels tested, default 1"
        echo " -f find the biggest file instead of directory, default false"
        echo " -h print this help"
        return 0
        ;;
      -n)
        e=$((i+2)); number=${!e}
        [[ $number =~ $re ]] && LINES=$number
        continue
        ;;
      -d)
        e=$((i+2)); number=${!e}
        [[ $number =~ $re ]] && DEEP=$number
        continue
        ;;
      -f)
        FILE=true
        ;;
    esac

    local parameter="${args[i]}"
    if [ ! "${parameter:0:1}" == "-" ]; then
      DIR=${args[i]}
    fi
  done

  if [ "$FILE" = true ]; then
    find "$DIR" -maxdepth "$DEEP" -type f -printf '%s  %p\n' | numfmt --field=1 --to=iec --suffix=B --padding=8 | sort -r -h | head -n "$LINES"
  else
    du -h --max-depth "$DEEP" "$DIR" | sort -h -r | head -n "$LINES"
  fi
}

cmd_same-size() { ## find files sharing the same size (largest first), grouped: candy same-size [dir]
  local DIR="${1:-.}"
  find "$DIR" -type f -printf '%s\t%p\0' |
    sort -z -n -r -k1,1 |
    awk -v RS='\0' -F '\t' '
    function human(x,   u, i) {
      split("B KiB MiB GiB TiB PiB", u, " ")
      i = 1
      while (x >= 1024 && i < 6) { x /= 1024; i++ }
      if (i == 1) return sprintf("%d %s", x, u[i])
      return sprintf("%.1f %s", x, u[i])
    }
    {
      size = $1
      file = $2

      if (size == prev_size) {
        if (!printed) {
          print ""
          print "Size: " human(size)
          print prev_file
          printed = 1
        }
        print file
      } else {
        printed = 0
      }

      prev_size = size
      prev_file = file
    }'
}

cmd_move-videos() { ## move video files from subdirectories into the current directory
  find . -type f \( -iname "*.mp4" -o -iname "*.mkv" -o -iname "*.avi" -o -iname "*.mov" -o -iname "*.flv" \) -exec mv -n {} ./ \;
}

cmd_del-empty-dir() { ## delete empty directories recursively under the current directory
  find . -type d -empty -delete
}

#==========================================================================================
#  Help
#==========================================================================================
usage() {
  echo ""
  echo "Usage:"
  echo -e "  candy \033[36m<command>\033[0m [args]"
  awk '
    /^cmd_[A-Za-z0-9_-]+\(\)[[:space:]]*\{[[:space:]]*##/ {
      name = $0; sub(/\(\).*/, "", name); sub(/^cmd_/, "", name)
      desc = $0; sub(/^.*## ?/, "", desc)
      printf "  \033[36m%-15s\033[0m %s\n", name, desc
    }
    /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($0, 5) }
  ' "$0"
  echo ""
}

main() {
  local sub="${1:-}"
  if [ -z "$sub" ]; then
    usage
    exit 0
  fi
  # hidden: list subcommand names, one per line (used by bash completion)
  if [ "$sub" = "--complete-commands" ]; then
    declare -F | sed -n 's/^declare -f cmd_//p'
    exit 0
  fi
  shift
  if declare -F "cmd_$sub" >/dev/null 2>&1; then
    "cmd_$sub" "$@"
  else
    echo "candy: unknown command '$sub'" >&2
    usage
    exit 1
  fi
}

main "$@"
