#!/usr/bin/env bash
# JT7 Launcher for Linux and macOS: installs Project Playtime Rewrite from the JT7 site, keeps it updated, joins the
# JT7 multiplayer network and starts the game. The game is the Windows build, run through a compatibility layer:
#   Linux: umu-launcher + GE-Proton (preferred), or the system's Wine with DXVK
#   macOS: CrossOver (D3DMetal), or a Wine found in PATH
# It does what the Windows launcher does: full zip into the install folder, the update zip on top whenever it
# changes, the offline Engine.ini block before every launch, then Tailscale for multiplayer.
#
#   jt7-launcher.sh             install / update, then play
#   jt7-launcher.sh --update    install / update only
#   jt7-launcher.sh --network   set up the multiplayer network again (even if you skipped it before)
#   jt7-launcher.sh --log       show the update log
#   jt7-launcher.sh --shortcut  (re)create the app menu / desktop shortcut
#
# Settings (environment variables): JT7_HOME (default ~/Games/JT7), JT7_PROTON (umu PROTONPATH, default GE-Proton),
# JT7_RUNNER (umu | wine | crossover) to force a runner.
set -u

SITE="${JT7_SITE:-https://jt7.jt7launcher.workers.dev}"
OS="$(uname -s)"
JT7_HOME="${JT7_HOME:-$HOME/Games/JT7}"
GAME_DIR="$JT7_HOME/Project Playtime"
EXE="$GAME_DIR/Playtime_Multiplayer.exe"
PREFIX="$JT7_HOME/prefix"
STATE="$JT7_HOME/state"
BOTTLE="JT7"
CX="/Applications/CrossOver.app/Contents/SharedSupport/CrossOver"
TS_SUFFIX="tail54adab.ts.net"
# UE4SS (dwmapi), the mod unlocker (dxgi) and the controller fix (xinput1_3) sit next to the game exe
OVERRIDES="dwmapi,dxgi,xinput1_3=n,b"

