#!/usr/bin/env bash
# Populate src-tauri/gen/android with the parts `tauri android init` cannot know
# about.
#
# `tauri android init` regenerates gen/android from tauri.conf.json, and gen/ is
# not tracked (see .gitignore). Anything hand-maintained therefore lives under
# src-tauri/android/ and is copied in by this script after every init. Run it
# between `tauri android init` and `tauri android build`.
#
# Two kinds of thing are copied:
#
#   1. Our own files — AndroidManifest.xml (BLE permissions), MainActivity.kt,
#      the app build.gradle.kts (signing config + BLE Java sources), ProGuard
#      keep rules.
#
#   2. btleplug's Android backend, which is a *hybrid* Rust/Java crate: the Rust
#      side registers native methods on Java classes that must be compiled into
#      the APK. Upstream tells you to publish a SNAPSHOT maven artifact; we
#      instead lift the Java straight out of the crate sources that Cargo has
#      already downloaded, keyed on the exact versions in Cargo.lock. That makes
#      a Java/Rust version mismatch — the failure mode here is a
#      NoSuchMethodError at first scan, not a build error — impossible by
#      construction.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC="$ROOT/src-tauri/android"
GEN="$ROOT/src-tauri/gen/android"
APP="$GEN/app/src/main"
PKG_PATH="paris/tourolle/bikecontrol"

if [ ! -d "$GEN" ]; then
  echo "❌ $GEN does not exist — run 'cargo tauri android init' first." >&2
  exit 1
fi

echo "Syncing Android sources into gen/android…"

# ---------------------------------------------------------------------------
# 1. Our own files.

mkdir -p "$APP/java/$PKG_PATH"
cp "$SRC/src/main/java/$PKG_PATH/MainActivity.kt" "$APP/java/$PKG_PATH/MainActivity.kt"
echo "  ✓ MainActivity.kt"

# Gradle reads ONLY the gen/ copy — there is no manifest-merger hook for our
# entries — so this tracked file must be the complete manifest.
cp "$SRC/src/main/AndroidManifest.xml" "$APP/AndroidManifest.xml"
echo "  ✓ AndroidManifest.xml"

cp "$SRC/app/build.gradle.kts" "$GEN/app/build.gradle.kts"
echo "  ✓ app/build.gradle.kts"

# build.gradle.kts globs **/*.pro, so dropping this in app/ is enough.
cp "$SRC/app/proguard-bikecontrol.pro" "$GEN/app/proguard-bikecontrol.pro"
echo "  ✓ proguard-bikecontrol.pro"

