package io.github.chinosk.gakumas.localify

import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.KeyEvent
import android.view.MotionEvent
import android.widget.Toast
import com.bytedance.shadowhook.ShadowHook
import com.bytedance.shadowhook.ShadowHook.ConfigBuilder
import io.github.chinosk.gakumas.localify.hookUtils.FileHotUpdater
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker
import io.github.chinosk.gakumas.localify.hookUtils.FilesChecker.localizationFilesDir
import io.github.chinosk.gakumas.localify.mainUtils.json
import io.github.chinosk.gakumas.localify.models.GakumasConfig
import io.github.chinosk.gakumas.localify.models.NativeInitProgress
import io.github.chinosk.gakumas.localify.models.ProgramConfig
import io.github.chinosk.gakumas.localify.ui.game_attach.InitProgressUI
import io.github.libxposed.api.XposedInterface
import io.github.libxposed.api.XposedModule
import io.github.libxposed.api.XposedModuleInterface
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File
import java.lang.reflect.Method
import java.util.Locale
import kotlin.system.measureTimeMillis

val TAG = "GakumasLocalify"

class GakumasHookMain : XposedModule() {
    private var modulePath: String = ""
    private var nativeLibLoadSuccess: Boolean = false
    private var alreadyInitialized = false
    private val targetPackageName = "com.bandainamcoent.idolmaster_gakuen"
    private val nativeLibName = "MarryKotone"

    private var gkmsDataInited = false

    private var getConfigError: Exception? = null
    private var externalFilesChecked: Boolean = false
    private var gameActivity: Activity? = null

    override fun onModuleLoaded(param: XposedModuleInterface.ModuleLoadedParam) {
        modulePath = getModuleApplicationInfo().sourceDir

        ShadowHook.init(
            ConfigBuilder()
                .setMode(ShadowHook.Mode.UNIQUE)
                .build()
        )

        nativeLibLoadSuccess = try {
            System.loadLibrary(nativeLibName)
            true
        } catch (_: UnsatisfiedLinkError) {
            false
        }
    }

    override fun onPackageReady(param: XposedModuleInterface.PackageReadyParam) {
        if (param.packageName != targetPackageName) {
            return
        }

        val classLoader = param.classLoader

        hookMethod(
            classLoader = classLoader,
            className = "android.app.Activity",
            methodName = "dispatchKeyEvent",
            parameterTypes = arrayOf(KeyEvent::class.java),
            before = { chain ->
                val keyEvent = chain.getArg(0) as KeyEvent
                keyboardEvent(keyEvent.keyCode, keyEvent.action)
            }
        )

        hookMethod(
            classLoader = classLoader,
            className = "android.app.Activity",
            methodName = "dispatchGenericMotionEvent",
            parameterTypes = arrayOf(MotionEvent::class.java),
            before = { chain ->
                val motionEvent = chain.getArg(0) as MotionEvent
                val action = motionEvent.action

                val leftStickX = motionEvent.getAxisValue(MotionEvent.AXIS_X)
                val leftStickY = motionEvent.getAxisValue(MotionEvent.AXIS_Y)
                val rightStickX = motionEvent.getAxisValue(MotionEvent.AXIS_Z)
                val rightStickY = motionEvent.getAxisValue(MotionEvent.AXIS_RZ)
                val leftTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_LTRIGGER)
                val rightTrigger = motionEvent.getAxisValue(MotionEvent.AXIS_RTRIGGER)
                val hatX = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X)
                val hatY = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y)

