/*!
 * mtx.js - mutable transaction object for bcoin
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var assert = require('assert');
var util = require('../utils/util');
var co = require('../utils/co');
var Script = require('../script/script');
var TX = require('./tx');
var Input = require('./input');
var Output = require('./output');
var Coin = require('./coin');
var Outpoint = require('./outpoint');
var CoinView = require('../coins/coinview');
var Address = require('./address');
var workerPool = require('../workers/workerpool').pool;
var encoding = require('../utils/encoding');
var consensus = require('../protocol/consensus');
var policy = require('../protocol/policy');
var Amount = require('../btc/amount');
var opcodes = Script.opcodes;

/**
 * A mutable transaction object.
 * @alias module:primitives.MTX
 * @extends TX
 * @constructor
 * @param {Object} options
 * @param {Number?} options.version
 * @param {Number?} options.changeIndex
 * @param {Input[]?} options.inputs
 * @param {Output[]?} options.outputs
 * @property {Number} version - Transaction version.
 * @property {Number} flag - Flag field for segregated witness.
 * Always non-zero (1 if not present).
 * @property {Input[]} inputs
 * @property {Output[]} outputs
 * @property {Number} locktime - nLockTime
 * @property {CoinView} view
 */

function MTX(options) {
  if (!(this instanceof MTX))
    return new MTX(options);

  TX.call(this);

  this.mutable = true;
  this.changeIndex = -1;
  this.view = new CoinView();

  if (options)
    this.fromOptions(options);
}

util.inherits(MTX, TX);

/**
 * Inject properties from options object.
 * @private
 * @param {Object} options
 */

MTX.prototype.fromOptions = function fromOptions(options) {
  var i;

  if (options.version != null) {
    assert(util.isUInt32(options.version), 'Version must a be uint32.');
    this.version = options.version;
  }

  if (options.flag != null) {
    assert(util.isUInt8(options.flag), 'Flag must be a uint8.');
    this.flag = options.flag;
  }

  if (options.inputs) {
    assert(Array.isArray(options.inputs), 'Inputs must be an array.');
    for (i = 0; i < options.inputs.length; i++)
      this.addInput(options.inputs[i]);
  }

  if (options.outputs) {
    assert(Array.isArray(options.outputs), 'Outputs must be an array.');
    for (i = 0; i < options.outputs.length; i++)
      this.addOutput(options.outputs[i]);
  }

  if (options.locktime != null) {
    assert(util.isUInt32(options.locktime), 'Locktime must be a uint32.');
    this.locktime = options.locktime;
  }

  if (options.changeIndex != null) {
    if (options.changeIndex !== -1) {
      assert(util.isUInt32(options.changeIndex),
        'Change index must be a uint32.');
      this.changeIndex = options.changeIndex;
    } else {
      this.changeIndex = -1;
    }
  }

  return this;
};

/**
 * Instantiate MTX from options.
 * @param {Object} options
 * @returns {MTX}
 */

MTX.fromOptions = function fromOptions(options) {
  return new MTX().fromOptions(options);
};

/**
 * Clone the transaction. Note that
 * this will not carry over the view.
 * @returns {MTX}
 */

MTX.prototype.clone = function clone() {
  var mtx = new MTX();
  mtx.inject(this);
  mtx.changeIndex = this.changeIndex;
  return mtx;
};

/**
 * Add an input to the transaction.
 * @param {Input|Object} options
 * @returns {Input}
 *
 * @example
 * mtx.addInput({ prevout: { hash: ... }, script: ... });
 * mtx.addInput(new Input());
 */

MTX.prototype.addInput = function addInput(options) {
  var input = Input.fromOptions(options);
  this.inputs.push(input);
  return input;
};

/**
 * Add an outpoint as an input.
 * @param {Outpoint|Object} outpoint
 * @returns {Input}
 *
 * @example
 * mtx.addOutpoint({ hash: ..., index: 0 });
 * mtx.addOutpoint(new Outpoint(hash, index));
 */

MTX.prototype.addOutpoint = function addOutpoint(outpoint) {
  var prevout = Outpoint.fromOptions(outpoint);
  var input = Input.fromOutpoint(prevout);
  this.inputs.push(input);
  return input;
};

/**
 * Add a coin as an input. Note that this will
 * add the coin to the internal coin viewpoint.
 * @param {Coin} coin
 * @returns {Input}
 *
 * @example
 * mtx.addCoin(Coin.fromTX(tx, 0, -1));
 */

MTX.prototype.addCoin = function addCoin(coin) {
  var input;

  assert(coin instanceof Coin, 'Cannot add non-coin.');

  input = Input.fromCoin(coin);

  this.inputs.push(input);
  this.view.addCoin(coin);

  return input;
};

/**
 * Add a transaction as an input. Note that
 * this will add the coin to the internal
 * coin viewpoint.
 * @param {TX} tx
 * @param {Number} index
 * @param {Number?} height
 * @returns {Input}
 *
 * @example
 * mtx.addTX(tx, 0);
 */

MTX.prototype.addTX = function addTX(tx, index, height) {
  var input, coin;

  assert(tx instanceof TX, 'Cannot add non-transaction.');

  if (height == null)
    height = -1;

  input = Input.fromTX(tx, index);
  coin = Coin.fromTX(tx, index, height);

  this.inputs.push(input);
  this.view.addCoin(coin);

  return input;
};

