#!/bin/sh
# Lightkeeper boot hook, linked from /var/lib/webosbrew/init.d/60-lightkeeper by
# setup.sh. It keeps the settings server running, opens Lightkeeper when the TV
# comes on (if "Open at power on" is set) and, while Lightkeeper has the Home key,
# when Back leaves the last app. The link points into the app: once the app is
# removed it leads nowhere, and nothing here runs again.
#
# A new start of this script stops the old one: the generation in
# /tmp/lightkeeper.gen, which every loop checks, and the old loops' luna-send stopped below. Log: /tmp/lightkeeper.log.
APP=io.github.chiotas.lightkeeper
HOOK=/var/lib/webosbrew/init.d/60-lightkeeper
CONFIG=/var/lib/lightkeeper/config.json
# The last full-screen app, kept across a cold start: what was in front when the TV went off.
FRONT=/var/lib/lightkeeper/front

# At power on, Lightkeeper opens over an input, LG's Home or nothing, and over the
# app that was in front when the TV went off, which webOS puts back (issue: the
# Homebrew Channel left open stayed in front). Not over an app that came up with
# the TV: its own button, a cast from a phone, an alarm. $1 in front, $2 the app
# in front at power off: open, cover, stay or self.
scelta() {
  case "$1" in
    "$APP") echo self ;;
    ""|com.webos.app.home|com.webos.app.livetv|com.webos.app.inputcommon|com.webos.app.hdmi*) echo open ;;
    *) if [ "$1" = "$2" ]; then echo cover; else echo stay; fi ;;
  esac
}
# For the tests: lightkeeper-init --decide <in front> <in front at power off>
[ "$1" = --decide ] && { scelta "$2" "$3"; exit 0; }

# The TV taken as off (1) when this script starts within five minutes of the TV
# starting: a cold start, whose first "Active" opens Lightkeeper. Later (0), it is
# the installer, an update or setup.sh starting it again with the TV on, and nothing
# opens over what is being watched. $1: the seconds in /proc/uptime.
spento_all_avvio() { if [ "${1%%.*}" -lt 300 ] 2>/dev/null; then echo 1; else echo 0; fi; }
# For the tests: lightkeeper-init --off-at-start <seconds since the TV started>
[ "$1" = --off-at-start ] && { spento_all_avvio "$2"; exit 0; }

