#!/usr/bin/env bash
# numerus-gui: compress files to .numerus or extract .numerus archives, with a progress
# window (zenity on GNOME and most desktops, kdialog on KDE, a terminal otherwise).
#
#   numerus-gui FILE...
#
# File managers run it from "Compress with Numerus" / "Extract with Numerus" and on a
# double-click on a .numerus archive. It runs `numerus --progress` and turns its
# "PROGRESS <done> <total>" lines into the progress bar.
set -u

NUMERUS=${NUMERUS:-$(command -v numerus || echo /usr/bin/numerus)}
# Replaced by linux/build_packages.sh with the line from assets/update-url.txt.
UPDATE_URL=${NUMERUS_UPDATE_URL:-https://numerusapp.ru/version.json}
STATE_DIR=${XDG_CACHE_HOME:-$HOME/.cache}/numerus
NO_AUTO=${XDG_CONFIG_HOME:-$HOME/.config}/numerus/no-auto-update
case "${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}" in ru*) RU=1 ;; *) RU=0 ;; esac
tr_() { if [ "$RU" = 1 ]; then printf '%s' "$1"; else printf '%s' "$2"; fi; }

# "1,07 ГБ" / "1.07 GB"
human() {
  awk -v b="$1" -v ru="$RU" 'BEGIN {
    split(ru ? "байт КБ МБ ГБ ТБ" : "bytes KB MB GB TB", u, " ")
    i = 1; while (b >= 1000 && i < 5) { b /= 1000; i++ }
    if (i == 1) s = sprintf("%d", b)
    else if (b < 10) s = sprintf("%.2f", b)
    else if (b < 100) s = sprintf("%.1f", b)
    else s = sprintf("%.0f", b)
    if (ru) gsub(/\./, ",", s)
    print s " " u[i]
  }'
}

# path, or "name 2.ext", "name 3.ext"... when it exists
free_name() {
  local path=$1 dir base stem ext n=2
  [ -e "$path" ] || { printf '%s' "$path"; return; }
  dir=$(dirname -- "$path"); base=$(basename -- "$path")
  if [[ $base == *.* ]]; then stem=${base%.*}; ext=.${base##*.}; else stem=$base; ext=; fi
  while [ -e "$dir/$stem $n$ext" ]; do n=$((n + 1)); done
  printf '%s' "$dir/$stem $n$ext"
}

# remaining time from bytes done, total and seconds elapsed
eta() {
  awk -v d="$1" -v t="$2" -v s="$3" -v ru="$RU" 'BEGIN {
    if (s < 1 || d <= 0) { print (ru ? "оценка времени..." : "estimating time..."); exit }
    left = (t - d) / (d / s)
    if (left < 60) print (ru ? "осталось меньше минуты" : "less than a minute left")
    else { m = int(left / 60) + 1; print (ru ? "осталось около " m " мин" : "about " m " min left") }
  }'
}

UI=none
if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
  if command -v zenity >/dev/null; then UI=zenity
  elif command -v kdialog >/dev/null && command -v qdbus >/dev/null; then UI=kdialog
  elif command -v kdialog >/dev/null && command -v qdbus6 >/dev/null; then UI=kdialog
  fi
fi
QDBUS=$(command -v qdbus || command -v qdbus6 || true)

message() { # kind (info|error) text
  case $UI in
    zenity) zenity --"$1" --title=Numerus --width=440 --text="$2" ${3:+--timeout=$3} 2>/dev/null ;;
    kdialog) if [ "$1" = error ]; then kdialog --title Numerus --error "$2"; else kdialog --title Numerus --passivepopup "$2" "${3:-5}"; fi ;;
    *) printf '%s\n' "$2" >&2 ;;
  esac
}