/**
 * Add an output.
 * @param {Address|Script|Output|Object} script - Script or output options.
 * @param {Amount?} value
 * @returns {Output}
 *
 * @example
 * mtx.addOutput(new Output());
 * mtx.addOutput({ address: ..., value: 100000 });
 * mtx.addOutput(address, 100000);
 * mtx.addOutput(script, 100000);
 */

MTX.prototype.addOutput = function addOutput(script, value) {
  var output;

  if (value != null) {
    assert(util.isUInt53(value), 'Value must be a uint53.');
    output = Output.fromScript(script, value);
  } else {
    output = Output.fromOptions(script);
  }

  this.outputs.push(output);

  return output;
};

/**
 * Verify all transaction inputs.
 * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
 * @returns {Boolean} Whether the inputs are valid.
 */

MTX.prototype.verify = function verify(flags) {
  return TX.prototype.verify.call(this, this.view, flags);
};

/**
 * Verify the transaction inputs on the worker pool
 * (if workers are enabled).
 * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS]
 * @returns {Promise}
 */

MTX.prototype.verifyAsync = function verifyAsync(flags) {
  return TX.prototype.verifyAsync.call(this, this.view, flags);
};

/**
 * Calculate the fee for the transaction.
 * @returns {Amount} fee (zero if not all coins are available).
 */

MTX.prototype.getFee = function getFee() {
  return TX.prototype.getFee.call(this, this.view);
};

/**
 * Calculate the total input value.
 * @returns {Amount} value
 */

MTX.prototype.getInputValue = function getInputValue() {
  return TX.prototype.getInputValue.call(this, this.view);
};

/**
 * Get all input addresses.
 * @returns {Address[]} addresses
 */

MTX.prototype.getInputAddresses = function getInputAddresses() {
  return TX.prototype.getInputAddresses.call(this, this.view);
};

/**
 * Get all addresses.
 * @returns {Address[]} addresses
 */

MTX.prototype.getAddresses = function getAddresses() {
  return TX.prototype.getAddresses.call(this, this.view);
};

/**
 * Get all input address hashes.
 * @returns {Hash[]} hashes
 */

MTX.prototype.getInputHashes = function getInputHashes(enc) {
  return TX.prototype.getInputHashes.call(this, this.view, enc);
};

/**
 * Get all address hashes.
 * @returns {Hash[]} hashes
 */

MTX.prototype.getHashes = function getHashes(enc) {
  return TX.prototype.getHashes.call(this, this.view, enc);
};

/**
 * Test whether the transaction has
 * all coins available/filled.
 * @returns {Boolean}
 */

MTX.prototype.hasCoins = function hasCoins() {
  return TX.prototype.hasCoins.call(this, this.view);
};

/**
 * Calculate virtual sigop count.
 * @param {VerifyFlags?} flags
 * @returns {Number} sigop count
 */

MTX.prototype.getSigops = function getSigops(flags) {
  return TX.prototype.getSigops.call(this, this.view, flags);
};

/**
 * Calculate sigops weight, taking into account witness programs.
 * @param {VerifyFlags?} flags
 * @returns {Number} sigop weight
 */

MTX.prototype.getSigopsCost = function getSigopsCost(flags) {
  return TX.prototype.getSigopsCost.call(this, this.view, flags);
};

/**
 * Calculate the virtual size of the transaction
 * (weighted against bytes per sigop cost).
 * @returns {Number} vsize
 */

MTX.prototype.getSigopsSize = function getSigopsSize() {
  return TX.prototype.getSigopsSize.call(this, this.getSigopsCost());
};

/**
 * Perform contextual checks to verify input, output,
 * and fee values, as well as coinbase spend maturity
 * (coinbases can only be spent 100 blocks or more
 * after they're created). Note that this function is
 * consensus critical.
 * @param {Number} spendHeight - Height at which the
 * transaction is being spent. In the mempool this is
 * the chain height plus one at the time it entered the pool.
 * @param {Object?} ret - Return object, may be
 * set with properties `reason` and `score`.
 * @returns {Boolean}
 */

MTX.prototype.checkInputs = function checkInputs(height, ret) {
  return TX.prototype.checkInputs.call(this, this.view, height, ret);
};

/**
 * Build input script (or witness) templates (with
 * OP_0 in place of signatures).
 * @param {Number} index - Input index.
 * @param {Coin|Output} coin
 * @param {KeyRing} ring
 * @returns {Boolean} Whether the script was able to be built.
 */

