package stellar.sdk.model.ledger

import cats.data.State
import stellar.sdk.model.xdr.{Decode, Encodable, Encode, Encoded}
import stellar.sdk.util.ByteArrays

/**
  * Meta data about the effect a transaction had on the ledger it was transacted in.
  * @param txnLevelChanges the ledger changes caused by the transactions themselves (not any one specific operation).
  *                        The first value is optional and represents the changes preceding the transaction (introduced in version 2 of this datatype).
  *                        The second value represents the changes following the transaction (introduced in version 1 of this datatype).
  *                        In earlier versions of the protocol, this field was not present. In such cases the field will be `None`.
  * @param operationLevelChanges the ledger changes caused by the individual operations. The order of the outer sequence
  *                              matched the order of operations in the transaction.
  */
case class TransactionLedgerEntries(txnLevelChanges: Option[(Option[Seq[LedgerEntryChange]], Seq[LedgerEntryChange])],
                                    operationLevelChanges: Seq[Seq[LedgerEntryChange]]) extends Encodable {

  val txnLevelChangesBefore: Option[Seq[LedgerEntryChange]] = txnLevelChanges.flatMap(_._1)
  val txnLevelChangesAfter: Option[Seq[LedgerEntryChange]] = txnLevelChanges.map(_._2)

  override def encode: LazyList[Byte] = txnLevelChanges match {
    case Some((Some(before), after)) => encode2(before, after)
    case Some((_, after)) => encode1(after)
    case _ => encode0
  }

  private def encode0: LazyList[Byte] = Encode.int(0) ++
    Encode.arr(operationLevelChanges.map(Encode.arr(_)).map(Encoded))

  private def encode1(txnLevel: Seq[LedgerEntryChange]): LazyList[Byte] = Encode.int(1) ++
    Encode.arr(txnLevel) ++
    Encode.arr(operationLevelChanges.map(Encode.arr(_)).map(Encoded))

  private def encode2(before: Seq[LedgerEntryChange], after: Seq[LedgerEntryChange]): LazyList[Byte] = Encode.int(2) ++
    Encode.arr(before) ++
    Encode.arr(operationLevelChanges.map(Encode.arr(_)).map(Encoded)) ++
    Encode.arr(after)

}

object TransactionLedgerEntries extends Decode {

  def decodeXDR(base64: String): TransactionLedgerEntries = decode.run(ByteArrays.base64(base64).toIndexedSeq).value._2

  private val decodeV0: State[Seq[Byte], TransactionLedgerEntries] = for {
    ops <- arr(arr(LedgerEntryChange.decode))
  } yield TransactionLedgerEntries(None, ops)

  private val decodeV1: State[Seq[Byte], TransactionLedgerEntries] = for {
    txnLevelChanges <- arr(LedgerEntryChange.decode)
    ops <- arr(arr(LedgerEntryChange.decode))
  } yield TransactionLedgerEntries(Some(None, txnLevelChanges), ops)

  private val decodeV2: State[Seq[Byte], TransactionLedgerEntries] = for {
    txnLevelChangesBefore <- arr(LedgerEntryChange.decode)
    ops <- arr(arr(LedgerEntryChange.decode))
    txnLevelChangesAfter <- arr(LedgerEntryChange.decode)
  } yield TransactionLedgerEntries(Some(Some(txnLevelChangesBefore), txnLevelChangesAfter), ops)

  val decode: State[Seq[Byte], TransactionLedgerEntries] = switch(decodeV0, decodeV1, decodeV2)

}