run_one() {
  local input=$1 mode output title dest messages fifo pid start total status cancelled=0
  if [ -d "$input" ]; then
    message error "$(tr_ "Папки не поддерживаются: выберите файлы." "Folders are not supported: choose files.")"
    return 1
  fi
  if [[ ${input,,} == *.numerus ]]; then
    mode=decompress; output=$(free_name "${input%.*}")
    title="$(tr_ "Распаковка" "Extracting") «$(basename -- "$input")»"
  else
    mode=compress; output=$(free_name "$input.numerus")
    title="$(tr_ "Сжатие" "Compressing") «$(basename -- "$input")»"
  fi
  dest="→ $(basename -- "$output")"
  messages=$(mktemp); fifo=$(mktemp -u); mkfifo "$fifo"
  "$NUMERUS" "$mode" -i "$input" -o "$output" --progress 2>"$fifo" >/dev/null &
  pid=$!
  start=$(date +%s)

  # Progress lines -> "percent" and "# text" lines for the progress window.
  feed() {
    local line done_ total_ now note=
    while IFS= read -r line; do
      case $line in
        "PROGRESS "*)
          read -r _ done_ total_ <<<"$line"
          now=$(date +%s)
          [ "$total_" -gt 0 ] && echo $((done_ * 100 / total_)) || echo 0
          echo "# $title"$'\n'"$dest"$'\n'"$(human "$done_") $(tr_ из of) $(human "$total_"), $(eta "$done_" "$total_" $((now - start)))${note:+$'\n'$note}"
          ;;
        "Numerus trial:"*) note=${line%%. *}. ;;
        "") ;;
        *"Input is not numeric"*)
          tr_ "Этот файл не подходит для Numerus. Он сжимает числовые данные: таблицы CSV и TSV, списки чисел, показания датчиков и логи. Для обычного текста и других файлов используйте zip или 7-Zip." "This file is not a fit for Numerus. It compresses numeric data: CSV and TSV tables, number lists, sensor readings and logs. Use zip or 7-Zip for text and other files." >>"$messages"; echo >>"$messages" ;;
        *) printf '%s\n' "${line#Error: }" >>"$messages" ;;
      esac
    done <"$fifo"
  }

  case $UI in
    zenity)
      feed | zenity --progress --title=Numerus --width=480 --auto-close \
        --text="$title"$'\n'"$dest" --percentage=0 2>/dev/null
      [ "${PIPESTATUS[1]}" -ne 0 ] && cancelled=maybe
      ;;
    kdialog)
      local ref
      ref=$(kdialog --title Numerus --progressbar "$title"$'\n'"$dest" 100)
      "$QDBUS" $ref showCancelButton true >/dev/null 2>&1
      feed | while IFS= read -r line; do
        case $line in
          "# "*) "$QDBUS" $ref setLabelText "${line#\# }" >/dev/null 2>&1 ;;
          *) "$QDBUS" $ref Set "" value "$line" >/dev/null 2>&1 ;;
        esac
        if [ "$("$QDBUS" $ref wasCancelled 2>/dev/null)" = true ]; then kill "$pid" 2>/dev/null; touch "$messages.cancel"; fi
      done
      "$QDBUS" $ref close >/dev/null 2>&1
      [ -e "$messages.cancel" ] && cancelled=1 && rm -f "$messages.cancel"
      ;;
    *)
      feed | while IFS= read -r line; do
        case $line in "# "*) ;; *) printf '\r%s %s%%' "$title" "$line" >&2 ;; esac
      done
      printf '\n' >&2
      ;;
  esac

  # zenity also ends with a non-zero code when its input closes early (an error), so the
  # window closing counts as a cancel only if numerus did not finish on its own.
  if [ "$cancelled" = maybe ]; then
    if kill -0 "$pid" 2>/dev/null || [ ! -s "$messages" ] || grep -qi "broken pipe\|failed printing" "$messages"; then
      cancelled=1
    else
      cancelled=0
    fi
  fi
  if [ "$cancelled" = 1 ]; then
    kill "$pid" 2>/dev/null
    wait "$pid" 2>/dev/null
    rm -f -- "$(dirname -- "$output")/.$(basename -- "$output").$pid.tmp"
    rm -f "$fifo" "$messages"
    message info "$(tr_ "Отменено" "Cancelled")" 3
    return 1
  fi
  wait "$pid"; status=$?
  rm -f "$fifo"
  if [ "$status" -eq 0 ]; then
    total=$(wc -c <"$input"); total=${total// /}
    local size; size=$(wc -c <"$output"); size=${size// /}
    local pct; pct=$(awk -v a="$size" -v b="$total" -v ru="$RU" 'BEGIN { s = sprintf("%.1f", b ? a / b * 100 : 0); if (ru) gsub(/\./, ",", s); print s }')
    message info "$(tr_ "Готово" "Done"): $(basename -- "$output")"$'\n'"$(human "$total") → $(human "$size") ($pct%)" 4
  else
    message error "$(cat "$messages" 2>/dev/null)"
  fi
  rm -f "$messages"
  return "$status"
}

version() { "$NUMERUS" --version 2>/dev/null | awk '{print $2}'; }

# Is version $1 newer than $2?
newer() {
  awk -v a="$1" -v b="$2" 'BEGIN {
    n = split(a, x, "."); m = split(b, y, "."); k = n > m ? n : m
    for (i = 1; i <= k; i++) { p = x[i] + 0; q = y[i] + 0; if (p != q) exit !(p > q) }
    exit 1
  }'
}