# Back from the last app where LG has no Home app (webOS 3 to 5), as seen on a B7
# with webOS 3.9 (issue #3): the app gives way to nothing in front, "", then Live
# TV. So Lightkeeper opens when a real app gives way to nothing, and again if Live
# TV takes its place within five seconds. Picking an input or Live TV with the
# remote goes straight from the app to it, and does not count. $1: the app in front now.
vera() { case "$1" in ""|"$APP"|com.webos.app.livetv|com.webos.app.inputcommon|com.webos.app.hdmi*|com.webos.app.home) return 1 ;; esac; }
uscita=0
su_davanti() {
  case "$1" in
    "") if vera "$prima" && set_to key '"home"'; then
          uscita=$(date +%s)
          diario "nothing in front after $prima: opening Lightkeeper"
          sleep 1; lancia "$APP"
        fi ;;
    com.webos.app.livetv)
        if [ $(( $(date +%s) - uscita )) -le 5 ]; then
          uscita=0
          diario "Live TV after leaving an app: opening Lightkeeper again"
          lancia "$APP"
        fi ;;
    *) uscita=0 ;;
  esac
  prima=$1
}
# Back from the last app where LG has a Home app (webOS 6 on). On a C1 with webOS
# 6.5.3, Back out of an app goes one of two ways:
# - Prime Video and the browser stop: background, nothing in front, stop 0.2 s later;
#   sam launched LG's Home 0.8 s after the stop (Prime Video), or did not (the browser).
#   Lightkeeper opens at the stop of the app that was in front, before LG's Home.
# - YouTube and PlxNative do not stop: sam launches LG's Home first, and it is in front
#   0.15 s later. Covered sooner than a second after it came to the front, LG's Home
#   took the screen back by itself 0.55 s later, twice in a row, and stayed (C1, 26/10):
#   so Lightkeeper waits a second, as up to 1.0.7 (sopra_home).
# Switching apps puts the next one in front in the same millisecond as the last one
# goes to the background, with no stop and no LG's Home; closing apps from Lightkeeper
# stops apps that are not in front. LG's Home launched or in front within three
# seconds of opening Lightkeeper, or after another full-screen app, is covered. Not
# after Lightkeeper otherwise: its Down arrow opens LG's Home on purpose. Screen savers
# and overlays do not count. $1 the event, $2 the app, $3 card for a full-screen
# window, $4 the time in s.
ritorno=0
su_evento() {
  case "$1" in
    stop)
      if [ "$2" = "$prima" ] && vera "$2" && acceso && set_to key '"home"'; then
        ritorno=$4
        diario "$2 closed: opening Lightkeeper"
        lancia "$APP"
      fi ;;
    foreground)
      [ "$3" = card ] || return 0
      if [ "$2" = com.webos.app.home ] && acceso && set_to key '"home"' &&
         { [ $(( $4 - ritorno )) -le 3 ] || { [ -n "$prima" ] && [ "$prima" != "$APP" ] && [ "$prima" != "$2" ]; }; }; then
        ritorno=0
        diario "LG's Home ($1) after ${prima:-nothing}"
        sopra_home
      fi
      prima=$2 ;;
  esac
}
# A pause in microseconds: BusyBox on webOS 6.5.3 has usleep, and its sleep takes
# whole seconds only ("invalid number '0.3'"); elsewhere sleep may take tenths.
pausa() { usleep "$1" 2>/dev/null || sleep "0.$(( $1 / 100000 ))" 2>/dev/null || sleep 1; }
# Over LG's Home: a second after it came to the front, as up to 1.0.7, then checked
# once more two seconds later. Sooner, it takes the screen back (see above).
sopra_home() {
  pausa 1000000; diario "opening Lightkeeper over LG's Home"; lancia "$APP"
  pausa 2000000; [ "$(davanti)" = com.webos.app.home ] && { diario "still LG's Home: again"; lancia "$APP"; }
}
# One line of getAppLifeEvents, as luna-send prints it: evento (stop,
# foreground or nothing else), app and card for a full-screen window.
evento_da() {
  case "$1" in
    *'"event":"stop"'*) evento=stop ;; *'"event":"foreground"'*) evento=foreground ;;
    *) evento= ; return ;;
  esac
  app=$(echo "$1" | sed -n 's/.*"appId" *: *"\([^"]*\)".*/\1/p')
  card=; case "$1" in *_WEBOS_WINDOW_TYPE_CARD*) card=card ;; esac
}
# The TV is on: an app stopping as it goes off must not bring Lightkeeper up.
acceso() { case "$(cat /tmp/lightkeeper.power 2>/dev/null)" in ""|Active) return 0 ;; esac; return 1; }

