package com.topjohnwu.magisk.utils

import android.annotation.TargetApi
import android.app.KeyguardManager
import android.content.Context
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.topjohnwu.magisk.Config
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

@TargetApi(Build.VERSION_CODES.M)
abstract class FingerprintHelper @Throws(Exception::class)
protected constructor() {

    private val manager: FingerprintManager?
    private val cipher: Cipher
    private var cancel: CancellationSignal? = null
    private val context: Context by inject()

    init {
        val keyStore = KeyStore.getInstance("AndroidKeyStore")
        manager = context.getSystemService(FingerprintManager::class.java)
        cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                + KeyProperties.BLOCK_MODE_CBC + "/"
                + KeyProperties.ENCRYPTION_PADDING_PKCS7)
        keyStore.load(null)
        var key = keyStore.getKey(SU_KEYSTORE_KEY, null) as SecretKey? ?: generateKey()
        runCatching {
            cipher.init(Cipher.ENCRYPT_MODE, key)
        }.onFailure {
            // Only happens on Marshmallow
            key = generateKey()
            cipher.init(Cipher.ENCRYPT_MODE, key)
        }
    }

    abstract fun onAuthenticationError(errorCode: Int, errString: CharSequence)

    abstract fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence)

    abstract fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult)

    abstract fun onAuthenticationFailed()

    fun authenticate() {
        cancel = CancellationSignal()
        val cryptoObject = FingerprintManager.CryptoObject(cipher)
        manager!!.authenticate(cryptoObject, cancel, 0, Callback(), null)
    }

    fun cancel() {
        if (cancel != null)
            cancel!!.cancel()
    }

    @Throws(Exception::class)
    private fun generateKey(): SecretKey {
        val keygen = KeyGenerator
                .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
        val builder = KeyGenParameterSpec.Builder(
                SU_KEYSTORE_KEY,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setUserAuthenticationRequired(true)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            builder.setInvalidatedByBiometricEnrollment(false)
        }
        keygen.init(builder.build())
        return keygen.generateKey()
    }

    private inner class Callback : FingerprintManager.AuthenticationCallback() {
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            this@FingerprintHelper.onAuthenticationError(errorCode, errString)
        }

        override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
            this@FingerprintHelper.onAuthenticationHelp(helpCode, helpString)
        }

        override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
            this@FingerprintHelper.onAuthenticationSucceeded(result)
        }

        override fun onAuthenticationFailed() {
            this@FingerprintHelper.onAuthenticationFailed()
        }
    }

    companion object {
        private const val SU_KEYSTORE_KEY = "su_key"

        fun useFingerprint(): Boolean {
            var fp = Config.suFingerprint
            if (fp && !canUseFingerprint()) {
                Config.suFingerprint = false
                fp = false
            }
            return fp
        }

        fun canUseFingerprint(context: Context = get()): Boolean {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
                return false
            val km = context.getSystemService(KeyguardManager::class.java)
            val fm = context.getSystemService(FingerprintManager::class.java)
            return km?.isKeyguardSecure ?: false &&
                    fm != null && fm.isHardwareDetected && fm.hasEnrolledFingerprints()
        }
    }
}
