#!/bin/sh

set -eu

cmd=$(basename $0)
PUNCHWALLET=${PUNCHWALLET:-$HOME/.local/share/$cmd/wallet}
statefile=$HOME/.local/share/$cmd/state

# Helper function to show an error and quit
err() {
  printf "Error: %s\n" "$1" > /dev/stderr
  exit 1
}

# List of memory states (cannot contain white spaces)
STATE_PAUSED="paused"
STATE_RUNNING="running"

# Create missing directories and state file
[ -d "$PUNCHWALLET" ] || mkdir -p "$PUNCHWALLET" || err "Failed to create wallet $PUNCHWALLET"
statedir=$(dirname "$statefile")
[ -d "$statedir" ] || mkdir -p "$statedir" || err "Failed to create directory to keep the state"
[ -r "$statefile" ] || touch "$statefile" || err "Could not create state file"

# Functions that define the respective commands
help() {
  printf "\
Usage: %s <command>
Punch and inspect your cards for time keeping.

Commands:
  ls                        List available cards in your wallet
  today [<card> ..]         Show time spent at \$DATE (today)
  week [<card> ..]          Show time spent in the week containing \$DATE (today)
  cat [<card> ..]           In detail show time spent at \$DATE (today)
  elapsed                   Show time elapsed in current timekeeping
  rm <card> [<card> ..]     Remove the specified cards
  start <card>              Start timekeeping in the specified card
  pause                     Pause timekeeping
  resume                    Resume timekeeping for paused card
  toggle                    Toggle between paused and running states
  stop                      Stop timekeeping
  state                     Print current state

By design, this application does not allow for double time keeping.

The cards are stored in the wallet \$PUNCHWALLET which is set to
~/.local/share/%s/wallet by default.

" "$cmd" "$cmd"
  exit 0
}

## State machine

# If timekeeping is currently active, then this function prints the card name
# and returns true. Otherwise, it prints nothing and returns false.
__is_running() {
  grep -q "$STATE_RUNNING" "$statefile" && cut -d ' ' -f 2- "$statefile"
}

# Same as __is_running but for the paused state
__is_paused() {
  grep -q "^$STATE_PAUSED " "$statefile" && cut -d ' ' -f 2- "$statefile"
}

# Set the current state
__set_running() {
  printf "%s %s" "$STATE_RUNNING" "$1" > "$statefile"
}

__set_paused() {
  printf "%s %s" "$STATE_PAUSED" "$1" > "$statefile"
}

__set_stopped() {
  printf "" > "$statefile"
}

## Database

# Check cards
# This function aborts if a card is not given and prints the card names with
# duplicates removed.
__check_cards() {
  [ "${1:-}" ] || err "No card specified"
  for card in $(echo "$@" | tr ' ' "\n" | sort -u); do
    [ ! -f "$PUNCHWALLET/$card" ] && err "Card \"$1\" does not exist"
    echo "$card"
  done
}

# Write time to punch card
# @argument $1: punch card
# @argument $2: if set, write end time
__time_write() {
  printf "%s%s" "${2:+ }" "$(date +"%s")" >> "$PUNCHWALLET/$1"
  [ ! "${2:-}" ] || (printf '\n' >> "$PUNCHWALLET/$1")
}

# Get times of specified cards
# This returns "closed" time spans in the sense that each line has two numbers,
# start and end time (even if some timekeeping process is still ongoing; just
# put `now`).
# @argument $1: start time
# @argument $2: range, any of {'day', 'week'}
# @argument $3, ...: cards
__times_retrieve() {
  start=$1
  shift
  range=$((24 * 60 * 60))
  [ "$1" = "week" ] && range=$((7 * range))
  shift
  # Check availability and duplicates
  # Retrieve times
  now=$(date +"%s")
  [ $now -lt $start ] && return
  for card in $@; do
    cat "$PUNCHWALLET/$card"
    echo ""
  done | sort -u | awk -vstart="$start" -vend="$((start + range))" -vnow="$now" '
  NF == 0               { next }
  $1 > end              { exit }
  NF == 1               { $2 = now }
  $2 < start            { next }
  $1 < start            { $1 = start }
  $2 > end              { $2 = end }
                        { print }'
}

## Commands

# List available cards
list_cards() {
  ls "$PUNCHWALLET"
}

# Show time spent today
show_today() {
  [ "$#" -eq 0 ] && set - $(ls "$PUNCHWALLET")
  cards=$(__check_cards "$@")
  daystart=$(date --date "$DATE 00:00" +"%s")
  __times_retrieve "$daystart" "day" "$cards" | \
    awk '{total = total + $2 - $1} END { if (total) print "@"total }' | \
    date --utc -f /dev/stdin +"%H:%M"
}

