@file:DependsOn("org.apache.commons:commons-text:1.10.0")

import jadx.api.metadata.ICodeNodeRef
import jadx.core.codegen.TypeGen
import jadx.core.dex.instructions.args.ArgType
import jadx.core.dex.nodes.ClassNode
import jadx.core.dex.nodes.FieldNode
import jadx.core.dex.nodes.MethodNode
import jadx.core.utils.exceptions.JadxRuntimeException
import org.apache.commons.text.StringEscapeUtils

val jadx = getJadxInstance()

jadx.gui.ifAvailable {
	addPopupMenuAction(
		"Custom Frida snippet (g)",
		enabled = ::isActionEnabled,
		keyBinding = "G",
		action = ::runAction,
	)
}

fun isActionEnabled(node: ICodeNodeRef): Boolean {
	return node is MethodNode || node is ClassNode || node is FieldNode
}

fun runAction(node: ICodeNodeRef) {
	try {
		val fridaSnippet = generateFridaSnippet(node)
		log.info { "Custom frida snippet:\n$fridaSnippet" }
		jadx.gui.copyToClipboard(fridaSnippet)
	} catch (e: Exception) {
		log.error(e) { "Failed to generate Frida code snippet" }
	}
}

fun generateFridaSnippet(node: ICodeNodeRef): String {
	return when (node) {
		is MethodNode -> generateMethodSnippet(node)
		is ClassNode -> generateClassSnippet(node)
		is FieldNode -> generateFieldSnippet(node)
		else -> throw JadxRuntimeException("Unsupported node type: " + node.javaClass)
	}
}

fun generateClassSnippet(cls: ClassNode): String {
	return """let ${cls.name} = Java.use("${StringEscapeUtils.escapeEcmaScript(cls.rawName)}");"""
}

fun generateMethodSnippet(mthNode: MethodNode): String {
	val methodInfo = mthNode.methodInfo
	val methodName = if (methodInfo.isConstructor) {
		"\$init"
	} else {
		StringEscapeUtils.escapeEcmaScript(methodInfo.name)
	}
	val overload = if (isOverloaded(mthNode)) {
		".overload(${methodInfo.argumentsTypes.joinToString(transform = this::parseArgType)})"
	} else {
		""
	}
	val shortClassName = mthNode.parentClass.name
	val argNames = mthNode.collectArgNodes().map { a -> a.name }
	val args = argNames.joinToString(separator = ", ")
	val logArgs = if (argNames.isNotEmpty()) {
		argNames.joinToString(separator = " + ', ' + ", prefix = " + ', ' + ") { p -> "'$p: ' + $p" }
	} else {
		""
	}
	val clsSnippet = generateClassSnippet(mthNode.parentClass)
	return if (methodInfo.isConstructor || methodInfo.returnType == ArgType.VOID) {
		// no return value
		"""
		$clsSnippet
		$shortClassName["$methodName"]$overload.implementation = function ($args) {
			console.log('$shortClassName.$methodName is called'$logArgs);
			this["$methodName"]($args);
		};
		""".trimIndent()
	} else {
		"""
		$clsSnippet
		$shortClassName["$methodName"]$overload.implementation = function ($args) {
			console.log('$shortClassName.$methodName is called'$logArgs);
			let ret = this["$methodName"]($args);
			console.log('$shortClassName.$methodName return: ' + ret);
			return ret;
		};
		""".trimIndent()
	}
}

fun generateFieldSnippet(fld: FieldNode): String {
	var rawFieldName = StringEscapeUtils.escapeEcmaScript(fld.name)
	for (methodNode in fld.parentClass.methods) {
		if (methodNode.name == rawFieldName) {
			rawFieldName = "_$rawFieldName"
			break
		}
	}
	return """
		${generateClassSnippet(fld.parentClass)}
		${fld.name} = ${fld.parentClass.name}.$rawFieldName.value;
	""".trimIndent()
}

fun isOverloaded(methodNode: MethodNode): Boolean {
	return methodNode.parentClass.methods.stream().anyMatch { m: MethodNode ->
		m.name == methodNode.name && methodNode.methodInfo.shortId != m.methodInfo.shortId
	}
}

fun parseArgType(x: ArgType): String {
	val typeStr = if (x.isArray) {
		TypeGen.signature(x).replace("/", ".")
	} else {
		x.toString()
	}
	return "'$typeStr'"
}