if [ -d "$SRC/src/main/res" ]; then
  for dir in "$SRC/src/main/res"/*/; do
    [ -d "$dir" ] || continue
    name="$(basename "$dir")"
    mkdir -p "$APP/res/$name"
    cp "$dir"/* "$APP/res/$name/"
    echo "  ✓ res/$name"
  done
fi

# ---------------------------------------------------------------------------
# 2. btleplug's Java backend, at the versions Cargo.lock pins.

crate_version() {
  # First `version = "x"` line after the crate's `name =` line in Cargo.lock.
  awk -v pkg="name = \"$1\"" '
    $0 == pkg { found = 1; next }
    found && /^version = / { gsub(/[",]/, "", $3); print $3; exit }
  ' "$ROOT/Cargo.lock"
}

crate_src() {
  local name="$1" version="$2" dir
  for dir in "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/"$name-$version"; do
    [ -d "$dir" ] && { printf '%s' "$dir"; return 0; }
  done
  return 1
}

BTLEPLUG_VERSION="$(crate_version btleplug)"
JNI_UTILS_VERSION="$(crate_version jni-utils)"

if [ -z "$BTLEPLUG_VERSION" ] || [ -z "$JNI_UTILS_VERSION" ]; then
  echo "❌ Could not read btleplug/jni-utils versions from Cargo.lock." >&2
  exit 1
fi

# jni-utils is an Android-only dependency of btleplug, so a plain `cargo fetch`
# for the host will not have downloaded it.
if ! crate_src jni-utils "$JNI_UTILS_VERSION" >/dev/null; then
  echo "  ↓ fetching Android-target crate sources"
  (cd "$ROOT" && cargo fetch --target aarch64-linux-android >/dev/null)
fi

BTLEPLUG_SRC="$(crate_src btleplug "$BTLEPLUG_VERSION")" || {
  echo "❌ btleplug $BTLEPLUG_VERSION sources not found in the cargo registry." >&2
  exit 1
}
JNI_UTILS_SRC="$(crate_src jni-utils "$JNI_UTILS_VERSION")" || {
  echo "❌ jni-utils $JNI_UTILS_VERSION sources not found in the cargo registry." >&2
  exit 1
}

# Drop any previous copy first: a class left behind from an older crate version
# would still compile and would still be found at runtime.
rm -rf "$APP/java/com/nonpolynomial" "$APP/java/io/github/gedgygedgy"
mkdir -p "$APP/java"

cp -r "$BTLEPLUG_SRC/src/droidplug/java/src/main/java/com" "$APP/java/"
echo "  ✓ btleplug $BTLEPLUG_VERSION Java backend (com.nonpolynomial.btleplug)"

cp -r "$JNI_UTILS_SRC/java/src/main/java/io" "$APP/java/"
echo "  ✓ jni-utils $JNI_UTILS_VERSION Java support (io.github.gedgygedgy.rust)"

# ---------------------------------------------------------------------------
# 3. Patch droidplug: subscribing to an *indicate* characteristic.
#
# Upstream writes ENABLE_NOTIFICATION_VALUE (0x01 0x00) to the CCCD whatever the
# characteristic supports. A characteristic that indicates but does not notify
# rejects that write, and the subscribe fails with "Unable to write descriptor".
# BlueZ picks the value from the properties, so this is Android-only — which is
# what made it look like a Click problem rather than a backend one.
#
# The Click's Sync TX channel (00000004-19ca-…) is read/indicate, so without
# this we lose it on Android while every desktop build is fine.
PERIPHERAL="$APP/java/com/nonpolynomial/btleplug/android/impl/Peripheral.java"
OLD_LINE='descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);'
NEW_LINE='descriptor.setValue(!enable ? BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE : (characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0 ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);'

if ! grep -qF "$OLD_LINE" "$PERIPHERAL"; then
  echo "❌ droidplug's CCCD write is not where this patch expects it." >&2
  echo "   btleplug $BTLEPLUG_VERSION may have fixed or moved it — re-check" >&2
  echo "   Peripheral.setCharacteristicNotification before dropping this step." >&2
  exit 1
fi
python3 - "$PERIPHERAL" "$OLD_LINE" "$NEW_LINE" <<'PATCH'
import sys
path, old, new = sys.argv[1], sys.argv[2], sys.argv[3]
text = open(path).read()
open(path, 'w').write(text.replace(old, new, 1))
PATCH
echo "  ✓ patched droidplug to subscribe to indicate characteristics"

# Patch droidplug: negotiate a usable ATT MTU.
#
# Upstream never calls requestMtu, so Android stays at the 23-byte default and a
# notification carries at most 20 bytes of payload. BlueZ negotiates 517, which
# is why this is invisible on desktop and why the `−` pod's button frames — 21
# bytes, one longer than the cap — arrive whole on a PC and truncated on a
# phone. A truncated frame fails to parse, so the paddle simply does nothing.
#
# Fire-and-forget: the request is queued on the connection and the negotiation
# completes long before service discovery and the CCCD writes, so there is
# nothing to wait for here. A refusal is survivable — it leaves us exactly where
# we already were.
MTU_ANCHOR='                                if (newState == BluetoothGatt.STATE_CONNECTED) {'
MTU_PATCH='                                if (newState == BluetoothGatt.STATE_CONNECTED) {
                                    gatt.requestMtu(517);'

if ! grep -qF "$MTU_ANCHOR" "$PERIPHERAL"; then
  echo "❌ droidplug's connect callback is not where this patch expects it." >&2
  exit 1
fi
python3 - "$PERIPHERAL" "$MTU_ANCHOR" "$MTU_PATCH" <<'PATCH'
import sys
path, old, new = sys.argv[1], sys.argv[2], sys.argv[3]
text = open(path).read()
assert text.count(old) == 1, f"expected exactly one connect callback, found {text.count(old)}"
open(path, 'w').write(text.replace(old, new, 1))
PATCH
echo "  ✓ patched droidplug to request a 517-byte MTU"

echo "✅ Android sources synced"
