Aer.java

/*
 * @copyright defined in LICENSE.txt
 */

package hera.api.model;

import static hera.util.ValidationUtils.assertNotEquals;
import static hera.util.ValidationUtils.assertNotNull;

import hera.annotation.ApiAudience;
import hera.annotation.ApiStability;
import hera.exception.HerajException;
import hera.spec.AergoSpec;
import java.math.BigDecimal;
import java.math.BigInteger;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@ApiAudience.Public
@ApiStability.Unstable
@EqualsAndHashCode
public class Aer implements Comparable<Aer> {

  public static final String EMPTY_STRING = "Aer(EMPTY)";

  public static final Aer EMPTY = new Aer();

  public static final Aer ZERO = new Aer(BigInteger.valueOf(0L));

  public static final Aer ONE = new Aer(BigInteger.valueOf(1L));

  public static final Aer GIGA_ONE = new Aer("1", Unit.GAER);

  public static final Aer AERGO_ONE = new Aer("1", Unit.AERGO);

  /**
   * Create {@code Aer} instance.
   *
   * @param amount an amount in string which is considered as {@link Unit#AER}
   * @return an aergo instance
   */
  @ApiAudience.Public
  public static Aer of(final String amount) {
    return new Aer(amount);
  }

  /**
   * Create {@code Aer} instance.
   *
   * @param amount an amount in string
   * @param unit an unit {@link Unit}
   * @return an aergo instance
   */
  @ApiAudience.Public
  public static Aer of(final String amount, final Unit unit) {
    return new Aer(amount, unit);
  }

  /**
   * Create an {@code Aer} instance.
   *
   * @param amount an amount in {@link Unit#AER}
   * @return an aergo instance
   */
  @ApiAudience.Public
  public static Aer of(final BigInteger amount) {
    return new Aer(amount);
  }

  /**
   * An unit to represent aergo. Consist of {@code AER, GAER, AERGO}.
   */
  @ApiAudience.Public
  @RequiredArgsConstructor
  public enum Unit {

    AER(AergoSpec.Unit.AER), GAER(AergoSpec.Unit.GAER), AERGO(AergoSpec.Unit.AERGO);

    protected final AergoSpec.Unit delegate;

    /**
     * Get name of the unit.
     *
     * @return an unit name
     */
    public String getName() {
      return delegate.getName();
    }

    /**
     * Get minimum value of unit to in an aer.
     *
     * @return an minimum value
     */
    public BigDecimal getMinimum() {
      return delegate.getMinimum();
    }

    /**
     * Get a ratio to aer.
     *
     * @return a ratio to aer
     */
    public BigDecimal getRatio() {
      return delegate.getRatio();
    }

  }

  @Getter
  protected final BigInteger value;

  protected Aer() {
    this.value = null;
  }

  /**
   * Create {@code Aer} instance.
   *
   * @param amount an amount in string which is considered as {@link Unit#AER}
   */
  @ApiAudience.Public
  public Aer(final String amount) {
    this(amount, Unit.AER);
  }

  /**
   * Create {@code Aer} instance.
   *
   * @param amount an amount in string
   * @param unit an unit {@link Unit}
   */
  @ApiAudience.Public
  public Aer(final String amount, final Unit unit) {
    assertNotNull(amount, "Amount must not null");
    assertNotNull(unit, "Unit must not null");
    this.value = parse(amount, unit);
  }

  /**
   * Aer constructor.
   *
   * @param amount an amount in {@link Unit#AER}
   */
  @ApiAudience.Public
  public Aer(final BigInteger amount) {
    this.value =
        null != amount ? (amount.compareTo(BigInteger.ZERO) >= 0 ? amount : BigInteger.ZERO)
            : BigInteger.ZERO;
  }

  protected BigInteger parse(final String value, final Unit unit) {
    try {
      final BigDecimal parsedValue = new BigDecimal(value);
      if (parsedValue.compareTo(BigDecimal.ZERO) == -1) {
        throw new HerajException("Amount should be postive");
      }
      if (parsedValue.compareTo(BigDecimal.ZERO) != 0
          && parsedValue.compareTo(unit.getMinimum()) == -1) {
        throw new HerajException(
            String.format("Amount is smaller then minimum : %s %s",
                unit.getMinimum().toPlainString(), unit.getName()));
      }
      return parsedValue.multiply(unit.getRatio()).toBigInteger();
    } catch (HerajException e) {
      throw e;
    } catch (NumberFormatException e) {
      throw new HerajException(e);
    }
  }

  /**
   * Returns a Aer whose value is {@code (this.value + other.value)}.
   *
   * @param other value to be added to this aer.
   * @return {@code this.value + other.value}
   */
  public Aer add(final Aer other) {
    assertNotNull(other, "Other is null");
    assertNotEquals(Aer.EMPTY, this, "Cannot add to Aer.EMPTY");
    assertNotEquals(Aer.EMPTY, other, "Cannot add with Aer.EMPTY");
    if (null == this.value) {
      return other;
    }
    if (null == other.value) {
      return this;
    }
    return new Aer(this.value.add(other.value));
  }

  /**
   * Returns a Aer whose value is {@code (this.value - other.value)}. If a subtracted value is less
   * than 0, return 0 {@link Unit#AER}.
   *
   * @param other value to be added to this aer.
   * @return {@code this.value - other.value}
   */
  public Aer subtract(final Aer other) {
    assertNotNull(other, "Other is null");
    assertNotEquals(Aer.EMPTY, this, "Cannot subtract to Aer.EMPTY");
    assertNotEquals(Aer.EMPTY, other, "Cannot add with Aer.EMPTY");
    final BigInteger subtracted = this.value.subtract(other.value);
    return new Aer(subtracted.compareTo(BigInteger.ZERO) < 0 ? BigInteger.ZERO : subtracted);
  }

  /**
   * {@inheritDoc}.
   */
  @Override
  public int compareTo(final Aer other) {
    assertNotNull(other, "Other is null");
    return this.value.compareTo(other.value);
  }

  @Override
  public String toString() {
    if (null == this.value) {
      return EMPTY_STRING;
    }
    return toString(Unit.AER);
  }

  /**
   * Convert aer with {@code unit}.
   *
   * @param unit an aergo unit
   * @return an aer in {@code unit}
   */
  public String toString(final Unit unit) {
    assertNotNull(unit, "Unit must not null");
    if (null == this.value) {
      return EMPTY_STRING;
    }
    final BigDecimal origin = new BigDecimal(this.value);
    final BigDecimal ratio = unit.getRatio();
    final BigDecimal calculated = origin.divide(ratio);
    return String.format("%s(value=%s)", unit.getName(), calculated.toPlainString());
  }

}