say()  { printf '\033[1;31m▶\033[0m %s\n' "$*"; }
note() { printf '  %s\n' "$*"; }
fail() { printf '\033[1;31m✖ %s\033[0m\n' "$*" >&2; pause_exit 1; }
pause_exit() {
    # opened from a shortcut: keep the window up long enough to read it
    if [ -t 0 ] && [ "${JT7_NO_PAUSE:-}" = "" ]; then printf '\nPress Enter to close.'; read -r _; fi
    exit "${1:-0}"
}
ask() { # ask "question" -> 0 for yes
    local a; printf '%s [Y/n] ' "$1"; read -r a || return 1
    case "$a" in [nN]*) return 1;; *) return 0;; esac
}
sha256() { if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi; }
version_of() { [ -f "$1" ] && sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$1" | head -n1; }
mkdir -p "$JT7_HOME" "$STATE" || fail "Can't create $JT7_HOME"
PATH="$PATH:$JT7_HOME/umu" # umu-launcher fetched by this script (see fetch_umu)

# ---- self-update: the site's copy of this script replaces this one when it changes -------------------------
self_update() {
    [ "${JT7_NO_SELF_UPDATE:-}" != "" ] && return
    local me new
    me="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
    [ -w "$me" ] || return
    new="$STATE/jt7-launcher.sh.new"
    curl -fsSL --max-time 20 -o "$new" "$SITE/files/jt7-launcher.sh" 2>/dev/null || { rm -f "$new"; return; }
    if head -n1 "$new" | grep -q '^#!/usr/bin/env bash' && grep -q 'JT7 Launcher for Linux and macOS' "$new" \
        && ! cmp -s "$new" "$me"; then
        chmod +x "$new" && mv "$new" "$me" && { say "JT7 Launcher updated itself, restarting."; exec "$me" "$@"; }
    fi
    rm -f "$new"
}

# ---- downloads: resumable, retried, checked to be a zip --------------------------------------------------------
fetch() { # fetch url dest
    local url="$1" dest="$2" part="$2.part" try
    for try in $(seq 1 30); do
        if curl -fL --connect-timeout 30 --speed-limit 1024 --speed-time 60 -C - -o "$part" --progress-bar "$url"; then
            break
        fi
        # 416: the part file already holds every byte
        if [ -s "$part" ] && curl -sI -r "$(wc -c <"$part" | tr -d ' ')-" "$url" 2>/dev/null | head -n1 | grep -q ' 416'; then break; fi
        [ "$try" = 30 ] && { rm -f "$part"; return 1; }
        note "Connection dropped, resuming in $((try * 2 > 30 ? 30 : try * 2)) s…"
        sleep $((try * 2 > 30 ? 30 : try * 2))
    done
    [ "$(head -c2 "$part")" = "PK" ] || { rm -f "$part"; fail "The download wasn't a zip (the site may be updating). Try again in a few minutes."; }
    mv -f "$part" "$dest"
}

unpack() { # unpack zip dir  (the full zip is ZIP64: a 12 GB .pak inside)
    mkdir -p "$2"
    if [ "$OS" = Darwin ]; then ditto -x -k "$1" "$2" && return
    elif command -v unzip >/dev/null; then unzip -o -q "$1" -d "$2" && return
    elif command -v bsdtar >/dev/null; then bsdtar -xf "$1" -C "$2" && return
    fi
    command -v python3 >/dev/null && python3 -c 'import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])' "$1" "$2" && return
    fail "Couldn't unpack $(basename "$1"). Install unzip (or bsdtar) and try again."
}

free_gb() { df -Pk "$1" | awk 'NR==2 {print int($4/1048576)}'; }

install_game() {
    [ -f "$EXE" ] && return
    say "Installing Project Playtime Rewrite into $GAME_DIR"
    local need=30 have; have="$(free_gb "$JT7_HOME")"
    [ "$have" -lt "$need" ] && fail "Installing needs about $need GB free in $JT7_HOME (there's $have GB). Free some space, or set JT7_HOME to another drive."
    local zip="$STATE/playtime-rewrite.zip"
    note "Downloading the full game (about 13.5 GB, it can be stopped and resumed)…"
    fetch "$SITE/files/playtime-rewrite.zip" "$zip" || fail "Download failed. Run the launcher again to continue where it stopped."
    note "Unpacking…"
    unpack "$zip" "$GAME_DIR"
    rm -f "$zip" "$GAME_DIR/JT7/update.sha256" # the full zip may predate the latest update
    [ -f "$EXE" ] || fail "Installed, but Playtime_Multiplayer.exe wasn't where it should be."
    say "Project Playtime Rewrite is installed."
}

# true when version $1 is newer than $2, number by number (1.05 > 1.04)
version_newer() {
    [ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -t. -k1,1n -k2,2n -k3,3n | tail -n 1)" = "$1" ]
}

update_game() {
    local remote="$STATE/release.json" local_v remote_v
    if curl -fsSL --max-time 20 -H 'Cache-Control: no-cache' -o "$remote" "$SITE/files/release.json" 2>/dev/null; then
        local_v="$(version_of "$GAME_DIR/JT7/release.json")"; remote_v="$(version_of "$remote")"
        # only a newer version updates (while a release is being published the update zip is already newer than release.json)
        if [ -n "$remote_v" ] && ! version_newer "$remote_v" "$local_v" && [ -f "$GAME_DIR/JT7/update.sha256" ]; then
            note "Up to date (version $local_v)."; return
        fi
    else
        note "Couldn't reach the JT7 site; playing the installed version."; return
    fi
    say "Checking for updates…"
    local zip="$STATE/playtime-rewrite-update.zip" hash old
    fetch "$SITE/files/playtime-rewrite-update.zip" "$zip" || { note "Update download failed; playing the installed version."; return; }
    hash="$(sha256 "$zip")"; old="$(cat "$GAME_DIR/JT7/update.sha256" 2>/dev/null)"
    if [ "$hash" != "$old" ]; then
        note "Updating…"
        unpack "$zip" "$GAME_DIR"
        mkdir -p "$GAME_DIR/JT7" && printf '%s' "$hash" >"$GAME_DIR/JT7/update.sha256"
        say "Updated to version $(version_of "$GAME_DIR/JT7/release.json"). What's new:"
        show_log 1
    fi
    rm -f "$zip"
}

show_log() { # show_log [versions]  (the newest N entries of the update log)
    local log="$GAME_DIR/JT7/update-log.txt"
    [ -f "$log" ] || { note "No update log yet."; return; }
    tr -d '\r' <"$log" | awk -v n="${1:-999}" '/^Version / {c++} c > n {exit} c >= 1 {print "  " $0}'
}

# ---- the compatibility layer ----------------------------------------------------------------------------------
pick_runner() {
    if [ -n "${JT7_RUNNER:-}" ]; then RUNNER="$JT7_RUNNER"; return; fi
    if [ "$OS" = Darwin ]; then
        if [ -x "$CX/bin/wine" ]; then RUNNER=crossover
        elif command -v wine64 >/dev/null || command -v wine >/dev/null; then RUNNER=wine
        else
            fail "macOS needs CrossOver to run the game: https://www.codeweavers.com/crossover (the free trial works).
  Install it, open it once, then run this launcher again."
        fi
    else
        if command -v umu-run >/dev/null; then RUNNER=umu
        elif command -v wine >/dev/null; then RUNNER=wine
        elif fetch_umu; then RUNNER=umu
        else
            fail "Linux needs umu-launcher (recommended) or Wine to run the game, and it couldn't be downloaded.
  umu-launcher: https://github.com/Open-Wine-Components/umu-launcher (Arch: umu-launcher, Fedora: umu-launcher,
  Ubuntu/Debian: the .deb from its releases page). Then run this launcher again."
        fi
    fi
}

