/*
 * Copyright 2016 OpenMarket Ltd
 * Copyright 2017 Vector Creations Ltd
 * Copyright 2018 New Vector Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package im.vector.matrix.android.internal.crypto

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import arrow.core.Try
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibilityContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.*
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.CryptoDatabase
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.toConfigurableTask
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.fetchCopied
import kotlinx.coroutines.*
import org.matrix.olm.OlmManager
import timber.log.Timber
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.coroutines.EmptyCoroutineContext

/**
 * A `CryptoService` class instance manages the end-to-end crypto for a session.
 *
 *
 * Messages posted by the user are automatically redirected to CryptoService in order to be encrypted
 * before sending.
 * In the other hand, received events goes through CryptoService for decrypting.
 * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto.
 * Specially, it tracks all room membership changes events in order to do keys updates.
 */
@SessionScope
internal class CryptoManager @Inject constructor(
        // Olm Manager
        private val olmManager: OlmManager,
        // The credentials,
        private val credentials: Credentials,
        private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
        // the crypto store
        private val cryptoStore: IMXCryptoStore,
        // Olm device
        private val olmDevice: MXOlmDevice,
        // Set of parameters used to configure/customize the end-to-end crypto.
        private val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
        // Device list manager
        private val deviceListManager: DeviceListManager,
        // The key backup service.
        private val keysBackup: KeysBackup,
        //
        private val objectSigner: ObjectSigner,
        //
        private val oneTimeKeysUploader: OneTimeKeysUploader,
        //
        private val roomDecryptorProvider: RoomDecryptorProvider,
        // The SAS verification service.
        private val sasVerificationService: DefaultSasVerificationService,
        //
        private val incomingRoomKeyRequestManager: IncomingRoomKeyRequestManager,
        //
        private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
        // Actions
        private val setDeviceVerificationAction: SetDeviceVerificationAction,
        private val megolmSessionDataImporter: MegolmSessionDataImporter,
        private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
        // Repository
        private val megolmEncryptionFactory: MXMegolmEncryptionFactory,
        private val olmEncryptionFactory: MXOlmEncryptionFactory,
        private val deleteDeviceTask: DeleteDeviceTask,
        private val deleteDeviceWithUserPasswordTask: DeleteDeviceWithUserPasswordTask,
        // Tasks
        private val getDevicesTask: GetDevicesTask,
        private val setDeviceNameTask: SetDeviceNameTask,
        private val uploadKeysTask: UploadKeysTask,
        private val loadRoomMembersTask: LoadRoomMembersTask,
        @CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
        private val monarchy: Monarchy,
        private val coroutineDispatchers: MatrixCoroutineDispatchers,
        private val taskExecutor: TaskExecutor
) : CryptoService {

    private val uiHandler = Handler(Looper.getMainLooper())

    // MXEncrypting instance for each room.
    private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap()
    private val isStarting = AtomicBoolean(false)
    private val isStarted = AtomicBoolean(false)

    fun onStateEvent(roomId: String, event: Event) {
        when {
            event.getClearType() == EventType.ENCRYPTION               -> onRoomEncryptionEvent(roomId, event)
            event.getClearType() == EventType.STATE_ROOM_MEMBER        -> onRoomMembershipEvent(roomId, event)
            event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
        }
    }

    fun onLiveEvent(roomId: String, event: Event) {
        when {
            event.getClearType() == EventType.ENCRYPTION               -> onRoomEncryptionEvent(roomId, event)
            event.getClearType() == EventType.STATE_ROOM_MEMBER        -> onRoomMembershipEvent(roomId, event)
            event.getClearType() == EventType.STATE_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
        }
    }

    override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
        setDeviceNameTask
                .configureWith(SetDeviceNameTask.Params(deviceId, deviceName))
                .dispatchTo(callback)
                .executeBy(taskExecutor)
    }

    override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
        deleteDeviceTask
                .configureWith(DeleteDeviceTask.Params(deviceId))
                .dispatchTo(callback)
                .executeBy(taskExecutor)
    }

    override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
        deleteDeviceWithUserPasswordTask
                .configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password))
                .dispatchTo(callback)
                .executeBy(taskExecutor)
    }

    override fun getCryptoVersion(context: Context, longFormat: Boolean): String {
        return if (longFormat) olmManager.getDetailedVersion(context) else olmManager.version
    }

    override fun getMyDevice(): MXDeviceInfo {
        return myDeviceInfoHolder.get().myDevice
    }

    override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
        getDevicesTask
                .toConfigurableTask()
                .dispatchTo(callback)
                .executeBy(taskExecutor)
    }

    override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
        return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
    }

    /**
     * Provides the tracking status
     *
     * @param userId the user id
     * @return the tracking status
     */
    override fun getDeviceTrackingStatus(userId: String): Int {
        return cryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED)
    }

    /**
     * Tell if the MXCrypto is started
     *
     * @return true if the crypto is started
     */
    fun isStarted(): Boolean {
        return isStarted.get()
    }

    /**
     * Tells if the MXCrypto is starting.
     *
     * @return true if the crypto is starting
     */
    fun isStarting(): Boolean {
        return isStarting.get()
    }

    /**
     * Start the crypto module.
     * Device keys will be uploaded, then one time keys if there are not enough on the homeserver
     * and, then, if this is the first time, this new device will be announced to all other users
     * devices.
     *
     * @param isInitialSync true if it starts from an initial sync
     */
    fun start(isInitialSync: Boolean) {
        if (isStarted.get() || isStarting.get()) {
            return
        }
        isStarting.set(true)
        CoroutineScope(coroutineDispatchers.crypto).launch {
            internalStart(isInitialSync)
        }
    }

    private suspend fun internalStart(isInitialSync: Boolean) {
        // Open the store
        cryptoStore.open()
        uploadDeviceKeys()
                .flatMap { oneTimeKeysUploader.maybeUploadOneTimeKeys() }
                .fold(
                        {
                            Timber.e("Start failed: $it")
                            delay(1000)
                            isStarting.set(false)
                            internalStart(isInitialSync)
                        },
                        {
                            outgoingRoomKeyRequestManager.start()
                            keysBackup.checkAndStartKeysBackup()
                            if (isInitialSync) {
                                // refresh the devices list for each known room members
                                deviceListManager.invalidateAllDeviceLists()
                                deviceListManager.refreshOutdatedDeviceLists()
                            } else {
                                incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
                            }
                            isStarting.set(false)
                            isStarted.set(true)
                        }
                )
    }

    /**
     * Close the crypto
     */
    fun close() {
        olmDevice.release()
        cryptoStore.close()
        outgoingRoomKeyRequestManager.stop()
    }

    override fun isCryptoEnabled(): Boolean {
        // TODO Check that this test is correct
        return olmDevice != null
    }

    /**
     * @return the Keys backup Service
     */
    override fun getKeysBackupService(): KeysBackupService {
        return keysBackup
    }

    /**
     * @return the SasVerificationService
     */
    override fun getSasVerificationService(): SasVerificationService {
        return sasVerificationService
    }

    /**
     * A sync response has been received
     *
     * @param syncResponse the syncResponse
     */
    fun onSyncCompleted(syncResponse: SyncResponse) {
        CoroutineScope(coroutineDispatchers.crypto).launch {
            if (syncResponse.deviceLists != null) {
                deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
            }
            if (syncResponse.deviceOneTimeKeysCount != null) {
                val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
                oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
            }
            if (isStarted()) {
                // Make sure we process to-device messages before generating new one-time-keys #2782
                deviceListManager.refreshOutdatedDeviceLists()
                oneTimeKeysUploader.maybeUploadOneTimeKeys()
                incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
            }
        }
    }

    /**
     * Find a device by curve25519 identity key
     *
     * @param senderKey the curve25519 key to match.
     * @param algorithm the encryption algorithm.
     * @return the device info, or null if not found / unsupported algorithm / crypto released
     */
    override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
        return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) {
            // We only deal in olm keys
            null
        } else cryptoStore.deviceWithIdentityKey(senderKey)
    }

    /**
     * Provides the device information for a device id and a user Id
     *
     * @param userId   the user id
     * @param deviceId the device id
     */
    override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
        return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
            cryptoStore.getUserDevice(deviceId!!, userId)
        } else {
            null
        }
    }

    /**
     * Set the devices as known
     *
     * @param devices  the devices. Note that the verified member of the devices in this list will not be updated by this method.
     * @param callback the asynchronous callback
     */
    override fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?) {
        // build a devices map
        val devicesIdListByUserId = HashMap<String, List<String>>()

        for (di in devices) {
            var deviceIdsList: MutableList<String>? = devicesIdListByUserId[di.userId]?.toMutableList()

            if (null == deviceIdsList) {
                deviceIdsList = ArrayList()
                devicesIdListByUserId[di.userId] = deviceIdsList
            }
            deviceIdsList.add(di.deviceId)
        }

        val userIds = devicesIdListByUserId.keys

        for (userId in userIds) {
            val storedDeviceIDs = cryptoStore.getUserDevices(userId)

            // sanity checks
            if (null != storedDeviceIDs) {
                var isUpdated = false
                val deviceIds = devicesIdListByUserId[userId]

                deviceIds?.forEach { deviceId ->
                    val device = storedDeviceIDs[deviceId]

                    // assume if the device is either verified or blocked
                    // it means that the device is known
                    if (device?.isUnknown == true) {
                        device.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED
                        isUpdated = true
                    }
                }

                if (isUpdated) {
                    cryptoStore.storeUserDevices(userId, storedDeviceIDs)
                }
            }
        }


        callback?.onSuccess(Unit)
    }

    /**
     * Update the blocked/verified state of the given device.
     *
     * @param verificationStatus the new verification status
     * @param deviceId           the unique identifier for the device.
     * @param userId             the owner of the device
     */
    override fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String) {
        setDeviceVerificationAction.handle(verificationStatus, deviceId, userId)
    }

    /**
     * Configure a room to use encryption.
     *
     * @param roomId             the room id to enable encryption in.
     * @param algorithm          the encryption config for the room.
     * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now)
     * @param membersId          list of members to start tracking their devices
     * @return true if the operation succeeds.
     */
    private suspend fun setEncryptionInRoom(roomId: String,
                                            algorithm: String?,
                                            inhibitDeviceQuery: Boolean,
                                            membersId: List<String>): Boolean {
        // If we already have encryption in this room, we should ignore this event
        // (for now at least. Maybe we should alert the user somehow?)
        val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)

        if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) {
            Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
            return false
        }

        val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)

        if (!encryptingClass) {
            Timber.e("## setEncryptionInRoom() : Unable to encrypt room ${roomId} with $algorithm")
            return false
        }

        cryptoStore.storeRoomAlgorithm(roomId, algorithm!!)

        val alg: IMXEncrypting = when (algorithm) {
            MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
            else                      -> olmEncryptionFactory.create(roomId)
        }

        synchronized(roomEncryptors) {
            roomEncryptors.put(roomId, alg)
        }

        // if encryption was not previously enabled in this room, we will have been
        // ignoring new device events for these users so far. We may well have
        // up-to-date lists for some users, for instance if we were sharing other
        // e2e rooms with them, so there is room for optimisation here, but for now
        // we just invalidate everyone in the room.
        if (null == existingAlgorithm) {
            Timber.v("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein")

            val userIds = ArrayList(membersId)

            deviceListManager.startTrackingDeviceList(userIds)

            if (!inhibitDeviceQuery) {
                deviceListManager.refreshOutdatedDeviceLists()
            }
        }

        return true
    }

    /**
     * Tells if a room is encrypted
     *
     * @param roomId the room id
     * @return true if the room is encrypted
     */
    override fun isRoomEncrypted(roomId: String): Boolean {
        val encryptionEvent = monarchy.fetchCopied {
            EventEntity.where(it, roomId = roomId, type = EventType.ENCRYPTION).findFirst()
        }
        return encryptionEvent != null
    }

    /**
     * @return the stored device keys for a user.
     */
    override fun getUserDevices(userId: String): MutableList<MXDeviceInfo> {
        val map = cryptoStore.getUserDevices(userId)
        return if (null != map) ArrayList(map.values) else ArrayList()
    }

    fun isEncryptionEnabledForInvitedUser(): Boolean {
        return cryptoConfig.enableEncryptionForInvitedMembers
    }

    override fun getEncryptionAlgorithm(roomId: String): String? {
        return cryptoStore.getRoomAlgorithm(roomId)
    }

    /**
     * Determine whether we should encrypt messages for invited users in this room.
     * <p>
     * Check here whether the invited members are allowed to read messages in the room history
     * from the point they were invited onwards.
     *
     * @return true if we should encrypt messages for invited users.
     */
    override fun shouldEncryptForInvitedMembers(roomId: String): Boolean {
        return cryptoStore.shouldEncryptForInvitedMembers(roomId)
    }

    /**
     * Encrypt an event content according to the configuration of the room.
     *
     * @param eventContent the content of the event.
     * @param eventType    the type of the event.
     * @param roomId       the room identifier the event will be sent.
     * @param callback     the asynchronous callback
     */
    override fun encryptEventContent(eventContent: Content,
                                     eventType: String,
                                     roomId: String,
                                     callback: MatrixCallback<MXEncryptEventContentResult>) {
        CoroutineScope(coroutineDispatchers.crypto).launch {
            if (!isStarted()) {
                Timber.v("## encryptEventContent() : wait after e2e init")
                internalStart(false)
            }
            val userIds = getRoomUserIds(roomId)
            var alg = synchronized(roomEncryptors) {
                roomEncryptors[roomId]
            }
            if (alg == null) {
                val algorithm = getEncryptionAlgorithm(roomId)
                if (algorithm != null) {
                    if (setEncryptionInRoom(roomId, algorithm, false, userIds)) {
                        synchronized(roomEncryptors) {
                            alg = roomEncryptors[roomId]
                        }
                    }
                }
            }
            val safeAlgorithm = alg
            if (safeAlgorithm != null) {
                val t0 = System.currentTimeMillis()
                Timber.v("## encryptEventContent() starts")
                safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
                        .fold(
                                { callback.onFailure(it) },
                                {
                                    Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
                                    callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
                                }
                        )
            } else {
                val algorithm = getEncryptionAlgorithm(roomId)
                val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
                        algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
                Timber.e("## encryptEventContent() : $reason")
                callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
            }
        }
    }

    /**
     * Decrypt an event
     *
     * @param event    the raw event.
     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
     * @return the MXEventDecryptionResult data, or throw in case of error
     */
    @Throws(MXCryptoError::class)
    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
        return runBlocking {
            internalDecryptEvent(event, timeline).fold(
                    { throw it },
                    { it }
            )
        }
    }

    /**
     * Decrypt an event asynchronously
     *
     * @param event    the raw event.
     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
     * @param callback the callback to return data or null
     */
    override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
        GlobalScope.launch(EmptyCoroutineContext) {
            val result = withContext(coroutineDispatchers.crypto) {
                internalDecryptEvent(event, timeline)
            }
            result.foldToCallback(callback)
        }
    }

    /**
     * Decrypt an event
     *
     * @param event    the raw event.
     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
     * @return the MXEventDecryptionResult data, or null in case of error wrapped into [Try]
     */
    private suspend fun internalDecryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult> {
        val eventContent = event.content
        return if (eventContent == null) {
            Timber.e("## decryptEvent : empty event content")
            Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON))
        } else {
            val algorithm = eventContent["algorithm"]?.toString()
            val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
            if (alg == null) {
                val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
                Timber.e("## decryptEvent() : $reason")
                Try.Failure(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason))
            } else {
                alg.decryptEvent(event, timeline)
            }
        }
    }

    /**
     * Reset replay attack data for the given timeline.
     *
     * @param timelineId the timeline id
     */
    fun resetReplayAttackCheckInTimeline(timelineId: String) {
        olmDevice.resetReplayAttackCheckInTimeline(timelineId)
    }

    /**
     * Handle the 'toDevice' event
     *
     * @param event the event
     */
    fun onToDeviceEvent(event: Event) {
        CoroutineScope(coroutineDispatchers.crypto).launch {
            when (event.getClearType()) {
                EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
                    onRoomKeyEvent(event)
                }
                EventType.ROOM_KEY_REQUEST                       -> {
                    incomingRoomKeyRequestManager.onRoomKeyRequestEvent(event)
                }
                else                                             -> {
                    //ignore
                }
            }
        }
    }

    /**
     * Handle a key event.
     *
     * @param event the key event.
     */
    private fun onRoomKeyEvent(event: Event) {
        val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
        if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) {
            Timber.e("## onRoomKeyEvent() : missing fields")
            return
        }
        val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
        if (alg == null) {
            Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
            return
        }
        alg.onRoomKeyEvent(event, keysBackup)
    }

    /**
     * Handle an m.room.encryption event.
     *
     * @param event the encryption event.
     */
    private fun onRoomEncryptionEvent(roomId: String, event: Event) {
        CoroutineScope(coroutineDispatchers.crypto).launch {
            val params = LoadRoomMembersTask.Params(roomId)
            loadRoomMembersTask
                    .execute(params)
                    .map { _ ->
                        val userIds = getRoomUserIds(roomId)
                        setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
                    }
        }
    }

    private fun getRoomUserIds(roomId: String): List<String> {
        var userIds: List<String> = emptyList()
        monarchy.doWithRealm { realm ->
            // Check whether the event content must be encrypted for the invited members.
            val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
                    && shouldEncryptForInvitedMembers(roomId)

            userIds = if (encryptForInvitedMembers) {
                RoomMembers(realm, roomId).getActiveRoomMemberIds()
            } else {
                RoomMembers(realm, roomId).getJoinedRoomMemberIds()
            }

        }
        return userIds
    }

    /**
     * Handle a change in the membership state of a member of a room.
     *
     * @param event the membership event causing the change
     */
    private fun onRoomMembershipEvent(roomId: String, event: Event) {
        val alg: IMXEncrypting?

        synchronized(roomEncryptors) {
            alg = roomEncryptors[roomId]
        }

        if (null == alg) {
            // No encrypting in this room
            return
        }
        event.stateKey?.let { userId ->
            val roomMember: RoomMember? = event.content.toModel()
            val membership = roomMember?.membership
            if (membership == Membership.JOIN) {
                // make sure we are tracking the deviceList for this user.
                deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
            } else if (membership == Membership.INVITE
                    && shouldEncryptForInvitedMembers(roomId)
                    && cryptoConfig.enableEncryptionForInvitedMembers) {
                // track the deviceList for this invited user.
                // Caution: there's a big edge case here in that federated servers do not
                // know what other servers are in the room at the time they've been invited.
                // They therefore will not send device updates if a user logs in whilst
                // their state is invite.
                deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
            }
        }
    }

    private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
        val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
        eventContent?.historyVisibility?.let {
            cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
        }
    }


    /**
     * Upload my user's device keys.
     */
    private suspend fun uploadDeviceKeys(): Try<KeysUploadResponse> {
        // Prepare the device keys data to send
        // Sign it
        val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
        getMyDevice().signatures = objectSigner.signObject(canonicalJson)

        // For now, we set the device id explicitly, as we may not be using the
        // same one as used in login.
        val uploadDeviceKeysParams = UploadKeysTask.Params(getMyDevice().toDeviceKeys(), null, getMyDevice().deviceId)
        return uploadKeysTask.execute(uploadDeviceKeysParams)
    }

    /**
     * Export the crypto keys
     *
     * @param password the password
     * @param callback the exported keys
     */
    override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
        exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback)
    }

    /**
     * Export the crypto keys
     *
     * @param password         the password
     * @param anIterationCount the encryption iteration count (0 means no encryption)
     * @param callback         the exported keys
     */
    private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
        GlobalScope.launch(coroutineDispatchers.main) {
            withContext(coroutineDispatchers.crypto) {
                Try {
                    val iterationCount = Math.max(0, anIterationCount)

                    val exportedSessions = ArrayList<MegolmSessionData>()

                    val inboundGroupSessions = cryptoStore.getInboundGroupSessions()

                    for (session in inboundGroupSessions) {
                        val megolmSessionData = session.exportKeys()

                        if (null != megolmSessionData) {
                            exportedSessions.add(megolmSessionData)
                        }
                    }

                    val adapter = MoshiProvider.providesMoshi()
                            .adapter(List::class.java)

                    MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
                }
            }.foldToCallback(callback)
        }
    }

    /**
     * Import the room keys
     *
     * @param roomKeysAsArray  the room keys as array.
     * @param password         the password
     * @param progressListener the progress listener
     * @param callback         the asynchronous callback.
     */
    override fun importRoomKeys(roomKeysAsArray: ByteArray,
                                password: String,
                                progressListener: ProgressListener?,
                                callback: MatrixCallback<ImportRoomKeysResult>) {
        GlobalScope.launch(coroutineDispatchers.main) {
            withContext(coroutineDispatchers.crypto) {
                Try {
                    Timber.v("## importRoomKeys starts")

                    val t0 = System.currentTimeMillis()
                    val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
                    val t1 = System.currentTimeMillis()

                    Timber.v("## importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")

                    val importedSessions = MoshiProvider.providesMoshi()
                            .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
                            .fromJson(roomKeys)

                    val t2 = System.currentTimeMillis()

                    Timber.v("## importRoomKeys : JSON parsing ${t2 - t1} ms")

                    if (importedSessions == null) {
                        throw Exception("Error")
                    }

                    megolmSessionDataImporter.handle(importedSessions, true, uiHandler, progressListener)
                }
            }.foldToCallback(callback)
        }
    }

    /**
     * Update the warn status when some unknown devices are detected.
     *
     * @param warn true to warn when some unknown devices are detected.
     */
    override fun setWarnOnUnknownDevices(warn: Boolean) {
        warnOnUnknownDevicesRepository.setWarnOnUnknownDevices(warn)
    }

    /**
     * Check if the user ids list have some unknown devices.
     * A success means there is no unknown devices.
     * If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered.
     *
     * @param userIds  the user ids list
     * @param callback the asynchronous callback.
     */
    fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
        // force the refresh to ensure that the devices list is up-to-date
        CoroutineScope(coroutineDispatchers.crypto).launch {
            deviceListManager
                    .downloadKeys(userIds, true)
                    .fold(
                            { callback.onFailure(it) },
                            {
                                val unknownDevices = getUnknownDevices(it)
                                if (unknownDevices.map.isEmpty()) {
                                    callback.onSuccess(Unit)
                                } else {
                                    // trigger an an unknown devices exception
                                    callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
                                }
                            }
                    )
        }
    }

    /**
     * Set the global override for whether the client should ever send encrypted
     * messages to unverified devices.
     * If false, it can still be overridden per-room.
     * If true, it overrides the per-room settings.
     *
     * @param block    true to unilaterally blacklist all
     */
    override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) {
        cryptoStore.setGlobalBlacklistUnverifiedDevices(block)
    }

    /**
     * Tells whether the client should ever send encrypted messages to unverified devices.
     * The default value is false.
     * This function must be called in the getEncryptingThreadHandler() thread.
     *
     * @return true to unilaterally blacklist all unverified devices.
     */
    override fun getGlobalBlacklistUnverifiedDevices(): Boolean {
        return cryptoStore.getGlobalBlacklistUnverifiedDevices()
    }

    /**
     * Tells whether the client should encrypt messages only for the verified devices
     * in this room.
     * The default value is false.
     *
     * @param roomId the room id
     * @return true if the client should encrypt messages only for the verified devices.
     */
