#!/bin/sh
# NSO Classic macOS App Launcher
#
# Invariants enforced here:
#   C1  The emulator resolves config/, rms/ and freej2me_system/ against the process working
#       directory, and /Applications is not writable by an ordinary player, so this script
#       creates the per-user support directory, seeds both configuration files, and changes
#       directory before starting Java.
#   F1  Both the system config and the per-app config are seeded. Seeding only the per-app
#       file is not enough: Config.init() creates a missing system file and calls saveConfig(),
#       which truncates the still-empty per-app settings map before the file is ever read.
#   F8  One emulator at a time per support directory. The writers there are destructive and
#       non-transactional, and a legacy ZIP copy of this app shares the same directory, so the
#       launcher holds a kernel lock and refuses to start while another emulator JVM is working
#       in that directory. See "Same-user launch exclusion" below for what this does and does
#       not cover.

set -eu

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
RESOURCES_DIR="$(cd "$SCRIPT_DIR/../Resources" && pwd)"

fail() {
    osascript -e "display dialog \"$1\" with title \"NSO Classic Error\" buttons {\"OK\"} default button \"OK\" with icon stop" >/dev/null 2>&1 || true
    echo "NSO Classic launcher error: $1" >&2
    exit 1
}

[ -n "${HOME:-}" ] || fail "Không xác định được thư mục người dùng (HOME). Không thể khởi chạy NSO Classic."

USER_DATA_DIR="$HOME/Library/Application Support/NSO Classic"
mkdir -p "$USER_DATA_DIR" 2>/dev/null || fail "Không tạo được thư mục dữ liệu người dùng tại $USER_DATA_DIR."
[ -w "$USER_DATA_DIR" ] || fail "Thư mục dữ liệu người dùng không ghi được: $USER_DATA_DIR"
# `lsof` reports the kernel's canonical path for a process working directory, and $HOME itself
# may be a symlink, so the comparison below uses the resolved directory.
USER_DATA_DIR_REAL="$(cd "$USER_DATA_DIR" && pwd -P)"

# --- Same-user launch exclusion -------------------------------------------------------
# Every writer in the support directory (config/, rms/, freej2me_system/) is destructive and
# non-transactional, so two emulators must never share it. Two guards cover different origins:
#
#   1. An advisory lock held by the kernel for as long as the game runs (`lockf`). The kernel
#      drops it when the holder dies, so a crashed session cannot leave a stale lock behind -
#      there is no reclamation step and therefore no window in which one launcher deletes
#      another launcher's lock.
#   2. A scan for a Java process already working in this support directory. Guard 1 only binds
#      processes that take the lock; a legacy ZIP copy of this app, or an older installed copy,
#      still writes here without asking. The scan is also what covers the emulator's own
#      Close/Restart action, which starts a replacement JVM and exits the original: the
#      replacement inherits this working directory, so guard 2 keeps the lock held while it
#      lives instead of releasing it the moment the original JVM exits.
#
# Not covered, by construction: a legacy copy started *after* this app is already running does
# not check anything, and only the player can end that session.
LOCK_FILE="$USER_DATA_DIR/.launch-lock"
PGREP="$(command -v pgrep || true)"
LSOF="$(command -v lsof || true)"
export NSO_LOCK_PGREP="$PGREP" NSO_LOCK_LSOF="$LSOF" NSO_LOCK_DIR="$USER_DATA_DIR_REAL"

# Print the pids of Java processes whose working directory is this support directory.
emulator_pids() {
    [ -n "$PGREP" ] && [ -n "$LSOF" ] || return 0
    for candidate_pid in $("$PGREP" -x java -U "$(id -u)" 2>/dev/null || true); do
        [ "$candidate_pid" = "$$" ] && continue
        candidate_cwd="$("$LSOF" -a -p "$candidate_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1)"
        if [ "$candidate_cwd" = "$USER_DATA_DIR_REAL" ]; then
            printf '%s\n' "$candidate_pid"
        fi
    done
}

[ -e "$LOCK_FILE" ] || : > "$LOCK_FILE" 2>/dev/null || fail "Không tạo được tệp khóa khởi chạy trong $USER_DATA_DIR."

