package app.revanced.patches.openinghours.misc.fix.crash

import app.revanced.patcher.extensions.instructions
import app.revanced.patcher.extensions.methodReference
import app.revanced.patcher.extensions.newLabel
import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction

@Suppress("unused")
val fixCrashPatch = bytecodePatch(
    name = "Fix crash",
    description = "Fixes a crash when opening a place.",
) {
    compatibleWith("de.simon.openinghours"("1.0"))

    apply {
        val indexedInstructions = setPlaceMethod.instructions.withIndex().toList()

        /**
         * This function replaces all `checkNotNull` instructions in the integer interval
         * from [startIndex] to [endIndex], both inclusive. In place of the `checkNotNull`
         * instruction an if-null check is inserted. If the if-null check yields that
         * the value is indeed null, we jump to a newly created label at `endIndex + 1`.
         */
        fun avoidNullPointerException(startIndex: Int, endIndex: Int) {
            val continueLabel = setPlaceMethod.newLabel(endIndex + 1)

            for (index in startIndex..endIndex) {
                val instruction = indexedInstructions[index].value

                if (!instruction.isCheckNotNullInstruction) {
                    continue
                }

                val checkNotNullInstruction = instruction as FiveRegisterInstruction
                val originalRegister = checkNotNullInstruction.registerC

                setPlaceMethod.replaceInstruction(
                    index,
                    BuilderInstruction21t(
                        Opcode.IF_EQZ,
                        originalRegister,
                        continueLabel,
                    ),
                )
            }
        }

        val getOpeningHoursIndex = getIndicesOfInvoke(
            indexedInstructions,
            "Lde/simon/openinghours/models/Place;",
            "getOpeningHours",
        )

        val setWeekDayTextIndex = getIndexOfInvoke(
            indexedInstructions,
            "Lde/simon/openinghours/views/custom/PlaceCard;",
            "setWeekDayText",
        )

        val startCalculateStatusIndex = getIndexOfInvoke(
            indexedInstructions,
            "Lde/simon/openinghours/views/custom/PlaceCard;",
            "startCalculateStatus",
        )

        // Replace the Intrinsics;->checkNotNull instructions with a null check
        // and jump to our newly created label if it returns true.
        // This avoids the NullPointerExceptions.
        avoidNullPointerException(getOpeningHoursIndex[1], startCalculateStatusIndex)
        avoidNullPointerException(getOpeningHoursIndex[0], setWeekDayTextIndex)
    }
}

private fun isInvokeInstruction(instruction: Instruction, className: String, methodName: String): Boolean {
    val methodRef = instruction.methodReference ?: return false
    return methodRef.definingClass == className && methodRef.name == methodName
}

private fun getIndicesOfInvoke(
    instructions: List<IndexedValue<Instruction>>,
    className: String,
    methodName: String,
): List<Int> = instructions.mapNotNull { (index, instruction) ->
    if (isInvokeInstruction(instruction, className, methodName)) {
        index
    } else {
        null
    }
}

private fun getIndexOfInvoke(
    instructions: List<IndexedValue<Instruction>>,
    className: String,
    methodName: String,
): Int = instructions.first { (_, instruction) ->
    isInvokeInstruction(instruction, className, methodName)
}.index

private val Instruction.isCheckNotNullInstruction
    get() = isInvokeInstruction(this, "Lkotlin/jvm/internal/Intrinsics;", "checkNotNull")