wine_bin() { command -v wine64 2>/dev/null || command -v wine; }

# Linux with nothing installed (a Steam Deck, say): umu-launcher's self-contained zipapp only needs python3
fetch_umu() {
    command -v python3 >/dev/null || return 1
    local url
    url="$(curl -fsSL --max-time 20 https://api.github.com/repos/Open-Wine-Components/umu-launcher/releases/latest \
        | grep -o 'https://[^"]*-zipapp\.tar' | head -n1)"
    [ -n "$url" ] || return 1
    say "Getting umu-launcher (runs the game with Proton)…"
    curl -fL --progress-bar -o "$STATE/umu.tar" "$url" && tar -xf "$STATE/umu.tar" -C "$JT7_HOME" && rm -f "$STATE/umu.tar"
    [ -x "$JT7_HOME/umu/umu-run" ]
}

# run_wine program args… inside the JT7 prefix / bottle
run_wine() {
    case "$RUNNER" in
        umu) WINEPREFIX="$PREFIX" GAMEID="${JT7_GAMEID:-umu-default}" PROTONPATH="${JT7_PROTON:-GE-Proton}" \
             WINEDLLOVERRIDES="$OVERRIDES" umu-run "$@" ;;
        crossover) WINEDLLOVERRIDES="$OVERRIDES" CX_GRAPHICS_BACKEND="${JT7_GRAPHICS:-d3dmetal}" \
             "$CX/bin/wine" --bottle "$BOTTLE" "$@" ;;
        *) WINEPREFIX="$PREFIX" WINEDLLOVERRIDES="$OVERRIDES" "$(wine_bin)" "$@" ;;
    esac
}

prefix_root() {
    if [ "$RUNNER" = crossover ]; then echo "$HOME/Library/Application Support/CrossOver/Bottles/$BOTTLE"; else echo "$PREFIX"; fi
}

