#!/usr/bin/env bash

# mainly based on https://www.sh-zam.com/2019/05/debugging-krita-on-android.html

set -euo pipefail
IFS=$'\n\t'

function get_buildoption() {
	jq -r '.[] | select(.name == "'$1'") | .value' < meson-info/intro-buildoptions.json
}

function get_cpp_compiler() {
	jq -r '.[] | select(.name == "'$1'") | .target_sources.[] | select(.language == "cpp") | .compiler.[]' < meson-info/intro-targets.json
}

# customize
default_app_id=uk.co.powdertoy.tpt
default_app_exe=powder
default_lldb_server=/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/17/lib/linux/aarch64/lldb-server
default_lldb_client=/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin/lldb.sh
in_build_site=no
if which jq >/dev/null && [[ -f meson-info/intro-buildoptions.json ]]; then
	>&2 echo "[+] pwd is a build site, auto-detecting parameters"
	in_build_site=yes
	default_app_id=$(get_buildoption app_id)
	default_app_exe=$(get_buildoption app_exe)
	found_lldb=no
	compiler_path=$(get_cpp_compiler $default_app_exe)
	for line in $compiler_path; do
		# iterate over the command array (might be for example [ "ccache", "aarch64-linux-android21-clang++" ])
		if (echo $line | grep toolchains && basename $line | grep android | grep clang++) >/dev/null; then
			arch=$(basename $line | cut -d '-' -f 1)
			default_lldb_server=$(realpath $(dirname $line)/../lib/clang/*/lib/linux/$arch/lldb-server)
			default_lldb_client=$(realpath $(dirname $line)/lldb.sh)
			found_lldb=yes
		fi
	done
	if [[ $found_lldb != yes ]]; then
		>&2 echo "[-]   cannot determine LLDB paths from compiler command array:"
		for line in $compiler_path; do
			>&2 echo "[-]     - $line"
		done
		exit 1
	fi
	>&2 echo "[+]   APP_ID: $default_app_id"
	>&2 echo "[+]   APP_EXE: $default_app_exe"
	>&2 echo "[+]   LLDB_SERVER: $default_lldb_server"
	>&2 echo "[+]   LLDB_CLIENT: $default_lldb_client"
else
	>&2 echo "[+] pwd is not a build site, not auto-detecting parameters"
fi
app_id=${APP_ID:-$default_app_id}
app_exe=${APP_EXE:-$default_app_exe}
lldb_server=${LLDB_SERVER:-$default_lldb_server}
lldb_server_port=${LLDB_SERVER_PORT:-9998}
jdb_port=${JDB_PORT:-13456}
lldb_client=${LLDB_CLIENT:-$default_lldb_client}
adb=${ADB:-adb}
jdb=${JDB:-jdb}

# don't customize unless necessary
app_activity=${APP_ACTIVITY:-PowderActivity}
lldb_server_staging=${LLDB_SERVER_STAGING:-/data/local/tmp/lldb-server}
lldb_server_remote=${LLDB_SERVER_REMOTE:-lldb-server}
pidof_retry_count=${PIDOF_RETRY_COUNT:-20}
pidof_retry_delay=${PIDOF_RETRY_DELAY:-0.1}

function check_which() {
	if ! which $1 >/dev/null; then
		>&2 echo "[-] can't run $1"
		return 1
	fi
}

function check_env() {
	if ! [[ -f $lldb_server ]]; then
		>&2 echo "[-] $lldb_server doesn't exist"
		return 1
	fi
	check_which $lldb_client
	check_which $adb
	check_which $jdb
}

function check_adb() {
	$adb shell whoami >/dev/null
}

function maybe_install_app() {
	if [[ $in_build_site != yes ]]; then
		return 0
	fi
	android_keystore=$(get_buildoption android_keystore)
	android_keyalias=$(get_buildoption android_keyalias)
	if [[ -z ${ANDROID_KEYSTORE_PASS-} ]]; then
		>&2 echo "[-] ANDROID_KEYSTORE_PASS not set"
		>&2 echo
		>&2 cat << HELP
The current directory seems to be a build site, but ANDROID_KEYSTORE_PASS is not set, so android/$app_exe.apk cannot be invoked. If you don't have a keystore yet, create one with:

	ANDROID_KEYSTORE_PASS=bagelsbagels keytool -genkey \\
		-keystore $android_keystore \\
		-alias $android_keyalias \\
		-storepass:env ANDROID_KEYSTORE_PASS \\
		-keypass:env ANDROID_KEYSTORE_PASS \\
		-dname CN=bagels

Then try again with:

	ANDROID_KEYSTORE_PASS=bagelsbagels $0

Naturally, replace bagelsbagels with an appropriate password.
HELP
		exit 1
	fi
	>&2 echo "[+] adb installing android/$app_exe.apk"
	if ! $adb install android/$app_exe.apk; then
		>&2 echo "[-]   failed"
		return 1
	fi
}

function check_debuggable() {
	$adb shell run-as $app_id whoami >/dev/null
}

function find_lldb_server() {
	$adb shell run-as $app_id pgrep $lldb_server_remote >/dev/null
}

function kill_lldb_server() {
	if ! $adb shell run-as $app_id pkill $lldb_server_remote; then
		>&2 echo "[-]   failed"
		return 1
	fi
}

function adb_forward() {
	>&2 echo "[+] adb forwarding tcp:$jdb_port jdwp:$pid"
	if ! ($adb forward tcp:$jdb_port jdwp:$pid | grep $jdb_port >/dev/null); then
		>&2 echo "[+]   failed"
		return 1
	fi
}

function adb_unforward() {
	$adb forward --remove tcp:$jdb_port
}

function undo_current_adb_forward() {
	>&2 echo "[+] adb un-forwarding orphaned tcp:$jdb_port"
	adb_unforward
}

function maybe_undo_previous_adb_forward() {
	if $adb forward --list | grep tcp:$jdb_port; then
		>&2 echo "[+] adb un-forwarding orphaned tcp:$jdb_port"
		adb_unforward
	fi
}

function maybe_kill_previous_lldb_server() {
	if find_lldb_server; then
		>&2 echo "[+] killing orphaned $lldb_server_remote"
		kill_lldb_server
	fi
}

function kill_current_lldb_server() {
	>&2 echo "[+] killing $lldb_server_remote"
	kill_lldb_server
}

function start_app() {
	>&2 echo "[+] starting $app_id/.$app_activity"
	set +e
	$adb shell am start -D -n "$app_id/.$app_activity" >/dev/null
	set -e
	local i
	local maybe_pid
	local pidof_result
	for ((i = 0; i <= $pidof_retry_count; i++)); do
		set +e
		maybe_pid=$($adb shell pidof $app_id)
		pidof_result=$?
		set -e
		if [[ $pidof_result == 0 ]]; then
			pid=$maybe_pid
			break
		fi
		sleep $pidof_retry_delay
	done
	if [[ -z ${pid-} ]]; then
		>&2 echo "[-]   failed"
		return 1
	fi
	echo $pid
}

function jdb_attach() {
	>&2 echo "[+] attaching jdb in the background"
	$jdb -attach localhost:$jdb_port >/dev/null 2>/dev/null &
	disown $!
	# at some point jdb exits because it doesn't have an stdin... fine by me
}

function maybe_deploy_lldb_server() {
	if ! $adb shell [[ -f $lldb_server_staging ]]; then
		>&2 echo "[+] $lldb_server_remote not present on host, deploying"
		if ! ($adb push $lldb_server $lldb_server_staging && $adb shell chmod +x $lldb_server_staging); then
			>&2 echo "[-]   failed"
		fi
	fi
}

function start_lldb_server() {
	if ! $adb shell run-as $app_id pgrep $lldb_server_remote >/dev/null; then
		>&2 echo "[+] $lldb_server_remote not running on host, starting"
		$adb shell run-as $app_id cp $lldb_server_staging /data/data/$app_id/$lldb_server_remote
		$adb shell run-as $app_id ./$lldb_server_remote platform --server --listen "*:$lldb_server_port" >/dev/null 2>/dev/null &
		disown $!
		if ! $adb shell run-as $app_id pgrep $lldb_server_remote >/dev/null; then
			>&2 echo "[-]   failed"
			return 1
		fi
	fi
}

function start_lldb() {
	local pid=$1
	>&2 echo "[+] starting $lldb_client"
	local lldb_init=$(mktemp)
	cat - << LLDB_INIT > $lldb_init
platform select remote-android
platform connect connect://localhost:$lldb_server_port
attach $pid
continue
LLDB_INIT
	local lldb_status
	set +e
	$lldb_client --source $lldb_init
	lldb_status=$?
	set -e
	>&2 echo "[+] $lldb_client exited with status $lldb_status"
	rm $lldb_init
}

check_env
check_adb
maybe_install_app
check_debuggable
maybe_kill_previous_lldb_server
maybe_undo_previous_adb_forward
if [[ ${1-} == clean ]]; then
	>&2 echo "[+] done"
	exit 0
fi
maybe_deploy_lldb_server
start_lldb_server
pid=$(start_app)
adb_forward
jdb_attach
start_lldb $pid
kill_current_lldb_server
undo_current_adb_forward
>&2 echo "[+] done"
