package org.stellar.sdk;

import com.google.common.io.BaseEncoding;
import org.stellar.sdk.xdr.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.google.common.base.Preconditions.checkNotNull;

public abstract class AbstractTransaction {
  protected final Network mNetwork;
  protected List<DecoratedSignature> mSignatures;
  public static final int MIN_BASE_FEE = 100;


  AbstractTransaction(Network network) {
    this.mNetwork = checkNotNull(network, "network cannot be null");
    this.mSignatures = new ArrayList<DecoratedSignature>();
  }

  /**
   * Adds a new signature ed25519PublicKey to this transaction.
   * @param signer {@link KeyPair} object representing a signer
   */
  public void sign(KeyPair signer) {
    checkNotNull(signer, "signer cannot be null");
    byte[] txHash = this.hash();
    mSignatures.add(signer.signDecorated(txHash));
  }

  /**
   * Adds a new sha256Hash signature to this transaction by revealing preimage.
   * @param preimage the sha256 hash of preimage should be equal to signer hash
   */
  public void sign(byte[] preimage) {
    checkNotNull(preimage, "preimage cannot be null");
    org.stellar.sdk.xdr.Signature signature = new org.stellar.sdk.xdr.Signature();
    signature.setSignature(preimage);

    byte[] hash = Util.hash(preimage);
    byte[] signatureHintBytes = Arrays.copyOfRange(hash, hash.length - 4, hash.length);
    SignatureHint signatureHint = new SignatureHint();
    signatureHint.setSignatureHint(signatureHintBytes);

    DecoratedSignature decoratedSignature = new DecoratedSignature();
    decoratedSignature.setHint(signatureHint);
    decoratedSignature.setSignature(signature);

    mSignatures.add(decoratedSignature);
  }

  /**
   * Returns transaction hash.
   */
  public byte[] hash() {
    return Util.hash(this.signatureBase());
  }

  /**
   * Returns transaction hash encoded as a hexadecimal string.
   */
  public String hashHex() {
    return BaseEncoding.base16().lowerCase().encode(this.hash());
  }

  /**
   * Returns signature base.
   */
  public abstract byte[] signatureBase();

  public Network getNetwork() {
    return mNetwork;
  }

  public List<DecoratedSignature> getSignatures() {
    return mSignatures;
  }

  public abstract TransactionEnvelope toEnvelopeXdr();

  /**
   * Returns base64-encoded TransactionEnvelope XDR object. Transaction need to have at least one signature.
   */
  public String toEnvelopeXdrBase64() {
    try {
      TransactionEnvelope envelope = this.toEnvelopeXdr();
      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
      XdrDataOutputStream xdrOutputStream = new XdrDataOutputStream(outputStream);
      TransactionEnvelope.encode(xdrOutputStream, envelope);

      BaseEncoding base64Encoding = BaseEncoding.base64();
      return base64Encoding.encode(outputStream.toByteArray());
    } catch (IOException e) {
      throw new AssertionError(e);
    }
  }

  /**
   * Creates a <code>AbstractTransaction</code> instance from previously build <code>TransactionEnvelope</code>
   * @param envelope
   * @return
   */
  public static AbstractTransaction fromEnvelopeXdr(TransactionEnvelope envelope, Network network) {
    switch (envelope.getDiscriminant()) {
      case ENVELOPE_TYPE_TX:
        return Transaction.fromV1EnvelopeXdr(envelope.getV1(), network);
      case ENVELOPE_TYPE_TX_V0:
        return Transaction.fromV0EnvelopeXdr(envelope.getV0(), network);
      case ENVELOPE_TYPE_TX_FEE_BUMP:
        return FeeBumpTransaction.fromFeeBumpTransactionEnvelope(envelope.getFeeBump(), network);
      default:
        throw new IllegalArgumentException("transaction type is not supported: "+envelope.getDiscriminant());
    }
  }

  /**
   * Creates a <code>Transaction</code> instance from previously build <code>TransactionEnvelope</code>
   * @param envelope Base-64 encoded <code>TransactionEnvelope</code>
   * @return
   * @throws IOException
   */
  public static AbstractTransaction fromEnvelopeXdr(String envelope, Network network) throws IOException {
    BaseEncoding base64Encoding = BaseEncoding.base64();
    byte[] bytes = base64Encoding.decode(envelope);

    TransactionEnvelope transactionEnvelope = TransactionEnvelope.decode(new XdrDataInputStream(new ByteArrayInputStream(bytes)));
    return fromEnvelopeXdr(transactionEnvelope, network);
  }
}