# Check the site's version.json; $1 = manual (1) or automatic (0).
check_updates() {
  local manual=$1 json current latest download
  mkdir -p "$STATE_DIR" && touch "$STATE_DIR/update-check"
  if command -v curl >/dev/null; then
    json=$(curl -s -f -L -m 10 -A "Numerus/$(version)" "$UPDATE_URL" 2>/dev/null)
  elif command -v wget >/dev/null; then
    json=$(wget -q -T 10 -O - "$UPDATE_URL" 2>/dev/null)
  fi
  current=$(version)
  latest=$(printf '%s' "${json:-}" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
  download=$(printf '%s' "${json:-}" | sed -n 's/.*"download"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
  if [ -z "$latest" ]; then
    [ "$manual" = 1 ] && message error "$(tr_ "Не удалось проверить обновления: сайт недоступен." "Could not check for updates: the site is not reachable.")"
    return
  fi
  if newer "$latest" "$current"; then
    local text
    text="$(tr_ "Доступна версия $latest, у вас $current. Открыть страницу загрузки?" "Version $latest is available; you have $current. Open the download page?")"
    if [ "$UI" = zenity ] && zenity --question --title=Numerus --width=420 --text="$text" 2>/dev/null; then
      xdg-open "$download" >/dev/null 2>&1 &
    elif [ "$UI" = kdialog ] && kdialog --title Numerus --yesno "$text"; then
      xdg-open "$download" >/dev/null 2>&1 &
    elif [ "$UI" = none ]; then
      printf '%s\n%s\n' "$text" "$download" >&2
    fi
  elif [ "$manual" = 1 ]; then
    message info "$(tr_ "Numerus $current - последняя версия." "Numerus $current is the latest version.")"
  fi
}

update_due() {
  [ -e "$NO_AUTO" ] && return 1
  [ -n "$(find "$STATE_DIR/update-check" -mmin -1440 2>/dev/null)" ] && return 1
  return 0
}

case ${1:-} in
  --about)
    message info "Numerus $(version)
$(tr_ "Архиватор без потерь для телеметрии, временных рядов, таблиц и логов." "Lossless archiver for telemetry, time series, tables and logs.")
$(tr_ "Покупка и поддержка" "Purchase and support"): i@pfrolov.ru"
    exit 0 ;;
  --check-updates) check_updates 1; exit 0 ;;
esac

if [ $# -eq 0 ]; then
  message info "$(tr_ "Выберите файлы в файловом менеджере и выберите «Сжать в Numerus»,
или дважды щёлкните архив .numerus." "Select files in your file manager and choose “Compress with Numerus”,
or double-click a .numerus archive.")"
  exit 0
fi
result=0
for file in "$@"; do
  run_one "$file" || result=1
done
if [ "$UI" != none ] && update_due; then
  check_updates 0
fi
exit $result
