/*
 * Copyright 2019 Jeremy Jamet / Kunzisoft.
 *
 * This file is part of KeePassDX.
 *
 *  KeePassDX is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  KeePassDX is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with KeePassDX.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.kunzisoft.keepass.credentialprovider.autofill

import android.app.assist.AssistStructure
import android.os.Build
import android.text.InputType
import android.util.Log
import android.view.View
import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import org.joda.time.DateTime
import java.util.Locale


/**
 * Parse AssistStructure and guess username and password fields.
 */
@RequiresApi(api = Build.VERSION_CODES.O)
class StructureParser(
    private val structure: AssistStructure,
    private val webViewDenied: Boolean = false
) {
    private var result: Result? = null
    private var usernameIdCandidate: AutofillId? = null
    private var usernameValueCandidate: AutofillValue? = null

    fun parseOrNull(saveValue: Boolean = false): Result? {
        val result = parse(saveValue)
        if (result != null && result.isValid())
            return result
        return null
    }

    fun parse(saveValue: Boolean): Result? {
        try {
            result = Result()
            result?.apply {
                allowSaveValues = saveValue
                usernameIdCandidate = null
                usernameValueCandidate = null
                mainLoop@ for (i in 0 until structure.windowNodeCount) {
                    val windowNode = structure.getWindowNodeAt(i)
                    applicationId = windowNode.title.toString().split("/")[0]
                    Log.d(TAG, "Autofill applicationId: $applicationId")

                    if (applicationId?.contains(APPLICATION_ID_POPUP_WINDOW) == false) {
                        if (parseViewNode(windowNode.rootViewNode))
                            break@mainLoop
                    }
                }
                // If not explicit username field found, add the field just before password field.
                if (usernameId == null && passwordId != null && usernameIdCandidate != null) {
                    usernameId = usernameIdCandidate
                    usernameValue = usernameValueCandidate
                }
            }
            return result
        } catch (e: Exception) {
            Log.e(TAG, "Autofill error", e)
            return null
        }
    }

    private fun parseViewNode(node: AssistStructure.ViewNode): Boolean {
        // WebView filter
        if (node.className?.contains("webview", ignoreCase = true) == true) {
            result?.isWebView = true
            if (webViewDenied) {
                Log.w(TAG, "Blocking webview Autofill for ${node.className}")
                return false
            } else {
                Log.d(TAG, "Enabling webview Autofill for ${node.className}")
            }
        }

        // Get the domain of a web app
        node.webDomain?.let { webDomain ->
            if (webDomain.isNotEmpty()) {
                result?.webDomain = webDomain
                Log.d(TAG, "Autofill domain: $webDomain")
            }
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            node.webScheme?.let { webScheme ->
                if (webScheme.isNotEmpty()) {
                    result?.webScheme = webScheme
                    Log.d(TAG, "Autofill scheme: $webScheme")
                }
            }
        }
        val domainNotEmpty = result?.webDomain?.isNotEmpty() == true

        var returnValue = false
        // Only parse visible nodes
        if (node.visibility == View.VISIBLE) {
            if (node.autofillId != null) {
                // Parse methods
                val hints = node.autofillHints
                if (!hints.isNullOrEmpty()) {
                    if (parseNodeByAutofillHint(node))
                        returnValue = true
                } else if (parseNodeByHtmlAttributes(node))
                    returnValue = true
                else if (parseNodeByAndroidInput(node))
                    returnValue = true
            }
            // Optimized return but only if domain not empty
            if (domainNotEmpty && returnValue)
                return true
            // Recursive method to process each node
            for (i in 0 until node.childCount) {
                if (parseViewNode(node.getChildAt(i)))
                    returnValue = true
                if (domainNotEmpty && returnValue)
                    return true
            }
        }
        return returnValue
    }

    private fun parseNodeByAutofillHint(node: AssistStructure.ViewNode): Boolean {
        val autofillId = node.autofillId
        node.autofillHints?.forEach {
            when {
                // OTP recognition at first to prevent HINT_PASSWORD contained in name
                // https://developer.android.com/reference/androidx/autofill/HintConstants#AUTOFILL_HINT_2FA_APP_OTP()
                it.contains("2faAppOTPCode", true)
                        || it.contains("one-time-code", true)
                        || it.contains("one-time-password", true) -> {
                    Log.d(TAG, "Autofill OTP token")
                    result?.otpTokenId = autofillId
                    result?.otpTokenValue = node.autofillValue
                }
                it.contains(View.AUTOFILL_HINT_USERNAME, true)
                        || it.contains(View.AUTOFILL_HINT_EMAIL_ADDRESS, true)
                        || it.contains("email", true)
                        || it.contains("login", true) -> {
                    // Replace username until we have a password
                    if (result?.passwordId == null) {
                        result?.usernameId = autofillId
                        result?.usernameValue = node.autofillValue
                        Log.d(TAG, "Autofill username hint if no password")
                    } else {
                        usernameIdCandidate = autofillId
                        usernameValueCandidate = node.autofillValue
                        Log.d(TAG, "Autofill username hint if password")
                    }
                }
                it.contains(View.AUTOFILL_HINT_PHONE, true) -> {
                    if (usernameIdCandidate == null) {
                        usernameIdCandidate = autofillId
                        usernameValueCandidate = node.autofillValue
                        Log.d(TAG, "Autofill phone")
                    }
                }
                it.contains(View.AUTOFILL_HINT_PASSWORD, true) -> {
                    // Password Id changed if it's the second times we are here,
                    // So the last username candidate is most appropriate
                    if (result?.passwordId != null && usernameIdCandidate != null) {
                        result?.usernameId = usernameIdCandidate
                        result?.usernameValue = usernameValueCandidate
                    }
                    result?.passwordId = autofillId
                    result?.passwordValue = node.autofillValue
                    Log.d(TAG, "Autofill password hint")
                    // Comment "return" to check all the tree
                    // return true
                }
                it.equals("cc-name", true) -> {
                    Log.d(TAG, "Autofill credit card name hint")
                    result?.creditCardHolderId = autofillId
                    result?.creditCardHolder = node.autofillValue
                }
                it.contains(View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, true)
                        || it.equals("cc-number", true) -> {
                    Log.d(TAG, "Autofill credit card number hint")
                    result?.creditCardNumberId = autofillId
                    result?.creditCardNumber = node.autofillValue
                }
                // expect date string as defined in https://html.spec.whatwg.org, e.g. 2014-12
                it.equals("cc-exp", true) -> {
                    Log.d(TAG, "Autofill credit card expiration date hint")
                    result?.creditCardExpirationDateId = autofillId
                    node.autofillValue?.let { value ->
                        if (value.isText && value.textValue.length == 7) {
                            value.textValue.let { date ->
                                try {
                                    result?.creditCardExpirationValue = DateTime()
                                        .withYear(date.substring(2, 4).toInt())
                                        .withMonthOfYear(date.substring(5, 7).toInt())
                                } catch(e: Exception) {
                                    Log.e(TAG, "Unable to retrieve expiration", e)
                                }
                            }
                        }
                    }
                }
                it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, true) -> {
                    Log.d(TAG, "Autofill credit card expiration date hint")
                    result?.creditCardExpirationDateId = autofillId
                    node.autofillValue?.let { value ->
                        if (value.isDate) {
                            result?.creditCardExpirationValue = DateTime(value.dateValue)
                        }
                    }
                }
                it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, true)
                        || it.equals("cc-exp-year", true) -> {
                    Log.d(TAG, "Autofill credit card expiration year hint")
                    result?.creditCardExpirationYearId = autofillId
                    if (node.autofillOptions != null) {
                        result?.creditCardExpirationYearOptions = node.autofillOptions
                    }
                    node.autofillValue?.let { value ->
                        var year = 0
                        try {
                            if (value.isText) {
                                year = value.textValue.toString().toInt()
                            }
                            if (value.isList) {
                                year = node.autofillOptions?.get(value.listValue).toString().toInt()
                            }
                        } catch (e: Exception) {
                            Log.e(TAG, "Unable to retrieve expiration year", e)
                        }
                        result?.creditCardExpirationYearValue = year % 100
                    }
                }
                it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, true)
                        || it.equals("cc-exp-month", true) -> {
                    Log.d(TAG, "Autofill credit card expiration month hint")
                    result?.creditCardExpirationMonthId = autofillId
                    if (node.autofillOptions != null) {
                        result?.creditCardExpirationMonthOptions = node.autofillOptions
                    }
                    node.autofillValue?.let { value ->
                        var month = 0
                        try {
                            if (value.isText) {
                                month = value.textValue.toString().toInt()
                            }
                            if (value.isList) {
                                // assume list starts with January (index 0)
                                month = value.listValue + 1
                            }
                        } catch (e: Exception) {
                            Log.e(TAG, "Unable to retrieve expiration month", e)
                        }
                        result?.creditCardExpirationMonthValue = month
                    }
                }
                it.contains(View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DAY, true)
                        || it.equals("cc-exp-day", true) -> {
                    Log.d(TAG, "Autofill credit card expiration day hint")
                    result?.creditCardExpirationDayId = autofillId
                    if (node.autofillOptions != null) {
                        result?.creditCardExpirationDayOptions = node.autofillOptions
                    }
                    node.autofillValue?.let { value ->
                        var day = 0
                        try {
                            if (value.isText) {
                                day = value.textValue.toString().toInt()
                            }
                            if (value.isList) {
                                day = node.autofillOptions?.get(value.listValue).toString().toInt()
                            }
                        } catch (e: Exception) {
                            Log.e(TAG, "Unable to retrieve expiration day", e)
                        }
                        result?.creditCardExpirationDayValue = day
                    }
                }
                it.contains(View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, true)
                        || it.contains("cc-csc", true) -> {
                    Log.d(TAG, "Autofill card security code hint")
                    result?.cardVerificationValueId = autofillId
                    result?.cardVerificationValue = node.autofillValue
                }
                // Ignore autocomplete="off"
                // https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
                it.equals("off", true) ||
                        it.equals("on", true) -> {
                    Log.d(TAG, "Autofill web hint")
                    return parseNodeByHtmlAttributes(node)
                }
                else -> Log.d(TAG, "Autofill unsupported hint $it")
            }
        }
        return false
    }

    private fun parseNodeByHtmlAttributes(node: AssistStructure.ViewNode): Boolean {
        val autofillId = node.autofillId
        val nodHtml = node.htmlInfo
        when (nodHtml?.tag?.lowercase(Locale.ENGLISH)) {
            "input" -> {
                nodHtml.attributes?.forEach { pairAttribute ->
                    when (pairAttribute.first.lowercase(Locale.ENGLISH)) {
                        "id", "name" -> {
                            when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
                                "2fa",
                                "2fpin",
                                "app_otp",
                                "app_totp",
                                "auth",
                                "challenge",
                                "code",
                                "idvpin",
                                "mfa",
                                "mfacode",
                                "otp",
                                "otpcode",
                                "token",
                                "totp",
                                "totppin",
                                "two-factor",
                                "twofa",
                                "twofactor",
                                "verification_pin" -> {
                                    result?.otpTokenId = autofillId
                                    result?.otpTokenValue = node.autofillValue
                                    Log.d(TAG, "Autofill OTP token web id: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
                                    return true
                                }
                            }
                        }
                        "type" -> {
                            when (pairAttribute.second.lowercase(Locale.ENGLISH)) {
                                "tel", "email" -> {
                                    if (result?.passwordId == null) {
                                        result?.usernameId = autofillId
                                        result?.usernameValue = node.autofillValue
                                        Log.d(TAG, "Autofill username web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
                                    }
                                }
                                "text" -> {
                                    // Assume username is before password
                                    if (result?.passwordId == null) {
                                        usernameIdCandidate = autofillId
                                        usernameValueCandidate = node.autofillValue
                                        Log.d(TAG, "Autofill username candidate web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
                                    }
                                }
                                "password" -> {
                                    result?.passwordId = autofillId
                                    result?.passwordValue = node.autofillValue
                                    Log.d(TAG, "Autofill password web type: ${node.htmlInfo?.tag} ${node.htmlInfo?.attributes}")
                                    return true
                                }
                            }
                        }
                    }
                }
            }
        }
        return false
    }

    private fun inputIsVariationType(inputType: Int, vararg type: Int): Boolean {
        type.forEach {
            if (inputType and InputType.TYPE_MASK_VARIATION == it)
                return true
        }
        return false
    }

    private fun showHexInputType(inputType: Int): String {
        return "0x${"%08x".format(inputType)}"
    }

    private fun manageTypeText(
        node: AssistStructure.ViewNode,
        autofillId: AutofillId?,
        inputType: Int
    ): Boolean {
        when {
            inputIsVariationType(inputType,
                InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
                InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS) -> {
                if (result?.passwordId == null) {
                    result?.usernameId = autofillId
                    result?.usernameValue = node.autofillValue
                    Log.d(TAG, "Autofill username android text type: ${showHexInputType(inputType)}")
                }
            }
            inputIsVariationType(inputType,
                InputType.TYPE_TEXT_VARIATION_NORMAL,
                InputType.TYPE_TEXT_VARIATION_PERSON_NAME,
                InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) -> {
                // Assume the username field is before the password field
                if (result?.passwordId == null) {
                    usernameIdCandidate = autofillId
                    usernameValueCandidate = node.autofillValue
                    Log.d(TAG, "Autofill username candidate android text type: ${showHexInputType(inputType)}")
                }
            }
            inputIsVariationType(inputType,
                InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) -> {
                // Some forms used visible password as username
                if (result?.passwordId == null &&
                    usernameIdCandidate == null && usernameValueCandidate == null) {
                    usernameIdCandidate = autofillId
                    usernameValueCandidate = node.autofillValue
                    Log.d(TAG, "Autofill visible password android text type (as username): ${showHexInputType(inputType)}")
                } else if (result?.passwordId == null && result?.passwordValue == null) {
                    result?.passwordId = autofillId
                    result?.passwordValue = node.autofillValue
                    Log.d(TAG, "Autofill visible password android text type (as password): ${showHexInputType(inputType)}")
                }
            }
            inputIsVariationType(inputType,
                InputType.TYPE_TEXT_VARIATION_PASSWORD,
                InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD) -> {
                result?.passwordId = autofillId
                result?.passwordValue = node.autofillValue
                Log.d(TAG, "Autofill password android text type: ${showHexInputType(inputType)}")
                return true
            }
            inputIsVariationType(inputType,
                InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT,
                InputType.TYPE_TEXT_VARIATION_FILTER,
                InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE,
                InputType.TYPE_TEXT_VARIATION_PHONETIC,
                InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS,
                InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE,
                InputType.TYPE_TEXT_VARIATION_URI) -> {
                // Type not used
                Log.d(TAG, "Autofill not used android text type: ${showHexInputType(inputType)}")
            }
            else -> {
                Log.d(TAG, "Autofill unknown android text type: ${showHexInputType(inputType)}")
            }
        }
        return false
    }

    private fun manageTypeNumber(
        node: AssistStructure.ViewNode,
        autofillId: AutofillId?,
        inputType: Int
    ): Boolean {
        when {
            inputIsVariationType(inputType,
                InputType.TYPE_NUMBER_VARIATION_NORMAL) -> {
                if (usernameIdCandidate == null) {
                    usernameIdCandidate = autofillId
                    usernameValueCandidate = node.autofillValue
                    Log.d(TAG, "Autofill username candidate android number type: ${showHexInputType(inputType)}")
                }
            }
            inputIsVariationType(inputType,
                InputType.TYPE_NUMBER_VARIATION_PASSWORD) -> {
                result?.passwordId = autofillId
                result?.passwordValue = node.autofillValue
                Log.d(TAG, "Autofill password android number type: ${showHexInputType(inputType)}")
                return true
            }
            else -> {
                Log.d(TAG, "Autofill unknown android number type: ${showHexInputType(inputType)}")
            }
        }
        return false
    }

    private fun manageTypeNull(
        node: AssistStructure.ViewNode,
        autofillId: AutofillId?,
        inputType: Int
    ): Boolean {
        if (node.className == "android.widget.EditText") {
            Log.d(TAG, "Autofill null android input type class: ${showHexInputType(inputType)}" +
                    ", get the EditText node class name!")
            if (result?.passwordId == null) {
                usernameIdCandidate = autofillId
                usernameValueCandidate = node.autofillValue
            }
        }
        return false
    }

    private fun parseNodeByAndroidInput(node: AssistStructure.ViewNode): Boolean {
        val autofillId = node.autofillId
        val inputType = node.inputType
        when (inputType and InputType.TYPE_MASK_CLASS) {
            InputType.TYPE_CLASS_TEXT -> {
                return manageTypeText(node, autofillId, inputType)
            }
            InputType.TYPE_CLASS_NUMBER -> {
                return manageTypeNumber(node, autofillId, inputType)
            }
            InputType.TYPE_NULL -> {
                return manageTypeNull(node, autofillId, inputType)
            }
        }
        return false
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    class Result {
        var isWebView: Boolean = false
        var applicationId: String? = null
        var webDomain: String? = null
            set(value) {
                if (field == null)
                    field = value
            }

        var webScheme: String? = null
            set(value) {
                if (field == null)
                    field = value
            }

        // if the user selects the credit card expiration date from a list of options
        // all options are stored here
        var creditCardExpirationYearOptions: Array<CharSequence>? = null
        var creditCardExpirationMonthOptions: Array<CharSequence>? = null
        var creditCardExpirationDayOptions: Array<CharSequence>? = null

        var usernameId: AutofillId? = null
        var passwordId: AutofillId? = null
        var creditCardHolderId: AutofillId? = null
        var creditCardNumberId: AutofillId? = null
        var creditCardExpirationDateId: AutofillId? = null
        var creditCardExpirationYearId: AutofillId? = null
        var creditCardExpirationMonthId: AutofillId? = null
        var creditCardExpirationDayId: AutofillId? = null
        var cardVerificationValueId: AutofillId? = null
        var otpTokenId: AutofillId? = null

        fun isValid(): Boolean {
            return passwordId != null || creditCardNumberId != null || otpTokenId != null
        }

        fun allAutofillIds(): Array<AutofillId> {
            val all = mutableListOf<AutofillId>()
            usernameId?.let {
                all.add(it)
            }
            passwordId?.let {
                all.add(it)
            }
            creditCardHolderId?.let {
                all.add(it)
            }
            creditCardNumberId?.let {
                all.add(it)
            }
            cardVerificationValueId?.let {
                all.add(it)
            }
            otpTokenId?.let {
                all.add(it)
            }
            return all.toTypedArray()
        }

        // Only in registration mode
        var allowSaveValues = false

        var usernameValue: AutofillValue? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        var passwordValue: AutofillValue? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        var creditCardHolder: AutofillValue? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        var creditCardNumber: AutofillValue? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        // format MMYY
        var creditCardExpirationValue: DateTime? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        // for year of CC expiration date: YY
        var creditCardExpirationYearValue = 0
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        // for month of CC expiration date: MM
        var creditCardExpirationMonthValue = 0
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        var creditCardExpirationDayValue = 0
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        // the security code for the credit card (also called CVV)
        var cardVerificationValue: AutofillValue? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }

        // OTP Token
        var otpTokenValue: AutofillValue? = null
            set(value) {
                if (allowSaveValues)
                    field = value
            }
    }

    companion object {
        private val TAG = StructureParser::class.java.name

        const val APPLICATION_ID_POPUP_WINDOW = "PopupWindow:"
    }
}