MTX.prototype.scriptInput = function scriptInput(index, coin, ring) {
  var input = this.inputs[index];
  var prev, redeem;

  assert(input, 'Input does not exist.');
  assert(coin, 'No coin passed.');

  // Don't bother with any below calculation
  // if the output is already templated.
  if (input.script.length !== 0
      || input.witness.length !== 0) {
    return true;
  }

  // Get the previous output's script
  prev = coin.script;

  // This is easily the hardest part about
  // building a transaction with segwit:
  // figuring out where the redeem script
  // and witness redeem scripts go.
  if (prev.isScripthash()) {
    redeem = ring.getRedeem(prev.get(1));

    if (!redeem)
      return false;

    // Witness program nested in regular P2SH.
    if (redeem.isProgram()) {
      // P2WSH nested within pay-to-scripthash.
      if (redeem.isWitnessScripthash()) {
        prev = ring.getRedeem(redeem.get(1));

        if (!prev)
          return false;

        if (!this.scriptVector(prev, input.witness, ring))
          return false;

        input.witness.push(prev.toRaw());
        input.witness.compile();

        input.script.push(redeem.toRaw());
        input.script.compile();

        return true;
      }

      // P2WPKH nested within pay-to-scripthash.
      if (redeem.isWitnessPubkeyhash()) {
        prev = Script.fromPubkeyhash(ring.getKeyHash());

        if (!this.scriptVector(prev, input.witness, ring))
          return false;

        input.script.push(redeem.toRaw());
        input.script.compile();

        return true;
      }

      // Unknown witness program.
      return false;
    }

    // Regular P2SH.
    if (!this.scriptVector(redeem, input.script, ring))
      return false;

    input.script.push(redeem.toRaw());
    input.script.compile();

    return true;
  }

  // Witness program.
  if (prev.isProgram()) {
    // Bare P2WSH.
    if (prev.isWitnessScripthash()) {
      redeem = ring.getRedeem(prev.get(1));

      if (!redeem)
        return false;

      if (!this.scriptVector(redeem, input.witness, ring))
        return false;

      input.witness.push(redeem.toRaw());
      input.witness.compile();

      return true;
    }

    // Bare P2WPKH.
    if (prev.isWitnessPubkeyhash()) {
      prev = Script.fromPubkeyhash(prev.get(1));

      if (!this.scriptVector(prev, input.witness, ring))
        return false;

      input.witness.compile();

      return true;
    }

    // Bare... who knows?
    return false;
  }

  // Wow, a normal output! Praise be to Jengus and Gord.
  return this.scriptVector(prev, input.script, ring);
};

/**
 * Build script for a single vector
 * based on a previous script.
 * @param {Script} prev
 * @param {Witness|Script} vector
 * @param {Buffer} ring
 * @return {Boolean}
 */

MTX.prototype.scriptVector = function scriptVector(prev, vector, ring) {
  var i, n;

  // P2PK
  if (prev.isPubkey()) {
    if (!util.equal(prev.get(0), ring.publicKey))
      return false;

    vector.set(0, opcodes.OP_0);

    return true;
  }

  // P2PKH
  if (prev.isPubkeyhash()) {
    if (!util.equal(prev.get(2), ring.getKeyHash()))
      return false;

    vector.set(0, opcodes.OP_0);
    vector.set(1, ring.publicKey);

    return true;
  }

  // Multisig
  if (prev.isMultisig()) {
    if (prev.indexOf(ring.publicKey) === -1)
      return false;

    // Technically we should create m signature slots,
    // but we create n signature slots so we can order
    // the signatures properly.
    vector.set(0, opcodes.OP_0);

    // Grab `n` value (number of keys).
    n = prev.getSmall(prev.length - 2);

    // Fill script with `n` signature slots.
    for (i = 0; i < n; i++)
      vector.set(i + 1, opcodes.OP_0);

    return true;
  }

  return false;
};

/**
 * Sign a transaction input on the worker pool
 * (if workers are enabled).
 * @param {Number} index
 * @param {Coin|Output} coin
 * @param {KeyRing} ring
 * @param {SighashType?} type
 * @returns {Promise}
 */

MTX.prototype.signInputAsync = function signInputAsync(index, coin, ring, type) {
  return workerPool.signInput(this, index, coin, ring, type);
};

/**
 * Sign an input.
 * @param {Number} index - Index of input being signed.
 * @param {Coin|Output} coin
 * @param {KeyRing} ring - Private key.
 * @param {SighashType} type
 * @returns {Boolean} Whether the input was able to be signed.
 */

MTX.prototype.signInput = function signInput(index, coin, ring, type) {
  var input = this.inputs[index];
  var version = 0;
  var redeem = false;
  var key = ring.privateKey;
  var prev, value, vector, sig, result;

  assert(input, 'Input does not exist.');
  assert(coin, 'No coin passed.');

  // Get the previous output's script
  prev = coin.script;
  value = coin.value;
  vector = input.script;

  // Grab regular p2sh redeem script.
  if (prev.isScripthash()) {
    prev = input.script.getRedeem();
    if (!prev)
      throw new Error('Input has not been templated.');
    redeem = true;
  }

  // If the output script is a witness program,
  // we have to switch the vector to the witness
  // and potentially alter the length. Note that
  // witnesses are stack items, so the `dummy`
  // _has_ to be an empty buffer (what OP_0
  // pushes onto the stack).
  if (prev.isWitnessScripthash()) {
    prev = input.witness.getRedeem();
    if (!prev)
      throw new Error('Input has not been templated.');
    vector = input.witness;
    redeem = true;
    version = 1;
  } else if (prev.isWitnessPubkeyhash()) {
    prev = Script.fromPubkeyhash(prev.get(1));
    vector = input.witness;
    redeem = false;
    version = 1;
  }

  // Create our signature.
  sig = this.signature(index, prev, value, key, type, version);

  if (redeem) {
    redeem = vector.pop();
    result = this.signVector(prev, vector, sig, ring);
    vector.push(redeem);
    vector.compile();
    return result;
  }

  return this.signVector(prev, vector, sig, ring);
};

/**
 * Add a signature to a vector
 * based on a previous script.
 * @param {Script} prev
 * @param {Witness|Script} vector
 * @param {Buffer} sig
 * @param {KeyRing} ring
 * @return {Boolean}
 */

