package org.openrs2.deob.bytecode.transform

import com.github.michaelbull.logging.InlineLogger
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
import org.openrs2.asm.classpath.ClassPath
import org.openrs2.asm.classpath.Library
import org.openrs2.asm.transform.Transformer
import javax.inject.Singleton

/**
 * A [Transformer] that replaces `INVOKESPECIAL` instructions in static methods
 * with `INVOKEVIRTUAL` equivalents.
 *
 * The client contains some methods in `final` classes that use `INVOKESPECIAL`
 * to invoke non-`<init>` methods on objects of exactly the same type as the
 * containing class. Furthermore, these `INVOKESPECIAL` instructions are
 * sometimes present in `static` methods.
 *
 * As the containing class is `final` and as the method reference is not a
 * superclass of the containing class, these calls actually end up being
 * treated in the exact same way as an `INVOKEVIRTRUAL` call.
 *
 * While these calls are unusual (and probably never generated by a standard
 * Java compiler, as there is no way to express them in Java source code), they
 * are permitted by the
 * [JVM specification](https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.invokespecial).
 *
 * It is likely that this unusual use of `INVOKESPECIAL` is an optimization
 * (`INVOKESPECIAL` was originally used as a more generic `INVOKENONVIRTUAL`
 * instruction, and non-virtual function calls are cheaper than virtual
 * function calls) or an obfuscation technique.
 *
 * The [RemapTransformer] moves static methods containing these `INVOKESPECIAL`
 * instructions to new classes. This change is still permitted by the JVM
 * specification, which does not place any restrictions on the class referenced
 * by the `INVOKESPECIAL` instruction.
 *
 * However, the
 * [verifier](https://github.com/openjdk/jdk11u/blob/3789983e89c9de252ef546a1b98a732a7d066650/src/java.base/share/native/libverify/check_code.c#L1342)
 * in modern JVMs is stricter and considers non-`<init>` `INVOKESPECIAL`
 * instructions referencing classes that are not the containing class or its
 * direct superclass illegal.
 *
 * This transformer replaces `INVOKESPECIAL` with equivalent `INVOKEVIRTUAL`
 * instructions where possible, allowing the [RemapTransformer] to produce
 * verifiable output.
 */
@Singleton
public class InvokeSpecialTransformer : Transformer() {
    private var invokeSpecialsReplaced = 0

    override fun preTransform(classPath: ClassPath) {
        invokeSpecialsReplaced = 0
    }

    override fun transformClass(classPath: ClassPath, library: Library, clazz: ClassNode): Boolean {
        require(clazz.access and (Opcodes.ACC_SUPER or Opcodes.ACC_INTERFACE) != 0)
        return false
    }

    override fun transformCode(classPath: ClassPath, library: Library, clazz: ClassNode, method: MethodNode): Boolean {
        if ((clazz.access and Opcodes.ACC_FINAL) == 0) {
            return false
        }

        for (insn in method.instructions) {
            if (insn !is MethodInsnNode || insn.opcode != Opcodes.INVOKESPECIAL) {
                continue
            } else if (insn.name == "<init>") {
                continue
            } else if (insn.owner != clazz.name) {
                continue
            }

            insn.opcode = Opcodes.INVOKEVIRTUAL
            invokeSpecialsReplaced++
        }

        return false
    }

    override fun postTransform(classPath: ClassPath) {
        logger.info { "Replaced $invokeSpecialsReplaced INVOKESPECIALs with INVOKEVIRTUAL" }
    }

    private companion object {
        private val logger = InlineLogger()
    }
}
