#!/usr/bin/env bash
# author: joe.zheng
# version: 24.10.17

set -e

SELF="$(basename $0)"
SELF_VERSION="$(sed -n '1,4s/# version: \(.*\)/\1/p' $0)"

FBDEV="/dev/fb0"  # framebuffer device
COLOR="red"       # color name or BGRA value, e.g. "\xFF\xFF\xFF\xFF"

WIDTH=1280        # framebuffer width
BPP=32            # bits per pixel, hardcoded for simplicity

DEBUG=${DEBUG:-n} # for debugging, y or n
if [[ $DEBUG == 'y' ]]; then
  STATUS_LINE=$(($(tput lines) - 2))
  function reset_cursor() {
    tput cup $STATUS_LINE 0 && tput el
  }
  function dbg() {
    reset_cursor && echo "> ${FUNCNAME[1]}: $@" >&2
  }
else
  function dbg() { :; }
fi

function main() {
  # parse command-line options
  while getopts ":c:f:h" opt; do
    case $opt in
      c) COLOR="$OPTARG" ;;
      f) FBDEV="$OPTARG" ;;
      h) usage; exit 0 ;;
      *) echo "Invalid option: -${OPTARG}" >&2; usage; exit 1 ;;
    esac
  done
  shift $((OPTIND-1))
  
  set_fbdev "$FBDEV"
  set_color "$COLOR"

  cmd="exec_cmds"
  if [[ -n "$1" ]]; then
    cmd="$1"
    shift
  fi
  case "$cmd" in
    set_pixel   | draw_line   | draw_rect    | fill_rect    | \
    draw_circle | fill_circle | draw_polygon | fill_polygon | \
    self_test   | exec_cmds   ) $cmd "$@" ;;
    *) echo "Invalid command: $cmd" >&2; usage; exit 1 ;;
  esac
  if [[ $DEBUG == 'y' ]]; then
    reset_cursor
  fi
}

function usage() {
  cat <<EOF
Usage: $SELF [options] [cmd] [args]

  Draw simple shapes to the framebuffer.

  If no command is specified, it will wait for input from STDIN,
  type the command line by line and press CTRL + D to finish.

  Switch to a non-desktop TTY first to prevent overwritten, e.g.:

    chvt 3

Options:
  -c <color>  color to draw (default: $COLOR)
  -f <fbdev>  framebuffer device (default: $FBDEV)
  -h          show this help message

Commands:
  set_pixel    <x> <y>
  draw_line    <x0> <y0> <x1> <y1>
  draw_rect    <x0> <y0> <x1> <y1>
  fill_rect    <x0> <y0> <x1> <y1>
  draw_circle  <x0> <y0> <r>
  fill_circle  <x0> <y0> <r>
  draw_polygon <x0> <y0> <x1> <y1> ...
  fill_polygon <x0> <y0> <x1> <y1> ...
  exec_cmds    [script]            run commands in script (default: /dev/stdin)
  self_test                        do the self-test to draw pre-defined shapes

Examples:
  1. Do the self-test

    sudo $SELF self_test

  2. Draw a line in blue

    sudo $SELF -c blue draw_line 10 10 20 20

  3. Draw multiple shapes in batch

    sudo $SELF <<EOS
    draw_line 0 0 10 10
    draw_rect 5 5 10 10
    EOS

  4. Fill a polygon

    sudo $SELF fill_polygon 10 10 30 20 70 8 28 28

  5. Fill a rectangle in DEBUG mode

    DEBUG=y $SELF fill_rect 0 0 10 5 2>dbg.log

Version: $SELF_VERSION
EOF

}

function exec_cmds() {
  dbg "$@"
  . "${1:-/dev/stdin}"
}

function self_test() {
  dbg "$@"
  local x=0 y=0 s=10
  local w=8 h=4 b=1
  local i t

  if [[ $DEBUG == 'y' ]]; then
    s=5
  fi

  draw_rect $x $y $((x + s * w)) $((y + s * h))
  for i in $(seq $w); do
    draw_line $x $y $((x + s * i)) $((y + s * h))
    draw_line $((x + s * w)) $y $((x + s * (i - 1))) $((y + s * h))
  done
  x=$((y + s * (w + b))) w=2 t=$COLOR
  for i in r g b w; do
    set_color $i
    fill_rect $x $y $((x + s * w)) $((y + s * h))
    x=$((x + s * (w + b)))
  done
  set_color $t # reset color
  y=$((y + s * (h + b))) w=3 x=0
  for i in $(seq $w); do
    draw_circle $((x + s * w)) $((y + s * w)) $((i * s))
  done
  x=$((x + s * (w * 2 + b))) t=$((y + s * (w + 1)))
  draw_polygon $x $y $((x + s)) $((y + s)) $((x + s * 2)) $y $((x + s)) $((y + s * 2))
  fill_polygon $x $t $((x + s)) $((t + s)) $((x + s * 2)) $t $((x + s)) $((t + s * 2))
  for i in $(seq $w); do
    fill_circle $((x + s * i)) $((y + s * w)) $((i * s))
    x=$((x + s * (i * 2 + b)))
  done
}

function set_fbdev() {
  dbg "$@"
  local fbdev="$1"
  local fbinfo="/sys/class/graphics/${fbdev##*/}"
  local fbsize="$fbinfo/virtual_size"
  local fbbpp="$fbinfo/bits_per_pixel"
  if [[ -e "$fbsize" ]]; then
    WIDTH="$(cut -d, -f1 $fbsize)"
  fi
  if [[ -e "$fbbpp" ]]; then
    local bpp="$(< $fbbpp)"
    if [[ "$bpp" != "$BPP" ]]; then
      echo "WARNING: bits per pixel $bpp is not supported"
    fi
  fi
  FBDEV="$fbdev"
}