MTX.prototype.signVector = function signVector(prev, vector, sig, ring) {
  var i, m, n, keys, keyIndex, total;

  // P2PK
  if (prev.isPubkey()) {
    // Make sure the pubkey is ours.
    if (!util.equal(ring.publicKey, prev.get(0)))
      return false;

    // Already signed.
    if (Script.isSignature(vector.get(0)))
      return true;

    if (vector.getSmall(0) !== 0)
      throw new Error('Input has not been templated.');

    vector.set(0, sig);
    vector.compile();

    return true;
  }

  // P2PKH
  if (prev.isPubkeyhash()) {
    // Make sure the pubkey hash is ours.
    if (!util.equal(ring.getKeyHash(), prev.get(2)))
      return false;

    // Already signed.
    if (Script.isSignature(vector.get(0)))
      return true;

    if (!Script.isKey(vector.get(1)))
      throw new Error('Input has not been templated.');

    vector.set(0, sig);
    vector.compile();

    return true;
  }

  // Multisig
  if (prev.isMultisig()) {
    // Grab the redeem script's keys to figure
    // out where our key should go.
    keys = [];

    for (i = 1; i < prev.length - 2; i++)
      keys.push(prev.get(i));

    // Grab `m` value (number of sigs required).
    m = prev.getSmall(0);

    // Grab `n` value (number of keys).
    n = prev.getSmall(prev.length - 2);

    if (vector.getSmall(0) !== 0)
      throw new Error('Input has not been templated.');

    // Too many signature slots. Abort.
    if (vector.length - 1 > n)
      return false;

    // Count the number of current signatures.
    total = 0;
    for (i = 1; i < vector.length; i++) {
      if (Script.isSignature(vector.get(i)))
        total++;
    }

    // Signatures are already finalized.
    if (total === m && vector.length - 1 === m)
      return true;

    // Add some signature slots for us to use if
    // there was for some reason not enough.
    while (vector.length - 1 < n)
      vector.push(opcodes.OP_0);

    // Find the key index so we can place
    // the signature in the same index.
    keyIndex = util.indexOf(keys, ring.publicKey);

    // Our public key is not in the prev_out
    // script. We tried to sign a transaction
    // that is not redeemable by us.
    if (keyIndex === -1)
      return false;

    // Offset key index by one to turn it into
    // "sig index". Accounts for OP_0 byte at
    // the start.
    keyIndex++;

    // Add our signature to the correct slot
    // and increment the total number of
    // signatures.
    if (keyIndex < vector.length && total < m) {
      if (vector.getSmall(keyIndex) === 0) {
        vector.set(keyIndex, sig);
        total++;
      }
    }

    // All signatures added. Finalize.
    if (total >= m) {
      // Remove empty slots left over.
      for (i = vector.length - 1; i >= 1; i--) {
        if (vector.getSmall(i) === 0)
          vector.remove(i);
      }

      // Remove signatures which are not required.
      // This should never happen.
      while (total > m) {
        vector.pop();
        total--;
      }

      // Sanity checks.
      assert(total === m);
      assert(vector.length - 1 === m);
    }

    vector.compile();

    if (total !== m)
      return false;

    return true;
  }

  return false;
};

/**
 * Create a signature suitable for inserting into scriptSigs/witnesses.
 * @param {Number} index - Index of input being signed.
 * @param {Script} prev - Previous output script or redeem script
 * (in the case of witnesspubkeyhash, this should be the generated
 * p2pkh script).
 * @param {Amount} value - Previous output value.
 * @param {SighashType} type
 * @param {Number} version - Sighash version (0=legacy, 1=segwit).
 * @returns {Buffer} Signature in DER format.
 */

MTX.prototype.signature = function signature(index, prev, value, key, type, version) {
  var hash;

  if (type == null)
    type = Script.hashType.ALL;

  if (typeof type === 'string') {
    type = Script.hashType[type.toUpperCase()];
    assert(type != null, 'Unknown sighash type.');
  }

  hash = this.signatureHash(index, prev, value, type, version);

  return Script.sign(hash, key, type);
};

/**
 * Test whether the transaction is fully-signed.
 * @returns {Boolean}
 */

MTX.prototype.isSigned = function isSigned() {
  var i, input, coin;

  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    coin = this.view.getOutput(input);

    if (!coin)
      return false;

    if (!this.isInputSigned(i, coin))
      return false;
  }

  return true;
};

/**
 * Test whether an input is fully-signed.
 * @param {Number} index
 * @param {Coin|Output} coin
 * @returns {Boolean}
 */

MTX.prototype.isInputSigned = function isInputSigned(index, coin) {
  var input = this.inputs[index];
  var prev, vector, redeem, result;

  assert(input, 'Input does not exist.');
  assert(coin, 'No coin passed.');

  prev = coin.script;
  vector = input.script;
  redeem = false;

  // Grab redeem script if possible.
  if (prev.isScripthash()) {
    prev = input.script.getRedeem();
    if (!prev)
      return false;
    redeem = true;
  }

  // If the output script is a witness program,
  // we have to switch the vector to the witness
  // and potentially alter the length.
  if (prev.isWitnessScripthash()) {
    prev = input.witness.getRedeem();
    if (!prev)
      return false;
    vector = input.witness;
    redeem = true;
  } else if (prev.isWitnessPubkeyhash()) {
    prev = Script.fromPubkeyhash(prev.get(1));
    vector = input.witness;
    redeem = false;
  }

  if (redeem) {
    redeem = vector.pop();
    result = this.isVectorSigned(prev, vector);
    vector.push(redeem);
    return result;
  }

  return this.isVectorSigned(prev, vector);
};

/**
 * Test whether a vector is fully-signed.
 * @param {Script} prev
 * @param {Script|Witness} vector
 * @returns {Boolean}
 */