// TODO add this info in CryptoRoomEntity?
    override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
        return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) }
                ?: false
    }

    /**
     * Manages the room black-listing for unverified devices.
     *
     * @param roomId   the room id
     * @param add      true to add the room id to the list, false to remove it.
     */
    private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) {
        val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()

        if (add) {
            if (!roomIds.contains(roomId)) {
                roomIds.add(roomId)
            }
        } else {
            roomIds.remove(roomId)
        }

        cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds)
    }


    /**
     * Add this room to the ones which don't encrypt messages to unverified devices.
     *
     * @param roomId   the room id
     */
    override fun setRoomBlacklistUnverifiedDevices(roomId: String) {
        setRoomBlacklistUnverifiedDevices(roomId, true)
    }

    /**
     * Remove this room to the ones which don't encrypt messages to unverified devices.
     *
     * @param roomId   the room id
     */
    override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) {
        setRoomBlacklistUnverifiedDevices(roomId, false)
    }

    // TODO Check if this method is still necessary
    /**
     * Cancel any earlier room key request
     *
     * @param requestBody requestBody
     */
    override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
        outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody)
    }

    /**
     * Re request the encryption keys required to decrypt an event.
     *
     * @param event the event to decrypt again.
     */
    override fun reRequestRoomKeyForEvent(event: Event) {
        val wireContent = event.content
        if (wireContent == null) {
            Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
            return
        }

        val requestBody = RoomKeyRequestBody()

        requestBody.roomId = event.roomId
        requestBody.algorithm = wireContent["algorithm"]?.toString()
        requestBody.senderKey = wireContent["sender_key"]?.toString()
        requestBody.sessionId = wireContent["session_id"]?.toString()

        outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
    }

    /**
     * Add a RoomKeysRequestListener listener.
     *
     * @param listener listener
     */
    override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
        incomingRoomKeyRequestManager.addRoomKeysRequestListener(listener)
    }

    /**
     * Add a RoomKeysRequestListener listener.
     *
     * @param listener listener
     */
    override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
        incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
    }

    /**
     * Provides the list of unknown devices
     *
     * @param devicesInRoom the devices map
     * @return the unknown devices map
     */
    private fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): MXUsersDevicesMap<MXDeviceInfo> {
        val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
        val userIds = devicesInRoom.userIds
        for (userId in userIds) {
            val deviceIds = devicesInRoom.getUserDeviceIds(userId)
            deviceIds?.forEach { deviceId ->
                devicesInRoom.getObject(userId, deviceId)
                        ?.takeIf { it.isUnknown }
                        ?.let {
                            unknownDevices.setObject(userId, deviceId, it)
                        }
            }
        }

        return unknownDevices
    }

    override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
        CoroutineScope(coroutineDispatchers.crypto).launch {
            deviceListManager
                    .downloadKeys(userIds, forceDownload)
                    .foldToCallback(callback)
        }
    }

    override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
        clearCryptoDataTask
                .toConfigurableTask()
                .dispatchTo(callback)
                .executeBy(taskExecutor)
    }

    override fun addNewSessionListener(newSessionListener: NewSessionListener) {
        roomDecryptorProvider.addNewSessionListener(newSessionListener)
    }

    override fun removeSessionListener(listener: NewSessionListener) {
        roomDecryptorProvider.removeSessionListener(listener)
    }
    /* ==========================================================================================
     * DEBUG INFO
     * ========================================================================================== */

    override fun toString(): String {
        return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
    }
}
