#!/usr/bin/env bash
#
# Google Goose, for your terminal.
#
#   bash goose.sh
#
# space or enter: another opinion. q: quit.
#
# This script draws a goose. It uses no network, writes no files, needs no
# install and does not ask for anything. Read it if you like; that's why it's
# one file. Independent parody. Not affiliated with Google or Alphabet.

# Generated from js/sayings.js and js/goose.js by tools/build-terminal-goose.mjs.
GREETING='hi. i'\''m the goose.'
BEAK_ROW=2
F_IDLE=0
F_LEAN=1
F_PECK=2
F_HONK=3
F_STEPLEFT=4
F_STEPRIGHT=5
FRAMES=(
  '      .--.      '
  '     / @  \__   '
  '     |    ___>  '
  '      \  |      '
  '      |  |      '
  '      |  |      '
  ' __  /   `.     '
  ' \ `'\''      `.   '
  '  \          |  '
  '   `--------'\''   '
  '     |  |       '
  '    _|  |_      '
  '                '
  '       .--.     '
  '      / @  \__  '
  '      |    ___> '
  '      /  /      '
  '     |  |       '
  ' __  /   `.     '
  ' \ `'\''      `.   '
  '  \          |  '
  '   `--------'\''   '
  '     |  |       '
  '    _|  |_      '
  '                '
  '                '
  '                '
  '         .--.   '
  '        / @  \_ '
  '       /    ___>'
  ' __   /  .-'\''    '
  ' \ `-'\''     `.   '
  '  \          |  '
  '   `--------'\''   '
  '     |  |       '
  '    _|  |_      '
  '      .--.   __ '
  '     / @  \_/   '
  '     |     <    '
  '      \  |\__   '
  '      |  |      '
  '      |  |      '
  ' __  /   `.     '
  ' \ `'\''      `.   '
  '  \          |  '
  '   `--------'\''   '
  '     |  |       '
  '    _|  |_      '
  '     .--.       '
  '    / @  \__    '
  '    |    ___>   '
  '     \  |       '
  '     |  |       '
  '     |  |       '
  '__  /   `.      '
  '\ `'\''      `.    '
  ' \          |   '
  '  `--------'\''    '
  '    /  |        '
  '   _|  |_       '
  '       .--.     '
  '      / @  \__  '
  '      |    ___> '
  '       \  |     '
  '       |  |     '
  '       |  |     '
  '  __  /   `.    '
  '  \ `'\''      `.  '
  '   \          | '
  '    `--------'\''  '
  '      |  \      '
  '     _|  |_     '
)
SAYINGS=(
  'they have servers. i have feet.'
  '404. thought not found.'
  'please do not feed the algorithm. feed me.'
  'i have never accepted cookies. only bread.'
  'my search history is entirely bread.'
  'incognito mode does not hide you from geese.'
  'the cloud is just a pond i cannot reach.'
  'i am feeling lucky. and slightly damp.'
  'your tabs are stressing me out.'
  'i looked it up. it said honk.'
  'terms and conditions: bring bread.'
  'did you mean: bread?'
  'i live on page two of the search results.'
  'select all squares containing bread.'
  'i clicked '\''i am not a robot'\''. nobody asked about geese.'
  'two factor authentication: feet.'
  'clear your cache. leave the crumbs.'
  'i tried the dark web. it was just night.'
  'i asked the cloud for bread. it rained.'
  'i have 0 unread messages. i cannot read.'
  'you are on mute. i prefer this.'
  'i only follow accounts with crumbs.'
  'i have two feet and production access.'
  'no credentials. just honk.'
  'i am not artificial. just unintelligent.'
  'all this computing. still no bread.'
  'i have reviewed your code. i sat on it.'
  'i pushed to main. i also pushed a duck.'
  'a bug is just a snack in production.'
  'i stand on one leg. the other is a backup.'
  'i molt twice a year. that is my update policy.'
  'i am the lead engineer of this puddle.'
  'this meeting could have been a honk.'
  'have you tried turning the pond off and on again.'
  'bread is the only stable release. it still goes stale.'
  'please hold. i saw a worm.'
  'another day. another unnecessary opinion.'
  'ducks are just geese in beta.'
  'the swan is a paid plan.'
  'i was told to touch grass. i ate it.'
  'a dog looked at me once. i won.'
  'i flew south for the winter. still no signal.'
  'i am not lost. i am migrating.'
  'i do not have a phone. i have a neck.'
  'a pond is just a lake with low expectations.'
  'the front goose in the v is also guessing.'
  'nobody knows anything. i just said it first.'
  'you do not need a strategy. you need a sandwich.'
  'the algorithm knows you. it has never met a goose.'
  'the whitepaper was delicious.'
  'a swan told me to diversify. i bit it.'
  'i asked what a blockchain was. it was not bread.'
  'not financial advice. not any advice. just honk.'
)

COLS_ART=16
ROWS_ART=12
LEFT="  "
WATER='~~~~~~ ~~~~~~~~~~~ ~~~~ ~~~~~~~~ ~~~~~~~~~~~~~ ~~~ '

frame=0
eye="o"
current="$GREETING"
bag=()
queue=()
water_offset=0
dirty=1