                joystickEvent(
                    action,
                    leftStickX,
                    leftStickY,
                    rightStickX,
                    rightStickY,
                    leftTrigger,
                    rightTrigger,
                    hatX,
                    hatY
                )
            }
        )

        val activityClass = classLoader.loadClass("android.app.Activity")
        hookAllMethods(activityClass, "onStart") { chain ->
            Log.d(TAG, "onStart")
            val currActivity = chain.thisObject as Activity
            gameActivity = currActivity
            if (getConfigError != null) {
                showGetConfigFailed(currActivity)
            } else {
                initGkmsConfig(currActivity)
            }
            chain.proceed()
        }

        hookAllMethods(activityClass, "onResume") { chain ->
            Log.d(TAG, "onResume")
            val currActivity = chain.thisObject as Activity
            gameActivity = currActivity
            if (getConfigError != null) {
                showGetConfigFailed(currActivity)
            } else {
                initGkmsConfig(currActivity)
            }
            chain.proceed()
        }

        val unityPlayerClass = classLoader.loadClass("com.unity3d.player.UnityPlayer")
        val loadNativeMethod = unityPlayerClass.getDeclaredMethod("loadNative", String::class.java)

        hook(loadNativeMethod).intercept { chain ->
            val result = chain.proceed()
            onUnityLoadNativeAfterHook()
            result
        }

        startLoop()
    }

    private fun hookMethod(
        classLoader: ClassLoader,
        className: String,
        methodName: String,
        parameterTypes: Array<Class<*>>,
        before: ((XposedInterface.Chain) -> Unit)? = null,
        after: ((XposedInterface.Chain, Any?) -> Unit)? = null,
    ) {
        val clazz = classLoader.loadClass(className)
        val method = clazz.getDeclaredMethod(methodName, *parameterTypes)
        hook(method).intercept { chain ->
            before?.invoke(chain)
            val result = chain.proceed()
            after?.invoke(chain, result)
            result
        }
    }

    private fun hookAllMethods(clazz: Class<*>, methodName: String, interceptor: (XposedInterface.Chain) -> Any?) {
        val allMethods = (clazz.declaredMethods.asSequence() + clazz.methods.asSequence())
            .filter { it.name == methodName }
            .distinctBy(Method::toGenericString)
            .toList()

        allMethods.forEach { method ->
            hook(method).intercept { chain -> interceptor(chain) }
        }
    }

    @SuppressLint("UnsafeDynamicallyLoadedCode")
    private fun onUnityLoadNativeAfterHook() {
        Log.i(TAG, "UnityPlayer.loadNative")

        if (alreadyInitialized) {
            return
        }

        val app = getCurrentApplication()
        if (app == null) {
            Log.e(TAG, "currentApplication is null")
            return
        }

        if (nativeLibLoadSuccess) {
            showToast("lib$nativeLibName.so loaded.")
        } else {
            showToast("Load native library lib$nativeLibName.so failed.")
            return
        }

        if (!gkmsDataInited) {
            requestConfig(app.applicationContext)
        }

        FilesChecker.initDir(app.filesDir, modulePath)
        initHook(
            "${app.applicationInfo.nativeLibraryDir}/libil2cpp.so",
            File(
                app.filesDir.absolutePath,
                FilesChecker.localizationFilesDir
            ).absolutePath
        )

        alreadyInitialized = true
    }

    private fun getCurrentApplication(): Application? {
        return try {
            val activityThreadClass = Class.forName("android.app.ActivityThread")
            val method = activityThreadClass.getDeclaredMethod("currentApplication")
            method.isAccessible = true
            method.invoke(null) as? Application
        } catch (_: Throwable) {
            null
        }
    }

    @OptIn(DelicateCoroutinesApi::class)
    private fun startLoop() {
        GlobalScope.launch {
            val interval = 1000L / 30
            var lastFrameStartInit = NativeInitProgress.startInit
            val initProgressUI = InitProgressUI()

            while (isActive) {
                val timeTaken = measureTimeMillis {
                    val returnValue = pluginCallbackLooper()
                    if (returnValue == 9) {
                        NativeInitProgress.startInit = true
                    }

                    if (NativeInitProgress.startInit) {
                        NativeInitProgress.pluginInitProgressLooper(NativeInitProgress)
                        gameActivity?.let { initProgressUI.updateData(it) }
                    }

                    if ((gameActivity != null) && (lastFrameStartInit != NativeInitProgress.startInit)) {
                        if (NativeInitProgress.startInit) {
                            initProgressUI.createView(gameActivity!!)
                        } else {
                            initProgressUI.finishLoad(gameActivity!!)
                        }
                    }
                    lastFrameStartInit = NativeInitProgress.startInit
                }
                delay(interval - timeTaken)
            }
        }
    }

    fun initGkmsConfig(activity: Activity) {
        val intent = activity.intent
        val gkmsData = intent.getStringExtra("gkmsData")
        val programData = intent.getStringExtra("localData")
        if (gkmsData != null) {
            val readVersion = intent.getStringExtra("lVerName")
            checkPluginVersion(activity, readVersion)

            gkmsDataInited = true
            val initConfig = try {
                json.decodeFromString<GakumasConfig>(gkmsData)
            }
            catch (e: Exception) {
                null
            }
            val programConfig = try {
                if (programData == null) {
                    ProgramConfig()
                } else {
                    json.decodeFromString<ProgramConfig>(programData)
                }
            }
            catch (e: Exception) {
                null
            }

            if (programConfig?.cleanLocalAssets == true) {
                FilesChecker.cleanAssets()
            }

            if (programConfig?.checkBuiltInAssets == true) {
                FilesChecker.initAndCheck(activity.filesDir, modulePath)
            }

            if (initConfig?.forceExportResource == true) {
                FilesChecker.updateFiles()
            }

            if ((programConfig?.useRemoteAssets == true) || (programConfig?.useAPIAssets == true)) {
                val dataUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    intent.getParcelableExtra("resource_file", Uri::class.java)
                } else {
                    @Suppress("DEPRECATION")
                    intent.getParcelableExtra<Uri>("resource_file")
                }

                if (dataUri != null) {
                    if (!externalFilesChecked) {
                        externalFilesChecked = true
                        FileHotUpdater.updateFilesFromZip(activity, dataUri, activity.filesDir,
                            programConfig.delRemoteAfterUpdate)
                    }
                }
                else if (programConfig.useAPIAssets) {
                    if (!File(activity.filesDir, localizationFilesDir).exists() &&
                        (initConfig?.forceExportResource == false)) {
                        FilesChecker.initAndCheck(activity.filesDir, modulePath)
                    }
                }
            }

            loadConfig(gkmsData)
            Log.d(TAG, "gkmsData: $gkmsData")
        }
    }

    private fun checkPluginVersion(activity: Activity, readVersion: String?) {
        val buildVersionName = BuildConfig.VERSION_NAME
        Log.i(TAG, "Checking Plugin Version: Build: $buildVersionName, Request: $readVersion")
        if (readVersion?.trim() == buildVersionName.trim()) {
            return
        }

        val builder = AlertDialog.Builder(activity)
        val infoBuilder = AlertDialog.Builder(activity)
        builder.setTitle("Warning")
        builder.setCancelable(false)
        builder.setMessage(when (getCurrentLanguage(activity)) {
            "zh" -> "检测到插件版本不一致\n内置版本: $buildVersionName\n请求版本: $readVersion\n\n这可能是使用了 LSPatch 的集成模式，仅更新了插件本体，未重新修补游戏导致的。请使用 $readVersion 版本的插件重新修补或使用本地模式。"
            else -> "Detected plugin version mismatch\nBuilt-in version: $buildVersionName\nRequested version: $readVersion\n\nThis may be caused by using the LSPatch integration mode, where only the plugin itself was updated without re-patching the game. Please re-patch the game using the $readVersion version of the plugin or use the local mode."
        })

        builder.setPositiveButton("OK") { dialog, _ ->
            dialog.dismiss()
        }

        builder.setNegativeButton("Exit") { dialog, _ ->
            dialog.dismiss()
            activity.finishAffinity()
        }

        val dialog = builder.create()

        infoBuilder.setOnCancelListener {
            dialog.show()
        }

        dialog.show()
    }

    private fun showGetConfigFailedImpl(activity: Context, title: String, msg: String, infoButton: String, dlButton: String, okButton: String) {
        if (getConfigError == null) return
        val builder = AlertDialog.Builder(activity)
        val infoBuilder = AlertDialog.Builder(activity)
        val errConfigStr = getConfigError.toString()
        builder.setTitle("$title: $errConfigStr")
        getConfigError = null
        builder.setCancelable(false)
        builder.setMessage(msg)

        builder.setPositiveButton(okButton) { dialog, _ ->
            dialog.dismiss()
        }

        builder.setNegativeButton(dlButton) { dialog, _ ->
            dialog.dismiss()
            val webpage = Uri.parse("https://github.com/chinosk6/gakuen-imas-localify")
            val intent = Intent(Intent.ACTION_VIEW, webpage)
            activity.startActivity(intent)
        }

        builder.setNeutralButton(infoButton) { _, _ ->
            infoBuilder.setTitle("Error Info")
            infoBuilder.setMessage(errConfigStr)
            val infoDialog = infoBuilder.create()
            infoDialog.show()
        }

        val dialog = builder.create()

        infoBuilder.setOnCancelListener {
            dialog.show()
        }

        dialog.show()
    }

    fun showGetConfigFailed(activity: Context) {
        val langData = when (getCurrentLanguage(activity)) {
            "zh" -> {
                mapOf(
                    "title" to "无法读取设置",
                    "message" to "配置读取失败，将使用默认配置。\n" +
                            "可能是您使用了 LSPatch 等工具的集成模式，也有可能是您拒绝了拉起插件的权限。\n" +
                            "若您使用了 LSPatch 等工具的集成模式，且没有单独安装插件本体，请下载插件本体。\n" +
                            "若您安装了插件本体，却弹出这个错误，请允许本应用拉起其他应用。",
                    "infoButton" to "详情",
                    "dlButton" to "下载",
                    "okButton" to "确定"
                )
            }
            else -> {
                mapOf(
                    "title" to "Get Config Failed",
                    "message" to "Configuration loading failed, the default configuration will be used.\n" +
                            "This might be due to the use the integration mode of LSPatch, or possibly because you denied the permission to launch the plugin.\n" +
                            "If you used the integration mode of LSPatch and did not install the plugin itself separately, please download the plugin.\n" +
                            "If you have installed the plugin but still see this error, please allow this application to launch other applications.",
                    "infoButton" to "Info",
                    "dlButton" to "Download",
                    "okButton" to "OK"
                )
            }
        }
        showGetConfigFailedImpl(activity, langData["title"]!!, langData["message"]!!, langData["infoButton"]!!,
            langData["dlButton"]!!, langData["okButton"]!!)
    }

    private fun getCurrentLanguage(context: Context): String {
        val locale: Locale = context.resources.configuration.locales.get(0)
        return locale.language
    }

    fun requestConfig(activity: Context) {
        try {
            val intent = Intent().apply {
                setClassName("io.github.chinosk.gakumas.localify", "io.github.chinosk.gakumas.localify.TranslucentActivity")
                putExtra("gkmsData", "requestConfig")
                flags = FLAG_ACTIVITY_NEW_TASK
            }
            activity.startActivity(intent)
        }
        catch (e: Exception) {
            getConfigError = e
            val fakeActivity = Activity().apply {
                intent = Intent().apply {
                    putExtra("gkmsData", "{}")
                }
            }
            initGkmsConfig(fakeActivity)
        }

    }

    companion object {
        @JvmStatic
        external fun initHook(targetLibraryPath: String, localizationFilesDir: String)
        @JvmStatic
        external fun keyboardEvent(keyCode: Int, action: Int)
        @JvmStatic
        external fun joystickEvent(
            action: Int,
            leftStickX: Float,
            leftStickY: Float,
            rightStickX: Float,
            rightStickY: Float,
            leftTrigger: Float,
            rightTrigger: Float,
            hatX: Float,
            hatY: Float
        )
        @JvmStatic
        external fun loadConfig(configJsonStr: String)

        private var toast: Toast? = null

        @JvmStatic
        fun showToast(message: String) {
            val app = try {
                val activityThreadClass = Class.forName("android.app.ActivityThread")
                val method = activityThreadClass.getDeclaredMethod("currentApplication")
                method.isAccessible = true
                method.invoke(null) as? Application
            } catch (_: Throwable) {
                null
            }

            val context = app?.applicationContext
            if (context != null) {
                val handler = Handler(Looper.getMainLooper())
                handler.post {
                    toast?.cancel()
                    toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
                    toast?.show()
                }
            }
            else {
                Log.e(TAG, "showToast: $message failed: applicationContext is null")
            }
        }

        @JvmStatic
        external fun pluginCallbackLooper(): Int
    }
}
