package com.gh.gamecenter.plugin

import com.android.build.api.transform.Format
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInvocation
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipEntry

class PluginAssetTransform : Transform() {

    override fun getName() = "PluginTransform"

    override fun getInputTypes() = TransformManager.CONTENT_CLASS

    override fun getScopes() = TransformManager.SCOPE_FULL_PROJECT

    override fun isIncremental() = false

    override fun transform(transformInvocation: TransformInvocation) {
        if (!isIncremental) {
            transformInvocation.outputProvider.deleteAll()
        }

        var index = 0

        transformInvocation.inputs.forEach { input ->
            // 处理 dir 部分
            input.directoryInputs.forEach { directoryInput ->
                val outputDir = transformInvocation.outputProvider
                    .getContentLocation(
                        directoryInput.name,
                        directoryInput.contentTypes,
                        directoryInput.scopes,
                        Format.DIRECTORY
                    )

                FileUtils.copyDirectory(directoryInput.file, outputDir)

                directoryInput.file.walkTopDown().forEach { file ->
                    if (file.isFile && file.extension == "class") {
                        val classReader = ClassReader(FileInputStream(file))
                        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        val classVisitor = AssetOpenClassVisitor(classWriter)

                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                        val fos = FileOutputStream(File(outputDir, file.relativeTo(directoryInput.file).path))
                        fos.write(classWriter.toByteArray())
                        fos.close()
                    }
                }
            }

            // 处理 jar 部分
            input.jarInputs.forEach { jarInput ->
                val jarFile = jarInput.file
                if (jarFile.exists() && jarFile.extension == "jar") {
                    val modifiedJarFile = modifyJar(jarFile)
                    // 必须给 jar 重新命名，否则会冲突
                    var jarName = jarFile.name
                    if (jarName.endsWith(".jar")) {
                        jarName = jarName.substring(0, jarName.length - 4)
                    }
                    val dest = transformInvocation.outputProvider?.getContentLocation(
                        jarName + "_" + index,
                        jarInput.contentTypes,
                        jarInput.scopes,
                        Format.JAR
                    )
                    FileUtils.copyFile(modifiedJarFile, dest)
                    index++
                }
            }
        }
    }

    /**
     * 主要步骤：
     * 1.遍历所有jar文件
     * 2.解压jar然后遍历所有的class
     * 3.读取class的输入流并使用 ASM 修改，然后保存到新的 jar 文件中
     */
    fun modifyJar(jarFile: File): File {
        val jarInput = JarFile(jarFile)
        val jarInputEntries = jarInput.entries()

        // 创建一个新的文件
        val jarOutput = File(jarFile.parentFile, "temp_" + jarFile.name)
        if (jarOutput.exists()) jarOutput.delete()

        val jarOutputStream = JarOutputStream(BufferedOutputStream(FileOutputStream(jarOutput)))

        while (jarInputEntries.hasMoreElements()) {
            val jarInputEntry = jarInputEntries.nextElement()
            val jarInputEntryName = jarInputEntry.name

            val jarInputStream = jarInput.getInputStream(jarInputEntry)

            val zipEntry = ZipEntry(jarInputEntryName)

            jarOutputStream.putNextEntry(zipEntry)

            if (shouldModify(jarInputEntryName)) {
                val classReader = ClassReader(IOUtils.toByteArray(jarInputStream))
                val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                val classVisitor = AssetOpenClassVisitor(classWriter)

                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                jarOutputStream.write(classWriter.toByteArray())
            } else {
                jarOutputStream.write(IOUtils.toByteArray(jarInputStream))
            }

            jarOutputStream.closeEntry()

            jarInputStream.close()
        }
        jarInput.close()
        jarOutputStream.flush()
        jarOutputStream.close()
        return jarOutput
    }

    private fun shouldModify(filePath: String): Boolean {
        return filePath.endsWith(".class")
                && !filePath.contains("PluginRedirectHelper.class")
                && !filePath.contains("R.class")
                && !filePath.contains("R$")
                && !filePath.contains("BuildConfig.class")
    }

    class AssetOpenClassVisitor(cv: ClassVisitor) : ClassVisitor(Opcodes.ASM9, cv) {
        override fun visitMethod(
            access: Int, name: String, descriptor: String,
            signature: String?, exceptions: Array<String>?
        ): MethodVisitor {
            val mv = super.visitMethod(access, name, descriptor, signature, exceptions)
            return AssetOpenMethodVisitor(mv)
        }
    }

    class AssetOpenMethodVisitor(mv: MethodVisitor) : MethodVisitor(Opcodes.ASM9, mv) {
        override fun visitMethodInsn(
            opcode: Int, owner: String, name: String,
            descriptor: String, isInterface: Boolean
        ) {

            // Print all method call in AssetManager
            if ("android/content/res/AssetManager" == owner) {
                println("owener:$owner method:$name opcode:$opcode desc:$descriptor")
            }

            // Replace AssetManager.open with com.gh.gamecenter.core.utils.PluginRedirectHelper.openAsset
            if ("android/content/res/AssetManager" == owner
                && "open" == name
                && opcode == Opcodes.INVOKEVIRTUAL
                && "(Ljava/lang/String;)Ljava/io/InputStream;" == descriptor) {

                println("owener:$owner method:$name opcode:$opcode desc:$descriptor make it!")

                return super.visitMethodInsn(
                    Opcodes.INVOKESTATIC,
                    "com/gh/gamecenter/core/utils/PluginRedirectHelper",
                    "openAsset",
                    "(Landroid/content/res/AssetManager;Ljava/lang/String;)Ljava/io/InputStream;",
                    isInterface
                )
            }

            // Replace AssetManager.list with com.gh.gamecenter.core.utils.PluginRedirectHelper.listAsset
            if ("android/content/res/AssetManager" == owner
                && "list" == name
                && opcode == Opcodes.INVOKEVIRTUAL
                && "(Ljava/lang/String;)[Ljava/lang/String;" == descriptor) {

                return super.visitMethodInsn(
                    Opcodes.INVOKESTATIC,
                    "com/gh/gamecenter/core/utils/PluginRedirectHelper",
                    "listAsset",
                    "(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;",
                    isInterface)
            }
            return super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
        }
    }

}
