package ch.threema.data.models

import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import ch.threema.app.multidevice.MultiDeviceManager
import ch.threema.domain.taskmanager.Task
import ch.threema.domain.taskmanager.TaskCodec
import ch.threema.domain.taskmanager.TaskManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

interface ModelDataFactory<TDataType, TDbType> {
    /**
     * Create the corresponding database type for this model data.
     */
    fun toDbType(value: TDataType): TDbType

    /**
     * Create the corresponding model for this database type.
     */
    fun toDataType(value: TDbType): TDataType
}

/**
 * This exception is thrown when a model, which was deleted, is being mutated.
 */
class ModelDeletedException(modelName: String, methodName: String) :
    RuntimeException("Cannot call method $methodName: $modelName was deleted")

/**
 * The base model is extended by every model.
 *
 * It handles reactivity and provides common APIs shared by all models.
 */
abstract class BaseModel<TData, TReflectionTask : Task<*, TaskCodec>?>(
    /**
     * Mutable state flow that holds the model data.
     *
     * The field is protected, and is only exposed through the [dataFlow] property.
     *
     * Initially, the data is present. If the model is deleted, the data is set to `null`.
     * From that point on, the model must not be modified anymore, and all methods that mutate
     * model state must throw [ModelDeletedException].
     *
     * NOTE: Access to [mutableData] should always be protected by a synchronized(this) block!
     *
     * TODO(ANDR-4042): Do not expose this field as mutable to subclasses
     */
    protected val mutableData: MutableStateFlow<TData?>,
    /**
     * The name of this model. Used for debugging purposes.
     */
    protected val modelName: String,
    /**
     * The multi device manager is needed to determine whether to reflect a change or not.
     */
    protected val multiDeviceManager: MultiDeviceManager,
    /**
     * The task manager is needed to schedule a task that reflects the changes.
     */
    protected val taskManager: TaskManager,
) {
    /**
     * State flow that holds
     */
    val dataFlow: StateFlow<TData?> = mutableData

    /**
     * The current data value. If the model has been deleted, this value is null.
     */
    val data: TData?
        get() = dataFlow.value

    /**
     * Get a [LiveData] for the internal data state flow.
     */
    fun liveData(): LiveData<TData?> = dataFlow.asLiveData()

    /**
     * This is true if the model has been deleted.
     */
    val isDeleted
        get() = data == null

    /**
     * @return the non null [data]
     *
     * @throws ModelDeletedException if data is null
     */
    protected fun ensureNotDeleted(methodName: String): TData {
        return data ?: throw ModelDeletedException(modelName, methodName)
    }

    /**
     * Helper function to update data in the model.
     *
     * @param methodName The name of the method using this helper.
     * @param detectChanges A function that determines whether or not data was modified.
     * @param updateData A function that receives the original data and returns the updated data.
     * @param updateDatabase A function that updates the database with the updated data.
     * @param onUpdated An optional function that is invoked at the end if data was updated.
     * @param reflectUpdateTask The task that should be executed after the fields have been updated.
     *
     * @throws [ModelDeletedException] if model is deleted.
     *
     * Note that the [reflectUpdateTask] is only executed when MD is active.
     */
    protected fun updateFields(
        methodName: String,
        detectChanges: (originalData: TData) -> Boolean,
        updateData: (originalData: TData) -> TData,
        updateDatabase: (updatedData: TData) -> Unit,
        onUpdated: ((updatedData: TData) -> Unit)?,
        reflectUpdateTask: TReflectionTask? = null,
    ) {
        val updatedData = synchronized(this) {
            val originalData = ensureNotDeleted(methodName)
            val dataChanged = detectChanges(originalData)
            if (dataChanged) {
                val updatedData = updateData(originalData)
                mutableData.value = updatedData
                updateDatabase(updatedData)
                if (reflectUpdateTask != null && multiDeviceManager.isMultiDeviceActive) {
                    @Suppress("DeferredResultUnused")
                    taskManager.schedule(reflectUpdateTask)
                }
                updatedData
            } else {
                null
            }
        }
        if (updatedData != null) {
            onUpdated?.invoke(updatedData)
        }
    }
}