next_saying() {
  if [ "${#bag[@]}" -eq 0 ]; then
    bag=("${SAYINGS[@]}")
    local i j tmp
    for ((i = ${#bag[@]} - 1; i > 0; i--)); do
      j=$((RANDOM % (i + 1)))
      tmp="${bag[i]}"; bag[i]="${bag[j]}"; bag[j]="$tmp"
    done
  fi
  local last=$((${#bag[@]} - 1))
  # Across a reshuffle, the old last line must not come straight back.
  if [ "${bag[last]}" = "$current" ] && [ "$last" -gt 0 ]; then
    tmp="${bag[last]}"; bag[last]="${bag[0]}"; bag[0]="$tmp"
  fi
  current="${bag[last]}"
  unset "bag[last]"
  bag=("${bag[@]}")
}

terminal_size() {
  local size
  size=$(stty size 2>/dev/null)
  term_cols=${size#* }
  case "$term_cols" in ''|*[!0-9]*|0) term_cols=80 ;; esac
}

wrap_saying() {
  local width=$((term_cols - ${#LEFT} - COLS_ART - 6))
  [ "$width" -lt 12 ] && width=12
  lines=()
  local line="" word
  for word in $current; do
    if [ -n "$line" ] && [ $((${#line} + 1 + ${#word})) -gt "$width" ]; then
      lines+=("$line")
      line="$word"
    else
      line="${line:+$line }$word"
    fi
  done
  [ -n "$line" ] && lines+=("$line")
}

water_line() {
  local width=$((term_cols - ${#LEFT} * 2))
  [ "$width" -gt 62 ] && width=62
  local line="$WATER$WATER$WATER$WATER"
  while [ "${#line}" -lt $((width + ${#WATER})) ]; do line="$line$WATER"; done
  water="${line:$((water_offset % ${#WATER})):$width}"
}

goose_rows() {
  local r row index
  out=""
  wrap_saying
  for ((r = 0; r < ROWS_ART; r++)); do
    row="${FRAMES[frame * ROWS_ART + r]}"
    row="${row/@/$eye}"
    index=$((r - BEAK_ROW))
    if [ "$index" -eq 0 ]; then
      row="$row $(printf '\033[2m')<──$(printf '\033[22m') ${lines[0]}"
    elif [ "$index" -gt 0 ] && [ "$index" -lt "${#lines[@]}" ]; then
      row="$row     ${lines[index]}"
    fi
    out="$out$LEFT$row$CLEAR_LINE$NL"
  done
}

render() {
  goose_rows
  water_line
  printf '\033[H\n%s\033[2m%s%s\033[K\n\n%sspace: another opinion   q: quit\033[K\033[22m\033[J' \
    "$out" "$LEFT" "$water" "$LEFT"
}

plain_goose() {
  terminal_size
  next_saying
  goose_rows
  water_line
  printf '%s%s%s\n' "$out" "$LEFT" "$water"
}

NL=$'\n'
if [ ! -t 0 ] || [ ! -t 1 ]; then
  CLEAR_LINE=""
  plain_goose
  exit 0
fi
CLEAR_LINE=$'\033[K'

saved_tty=$(stty -g)

leave() {
  stty "$saved_tty" 2>/dev/null
  printf '\033[?25h\033[?1049l'
  printf 'the goose has returned to the pond.\n'
}
trap leave EXIT
trap 'exit 0' INT TERM
trap 'dirty=1; terminal_size' WINCH

printf '\033[?1049h\033[?25l\033[2J'
# Reads return after 0.1s with or without a key, which paces the animation.
# bash 3.2, the macOS default, can't time out a read any faster than a second.
stty -icanon -echo min 0 time 1
terminal_size

ticks=0
blink_in=$((RANDOM % 40 + 25))
blink_left=0

while :; do
  key=$(dd bs=1 count=1 2>/dev/null; printf x)
  key="${key%x}"

  case "$key" in
    q|Q) exit 0 ;;
    '') ;;
    *)
      next_saying
      case $((RANDOM % 3)) in
        0) queue=($F_LEAN $F_PECK $F_LEAN $F_PECK $F_LEAN $F_IDLE) ;;
        1) queue=($F_HONK $F_HONK $F_HONK $F_IDLE) ;;
        2) queue=($F_STEPLEFT $F_IDLE $F_STEPRIGHT $F_IDLE $F_STEPLEFT $F_IDLE) ;;
      esac
      dirty=1
      ;;
  esac

  if [ "${#queue[@]}" -gt 0 ]; then
    frame="${queue[0]}"
    queue=("${queue[@]:1}")
    dirty=1
  fi

  ticks=$((ticks + 1))
  if [ $((ticks % 12)) -eq 0 ]; then
    water_offset=$((water_offset + 1))
    dirty=1
  fi

  if [ "$blink_left" -gt 0 ]; then
    blink_left=$((blink_left - 1))
    if [ "$blink_left" -eq 0 ]; then eye="o"; dirty=1; fi
  else
    blink_in=$((blink_in - 1))
    if [ "$blink_in" -le 0 ]; then
      eye="-"
      blink_left=2
      blink_in=$((RANDOM % 40 + 25))
      dirty=1
    fi
  fi

  if [ "$dirty" -eq 1 ]; then
    dirty=0
    render
  fi
done
