package stellar.sdk.model

import java.math.{MathContext, RoundingMode}
import java.util.Locale

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

import scala.util.Try

sealed trait Amount extends Encodable {
  val units: Long
  val asset: Asset

  def toDisplayUnits: String = "%.7f".formatLocal(Locale.ROOT, BigDecimal(units) / Amount.toIntegralFactor)

  def encode: LazyList[Byte] = asset.encode ++ Encode.long(units)
}

case class NativeAmount(units: Long) extends Amount {
  override val asset: Asset = NativeAsset
  override def toString: String = s"$toDisplayUnits XLM"
}

case class IssuedAmount(units: Long, asset: NonNativeAsset) extends Amount {
  override def toString: String = s"$toDisplayUnits $asset"
}

object Amount extends Decode {
  private val decimalPlaces = 7
  private val toIntegralFactor = BigDecimal(math.pow(10, decimalPlaces))

  def toBaseUnits(d: Double): Try[Long] = toBaseUnits(BigDecimal(d))

  def toBaseUnits(s: String): Try[Long] = Try(BigDecimal(s)).flatMap(toBaseUnits)

  def toBaseUnits(bd: BigDecimal): Try[Long] = Try {
    (bd * toIntegralFactor.round(new MathContext(0, RoundingMode.DOWN))).toLongExact
  }

  def apply(units: Long, asset: Asset): Amount = {
    asset match {
      case NativeAsset => NativeAmount(units)
      case a: NonNativeAsset => IssuedAmount(units, a)
    }
  }

  /**
    * Convenience method to create native amount denoted in lumens.
    *
    * @param units quantity of lumens
    * @return NativeAmount of the given quantity
    */
  def lumens(units: Double): NativeAmount = toBaseUnits(units).map(NativeAmount).getOrElse(
    throw new IllegalArgumentException(s"Too many digits in fractional portion of $units. Limit is $decimalPlaces")
  )

  def decode: State[Seq[Byte], Amount] = for {
    asset <- Asset.decode
    units <- long
  } yield apply(units, asset)
}

object IssuedAmount {
  def decode: State[Seq[Byte], IssuedAmount] = Amount.decode.map(x => x.asInstanceOf[IssuedAmount])
}