# Checked before taking the lock so a running legacy copy is reported rather than silently
# sharing the directory. Guard 1 still settles simultaneous launches of this app.
running_pids="$(emulator_pids)"
[ -z "$running_pids" ] || fail "NSO Classic đang chạy (tiến trình: $running_pids). Vui lòng đóng cửa sổ game hiện tại (kể cả bản ZIP cũ) rồi mở lại."

# --- First-launch seed: copy each missing file, never replace an existing one ----------
seed_if_missing() {
    template="$RESOURCES_DIR/config/$1"
    destination="$USER_DATA_DIR/$2"
    [ -f "$template" ] || return 0
    [ -e "$destination" ] && return 0
    mkdir -p "$(dirname "$destination")" 2>/dev/null || return 0
    cp "$template" "$destination" 2>/dev/null || true
}

seed_if_missing "freej2me_system/freej2me.conf" "freej2me_system/freej2me.conf"
seed_if_missing "NSO Classic/game.conf" "config/NSO Classic/game.conf"

# --- Locate the embedded runtime ------------------------------------------------------
JAVA_EXEC="$RESOURCES_DIR/jre/bin/java"
if [ ! -x "$JAVA_EXEC" ]; then
    JAVA_EXEC="$(command -v java || true)"
fi
[ -n "$JAVA_EXEC" ] && [ -x "$JAVA_EXEC" ] || fail "Không tìm thấy Java Runtime để khởi chạy NSO Classic. Vui lòng thử cài lại ứng dụng."

EMULATOR_JAR="$RESOURCES_DIR/freej2me.jar"
GAME_JAR="$RESOURCES_DIR/game.jar"
ICON_PATH="$RESOURCES_DIR/AppIcon.icns"

# Report an unreadable payload instead of letting java fail with a message nobody sees when the
# app is launched from Finder. An installed bundle is root-owned, so a file that is not
# world-readable is unreadable to the player.
for required_file in "$EMULATOR_JAR" "$GAME_JAR"; do
    [ -r "$required_file" ] || fail "Không đọc được tệp bắt buộc: $required_file. Vui lòng cài lại ứng dụng."
done

# --- Start Java from the per-user working directory -----------------------------------
cd "$USER_DATA_DIR" || fail "Không truy cập được thư mục dữ liệu người dùng: $USER_DATA_DIR"

set -- \
    -Dfile.encoding=ISO_8859_1 \
    -Xdock:name="NSO Classic"

if [ -f "$ICON_PATH" ]; then
    set -- "$@" "-Xdock:icon=$ICON_PATH"
fi

# No frame-cap argument is passed: argument index 6 overwrites the player's saved fps on
# every launch (FreeJ2ME.java). The first-launch cap comes from the seeded game.conf.
set -- "$@" -jar "$EMULATOR_JAR" "$GAME_JAR" 0 640 360 1

# The kernel holds the lock for as long as this locked shell lives. The shell waits for the
# game, then for any replacement JVM the emulator's Close/Restart action left behind in this
# working directory, so the lock covers the whole session rather than the first process only.
status=0
lockf -t 0 "$LOCK_FILE" /bin/sh -c '
    set -u
    java_exec="$1"
    shift
    "$java_exec" "$@"
    game_status=$?
    while :; do
        replacement=""
        for pid in $("$NSO_LOCK_PGREP" -x java -U "$(id -u)" 2>/dev/null || true); do
            cwd="$("$NSO_LOCK_LSOF" -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n "s/^n//p" | head -n 1)"
            [ "$cwd" = "$NSO_LOCK_DIR" ] && replacement="$replacement $pid"
        done
        [ -n "$replacement" ] || break
        sleep 1
    done
    exit "$game_status"
' nso-launcher "$JAVA_EXEC" "$@" || status=$?

if [ "$status" -eq 75 ]; then
    fail "NSO Classic đang chạy. Vui lòng đóng cửa sổ game hiện tại (kể cả bản ZIP cũ) rồi mở lại."
fi

exit "$status"