# For the tests: lightkeeper-init --replay-life, one life event per line on stdin,
# "<seconds> <line from luna-send>", or "<seconds> front <appId>" for what is in front
# from then on, which opening Lightkeeper also sets; prints where Lightkeeper would
# open. POWER sets the TV's power state, Active by default.
if [ "$1" = --replay-life ]; then
  diario() { :; }; set_to() { return 0; }; sleep() { :; }; usleep() { :; }
  acceso() { [ "${POWER:-Active}" = Active ]; }
  davanti() { echo "$fronte"; }
  lancia() { fronte=$APP; echo "open ($evento $app)"; }
  prima= fronte=
  while read -r t riga; do
    case "$riga" in "front "*|front) fronte=${riga#front}; fronte=${fronte# }; continue ;; esac
    evento_da "$riga"; [ -n "$evento" ] && su_evento "$evento" "$app" "$card" "$t"
  done
  exit 0
fi

# For the tests: lightkeeper-init --replay, one foreground appId per line on stdin;
# prints where Lightkeeper would open.
if [ "$1" = --replay ]; then
  diario() { :; }; set_to() { return 0; }; sleep() { :; }
  lancia() { echo "open ($prima -> ${app:-nothing})"; }
  prima=
  while read -r app; do su_davanti "$app"; done
  exit 0
fi

LOG=/tmp/lightkeeper.log
LS=/usr/bin/luna-send
HERE=$(dirname "$(readlink -f "$0")")
GEN=$$
# node fails at every start if its working directory is gone, as an app update leaves it.
cd / || exit 0
[ -x /usr/bin/node ] && [ -f "$HERE/server.js" ] || exit 0
echo "$GEN" > /tmp/lightkeeper.gen
# The random background is kept while the TV stays on: a new mark here at a cold start,
# and each time the TV goes off; an update, which starts this script again, keeps it.
SESSIONE=/tmp/lightkeeper.session
[ -f "$SESSIONE" ] || date +%s > "$SESSIONE"
[ "$(wc -c < "$LOG" 2>/dev/null || echo 0)" -gt 65536 ] && mv -f "$LOG" "$LOG.old"

diario() { echo "$(date '+%F %T') $*" >> "$LOG"; }
vivo() { [ -e "$HOOK" ] && [ "$(cat /tmp/lightkeeper.gen 2>/dev/null)" = "$GEN" ]; }
# The config is written pretty-printed by the server, one setting per line.
set_to() { grep -q "\"$1\": $2" "$CONFIG" 2>/dev/null; }
davanti() {
  $LS -n 1 -w 3000 luna://com.webos.applicationManager/getForegroundAppInfo '{}' 2>/dev/null |
    sed -n 's/.*"appId" *: *"\([^"]*\)".*/\1/p'
}
lancia() { $LS -n 1 -w 5000 luna://com.webos.applicationManager/launch "{\"id\":\"$1\"}" >/dev/null 2>&1; }

# The loops of an earlier start stay blocked on their luna-send for good, events or not:
# those luna-send are stopped, and the loops, no longer the current generation, end.
for s in $(ps -ef | awk -v me=$$ '/[6]0-lightkeeper/ && $2 != me {print $2}'); do
  for c in $(ps -ef | awk -v p="$s" '$3 == p && /[l]una-send/ {print $2}'); do kill "$c" 2>/dev/null; done
done

# The server, restarted if it stops, but not more than ten times in a row
# within a minute of starting: a server that cannot start must not loop at boot.
# The old server, checked by name: a pid left from before a reboot may belong to anything now.
vecchio=$(cat /tmp/lightkeeper.pid 2>/dev/null)
[ -n "$vecchio" ] && grep -q lightkeeper "/proc/$vecchio/cmdline" 2>/dev/null && kill "$vecchio" 2>/dev/null
(
  fails=0
  while vivo && [ $fails -lt 10 ]; do
    started=$(date +%s)
    /usr/bin/node "$HERE/server.js" >> "$LOG" 2>&1 &
    echo $! > /tmp/lightkeeper.pid
    wait $!
    vivo || break
    if [ $(( $(date +%s) - started )) -lt 60 ]; then fails=$((fails+1)); else fails=0; fi
    diario "server stopped, starting it again"
    sleep 5
  done
) </dev/null >/dev/null 2>&1 &

# Seconds since the TV started, to the hundredth: the log's clock has whole seconds only.
secondi() { cut -d' ' -f1 /proc/uptime 2>/dev/null; }
# What is in front, for apri: fg, and luna-send's own answer, cut short, when it names no app.
guarda() {
  r=$($LS -n 1 -w 3000 luna://com.webos.applicationManager/getForegroundAppInfo '{}' 2>&1)
  fg=$(echo "$r" | sed -n 's/.*"appId" *: *"\([^"]*\)".*/\1/p')
  visto=${fg:-nothing}
  [ -n "$fg" ] || visto="nothing (luna-send: $(echo "$r" | tr '\n' ' ' | cut -c1-200 | sed 's/ *$//'))"
}

# $1: the app in front when the TV went off. Whoever switches on with the Netflix button stays on Netflix.
# Each look at what is in front is logged, to see where the time goes at power on.
apri() {
  sleep 1
  guarda
  diario "1 s after Active, in front: $visto (up $(secondi) s)"
  # An app switched on with the TV, as by the Netflix button, is still starting:
  # with nothing in front, it is given up to eight seconds to come up.
  n=0
  while [ -z "$fg" ] && [ $n -lt 8 ]; do
    sleep 1; guarda; n=$((n+1))
    diario "$((n+1)) s after Active, in front: $visto (up $(secondi) s)"
  done
  case $(scelta "$fg" "$1") in
    open) diario "opening Lightkeeper (in front: ${fg:-nothing}) (up $(secondi) s)"; lancia "$APP" ;;
    cover) diario "opening Lightkeeper over $fg, in front when the TV went off (up $(secondi) s)"; lancia "$APP" ;;
    *) diario "not opening: $fg is in front" ;;
  esac
}

# Signed in to an LG account, webOS 6 opens com.webos.app.membership about 42 s
# after a cold start, and on some TVs it spins for good: for 2 minutes after
# switching on, it is closed as soon as it shows and Lightkeeper is put back.
via_account() {
  n=0
  while [ $n -lt 40 ]; do
    sleep 3; n=$((n+1))
    [ "$(davanti)" = com.webos.app.membership ] || continue
    diario "closing com.webos.app.membership"
    $LS -n 1 -w 5000 luna://com.webos.applicationManager/closeByAppId '{"id":"com.webos.app.membership"}' >/dev/null 2>&1
    sleep 1
    lancia "$APP"
    return
  done
}

