package com.r3corda.client.model

import com.r3corda.client.fxutils.*
import com.r3corda.core.contracts.ContractState
import com.r3corda.core.contracts.StateAndRef
import com.r3corda.core.contracts.StateRef
import com.r3corda.client.fxutils.recordInSequence
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.StateMachineTransactionMapping
import com.r3corda.core.protocols.StateMachineRunId
import com.r3corda.core.transactions.SignedTransaction
import com.r3corda.node.services.messaging.StateMachineUpdate
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.value.ObservableValue
import javafx.collections.ObservableList
import javafx.collections.ObservableMap
import org.fxmisc.easybind.EasyBind
import org.slf4j.LoggerFactory
import rx.Observable

data class GatheredTransactionData(
        val transaction: PartiallyResolvedTransaction,
        val stateMachines: ObservableList<out StateMachineData>
)

/**
 * [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
 * to prepare clients for cases where an input can only be resolved in the future/cannot be resolved at all (for example
 * because of permissioning)
 */
data class PartiallyResolvedTransaction(
        val transaction: SignedTransaction,
        val inputs: List<ObservableValue<InputResolution>>
) {
    val id = transaction.id
    sealed class InputResolution(val stateRef: StateRef) {
        class Unresolved(stateRef: StateRef) : InputResolution(stateRef)
        class Resolved(val stateAndRef: StateAndRef<ContractState>) : InputResolution(stateAndRef.ref)
    }

    companion object {
        fun fromSignedTransaction(
                transaction: SignedTransaction,
                transactions: ObservableMap<SecureHash, SignedTransaction>
        ) = PartiallyResolvedTransaction(
                transaction = transaction,
                inputs = transaction.tx.inputs.map { stateRef ->
                    EasyBind.map(transactions.getObservableValue(stateRef.txhash)) {
                        if (it == null) {
                            InputResolution.Unresolved(stateRef)
                        } else {
                            InputResolution.Resolved(it.tx.outRef(stateRef.index))
                        }
                    }
                }
        )
    }
}

sealed class TransactionCreateStatus(val message: String?) {
    class Started(message: String?) : TransactionCreateStatus(message)
    class Failed(message: String?) : TransactionCreateStatus(message)
    override fun toString(): String = message ?: javaClass.simpleName
}

data class ProtocolStatus(
        val status: String
)

sealed class StateMachineStatus(val stateMachineName: String) {
    class Added(stateMachineName: String): StateMachineStatus(stateMachineName)
    class Removed(stateMachineName: String): StateMachineStatus(stateMachineName)
    override fun toString(): String = "${javaClass.simpleName}($stateMachineName)"
}

data class StateMachineData(
        val id: StateMachineRunId,
        val protocolStatus: ObservableValue<ProtocolStatus?>,
        val stateMachineStatus: ObservableValue<StateMachineStatus>
)

/**
 * This model provides an observable list of transactions and what state machines/protocols recorded them
 */
class GatheredTransactionDataModel {

    private val transactions: Observable<SignedTransaction> by observable(NodeMonitorModel::transactions)
    private val stateMachineUpdates: Observable<StateMachineUpdate> by observable(NodeMonitorModel::stateMachineUpdates)
    private val progressTracking: Observable<ProgressTrackingEvent> by observable(NodeMonitorModel::progressTracking)
    private val stateMachineTransactionMapping: Observable<StateMachineTransactionMapping> by observable(NodeMonitorModel::stateMachineTransactionMapping)

    val collectedTransactions = transactions.recordInSequence()
    val transactionMap = collectedTransactions.associateBy(SignedTransaction::id)
    val progressEvents = progressTracking.recordAsAssociation(ProgressTrackingEvent::stateMachineId)
    val stateMachineStatus: ObservableMap<StateMachineRunId, out ObservableValue<StateMachineStatus>> =
            stateMachineUpdates.foldToObservableMap(Unit) { update, _unit, map: ObservableMap<StateMachineRunId, SimpleObjectProperty<StateMachineStatus>> ->
                when (update) {
                    is StateMachineUpdate.Added -> {
                        val added: SimpleObjectProperty<StateMachineStatus> =
                                SimpleObjectProperty(StateMachineStatus.Added(update.stateMachineInfo.protocolLogicClassName))
                        map[update.id] = added
                    }
                    is StateMachineUpdate.Removed -> {
                        val added = map[update.id]
                        added ?: throw Exception("State machine removed with unknown id ${update.id}")
                        added.set(StateMachineStatus.Removed(added.value.stateMachineName))
                    }
                }
            }
    val stateMachineDataList: ObservableList<StateMachineData> =
            LeftOuterJoinedMap(stateMachineStatus, progressEvents) { id, status, progress ->
                StateMachineData(id, progress.map { it?.let { ProtocolStatus(it.message) } }, status)
            }.getObservableValues()
    val stateMachineDataMap = stateMachineDataList.associateBy(StateMachineData::id)
    val smTxMappingList = stateMachineTransactionMapping.recordInSequence()
    val partiallyResolvedTransactions = collectedTransactions.map {
        PartiallyResolvedTransaction.fromSignedTransaction(it, transactionMap)
    }

    /**
     * We JOIN the transaction list with state machines
     */
    val gatheredTransactionDataList: ObservableList<out GatheredTransactionData> =
            partiallyResolvedTransactions.leftOuterJoin(
                    smTxMappingList,
                    PartiallyResolvedTransaction::id,
                    StateMachineTransactionMapping::transactionId
            ) { transaction, mappings ->
                GatheredTransactionData(
                        transaction,
                        mappings.map { mapping ->
                            stateMachineDataMap.getObservableValue(mapping.stateMachineRunId)
                        }.flatten().filterNotNull()
                )
            }
}