MTX.prototype.isVectorSigned = function isVectorSigned(prev, vector) {
  var i, m;

  if (prev.isPubkey()) {
    if (!Script.isSignature(vector.get(0)))
      return false;
    return true;
  }

  if (prev.isPubkeyhash()) {
    if (!Script.isSignature(vector.get(0)))
      return false;
    return true;
  }

  if (prev.isMultisig()) {
    // Grab `m` value (number of required sigs).
    m = prev.getSmall(0);

    // Ensure all members are signatures.
    for (i = 1; i < vector.length; i++) {
      if (!Script.isSignature(vector.get(i)))
        return false;
    }

    // Ensure we have the correct number
    // of required signatures.
    if (vector.length - 1 !== m)
      return false;

    return true;
  }

  return false;
};

/**
 * Build input scripts (or witnesses).
 * @param {KeyRing} ring - Address used to sign. The address
 * must be able to redeem the coin.
 * @returns {Number} Number of inputs templated.
 */

MTX.prototype.template = function template(ring) {
  var total = 0;
  var i, input, coin;

  if (Array.isArray(ring)) {
    for (i = 0; i < ring.length; i++)
      total += this.template(ring[i]);
    return total;
  }

  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    coin = this.view.getOutput(input);

    if (!coin)
      continue;

    if (!ring.ownOutput(coin))
      continue;

    // Build script for input
    if (!this.scriptInput(i, coin, ring))
      continue;

    total++;
  }

  return total;
};

/**
 * Built input scripts (or witnesses) and sign the inputs.
 * @param {KeyRing} ring - Address used to sign. The address
 * must be able to redeem the coin.
 * @param {SighashType} type
 * @returns {Number} Number of inputs signed.
 */

MTX.prototype.sign = function sign(ring, type) {
  var total = 0;
  var i, input, coin;

  if (Array.isArray(ring)) {
    for (i = 0; i < ring.length; i++)
      total += this.sign(ring[i], type);
    return total;
  }

  assert(ring.privateKey, 'No private key available.');

  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    coin = this.view.getOutput(input);

    if (!coin)
      continue;

    if (!ring.ownOutput(coin))
      continue;

    // Build script for input
    if (!this.scriptInput(i, coin, ring))
      continue;

    // Sign input
    if (!this.signInput(i, coin, ring, type))
      continue;

    total++;
  }

  return total;
};

/**
 * Sign the transaction inputs on the worker pool
 * (if workers are enabled).
 * @param {KeyRing} ring
 * @param {SighashType?} type
 * @returns {Promise}
 */

MTX.prototype.signAsync = function signAsync(ring, type) {
  return workerPool.sign(this, ring, type);
};

/**
 * Estimate maximum possible size.
 * @param {Function?} estimate - Input script size estimator.
 * @returns {Number}
 */

MTX.prototype.estimateSize = co(function* estimateSize(estimate) {
  var scale = consensus.WITNESS_SCALE_FACTOR;
  var total = 0;
  var i, input, output, size, prev, coin;

  // Calculate the size, minus the input scripts.
  total += 4;
  total += encoding.sizeVarint(this.inputs.length);
  total += this.inputs.length * 40;

  total += encoding.sizeVarint(this.outputs.length);

  for (i = 0; i < this.outputs.length; i++) {
    output = this.outputs[i];
    total += output.getSize();
  }

  total += 4;

  // Add size for signatures and public keys
  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    coin = this.view.getOutput(input);
    size = 0;

    // We're out of luck here.
    // Just assume it's a p2pkh.
    if (!coin) {
      total += 110;
      continue;
    }

    // Previous output script.
    prev = coin.script;

    // P2PK
    if (prev.isPubkey()) {
      // varint script size
      size += 1;
      // OP_PUSHDATA0 [signature]
      size += 1 + 73;
      total += size;
      continue;
    }

    // P2PKH
    if (prev.isPubkeyhash()) {
      // varint script size
      size += 1;
      // OP_PUSHDATA0 [signature]
      size += 1 + 73;
      // OP_PUSHDATA0 [key]
      size += 1 + 33;
      total += size;
      continue;
    }

    if (prev.isMultisig()) {
      // Bare Multisig
      // OP_0
      size += 1;
      // OP_PUSHDATA0 [signature] ...
      size += (1 + 73) * prev.getSmall(0);
      // varint len
      size += encoding.sizeVarint(size);
      total += size;
      continue;
    }

    // P2WPKH
    if (prev.isWitnessPubkeyhash()) {
      // varint-items-len
      size += 1;
      // varint-len [signature]
      size += 1 + 73;
      // varint-len [key]
      size += 1 + 33;
      // vsize
      size = (size + scale - 1) / scale | 0;
      total += size;
      continue;
    }

    // Call out to the custom estimator.
    if (estimate) {
      size = yield estimate(prev);
      if (size !== -1) {
        total += size;
        continue;
      }
    }

    // P2SH
    if (prev.isScripthash()) {
      // varint size
      total += 1;
      // 2-of-3 multisig input
      total += 149;
      continue;
    }

    // P2WSH
    if (prev.isWitnessScripthash()) {
      // varint-items-len
      size += 1;
      // 2-of-3 multisig input
      size += 149;
      // vsize
      size = (size + scale - 1) / scale | 0;
      total += size;
      continue;
    }

    // Unknown.
    total += 110;
  }

  return total;
});

/**
 * Select necessary coins based on total output value.
 * @param {Coin[]} coins
 * @param {Object?} options
 * @returns {CoinSelection}
 * @throws on not enough funds available.
 */