# Switching on, from standby (Quick Start+) or a cold start.
# Within five minutes of booting this is a cold start, and the app in front before
# it is the one kept in $FRONT; later, an update has started this script again,
# and the app in front is the one in use.
ultima=
[ "$(cut -d. -f1 /proc/uptime)" -lt 300 ] && ultima=$(cat "$FRONT" 2>/dev/null)
(
  # This script starts about 40 s into a cold start: then the first "Active" is the start.
  spento=$(spento_all_avvio "$(secondi)")
  primo=1
  while vivo; do
    $LS -i luna://com.webos.service.tvpower/power/getPowerState '{"subscribe":true}' 2>/dev/null |
    while read -r riga; do
      vivo || exit
      stato=$(echo "$riga" | sed -n 's/.*"state" *: *"\([^"]*\)".*/\1/p')
      [ -n "$stato" ] || continue
      echo "$stato" > /tmp/lightkeeper.power
      # The first state this script reads is how it found the TV, not a change.
      if [ "$primo" = 1 ]; then
        primo=0
        if [ "$stato" = Active ]; then quando="TV on"; else quando="TV $stato"; fi
        if [ "$spento" = 1 ]; then quando="$quando, a cold start"; else quando="$quando, not opening"; fi
        diario "script started, $quando (up $(secondi) s)"
        primo_stato=1
      else
        primo_stato=0
      fi
      case "$stato" in
        Active)
          if [ "$spento" = 1 ]; then
            [ "$primo_stato" = 1 ] || diario "TV on (Active) (up $(secondi) s)"
            set_to openAtPowerOn true && { apri "$ultima"; via_account & }
          fi
          spento=0 ;;
        "Screen Saver"|"Screen Off") spento=0 ;;
        Prepared) ;;
        # Going off: the app in front now is the one webOS will put back, and the next
        # time on gets a new random background.
        *) [ "$spento" = 0 ] && { ultima=$(cat "$FRONT" 2>/dev/null); date +%s > "$SESSIONE"; }; spento=1 ;;
      esac
    done
    spento=0 primo=0 # luna-send started again with the TV already on
    sleep 5
  done
) </dev/null >/dev/null 2>&1 &

# Back from the last app, while Lightkeeper has the Home key. Where LG has a Home
# app (webOS 6 on), sam opens it, for the same reason ("undefined") as any other
# launch: the life events are watched (su_evento above). Where there is none, the
# foreground app is watched instead (su_davanti above); webOS 3 has no life events.
# Luna may be slow after a cold boot: asked until it answers, else as on webOS 6.
(
  home_lg=si
  for i in 1 2 3 4 5 6 7 8 9 10; do
    vivo || exit
    r=$($LS -n 1 -w 5000 luna://com.webos.applicationManager/getAppInfo '{"id":"com.webos.app.home"}' 2>/dev/null)
    case "$r" in *'"returnValue"'*) echo "$r" | grep -q '"returnValue" *: *false' && home_lg=no; break ;; esac
    sleep 6
  done
  [ "$home_lg" = no ] && diario "no LG Home app: Back from the last app watched by the app in front"
  while vivo; do
    if [ "$home_lg" = no ]; then
      $LS -i luna://com.webos.applicationManager/getForegroundAppInfo '{"subscribe":true}' 2>/dev/null |
      while read -r riga; do
        vivo || exit
        case "$riga" in *'"appId"'*) ;; *) continue ;; esac
        app=$(echo "$riga" | sed -n 's/.*"appId" *: *"\([^"]*\)".*/\1/p')
        [ -n "$app" ] && [ "$app" != "$prima" ] && echo "$app" > "$FRONT" 2>/dev/null
        su_davanti "$app"
      done
    else
      $LS -i luna://com.webos.applicationManager/getAppLifeEvents '{"subscribe":true}' 2>/dev/null |
      while read -r riga; do
        vivo || exit
        evento_da "$riga"
        [ -n "$evento" ] || continue
        [ "$evento" = foreground ] && [ "$card" = card ] && [ -n "$app" ] && [ "$app" != "$prima" ] && echo "$app" > "$FRONT" 2>/dev/null
        su_evento "$evento" "$app" "$card" "$(date +%s)"
      done
    fi
    sleep 5
  done
) </dev/null >/dev/null 2>&1 &
