package org.openrs2.patcher.transform

import com.github.michaelbull.logging.InlineLogger
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.InsnList
import org.objectweb.asm.tree.InsnNode
import org.objectweb.asm.tree.IntInsnNode
import org.objectweb.asm.tree.LdcInsnNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.tree.TypeInsnNode
import org.objectweb.asm.tree.VarInsnNode
import org.openrs2.asm.InsnMatcher
import org.openrs2.asm.classpath.ClassPath
import org.openrs2.asm.classpath.Library
import org.openrs2.asm.toAbstractInsnNode
import org.openrs2.asm.transform.Transformer
import org.openrs2.patcher.Resource

public class ResourceTransformer(
    private val resources: List<Resource>? = null,
    private val glResources: List<List<Resource>> = Resource.compressGlNatives(),
    private val miscResources: List<Resource> = Resource.compressMiscNatives()
) : Transformer() {
    private var blocks = 0
    private var checksums = 0
    private var glBlocks = 0
    private var miscBlocks = 0

    override fun preTransform(classPath: ClassPath) {
        blocks = 0
        checksums = 0
        glBlocks = 0
        miscBlocks = 0
    }

    override fun transformCode(classPath: ClassPath, library: Library, clazz: ClassNode, method: MethodNode): Boolean {
        if (resources != null) {
            for ((i, match) in RESOURCE_MATCHER.match(method).withIndex()) {
                val resource = resources[i]

                // sanity check that the destination matches up
                val destination = match[2] as LdcInsnNode
                require(destination.cst == resource.destination)

                // update the source (the CRC may have changed)
                val source = match[3] as LdcInsnNode
                source.cst = resource.sourceWithChecksum

                // update file sizes
                method.instructions.set(match[22], resource.uncompressedSize.toAbstractInsnNode())
                method.instructions.set(match[23], resource.compressedSize.toAbstractInsnNode())

                // update digest
                for ((j, byte) in resource.digest.withIndex()) {
                    method.instructions.set(match[28 + 4 * j], byte.toInt().toAbstractInsnNode())
                }

                blocks++
            }

            val checksumMatch = CLIENT_CHECKSUM_MATCHER.match(method).filter { m ->
                val astore = m[0] as VarInsnNode
                val aload = m[3] as VarInsnNode
                val instanceof = m[4] as TypeInsnNode
                astore.`var` == aload.`var` && instanceof.desc == "java/lang/reflect/InvocationTargetException"
            }.singleOrNull()
            if (checksumMatch != null) {
                val client = resources.single { it.source == "runescape.js5" || it.source == "runescape_gl.js5" }
                method.instructions.set(checksumMatch[1], client.checksum.toAbstractInsnNode())

                checksums++
            }
        }

        val match = GL_RESOURCES_MATCHER.match(method).singleOrNull()
        if (match != null) {
            // remove everything but the PUTSTATIC
            for (i in 0 until match.size - 1) {
                method.instructions.remove(match[i])
            }

            // find the type of the resource class
            val new = match[8] as TypeInsnNode
            val type = new.desc

            // create our own resource array
            val list = InsnList()
            create2dResourceArray(list, type, glResources, GL_LOADING_MESSAGES)

            // insert our own resource array before the PUTSTATIC
            method.instructions.insertBefore(match[match.size - 1], list)

            glBlocks++
        }

        val miscMatch = MISC_RESOURCES_MATCHER.match(method).singleOrNull()
        if (miscMatch != null) {
            // remove everything but the PUTSTATIC
            for (i in 0 until miscMatch.size - 1) {
                method.instructions.remove(miscMatch[i])
            }

            // find the type of the resource class
            val new = miscMatch[4] as TypeInsnNode
            val type = new.desc

            // create our own resource array
            val list = InsnList()
            createResourceArray(list, type, miscResources, MISC_LOADING_MESSAGES)

            // insert our own resource array before the PUTSTATIC
            method.instructions.insertBefore(miscMatch[miscMatch.size - 1], list)

            miscBlocks++
        }

        return false
    }

    private fun create2dResourceArray(
        list: InsnList,
        type: String,
        resources: List<List<Resource>>,
        messages: List<String>
    ) {
        list.add(resources.size.toAbstractInsnNode())
        list.add(TypeInsnNode(Opcodes.ANEWARRAY, "[L$type;"))

        for ((i, innerResources) in resources.withIndex()) {
            list.add(InsnNode(Opcodes.DUP))
            list.add(i.toAbstractInsnNode())

            createResourceArray(list, type, innerResources, messages, progressSufix = true)

            list.add(InsnNode(Opcodes.AASTORE))
        }
    }

    private fun createResourceArray(
        list: InsnList,
        type: String,
        resources: List<Resource>,
        messages: List<String>,
        progressSufix: Boolean = false
    ) {
        list.add(resources.size.toAbstractInsnNode())
        list.add(TypeInsnNode(Opcodes.ANEWARRAY, type))

        for ((i, resource) in resources.withIndex()) {
            list.add(InsnNode(Opcodes.DUP))
            list.add(i.toAbstractInsnNode())

            list.add(TypeInsnNode(Opcodes.NEW, type))
            list.add(InsnNode(Opcodes.DUP))
            list.add(LdcInsnNode(resource.destination))
            list.add(LdcInsnNode(resource.sourceWithChecksum))

            list.add(messages.size.toAbstractInsnNode())
            list.add(TypeInsnNode(Opcodes.ANEWARRAY, "java/lang/String"))

            for ((j, message) in messages.withIndex()) {
                val messageWithSuffix = if (progressSufix && resources.size > 1) {
                    "$message (${i + 1}/${resources.size})"
                } else {
                    message
                }

                list.add(InsnNode(Opcodes.DUP))
                list.add(j.toAbstractInsnNode())
                list.add(LdcInsnNode(messageWithSuffix))
                list.add(InsnNode(Opcodes.AASTORE))
            }

            list.add(resource.uncompressedSize.toAbstractInsnNode())
            list.add(resource.compressedSize.toAbstractInsnNode())

            list.add(resource.digest.size.toAbstractInsnNode())
            list.add(IntInsnNode(Opcodes.NEWARRAY, Opcodes.T_INT))

            for ((j, byte) in resource.digest.withIndex()) {
                list.add(InsnNode(Opcodes.DUP))
                list.add(j.toAbstractInsnNode())
                list.add(byte.toInt().toAbstractInsnNode())
                list.add(InsnNode(Opcodes.IASTORE))
            }

            list.add(
                MethodInsnNode(
                    Opcodes.INVOKESPECIAL,
                    type,
                    "<init>",
                    "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;II[I)V"
                )
            )
            list.add(InsnNode(Opcodes.AASTORE))
        }
    }

    override fun postTransform(classPath: ClassPath) {
        logger.info { "Replaced $blocks resource constructor blocks and $checksums tracing exception checksums" }
        logger.info { "Replaced $glBlocks jaggl and $miscBlocks jagmisc resource constructor blocks" }
    }

    private companion object {
        private val logger = InlineLogger()

        private const val RESOURCE_CONSTRUCTOR = """
            NEW DUP
            LDC
            LDC
            ICONST_4 ANEWARRAY (DUP ICONST LDC AASTORE)+
            (ICONST | BIPUSH | SIPUSH | LDC)
            (ICONST | BIPUSH | SIPUSH | LDC)
            BIPUSH
            NEWARRAY
            (DUP (ICONST | BIPUSH) (ICONST | BIPUSH) IASTORE)+
            INVOKESPECIAL
        """
        private const val RESOURCE_ARRAY_CONSTRUCTOR = "ICONST ANEWARRAY (DUP ICONST $RESOURCE_CONSTRUCTOR AASTORE)+"

        private val RESOURCE_MATCHER = InsnMatcher.compile("$RESOURCE_CONSTRUCTOR PUTSTATIC")
        private val GL_RESOURCES_MATCHER = InsnMatcher.compile(
            """
            ICONST ANEWARRAY
            (
                DUP ICONST
                $RESOURCE_ARRAY_CONSTRUCTOR
                AASTORE
            )+
            PUTSTATIC
            """
        )
        private val MISC_RESOURCES_MATCHER = InsnMatcher.compile("$RESOURCE_ARRAY_CONSTRUCTOR PUTSTATIC")
        private val CLIENT_CHECKSUM_MATCHER = InsnMatcher.compile(
            "ASTORE (ICONST | BIPUSH | SIPUSH | LDC) PUTSTATIC ALOAD INSTANCEOF"
        )

        private val GL_LOADING_MESSAGES = listOf(
            "Loading 3D library",
            "Lade 3D-Softwarebibliothek",
            "Chargement de la librairie 3D",
            "Carregando biblioteca 3D"
        )
        private val MISC_LOADING_MESSAGES = listOf(
            "Loading utility library",
            "Lade Utility-Softwarebibliothek",
            "Chargement de la librairie utilitaire",
            "Carregando biblioteca de ferramentas"
        )
    }
}