setup_prefix() {
    local root; root="$(prefix_root)"
    [ -d "$root/drive_c/users" ] && return
    say "Setting up the Windows layer (first run only, takes a minute)…"
    if [ "$RUNNER" = crossover ]; then
        "$CX/bin/cxbottle" --bottle "$BOTTLE" --create --template win10_64 >/dev/null 2>&1 \
            || fail "CrossOver couldn't create the JT7 bottle. Open CrossOver once, then try again."
    fi
    run_wine wineboot -u >/dev/null 2>&1
    if [ "$RUNNER" = wine ] && [ "$OS" != Darwin ]; then
        # plain Wine has no DXVK; the game is DirectX 11
        if command -v winetricks >/dev/null; then
            note "Adding DXVK (DirectX 11 on Vulkan)…"
            WINEPREFIX="$PREFIX" winetricks -q dxvk >/dev/null 2>&1 || note "DXVK didn't install; the game may run slowly."
        else
            note "Tip: install winetricks (or umu-launcher) for DXVK; without it the game may run slowly."
        fi
    fi
    [ -d "$root/drive_c/users" ] || fail "The Windows layer didn't set up (no $root/drive_c/users)."
}

# %LOCALAPPDATA% inside the prefix: drive_c/users/<steamuser | crossover | you>/AppData/Local
local_appdata() {
    local root u; root="$(prefix_root)"
    for u in "$root"/drive_c/users/*; do
        case "$(basename "$u")" in Public|public) continue;; esac
        [ -d "$u" ] && { echo "$u/AppData/Local"; return; }
    done
}

# Before each launch: the offline block from JT7/Engine-offline.ini at the end of the user's Engine.ini (replacing an
# older copy, found by the block's first line). The game can rewrite its ini on exit, so this runs every time.
apply_user_ini() {
    local src="$GAME_DIR/JT7/Engine-offline.ini" la dest mark
    [ -f "$src" ] || return
    la="$(local_appdata)"; [ -n "$la" ] || return
    dest="$la/Playtime_Multiplayer/Saved/Config/WindowsNoEditor/Engine.ini"
    mkdir -p "$(dirname "$dest")"; touch "$dest"
    mark="$(head -n1 "$src" | tr -d '\r')"
    # the user's own settings up to an older copy of the block, without trailing blank lines (CRLF like Windows)
    awk -v mark="$mark" '{ line = $0; sub(/\r$/, "", line); if (line == mark) exit; lines[++n] = line }
        END { while (n > 0 && lines[n] ~ /^[[:space:]]*$/) n--
              for (i = 1; i <= n; i++) printf "%s\r\n", lines[i]
              if (n > 0) printf "\r\n" }' "$dest" >"$dest.tmp"
    tr -d '\r' <"$src" | awk '{ printf "%s\r\n", $0 }' >>"$dest.tmp"
    mv -f "$dest.tmp" "$dest"
}

# ---- the JT7 multiplayer network (Tailscale runs natively, outside the Windows layer) ---------------------------
ts_cli() {
    if command -v tailscale >/dev/null; then command -v tailscale
    elif [ -x /Applications/Tailscale.app/Contents/MacOS/Tailscale ]; then echo /Applications/Tailscale.app/Contents/MacOS/Tailscale
    fi
}
ts_dns() { # this computer's name on the JT7 network, e.g. jt7-deck.tail54adab.ts.net
    local ts; ts="$(ts_cli)"; [ -n "$ts" ] || return
    "$ts" status --json 2>/dev/null | awk '/"Self"/ {s=1} s && /"DNSName"/ {print; exit}' \
        | sed -E 's/.*"DNSName":[[:space:]]*"([^"]*)".*/\1/; s/\.$//'
}
setup_network() {
    local force="${1:-}" keyfile="$GAME_DIR/JT7/tailscale-key.txt" key ts host dns sudo=""
    [ -f "$keyfile" ] || return
    dns="$(ts_dns)"
    case "$dns" in *".$TS_SUFFIX") [ -z "$force" ] && return;; esac
    [ -z "$force" ] && [ -f "$STATE/network-declined" ] && return
    say "Multiplayer: friends reach your games over the JT7 private network (Tailscale)."
    if ! ask "Set it up now?"; then
        touch "$STATE/network-declined"; note "Skipped. Run: $0 --network to do it later."; return
    fi
    rm -f "$STATE/network-declined"
    ts="$(ts_cli)"
    if [ -z "$ts" ]; then
        if [ "$OS" = Darwin ]; then
            note "Install Tailscale from https://tailscale.com/download/mac (the standalone version), open it once,"
            note "then run: $0 --network"; return
        fi
        note "Installing Tailscale (it asks for your password)…"
        curl -fsSL https://tailscale.com/install.sh | sh || { note "Tailscale didn't install."; return; }
        ts="$(ts_cli)"; [ -n "$ts" ] || return
    fi
    key="$(tr -d '\r' <"$keyfile" | grep -m1 '^tskey-')"; [ -n "$key" ] || return
    host="jt7-$(hostname -s 2>/dev/null | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9-]/-/g')"
    [ "$OS" != Darwin ] && [ "$(id -u)" != 0 ] && sudo="sudo"
    $sudo "$ts" up --authkey="$key" --hostname="$host" --reset >/dev/null || { note "Tailscale couldn't connect."; return; }
    say "Multiplayer network ready: $(ts_dns)"
}