MTX.prototype.selectCoins = function selectCoins(coins, options) {
  var selector = new CoinSelector(this, options);
  return selector.select(coins);
};

/**
 * Attempt to subtract a fee from outputs.
 * @param {Amount} fee
 * @param {Number?} index
 */

MTX.prototype.subtractFee = function subtractFee(fee, index) {
  var i, min, output;

  if (typeof index === 'number') {
    output = this.outputs[index];

    if (!output)
      throw new Error('Subtraction index does not exist.');

    min = fee + output.getDustThreshold();

    if (output.value < min)
      throw new Error('Could not subtract fee.');

    output.value -= fee;

    return;
  }

  for (i = 0; i < this.outputs.length; i++) {
    output = this.outputs[i];
    min = fee + output.getDustThreshold();

    if (output.value >= min) {
      output.value -= fee;
      return;
    }
  }

  throw new Error('Could not subtract fee.');
};

/**
 * Select coins and fill the inputs.
 * @param {Coin[]} coins
 * @param {Object} options - See {@link MTX#selectCoins} options.
 * @returns {CoinSelector}
 */

MTX.prototype.fund = co(function* fund(coins, options) {
  var i, select, change;

  assert(options, 'Options are required.');
  assert(options.changeAddress, 'Change address is required.');
  assert(this.inputs.length === 0, 'TX is already funded.');

  // Select necessary coins.
  select = yield this.selectCoins(coins, options);

  // Add coins to transaction.
  for (i = 0; i < select.chosen.length; i++)
    this.addCoin(select.chosen[i]);

  // Attempt to subtract fee.
  if (select.shouldSubtract)
    this.subtractFee(select.fee, select.subtractFee);

  // Add a change output.
  change = new Output();
  change.value = select.change;
  change.script.fromAddress(select.changeAddress);

  if (change.isDust(policy.MIN_RELAY)) {
    // Do nothing. Change is added to fee.
    this.changeIndex = -1;
    assert.equal(this.getFee(), select.fee + select.change);
  } else {
    this.outputs.push(change);
    this.changeIndex = this.outputs.length - 1;
    assert.equal(this.getFee(), select.fee);
  }

  return select;
});

/**
 * Sort inputs and outputs according to BIP69.
 * @see https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
 */

MTX.prototype.sortMembers = function sortMembers() {
  var changeOutput;

  if (this.changeIndex !== -1) {
    changeOutput = this.outputs[this.changeIndex];
    assert(changeOutput);
  }

  this.inputs.sort(sortInputs);
  this.outputs.sort(sortOutputs);

  if (this.changeIndex !== -1) {
    this.changeIndex = this.outputs.indexOf(changeOutput);
    assert(this.changeIndex !== -1);
  }
};

/**
 * Avoid fee sniping.
 * @param {Number} - Current chain height.
 * @see bitcoin/src/wallet/wallet.cpp
 */

MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) {
  assert(typeof height === 'number', 'Must pass in height.');

  if (util.random(0, 10) === 0) {
    height -= util.random(0, 100);

    if (height < 0)
      height = 0;
  }

  this.setLocktime(height);
};

/**
 * Set locktime and sequences appropriately.
 * @param {Number} locktime
 */

MTX.prototype.setLocktime = function setLocktime(locktime) {
  var i, input;

  assert(util.isUInt32(locktime), 'Locktime must be a uint32.');
  assert(this.inputs.length > 0, 'Cannot set sequence with no inputs.');

  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    if (input.sequence === 0xffffffff)
      input.sequence = 0xfffffffe;
  }

  this.locktime = locktime;
};

/**
 * Set sequence locktime.
 * @param {Number} index - Input index.
 * @param {Number} locktime
 * @param {Boolean?} seconds
 */

MTX.prototype.setSequence = function setSequence(index, locktime, seconds) {
  var input = this.inputs[index];

  assert(input, 'Input does not exist.');
  assert(util.isUInt32(locktime), 'Locktime must be a uint32.');

  this.version = 2;

  if (seconds) {
    locktime >>>= consensus.SEQUENCE_GRANULARITY;
    locktime = consensus.SEQUENCE_TYPE_FLAG | locktime;
  } else {
    locktime = consensus.SEQUENCE_MASK & locktime;
  }

  input.sequence = locktime;
};

/**
 * Inspect the transaction.
 * @returns {Object}
 */

MTX.prototype.inspect = function inspect() {
  return this.format();
};

/**
 * Inspect the transaction.
 * @returns {Object}
 */

MTX.prototype.format = function format() {
  return TX.prototype.format.call(this, this.view);
};

/**
 * Convert transaction to JSON.
 * @returns {Object}
 */

MTX.prototype.toJSON = function toJSON() {
  return TX.prototype.getJSON.call(this, null, this.view);
};

/**
 * Convert transaction to JSON.
 * @param {Network} network
 * @returns {Object}
 */

MTX.prototype.getJSON = function getJSON(network) {
  return TX.prototype.getJSON.call(this, network, this.view);
};

/**
 * Instantiate a transaction from a
 * jsonified transaction object.
 * @param {Object} json - The jsonified transaction object.
 * @returns {MTX}
 */

MTX.fromJSON = function fromJSON(json) {
  return new MTX().fromJSON(json);
};

/**
 * Instantiate a transaction from a buffer reader.
 * @param {BufferReader} br
 * @returns {MTX}
 */

MTX.fromReader = function fromReader(br) {
  return new MTX().fromReader(br);
};