# Print weekly summary
show_week() {
  [ "$#" -eq 0 ] && set - $(ls "$PUNCHWALLET")
  cards=$(__check_cards "$@")
  echo " Mon    Tue    Wed    Thu    Fri    Sat    Sun"
  startofweek=$(date --date "$DATE - $(date --date "$DATE" +"%u") days + 1 day 00:00:00" +"%s")
  now=$(date +"%s")
  # Filter times, split at midnight, sort, sum, print, and also add "now now"
  # as a trick to fill the times until today
  __times_retrieve "$startofweek" "week" "$cards" | \
    awk -vstart="$startofweek" -vsecperday="$((60 * 60 * 24))" -vnow="$now" '{
      t0 = $1 - start
      t1 = $2 - start
      day0 = int(t0 / secperday)
      day1 = int(t1 / secperday)
      delta = 0
      while(day0 + delta < day1) {
        delta = delta + 1
        p = (day0+delta)*secperday+start-1
        print $1, p
        $1 = p + 1
      }
      print $1, $2
    }
    END { for(d=0; d<7 && start + d * secperday < now; d++)
        print start + d * secperday, start + d * secperday }' | \
    sort | \
    awk -vstart="$startofweek" -vsecperday="$((60 * 60 * 24))" '
      BEGIN { total = 0 }
      {
        day = int(($1 - start) / secperday)
          if (day > currentday) {
            print "@"total
            currentday = day
            total = 0
          }
        total = total + $2 - $1
      }
      END {print "@"total}' | \
    date --utc -f /dev/stdin +"%H:%M" | xargs -I {} printf "%s  " {}
  printf "\n"
}

# Show details of today
show_details() {
  [ "$#" -eq 0 ] && set - $(ls "$PUNCHWALLET")
  cards=$(__check_cards "$@")
  daystart=$(date --date "$DATE 00:00" +"%s")
  tmpf=$(mktemp)
  __times_retrieve "$daystart" "day" "$cards" > "$tmpf"
  # Start times
  fstart=$(mktemp)
  awk '{print "@"$1}' < "$tmpf" | date -f /dev/stdin +"%H:%M" > "$fstart"
  # duration
  fdur=$(mktemp)
  awk '{print "@"($2-$1)}' < "$tmpf" | date --utc -f /dev/stdin +"%H:%M:%S" | \
    sed 's/^\(.*\)$/(up \1)/'> "$fdur"
  # combine
  paste "$fstart" "$fdur"
  # clean up
  rm -f "$tmpf" "$fstart" "$fdur"
}


# Delete punch card
remove_card() {
  [ "${1:-}" ] || err "No card specified"
  for card in $@; do
    [ ! -f "$PUNCHWALLET/$card" ] && err "Card \"$card\" does not exist"
    rm -i "$PUNCHWALLET/$card"
  done
  # Update state if necessary
  active=$(__is_running || __is_paused)
  for card in $@; do
    if [ "$active" = "$card" ]; then
      __set_stopped
      break
    fi
  done
}

# Pause currently running time-keeping
pause_timekeeping() {
  card=$(__is_running)
  __time_write "$card" end
  __set_paused "$card"
}

# Resume paused time-keeping
resume_timekeeping() {
  card=$(__is_paused)
  __time_write "$card"
  __set_running "$card"
}

# Toggle pause/resume
toggle_timekeeping() {
  __is_running && pause_timekeeping || (__is_paused && resume_timekeeping)
}

# Stop time-keeping
stop_timekeeping() {
  __is_running && pause_timekeeping || true
  __is_paused && __set_stopped
}

# Punch card
punch_card() {
  [ "${1:-}" ] || err "No card specified"
  echo "$1" | grep -q '[^A-Za-z0-9]' && err "Use only alphanumeric characters"
  __is_running && stop_timekeeping || true
  __time_write "$1"
  __set_running "$1"
}

# Show elapsed time
show_elapsed() {
  card=$(__is_running)
  t0=$(tail -1 "$PUNCHWALLET/$card")
  t1=$(date +"%s")
  date --date @$((t1 - t0)) --utc +"%T"
}

# Show current state
show_state() {
  cat "$statefile"
}

# Run command
DATE=${DATE:-today}

case "${1:-}" in
  ls) list_cards ;;
  today) shift; show_today "$@" ;;
  week) shift; show_week "$@" ;;
  cat) shift; show_details "$@" ;;
  rm) shift; remove_card "$@" ;;
  start) punch_card "${2:-}" > /dev/null ;;
  pause) pause_timekeeping > /dev/null ;;
  resume) resume_timekeeping > /dev/null ;;
  toggle) toggle_timekeeping > /dev/null ;;
  stop) stop_timekeeping > /dev/null ;;
  state) show_state ;;
  elapsed) show_elapsed ;;
  *) help ;;
esac
