/*
 * 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.activities.legacy

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.dialogs.DeleteNodesDialogFragment
import com.kunzisoft.keepass.activities.dialogs.PasswordEncodingDialogFragment
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.removeModes
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.MainCredential
import com.kunzisoft.keepass.database.element.Entry
import com.kunzisoft.keepass.database.element.Group
import com.kunzisoft.keepass.database.element.node.Node
import com.kunzisoft.keepass.database.element.node.NodeId
import com.kunzisoft.keepass.model.GroupInfo
import com.kunzisoft.keepass.services.DatabaseTaskNotificationService
import com.kunzisoft.keepass.settings.PreferencesUtil
import com.kunzisoft.keepass.tasks.ActionRunnable
import com.kunzisoft.keepass.timeout.TimeoutHelper
import com.kunzisoft.keepass.utils.LOCK_ACTION
import com.kunzisoft.keepass.utils.LockReceiver
import com.kunzisoft.keepass.utils.closeDatabase
import com.kunzisoft.keepass.utils.registerLockReceiver
import com.kunzisoft.keepass.utils.unregisterLockReceiver
import com.kunzisoft.keepass.view.showActionErrorIfNeeded
import com.kunzisoft.keepass.viewmodels.NodesViewModel
import java.util.UUID

abstract class DatabaseLockActivity : DatabaseModeActivity(),
    PasswordEncodingDialogFragment.Listener {

    private val mNodesViewModel: NodesViewModel by viewModels()

    protected var mTimeoutEnable: Boolean = true

    private var mLockReceiver: LockReceiver? = null
    private var mExitLock: Boolean = false

    protected var mDatabaseReadOnly: Boolean = true
    protected var mMergeDataAllowed: Boolean = false
    private var mAutoSaveEnable: Boolean = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (savedInstanceState != null
            && savedInstanceState.containsKey(TIMEOUT_ENABLE_KEY)
        ) {
            mTimeoutEnable = savedInstanceState.getBoolean(TIMEOUT_ENABLE_KEY)
        } else {
            if (intent != null)
                mTimeoutEnable =
                    intent.getBooleanExtra(TIMEOUT_ENABLE_KEY, TIMEOUT_ENABLE_KEY_DEFAULT)
        }

        mNodesViewModel.nodesToPermanentlyDelete.observe(this) { nodes ->
            deleteDatabaseNodes(nodes)
        }

        mExitLock = false
    }

    override fun onDatabaseRetrieved(database: ContextualDatabase) {
        // End activity if database not loaded
        if (database.loaded.not())
            finish()

        // Focus view to reinitialize timeout,
        // view is not necessary loaded so retry later in resume
        viewToInvalidateTimeout()
            ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)

        // check timeout
        if (mTimeoutEnable) {
            if (mLockReceiver == null) {
                mLockReceiver = LockReceiver {
                    closeDatabase(database)
                    mExitLock = true
                    closeOptionsMenu()
                    finish()
                }
                registerLockReceiver(mLockReceiver)
            }

            // After the first creation
            // or If simply swipe with another application
            // If the time is out -> close the Activity
            TimeoutHelper.checkTimeAndLockIfTimeout(this)
            // If onCreate already record time
            if (!mExitLock)
                TimeoutHelper.recordTime(this, database.loaded)
        }

        mDatabaseReadOnly = database.isReadOnly
        mMergeDataAllowed = database.isMergeDataAllowed()

        checkRegister()
    }

    override fun finish() {
        // To fix weird crash
        try {
            super.finish()
        } catch (e: Exception) {
            Log.e(TAG, "Unable to finish the activity", e)
        }
    }

    abstract fun viewToInvalidateTimeout(): View?

    override fun onDatabaseActionFinished(
        database: ContextualDatabase,
        actionTask: String,
        result: ActionRunnable.Result
    ) {
        when (actionTask) {
            DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK,
            DatabaseTaskNotificationService.ACTION_DATABASE_RELOAD_TASK -> {
                // Reload the current activity
                if (result.isSuccess) {
                    reloadActivity()
                    if (actionTask == DatabaseTaskNotificationService.ACTION_DATABASE_MERGE_TASK) {
                        Toast.makeText(this, R.string.merge_success, Toast.LENGTH_LONG).show()
                    }
                } else {
                    this.showActionErrorIfNeeded(result)
                    finish()
                }
            }
        }
    }

    override fun onPasswordEncodingValidateListener(
        databaseUri: Uri?,
        mainCredential: MainCredential
    ) {
        mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
    }

    fun assignMainCredential(mainCredential: MainCredential) {
        mDatabase?.let { database ->
            database.fileUri?.let { databaseUri ->
                // Show the progress dialog now or after dialog confirmation
                if (database.isValidCredential(mainCredential.toMasterCredential(contentResolver))) {
                    mDatabaseViewModel.assignMainCredential(databaseUri, mainCredential)
                } else {
                    PasswordEncodingDialogFragment.getInstance(databaseUri, mainCredential)
                        .show(supportFragmentManager, "passwordEncodingTag")
                }
            }
        }
    }

    fun saveDatabase() {
        mDatabaseViewModel.saveDatabase(save = true)
    }

    fun saveDatabaseTo(uri: Uri) {
        mDatabaseViewModel.saveDatabase(save = true, saveToUri = uri)
    }

    fun mergeDatabase() {
        mDatabaseViewModel.mergeDatabase(save = mAutoSaveEnable)
    }

    fun mergeDatabaseFrom(uri: Uri, mainCredential: MainCredential) {
        mDatabaseViewModel.mergeDatabase(mAutoSaveEnable, uri, mainCredential)
    }

    fun reloadDatabase() {
        mDatabaseViewModel.reloadDatabase(fixDuplicateUuid = false)
    }

    fun createEntry(
        newEntry: Entry,
        parent: Group
    ) {
        mDatabaseViewModel.createEntry(newEntry, parent, mAutoSaveEnable)
    }

    fun updateEntry(
        oldEntry: Entry,
        entryToUpdate: Entry
    ) {
        mDatabaseViewModel.updateEntry(oldEntry, entryToUpdate, mAutoSaveEnable)
    }

    fun copyNodes(
        nodesToCopy: List<Node>,
        newParent: Group
    ) {
        mDatabaseViewModel.copyNodes(nodesToCopy, newParent, mAutoSaveEnable)
    }

    fun moveNodes(
        nodesToMove: List<Node>,
        newParent: Group
    ) {
        mDatabaseViewModel.moveNodes(nodesToMove, newParent, mAutoSaveEnable)
    }

    private fun eachNodeRecyclable(database: ContextualDatabase, nodes: List<Node>): Boolean {
        return nodes.find { node ->
            var cannotRecycle = true
            if (node is Entry) {
                cannotRecycle = !database.canRecycle(node)
            } else if (node is Group) {
                cannotRecycle = !database.canRecycle(node)
            }
            cannotRecycle
        } == null
    }

    fun deleteNodes(nodes: List<Node>, recycleBin: Boolean = false) {
        // TODO Move in ViewModel
        mDatabase?.let { database ->
            // If recycle bin enabled, ensure it exists
            if (database.isRecycleBinEnabled) {
                database.ensureRecycleBinExists(resources.getString(R.string.recycle_bin))
            }

            // If recycle bin enabled and not in recycle bin, move in recycle bin
            if (eachNodeRecyclable(database, nodes)) {
                deleteDatabaseNodes(nodes)
            }
            // else open the dialog to confirm deletion
            else {
                DeleteNodesDialogFragment.getInstance(recycleBin)
                    .show(supportFragmentManager, "deleteNodesDialogFragment")
                mNodesViewModel.deleteNodes(nodes)
            }
        }
    }

    private fun deleteDatabaseNodes(nodes: List<Node>) {
        mDatabaseViewModel.deleteNodes(nodes, mAutoSaveEnable)
    }

    fun createGroup(
        parent: Group,
        groupInfo: GroupInfo?
    ) {
        // TODO Move in ViewModel
        // Build the group
        mDatabase?.createGroup()?.let { newGroup ->
            groupInfo?.let { info ->
                newGroup.setGroupInfo(info)
            }
            // Not really needed here because added in runnable but safe
            newGroup.parent = parent
            mDatabaseViewModel.createGroup(newGroup, parent, mAutoSaveEnable)
        }
    }

    fun updateGroup(
        oldGroup: Group,
        groupInfo: GroupInfo
    ) {
        // TODO Move in ViewModel
        // If group updated save it in the database
        val updateGroup = Group(oldGroup).let { updateGroup ->
            updateGroup.apply {
                // WARNING remove parent and children to keep memory
                removeParent()
                removeChildren()
                this.setGroupInfo(groupInfo)
            }
        }
        mDatabaseViewModel.updateGroup(oldGroup, updateGroup, mAutoSaveEnable)
    }

    fun restoreEntryHistory(
        mainEntryId: NodeId<UUID>,
        entryHistoryPosition: Int
    ) {
        mDatabaseViewModel.restoreEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
    }

    fun deleteEntryHistory(
        mainEntryId: NodeId<UUID>,
        entryHistoryPosition: Int
    ) {
        mDatabaseViewModel.deleteEntryHistory(mainEntryId, entryHistoryPosition, mAutoSaveEnable)
    }

    private fun checkRegister() {
        // If in registration mode, don't allow read only
        if (mSpecialMode == SpecialMode.REGISTRATION && mDatabaseReadOnly) {
            Toast.makeText(this, R.string.error_registration_read_only , Toast.LENGTH_LONG).show()
            intent.removeModes()
            finish()
        }
    }

    override fun onResume() {
        super.onResume()

        // To refresh when back to normal workflow from selection workflow
        mAutoSaveEnable = PreferencesUtil.isAutoSaveDatabaseEnabled(this)

        // Invalidate timeout by touch
        mDatabase?.let { database ->
            viewToInvalidateTimeout()
                ?.resetAppTimeoutWhenViewTouchedOrFocused(this, database.loaded)
        }

        invalidateOptionsMenu()
    }

    protected fun checkTimeAndLockIfTimeoutOrResetTimeout(action: (() -> Unit)? = null) {
        TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
            mDatabase?.loaded == true,
            action)
    }

    override fun onSaveInstanceState(outState: Bundle) {
        outState.putBoolean(TIMEOUT_ENABLE_KEY, mTimeoutEnable)
        super.onSaveInstanceState(outState)
    }

    override fun onPause() {
        super.onPause()

        if (mTimeoutEnable) {
            // If the time is out during our navigation in activity -> close the Activity
            TimeoutHelper.checkTimeAndLockIfTimeout(this)
        }
    }

    override fun onDestroy() {
        unregisterLockReceiver(mLockReceiver)
        super.onDestroy()
    }

    protected fun lockAndExit() {
        // Ask confirmation if modification not saved
        if (mDatabase?.dataModifiedSinceLastLoading == true) {
            AlertDialog.Builder(this)
                .setMessage(R.string.discard_changes)
                .setNegativeButton(android.R.string.cancel, null)
                .setPositiveButton(R.string.lock) { _, _ ->
                    sendBroadcast(Intent(LOCK_ACTION))
                }.create().show()
        } else {
            sendBroadcast(Intent(LOCK_ACTION))
        }
    }

    fun resetAppTimeout() {
        TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
            mDatabase?.loaded ?: false)
    }

    override fun onDatabaseBackPressed() {
        if (mTimeoutEnable) {
            TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(this,
                mDatabase?.loaded == true) {
                super.onDatabaseBackPressed()
            }
        } else {
            super.onDatabaseBackPressed()
        }
    }

    companion object {

        const val TAG = "LockingActivity"

        const val TIMEOUT_ENABLE_KEY = "TIMEOUT_ENABLE_KEY"
        const val TIMEOUT_ENABLE_KEY_DEFAULT = true
    }
}

/**
 * To reset the app timeout when a view is focused or changed
 */
@SuppressLint("ClickableViewAccessibility")
fun View.resetAppTimeoutWhenViewTouchedOrFocused(context: Context, databaseLoaded: Boolean?) {
    try {
        // Log.d(DatabaseLockActivity.TAG, "View prepared to reset app timeout")
        setOnTouchListener { _, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    // Log.d(DatabaseLockActivity.TAG, "View touched, try to reset app timeout")
                    TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
                        context,
                        databaseLoaded ?: false
                    )
                }
            }
            false
        }
        setOnFocusChangeListener { _, _ ->
            // Log.d(DatabaseLockActivity.TAG, "View focused, try to reset app timeout")
            TimeoutHelper.checkTimeAndLockIfTimeoutOrResetTimeout(
                context,
                databaseLoaded ?: false
            )
        }
        if (this is ViewGroup) {
            for (i in 0..childCount) {
                getChildAt(i)?.resetAppTimeoutWhenViewTouchedOrFocused(context, databaseLoaded)
            }
        }
    } catch (e: Exception) {
        Log.e("AppTimeout", "Unable to reset app timeout", e)
    }
}