/**
 * Instantiate a transaction from a serialized Buffer.
 * @param {Buffer} data
 * @param {String?} enc - Encoding, can be `'hex'` or null.
 * @returns {MTX}
 */

MTX.fromRaw = function fromRaw(data, enc) {
  if (typeof data === 'string')
    data = new Buffer(data, enc);
  return new MTX().fromRaw(data);
};

/**
 * Convert the MTX to a TX.
 * @returns {TX}
 */

MTX.prototype.toTX = function toTX() {
  return new TX().inject(this);
};

/**
 * Instantiate MTX from TX.
 * @param {TX} tx
 * @returns {MTX}
 */

MTX.fromTX = function fromTX(tx) {
  return new MTX().inject(tx);
};

/**
 * Test whether an object is an MTX.
 * @param {Object} obj
 * @returns {Boolean}
 */

MTX.isMTX = function isMTX(obj) {
  return obj
    && Array.isArray(obj.inputs)
    && typeof obj.locktime === 'number'
    && typeof obj.scriptInput === 'function';
};

/**
 * Coin Selector
 * @alias module:primitives.CoinSelector
 * @constructor
 * @param {TX} tx
 * @param {Object?} options
 */

function CoinSelector(tx, options) {
  if (!(this instanceof CoinSelector))
    return new CoinSelector(tx, options);

  this.tx = tx.clone();
  this.coins = [];
  this.outputValue = 0;
  this.index = 0;
  this.chosen = [];
  this.change = 0;
  this.fee = CoinSelector.MIN_FEE;

  this.selection = 'age';
  this.shouldSubtract = false;
  this.subtractFee = null;
  this.height = -1;
  this.depth = -1;
  this.hardFee = -1;
  this.rate = CoinSelector.FEE_RATE;
  this.maxFee = -1;
  this.round = false;
  this.changeAddress = null;

  // Needed for size estimation.
  this.estimate = null;

  if (options)
    this.fromOptions(options);
}

/**
 * Default fee rate
 * for coin selection.
 * @const {Amount}
 * @default
 */

CoinSelector.FEE_RATE = 10000;

/**
 * Minimum fee to start with
 * during coin selection.
 * @const {Amount}
 * @default
 */

CoinSelector.MIN_FEE = 10000;

/**
 * Maximum fee to allow
 * after coin selection.
 * @const {Amount}
 * @default
 */

CoinSelector.MAX_FEE = consensus.COIN / 10;

/**
 * Initialize selector options.
 * @param {Object} options
 * @private
 */

CoinSelector.prototype.fromOptions = function fromOptions(options) {
  var addr;

  if (options.selection) {
    assert(typeof options.selection === 'string');
    this.selection = options.selection;
  }

  if (options.subtractFee != null) {
    if (typeof options.subtractFee === 'number') {
      assert(util.isUInt32(options.subtractFee));
      this.subtractFee = options.subtractFee;
      this.shouldSubtract = true;
    } else {
      assert(typeof options.subtractFee === 'boolean');
      this.subtractFee = options.subtractFee;
      this.shouldSubtract = options.subtractFee;
    }
  }

  if (options.height != null) {
    assert(util.isNumber(options.height));
    assert(options.height >= -1);
    this.height = options.height;
  }

  if (options.confirmations != null) {
    assert(util.isNumber(options.confirmations));
    assert(options.confirmations >= -1);
    this.depth = options.confirmations;
  }

  if (options.depth != null) {
    assert(util.isNumber(options.depth));
    assert(options.depth >= -1);
    this.depth = options.depth;
  }

  if (options.hardFee != null) {
    assert(util.isNumber(options.hardFee));
    assert(options.hardFee >= -1);
    this.hardFee = options.hardFee;
  }

  if (options.rate != null) {
    assert(util.isNumber(options.rate));
    assert(options.rate >= 0);
    this.rate = options.rate;
  }

  if (options.maxFee != null) {
    assert(util.isNumber(options.maxFee));
    assert(options.maxFee >= -1);
    this.maxFee = options.maxFee;
  }

  if (options.round != null) {
    assert(typeof options.round === 'boolean');
    this.round = options.round;
  }

  if (options.changeAddress) {
    addr = options.changeAddress;
    if (typeof addr === 'string') {
      this.changeAddress = Address.fromBase58(addr);
    } else {
      assert(addr instanceof Address);
      this.changeAddress = addr;
    }
  }

  if (options.estimate) {
    assert(typeof options.estimate === 'function');
    this.estimate = options.estimate;
  }

  return this;
};

/**
 * Initialize the selector with coins to select from.
 * @param {Coin[]} coins
 */

CoinSelector.prototype.init = function init(coins) {
  this.coins = coins.slice();
  this.outputValue = this.tx.getOutputValue();
  this.index = 0;
  this.chosen = [];
  this.change = 0;
  this.fee = CoinSelector.MIN_FEE;
  this.tx.inputs.length = 0;

  switch (this.selection) {
    case 'all':
    case 'random':
      this.coins.sort(sortRandom);
      break;
    case 'age':
      this.coins.sort(sortAge);
      break;
    default:
      throw new FundingError('Bad selection type: ' + this.selection);
  }
};

/**
 * Calculate total value required.
 * @returns {Amount}
 */

CoinSelector.prototype.total = function total() {
  if (this.shouldSubtract)
    return this.outputValue;
  return this.outputValue + this.fee;
};

/**
 * Test whether the selector has
 * completely funded the transaction.
 * @returns {Boolean}
 */