# ---- shortcuts --------------------------------------------------------------------------------------------------
make_shortcut() {
    local me; me="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
    if [ "$OS" = Darwin ]; then
        local cmd="$HOME/Desktop/JT7 Launcher.command"
        printf '#!/bin/bash\nexec "%s"\n' "$me" >"$cmd" && chmod +x "$cmd" && note "Shortcut: $cmd"
    else
        local apps="${XDG_DATA_HOME:-$HOME/.local/share}/applications" icon="$JT7_HOME/jt7.png"
        mkdir -p "$apps"
        [ -f "$icon" ] || curl -fsSL --max-time 20 -o "$icon" "$SITE/files/jt7.png" 2>/dev/null || icon=applications-games
        cat >"$apps/jt7-launcher.desktop" <<EOF
[Desktop Entry]
Type=Application
Name=JT7 Launcher
Comment=Project Playtime Rewrite
Exec="$me"
Icon=$icon
Terminal=true
Categories=Game;
EOF
        note "Added JT7 Launcher to your app menu."
    fi
    touch "$STATE/shortcut"
}

# ---- play -------------------------------------------------------------------------------------------------------
play() {
    setup_prefix
    apply_user_ini
    # crossplay: the game advertises this computer's 100.x address (a plain address works from every platform) and
    # which platform it's on
    local ts ip; ts="$(ts_cli)"
    [ -n "$ts" ] && ip="$("$ts" ip -4 2>/dev/null | head -n1)"
    case "$ip" in 100.*) export JT7_TAILSCALE_HOST="$ip";; esac
    if [ "$OS" = Darwin ]; then export JT7_PLATFORM=Mac; else export JT7_PLATFORM=Linux; fi
    say "Starting Project Playtime Rewrite…"
    cd "$GAME_DIR" || fail "Missing $GAME_DIR"
    run_wine "$EXE" >"$STATE/game.log" 2>&1
    note "Game closed. (Wine's output is in $STATE/game.log)"
}

main() {
    self_update "$@"
    [ -f "$STATE/shortcut" ] || make_shortcut
    case "${1:-}" in
        --log) show_log; pause_exit 0 ;;
        --shortcut) make_shortcut; pause_exit 0 ;;
    esac
    printf '\033[1;31mJT7 Launcher\033[0m · Project Playtime Rewrite (%s)\n\n' "$([ "$OS" = Darwin ] && echo macOS || echo Linux)"
    pick_runner
    install_game
    update_game
    case "${1:-}" in
        --network) setup_network force; pause_exit 0 ;;
        --update) pause_exit 0 ;;
    esac
    setup_network
    play
}
main "$@"