function set_color() {
  dbg "$@"
  local color="$1"
  # convert known color name to value
  case "$color" in
    b|blue)  color="\xFF\x00\x00\xFF" ;;
    g|green) color="\x00\xFF\x00\xFF" ;;
    r|red)   color="\x00\x00\xFF\xFF" ;;
    w|white) color="\xFF\xFF\xFF\xFF" ;;
  esac
  COLOR="$color"
}

function set_pixel() {
  #dbg "$@"
  local x=$1
  local y=$2

  if [[ $DEBUG == 'y' ]]; then
    tput cup $y $x && printf "+"
  else
    local offset=$((y * WIDTH + x))
    printf "$COLOR" | dd bs=$((BPP / 8)) seek=$offset of=$FBDEV &>/dev/null
  fi
}

function draw_line() {
  dbg "$@"
  local x0=$1
  local y0=$2
  local x1=$3
  local y1=$4

  # Bresenham's algorithm
  local dx=$((x0 < x1 ? x1 - x0 : x0 - x1)) #  abs
  local dy=$((y0 < y1 ? y0 - y1 : y1 - y0)) # -abs
  local ix=$((x0 < x1 ? 1 : -1))
  local iy=$((y0 < y1 ? 1 : -1))
  local er=$((dx + dy))

  while true; do
    set_pixel $x0 $y0
    if ((er * 2 >= dy)); then
      if ((x0 == x1)); then
        break
      fi
      er=$((er + dy))
      x0=$((x0 + ix))
    fi
    if ((er * 2 <= dx)); then
      if ((y0 == y1)); then
        break
      fi
      er=$((er + dx))
      y0=$((y0 + iy))
    fi
  done
}

function draw_rect() {
  dbg "$@"
  local x0=$1
  local y0=$2
  local x1=$3
  local y1=$4

  # the top and bottom sides
  draw_line $x0 $y0 $x1 $y0
  draw_line $x0 $y1 $x1 $y1

  # the left and right sides
  draw_line $x0 $y0 $x0 $y1
  draw_line $x1 $y0 $x1 $y1
}

function fill_rect() {
  dbg "$@"
  local x0=$1
  local y0=$2
  local x1=$3
  local y1=$4

  # draw line by line
  local iy=$((y0 < y1 ? 1 : -1))
  while true; do
    draw_line $x0 $y0 $x1 $y0
    if ((y0 == y1)); then
      break
    else
      y0=$((y0 + iy))
    fi
  done
}

function draw_circle() {
  dbg "$@"
  _midpoint_circle "$@" n
}

function fill_circle() {
  dbg "$@"
  _midpoint_circle "$@" y
}

function _midpoint_circle() {
  #dbg "$@"
  local x0=$1
  local y0=$2
  local r=$3
  local f="${4:-n}" # fill or not

  # Midpoint circle algorithm
  local x=$((-r))
  local y=0
  local e=$((2 - r * 2))
  local t

  while true; do
    if [[ $f != 'n' ]]; then
      draw_line $((x0 - x)) $((y0 + y)) $((x0 + x)) $((y0 + y))
      draw_line $((x0 - x)) $((y0 - y)) $((x0 + x)) $((y0 - y))
    else
      set_pixel $((x0 - x)) $((y0 + y))
      set_pixel $((x0 - y)) $((y0 - x))
      set_pixel $((x0 + x)) $((y0 - y))
      set_pixel $((x0 + y)) $((y0 + x))
    fi
    t=e
    if ((t > x)); then
      x=$((x + 1))
      e=$((e + x * 2 + 1))
    fi
    if ((t <= y)); then
      y=$((y + 1))
      e=$((e + y * 2 + 1))
    fi
    if ((x >= 0)); then
      break
    fi
  done
}

function draw_polygon() {
  dbg "$@"
  local v=("$@")     # vertices: x0 y0 x1 y1 ...
  local n=${#v[@]}   # total number of v

  local i t
  for ((i = 0; i < n; i += 2)); do
    t=$(( (i+2) % n ))
    draw_line ${v[$i]} ${v[$i+1]} ${v[$t]} ${v[$t+1]}
  done
}

function fill_polygon() {
  dbg "$@"
  local v=("$@")     # vertices: x0 y0 x1 y1 ...
  local n=${#v[@]}   # total number of v
  local ymin=${v[1]}
  local ymax=${v[1]}
  local y i t

  # Scan-line polygon fill algorithm
  for ((i = 0; i < n; i += 2)); do
    t=${v[$((i + 1))]}
    if ((t < ymin)); then
      ymin=$t
    fi
    if ((t > ymax)); then
      ymax=$t
    fi
  done

  for ((y = ymin; y <= ymax; y++)); do
    local x=() x0 y0 x1 y1
    for ((i = 0; i < n; i += 2)); do
      t=$(( (i+2) % n ))
      x0=${v[$i]} y0=${v[$i+1]}
      x1=${v[$t]} y1=${v[$t+1]}
      if ((y0 == y1)); then
        continue # skip horizontal edges
      fi
      if ((y0 <= y && y <= y1)) || ((y1 <= y && y <= y0)); then
        x+=($((x0 + (y - y0) * (x1 - x0) / (y1 - y0))))
      fi
    done

    IFS=$'\n' x=($(sort -n <<<"${x[*]}"))

    for ((i = 0; i < ${#x[@]}; i += 2)); do
      draw_line ${x[$i]} $y ${x[$i+1]} $y
    done
  done
}

main "$@"