CoinSelector.prototype.isFull = function isFull() {
  return this.tx.getInputValue() >= this.total();
};

/**
 * Test whether a coin is spendable
 * with regards to the options.
 * @param {Coin}
 * @returns {Boolean}
 */

CoinSelector.prototype.isSpendable = function isSpendable(coin) {
  var depth;

  if (this.height === -1)
    return true;

  if (coin.coinbase) {
    if (coin.height === -1)
      return false;

    if (this.height + 1 < coin.height + consensus.COINBASE_MATURITY)
      return false;

    return true;
  }

  if (this.depth === -1)
    return true;

  depth = coin.getDepth(this.height);

  if (depth < this.depth)
    return false;

  return true;
};

/**
 * Get the current fee based on a size.
 * @param {Number} size
 * @returns {Amount}
 */

CoinSelector.prototype.getFee = function getFee(size) {
  var fee;

  if (this.round) {
    // This is mostly here for testing.
    // i.e. A fee rounded to the nearest
    // kb is easier to predict ahead of time.
    fee = policy.getRoundFee(size, this.rate);
  } else {
    fee = policy.getMinFee(size, this.rate);
  }

  return Math.min(fee, CoinSelector.MAX_FEE);
};

/**
 * Fund the transaction with more
 * coins if the `output value + fee`
 * total was updated.
 */

CoinSelector.prototype.fund = function fund() {
  var coin;

  while (this.index < this.coins.length) {
    coin = this.coins[this.index++];

    if (!this.isSpendable(coin))
      continue;

    this.tx.addCoin(coin);
    this.chosen.push(coin);

    if (this.selection === 'all')
      continue;

    if (this.isFull())
      break;
  }
};

/**
 * Initiate selection from `coins`.
 * @param {Coin[]} coins
 * @returns {CoinSelector}
 */

CoinSelector.prototype.select = co(function* select(coins) {
  this.init(coins);

  if (this.hardFee !== -1) {
    this.selectHard();
  } else {
    // This is potentially asynchronous:
    // it may invoke the size estimator
    // required for redeem scripts (we
    // may be calling out to a wallet
    // or something similar).
    yield this.selectEstimate();
  }

  if (!this.isFull()) {
    // Still failing to get enough funds.
    throw new FundingError(
      'Not enough funds.',
      this.tx.getInputValue(),
      this.total());
  }

  // How much money is left after filling outputs.
  this.change = this.tx.getInputValue() - this.total();

  return this;
});

/**
 * Initialize selection based on size estimate.
 */

CoinSelector.prototype.selectEstimate = co(function* selectEstimate() {
  var change, size;

  // Set minimum fee and do
  // an initial round of funding.
  this.fee = CoinSelector.MIN_FEE;
  this.fund();

  // Add dummy output for change.
  change = new Output();

  if (this.changeAddress) {
    change.script.fromAddress(this.changeAddress);
  } else {
    // In case we don't have a change address,
    // we use a fake p2pkh output to gauge size.
    change.script.fromPubkeyhash(encoding.ZERO_HASH160);
  }

  this.tx.outputs.push(change);

  // Keep recalculating the fee and funding
  // until we reach some sort of equilibrium.
  do {
    size = yield this.tx.estimateSize(this.estimate);

    this.fee = this.getFee(size);

    if (this.maxFee > 0 && this.fee > this.maxFee)
      throw new FundingError('Fee is too high.');

    // Failed to get enough funds, add more coins.
    if (!this.isFull())
      this.fund();
  } while (!this.isFull() && this.index < this.coins.length);
});

/**
 * Initiate selection based on a hard fee.
 */

CoinSelector.prototype.selectHard = function selectHard() {
  this.fee = Math.min(this.hardFee, CoinSelector.MAX_FEE);
  this.fund();
};

/**
 * An error thrown from the coin selector.
 * @constructor
 * @ignore
 * @extends Error
 * @param {String} msg
 * @param {Amount} available
 * @param {Amount} required
 * @property {String} message - Error message.
 * @property {Amount} availableFunds
 * @property {Amount} requiredFunds
 */

function FundingError(msg, available, required) {
  Error.call(this);

  if (Error.captureStackTrace)
    Error.captureStackTrace(this, FundingError);

  this.type = 'FundingError';
  this.message = msg;
  this.availableFunds = -1;
  this.requiredFunds = -1;

  if (available != null) {
    this.message += ' (available=' + Amount.btc(available) + ',';
    this.message += ' required=' + Amount.btc(required) + ')';
    this.availableFunds = available;
    this.requiredFunds = required;
  }
}

util.inherits(FundingError, Error);

/*
 * Helpers
 */

function sortAge(a, b) {
  a = a.height === -1 ? 0x7fffffff : a.height;
  b = b.height === -1 ? 0x7fffffff : b.height;
  return a - b;
}

function sortRandom(a, b) {
  return Math.random() > 0.5 ? 1 : -1;
}

function sortInputs(a, b) {
  var ahash = a.prevout.txid();
  var bhash = b.prevout.txid();
  var cmp = util.strcmp(ahash, bhash);

  if (cmp !== 0)
    return cmp;

  return a.prevout.index - b.prevout.index;
}

function sortOutputs(a, b) {
  var cmp = a.value - b.value;

  if (cmp !== 0)
    return cmp;

  return util.cmp(a.script.toRaw(), b.script.toRaw());
}

/*
 * Expose
 */

exports = MTX;
exports.MTX = MTX;
exports.Selector = CoinSelector;
exports.FundingError = FundingError;

module.exports = exports;
