/*!
 * mempool.js - mempool for bcoin
 * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var AsyncObject = require('../utils/async');
var constants = require('../protocol/constants');
var utils = require('../utils/utils');
var co = require('../utils/co');
var assert = require('assert');
var crypto = require('../crypto/crypto');
var VerifyError = require('../utils/errors').VerifyError;
var VerifyResult = utils.VerifyResult;
var flags = constants.flags;
var Bloom = require('../utils/bloom');
var Address = require('../primitives/address');
var Coin = require('../primitives/coin');
var Locker = require('../utils/locker');
var Outpoint = require('../primitives/outpoint');
var TX = require('../primitives/tx');
var MempoolEntry = require('./mempoolentry');

/**
 * Represents a mempool.
 * @exports Mempool
 * @constructor
 * @param {Object} options
 * @param {String?} options.name - Database name.
 * @param {String?} options.location - Database file location.
 * @param {String?} options.db - Database backend (`"memory"` by default).
 * @param {Boolean?} options.limitFree
 * @param {Number?} options.limitFreeRelay
 * @param {Number?} options.maxSize - Max pool size (default ~300mb).
 * @param {Boolean?} options.relayPriority
 * @param {Boolean?} options.requireStandard
 * @param {Boolean?} options.rejectAbsurdFees
 * @param {Boolean?} options.relay
 * @property {Boolean} loaded
 * @property {Object} db
 * @property {Number} size
 * @property {Number} totalOrphans
 * @property {Locker} locker
 * @property {Number} freeCount
 * @property {Number} lastTime
 * @property {Number} maxSize
 * @property {Boolean} blockSinceBump
 * @property {Number} lastFeeUpdate
 * @property {Rate} minRate
 * @property {Rate} minReasonable
 * @property {Rate} minRelayFee
 * @emits Mempool#open
 * @emits Mempool#error
 * @emits Mempool#tx
 * @emits Mempool#add tx
 * @emits Mempool#remove tx
 */

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

  AsyncObject.call(this);

  if (!options)
    options = {};

  this.options = options;
  this.chain = options.chain;
  this.fees = options.fees;

  assert(this.chain, 'Mempool requires a blockchain.');

  this.network = this.chain.network;
  this.logger = options.logger || this.chain.logger;
  this.loaded = false;

  this.locker = new Locker(true);

  this.size = 0;
  this.totalOrphans = 0;
  this.totalTX = 0;

  this.waiting = {};
  this.orphans = {};
  this.tx = {};
  this.spents = {};
  this.currentTX = null;
  this.coinIndex = new AddressIndex(this);
  this.txIndex = new AddressIndex(this);

  this.rejects = new Bloom.Rolling(120000, 0.000001);

  this.freeCount = 0;
  this.lastTime = 0;

  this.limitFree = this.options.limitFree !== false;
  this.limitFreeRelay = this.options.limitFreeRelay || 15;
  this.relayPriority = this.options.relayPriority !== false;
  this.rejectFee = this.options.rejectFee === true;
  this.requireStandard = this.options.requireStandard != null
    ? this.options.requireStandard
    : this.network.requireStandard;
  this.rejectAbsurdFees = this.options.rejectAbsurdFees !== false;
  this.prematureWitness = !!this.options.prematureWitness;
  this.paranoid = !!this.options.paranoid;

  this.maxSize = options.maxSize || constants.mempool.MAX_MEMPOOL_SIZE;
  this.expiryTime = options.expiryTime || constants.mempool.MEMPOOL_EXPIRY;
  this.blockSinceBump = false;
  this.lastFeeUpdate = utils.now();
  this.minRate = 0;
  this.minReasonable = this.network.minRelay;
  this.minRelay = this.network.minRelay;
}

utils.inherits(Mempool, AsyncObject);

/**
 * Open the chain, wait for the database to load.
 * @alias Mempool#open
 * @returns {Promise}
 */

Mempool.prototype._open = co(function* open() {
  var size = (this.maxSize / 1024).toFixed(2);
  yield this.chain.open();
  this.logger.info('Mempool loaded (maxsize=%dkb).', size);
});

/**
 * Close the chain, wait for the database to close.
 * @alias Mempool#close
 * @returns {Promise}
 */

Mempool.prototype._close = function close() {
  return Promise.resolve(null);
};

/**
 * Notify the mempool that a new block has come
 * in (removes all transactions contained in the
 * block from the mempool).
 * @param {Block} block
 * @returns {Promise}
 */

Mempool.prototype.addBlock = co(function* addBlock(block) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._addBlock(block);
  } finally {
    unlock();
  }
});

/**
 * Notify the mempool that a new block
 * has come without a lock.
 * @private
 * @param {Block} block
 * @returns {Promise}
 */

Mempool.prototype._addBlock = co(function* addBlock(block) {
  var entries = [];
  var i, entry, tx, hash;

  for (i = block.txs.length - 1; i >= 0; i--) {
    tx = block.txs[i];
    hash = tx.hash('hex');

    if (tx.isCoinbase())
      continue;

    entry = this.getEntry(hash);

    if (!entry) {
      this.removeOrphan(hash);
      continue;
    }

    this.removeUnchecked(entry);
    this.emit('confirmed', tx, block);

    entries.push(entry);
  }

  this.blockSinceBump = true;
  this.lastFeeUpdate = utils.now();

  if (this.fees)
    this.fees.processBlock(block.height, entries, this.chain.isFull());

  // We need to reset the rejects filter periodically.
  // There may be a locktime in a TX that is now valid.
  this.rejects.reset();

  yield co.wait();
});

/**
 * Notify the mempool that a block has been disconnected
 * from the main chain (reinserts transactions into the mempool).
 * @param {Block} block
 * @returns {Promise}
 */

Mempool.prototype.removeBlock = co(function* removeBlock(block) {
  var unlock = yield this.lock();
  try {
    return yield this._removeBlock(block);
  } finally {
    unlock();
  }
});

/**
 * Notify the mempool that a block
 * has been disconnected without a lock.
 * @private
 * @param {Block} block
 * @returns {Promise}
 */

Mempool.prototype._removeBlock = co(function* removeBlock(block) {
  var i, entry, tx, hash;

  for (i = 0; i < block.txs.length; i++) {
    tx = block.txs[i];
    hash = tx.hash('hex');

    if (tx.isCoinbase())
      continue;

    if (this.hasTX(hash))
      continue;

    entry = MempoolEntry.fromTX(tx, block.height);

    yield this._addUnchecked(entry);

    this.emit('unconfirmed', tx, block);
  }

  this.rejects.reset();
});

/**
 * Ensure the size of the mempool stays below 300mb.
 * @param {Hash} entryHash - TX that initiated the trim.
 * @returns {Promise}
 */

Mempool.prototype.limitMempoolSize = function limitMempoolSize(entryHash) {
  var trimmed = false;
  var i, hashes, hash, end, entry;

  if (this.getSize() <= this.maxSize)
    return trimmed;

  hashes = this.getSnapshot();
  end = utils.now() - this.expiryTime;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    entry = this.getEntry(hash);

    if (!entry)
      continue;

    if (entry.ts >= end)
      continue;

    if (!trimmed && hash === entryHash)
      trimmed = true;

    this.removeUnchecked(entry, true);

    if (this.getSize() <= this.maxSize)
      return trimmed;
  }

  hashes = this.getSnapshot();

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    entry = this.getEntry(hash);

    if (!entry)
      continue;

    if (!trimmed && hash === entryHash)
      trimmed = true;

    this.removeUnchecked(entry, true);

    if (this.getSize() <= this.maxSize)
      return trimmed;
  }

  return trimmed;
};

/**
 * Purge orphan transactions from the mempool.
 */

Mempool.prototype.limitOrphans = function limitOrphans() {
  var orphans = Object.keys(this.orphans);
  var i, hash;

  while (this.totalOrphans > constants.mempool.MAX_ORPHAN_TX) {
    i = crypto.randomRange(0, orphans.length);
    hash = orphans[i];
    orphans.splice(i, 1);

    this.logger.spam('Removing orphan %s from mempool.', utils.revHex(hash));

    this.removeOrphan(hash);
  }
};

/**
 * Retrieve a transaction from the mempool.
 * Note that this will not be filled with coins.
 * @param {TX|Hash} hash
 * @returns {TX}
 */

Mempool.prototype.getTX = function getTX(hash) {
  var entry = this.tx[hash];
  if (!entry)
    return;
  return entry.tx;
};

/**
 * Retrieve a transaction from the mempool.
 * Note that this will not be filled with coins.
 * @param {TX|Hash} hash
 * @returns {MempoolEntry}
 */

Mempool.prototype.getEntry = function getEntry(hash) {
  return this.tx[hash];
};

/**
 * Retrieve a coin from the mempool (unspents only).
 * @param {Hash} hash
 * @param {Number} index
 * @returns {Coin}
 */

Mempool.prototype.getCoin = function getCoin(hash, index) {
  var entry = this.tx[hash];

  if (!entry)
    return;

  if (this.isSpent(hash, index))
    return;

  if (index >= entry.tx.outputs.length)
    return;

  return Coin.fromTX(entry.tx, index);
};

/**
 * Check to see if a coin has been spent. This differs from
 * {@link ChainDB#isSpent} in that it actually maintains a
 * map of spent coins, whereas ChainDB may return `true`
 * for transaction outputs that never existed.
 * @param {Hash} hash
 * @param {Number} index
 * @returns {Boolean}
 */

Mempool.prototype.isSpent = function isSpent(hash, index) {
  return this.spents[hash + index];
};

/**
 * Find all coins pertaining to a certain address.
 * @param {Address[]} addresses
 * @returns {Coin[]}
 */

Mempool.prototype.getCoinsByAddress = function getCoinsByAddress(addresses) {
  var coins = [];
  var i, j, coin, hash;

  if (!Array.isArray(addresses))
    addresses = [addresses];

  for (i = 0; i < addresses.length; i++) {
    hash = Address.getHash(addresses[i], 'hex');

    if (!hash)
      continue;

    coin = this.coinIndex.getCoins(hash);

    for (j = 0; j < coin.length; j++)
      coins.push(coin[j]);
  }

  return coins;
};

/**
 * Find all transactions pertaining to a certain address.
 * @param {Address[]} addresses
 * @returns {TX[]}
 */

Mempool.prototype.getTXByAddress = function getTXByAddress(addresses) {
  var txs = [];
  var i, j, tx, hash;

  if (!Array.isArray(addresses))
    addresses = [addresses];

  for (i = 0; i < addresses.length; i++) {
    hash = Address.getHash(addresses[i], 'hex');

    if (!hash)
      continue;

    tx = this.txIndex.getTX(hash);

    for (j = 0; j < tx.length; j++)
      txs.push(tx[j]);
  }

  return txs;
};

/**
 * Fill a transaction with all available transaction outputs
 * in the mempool. This differs from {@link Mempool#fillCoins}
 * in that it will fill with all historical coins and not
 * just unspent coins.
 * @param {TX} tx
 */

Mempool.prototype.fillHistory = function fillHistory(tx) {
  var i, input, prevout, prev;

  if (tx.isCoinbase())
    return;

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];

    if (input.coin)
      continue;

    prevout = input.prevout;
    prev = this.getTX(prevout.hash);

    if (!prev)
      continue;

    input.coin = Coin.fromTX(prev, prevout.index);
  }
};

/**
 * Fill a transaction with all available (unspent) coins
 * in the mempool.
 * @param {TX} tx
 */

Mempool.prototype.fillCoins = function fillCoins(tx) {
  var i, input, prevout, coin;

  if (tx.isCoinbase())
    return;

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];

    if (input.coin)
      continue;

    prevout = input.prevout;
    coin = this.getCoin(prevout.hash, prevout.index);

    if (!coin)
      continue;

    input.coin = coin;
  }
};

/**
 * Test the mempool to see if it contains a transaction.
 * @param {Hash} hash
 * @returns {Boolean}
 */

Mempool.prototype.hasTX = function hasTX(hash) {
  return this.tx[hash] != null;
};

/**
 * Test the mempool to see if it
 * contains a transaction or an orphan.
 * @param {Hash} hash
 * @returns {Boolean}
 */

Mempool.prototype.has = function has(hash) {
  if (this.locker.hasPending(hash))
    return true;

  if (hash === this.currentTX)
    return true;

  return this.exists(hash);
};

/**
 * Test the mempool to see if it
 * contains a transaction or an orphan.
 * @private
 * @param {Hash} hash
 * @returns {Boolean}
 */

Mempool.prototype.exists = function exists(hash) {
  if (this.hasOrphan(hash))
    return true;

  return this.hasTX(hash);
};

/**
 * Test the mempool to see if it
 * contains a recent reject.
 * @param {Hash} hash
 * @returns {Boolean}
 */

Mempool.prototype.hasReject = function hasReject(hash) {
  return this.rejects.test(hash, 'hex');
};

/**
 * Add a transaction to the mempool. Note that this
 * will lock the mempool until the transaction is
 * fully processed.
 * @param {TX} tx
 * @returns {Promise}
 */

Mempool.prototype.addTX = co(function* addTX(tx) {
  var unlock = yield this.locker.lock(tx);

  assert(!this.currentTX);

  this.currentTX = tx.hash('hex');

  try {
    return yield this._addTX(tx);
  } catch (err) {
    if (err.type === 'VerifyError') {
      if (!tx.hasWitness() && !err.malleated)
        this.rejects.add(tx.hash());
    }
    throw err;
  } finally {
    this.currentTX = null;
    unlock();
  }
});

/**
 * Add a transaction to the mempool without a lock.
 * @private
 * @param {TX} tx
 * @returns {Promise}
 */

Mempool.prototype._addTX = co(function* _addTX(tx) {
  var lockFlags = constants.flags.STANDARD_LOCKTIME_FLAGS;
  var hash = tx.hash('hex');
  var ret, entry, result, exists;

  assert(!tx.mutable, 'Cannot add mutable TX to mempool.');

  ret = new VerifyResult();

  if (tx.ts !== 0) {
    throw new VerifyError(tx,
      'alreadyknown',
      'txn-already-known',
      0);
  }

  if (!tx.isSane(ret)) {
    throw new VerifyError(tx,
      'invalid',
      ret.reason,
      ret.score);
  }

  if (tx.isCoinbase()) {
    throw new VerifyError(tx,
      'invalid',
      'coinbase',
      100);
  }

  if (this.requireStandard) {
    if (!this.chain.state.hasCSV() && tx.version >= 2) {
      throw new VerifyError(tx,
        'nonstandard',
        'premature-version2-tx',
        0);
    }
  }

  if (!this.chain.state.hasWitness() && !this.prematureWitness) {
    if (tx.hasWitness()) {
      throw new VerifyError(tx,
        'nonstandard',
        'no-witness-yet',
        0);
    }
  }

  if (this.requireStandard) {
    if (!tx.isStandard(ret)) {
      throw new VerifyError(tx,
        'nonstandard',
        ret.reason,
        ret.score);
    }
  }

  result = yield this.chain.checkFinal(this.chain.tip, tx, lockFlags);

  if (!result) {
    throw new VerifyError(tx,
      'nonstandard',
      'non-final',
      0);
  }

  if (this.exists(hash)) {
    throw new VerifyError(tx,
      'alreadyknown',
      'txn-already-in-mempool',
      0);
  }

  exists = yield this.chain.db.hasCoins(hash);

  if (exists) {
    throw new VerifyError(tx,
      'alreadyknown',
      'txn-already-known',
      0);
  }

  if (this.isDoubleSpend(tx)) {
    throw new VerifyError(tx,
      'duplicate',
      'bad-txns-inputs-spent',
      0);
  }

  yield this.fillAllCoins(tx);

  if (!tx.hasCoins())
    return this.storeOrphan(tx);

  entry = MempoolEntry.fromTX(tx, this.chain.height);

  yield this.verify(entry);
  yield this._addUnchecked(entry);

  if (this.limitMempoolSize(hash)) {
    throw new VerifyError(tx,
      'insufficientfee',
      'mempool full',
      0);
  }
});

/**
 * Add a transaction to the mempool without performing any
 * validation. Note that this method does not lock the mempool
 * and may lend itself to race conditions if used unwisely.
 * This function will also resolve orphans if possible (the
 * resolved orphans _will_ be validated).
 * @param {MempoolEntry} entry
 * @returns {Promise}
 */

Mempool.prototype.addUnchecked = co(function* addUnchecked(entry) {
  var unlock = yield this.locker.lock();
  try {
    return yield this._addUnchecked(entry);
  } finally {
    unlock();
  }
});

/**
 * Add a transaction to the mempool without a lock.
 * @private
 * @param {MempoolEntry} entry
 * @returns {Promise}
 */

Mempool.prototype._addUnchecked = co(function* addUnchecked(entry) {
  var i, resolved, tx, orphan;

  this.trackEntry(entry);

  this.emit('tx', entry.tx);
  this.emit('add tx', entry.tx);

  if (this.fees)
    this.fees.processTX(entry, this.chain.isFull());

  this.logger.debug('Added tx %s to mempool.', entry.tx.rhash);

  resolved = this.resolveOrphans(entry.tx);

  for (i = 0; i < resolved.length; i++) {
    tx = resolved[i];
    orphan = MempoolEntry.fromTX(tx, this.chain.height);

    try {
      yield this.verify(orphan);
    } catch (err) {
      if (err.type === 'VerifyError') {
        this.logger.debug('Could not resolve orphan %s: %s.',
          tx.rhash,
          err.message);

        if (!tx.hasWitness() && !err.malleated)
          this.rejects.add(tx.hash());

        continue;
      }
      this.emit('error', err);
      continue;
    }

    try {
      yield this._addUnchecked(orphan);
    } catch (err) {
      this.emit('error', err);
      continue;
    }

    this.logger.spam('Resolved orphan %s in mempool.', orphan.tx.rhash);
  }
});

/**
 * Remove a transaction from the mempool. Generally
 * only called when a new block is added to the main chain.
 * @param {MempoolEntry} entry
 * @param {Boolean} limit
 */

Mempool.prototype.removeUnchecked = function removeUnchecked(entry, limit) {
  var rate, hash;

  this.removeOrphan(entry.tx);

  // We do not remove spenders if this is
  // being removed for a block. The spenders
  // are still spending valid coins (which
  // now exist on the blockchain).
  if (limit) {
    this.removeSpenders(entry);
    this.logger.debug('Evicting %s from the mempool.', entry.tx.rhash);
  }

  this.untrackEntry(entry);

  if (this.fees) {
    hash = entry.tx.hash('hex');
    this.fees.removeTX(hash);
  }

  if (limit) {
    this.logger.spam('Removed tx %s from mempool.', entry.tx.rhash);
    rate = TX.getRate(entry.sizes, entry.fees);
    rate += this.minReasonable;
    if (rate > this.minRate) {
      this.minRate = rate;
      this.blockSinceBump = false;
    }
  } else {
    this.logger.spam('Removed block tx %s from mempool.', entry.tx.rhash);
  }

  this.emit('remove tx', entry.tx);
};

/**
 * Calculate and update the minimum rolling fee rate.
 * @returns {Rate} Rate.
 */

Mempool.prototype.getMinRate = function getMinRate() {
  var now, halflife, size;

  if (!this.blockSinceBump || this.minRate === 0)
    return this.minRate;

  now = utils.now();

  if (now > this.lastFeeUpdate + 10) {
    halflife = constants.mempool.FEE_HALFLIFE;
    size = this.getSize();

    if (size < this.maxSize / 4)
      halflife >>>= 2;
    else if (size < this.maxSize / 2)
      halflife >>>= 1;

    this.minRate /= Math.pow(2.0, (now - this.lastFeeUpdate) / halflife | 0);
    this.minRate |= 0;
    this.lastFeeUpdate = now;

    if (this.minRate < this.minReasonable / 2) {
      this.minRate = 0;
      return 0;
    }
  }

  return Math.max(this.minRate, this.minReasonable);
};

/**
 * Verify a transaction with mempool standards.
 * @param {TX} tx
 * @returns {Promise}
 */

Mempool.prototype.verify = co(function* verify(entry) {
  var height = this.chain.height + 1;
  var lockFlags = flags.STANDARD_LOCKTIME_FLAGS;
  var flags1 = flags.STANDARD_VERIFY_FLAGS;
  var flags2 = flags1 & ~(flags.VERIFY_WITNESS | flags.VERIFY_CLEANSTACK);
  var flags3 = flags1 & ~flags.VERIFY_CLEANSTACK;
  var mandatory = flags.MANDATORY_VERIFY_FLAGS;
  var tx = entry.tx;
  var ret = new VerifyResult();
  var fee, modFee, now, size, minRate;
  var rejectFee, minRelayFee, count, result;

  result = yield this.checkLocks(tx, lockFlags);

  if (!result) {
    throw new VerifyError(tx,
      'nonstandard',
      'non-BIP68-final',
      0);
  }

  if (this.requireStandard) {
    if (!tx.hasStandardInputs()) {
      throw new VerifyError(tx,
        'nonstandard',
        'bad-txns-nonstandard-inputs',
        0);
    }
    if (this.chain.state.hasWitness()) {
      if (!tx.hasStandardWitness(ret)) {
        ret = new VerifyError(tx,
          'nonstandard',
          ret.reason,
          ret.score);
        ret.malleated = ret.score > 0;
        throw ret;
      }
    }
  }

  if (tx.getSigopsWeight(flags) > constants.tx.MAX_SIGOPS_WEIGHT) {
    throw new VerifyError(tx,
      'nonstandard',
      'bad-txns-too-many-sigops',
      0);
  }

  fee = tx.getFee();
  modFee = entry.fees;
  size = entry.size;

  if (this.rejectFee) {
    minRate = this.getMinRate();
    rejectFee = tx.getMinFee(size, minRate);
    if (rejectFee > 0 && modFee < rejectFee) {
      throw new VerifyError(tx,
        'insufficientfee',
        'mempool min fee not met',
        0);
    }
  }

  minRelayFee = tx.getMinFee(size, this.minRelay);

  if (this.relayPriority && modFee < minRelayFee) {
    if (!entry.isFree(height)) {
      throw new VerifyError(tx,
        'insufficientfee',
        'insufficient priority',
        0);
    }
  }

  // Continuously rate-limit free (really, very-low-fee)
  // transactions. This mitigates 'penny-flooding'. i.e.
  // sending thousands of free transactions just to be
  // annoying or make others' transactions take longer
  // to confirm.
  if (this.limitFree && modFee < minRelayFee) {
    now = utils.now();

    // Use an exponentially decaying ~10-minute window:
    this.freeCount *= Math.pow(1 - 1 / 600, now - this.lastTime);
    this.lastTime = now;

    // The limitFreeRelay unit is thousand-bytes-per-minute
    // At default rate it would take over a month to fill 1GB
    if (this.freeCount > this.limitFreeRelay * 10 * 1000) {
      throw new VerifyError(tx,
        'insufficientfee',
        'rate limited free transaction',
        0);
    }

    this.freeCount += size;
  }

  if (this.rejectAbsurdFees && fee > minRelayFee * 10000)
    throw new VerifyError(tx, 'highfee', 'absurdly-high-fee', 0);

  count = this.countAncestors(tx);

  if (count > constants.mempool.ANCESTOR_LIMIT) {
    throw new VerifyError(tx,
      'nonstandard',
      'too-long-mempool-chain',
      0);
  }

  if (!tx.checkInputs(height, ret))
    throw new VerifyError(tx, 'invalid', ret.reason, ret.score);

  // Standard verification
  try {
    yield this.checkInputs(tx, flags1);
  } catch (error) {
    if (tx.hasWitness())
      throw error;

    // Try without segwit and cleanstack.
    result = yield this.checkResult(tx, flags2);

    // If it failed, the first verification
    // was the only result we needed.
    if (!result)
      throw error;

    // If it succeeded, segwit may be causing the
    // failure. Try with segwit but without cleanstack.
    result = yield this.checkResult(tx, flags3);

    // Cleanstack was causing the failure.
    if (result)
      throw error;

    // Do not insert into reject cache.
    error.malleated = true;
    throw error;
  }

  // Paranoid checks.
  if (this.paranoid) {
    result = yield this.checkResult(tx, mandatory);
    assert(result, 'BUG: Verify failed for mandatory but not standard.');
  }
});

/**
 * Verify inputs, return a boolean
 * instead of an error based on success.
 * @param {TX} tx
 * @param {VerifyFlags} flags
 * @returns {Promise}
 */

Mempool.prototype.checkResult = co(function* checkResult(tx, flags) {
  try {
    yield this.checkInputs(tx, flags);
  } catch (err) {
    if (err.type === 'VerifyError')
      return false;
    throw err;
  }
  return true;
});

/**
 * Verify inputs for standard
 * _and_ mandatory flags on failure.
 * @param {TX} tx
 * @param {VerifyFlags} flags
 * @returns {Promise}
 */

Mempool.prototype.checkInputs = co(function* checkInputs(tx, flags) {
  var result = yield tx.verifyAsync(flags);
  if (result)
    return;

  if (!(flags & constants.flags.UNSTANDARD_VERIFY_FLAGS)) {
    throw new VerifyError(tx,
      'nonstandard',
      'non-mandatory-script-verify-flag',
      0);
  }

  flags &= ~constants.flags.UNSTANDARD_VERIFY_FLAGS;

  result = yield tx.verifyAsync(flags);

  if (result) {
    throw new VerifyError(tx,
      'nonstandard',
      'non-mandatory-script-verify-flag',
      0);
  }

  throw new VerifyError(tx,
    'nonstandard',
    'mandatory-script-verify-flag',
    100);
});

/**
 * Count the highest number of
 * ancestors a transaction may have.
 * @param {TX} tx
 * @returns {Number}
 */

Mempool.prototype.countAncestors = function countAncestors(tx) {
  var max = 0;
  var i, input, count, prev;

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    prev = this.getTX(input.prevout.hash);

    if (!prev)
      continue;

    count = 1;
    count += this.countAncestors(prev);

    if (count > max)
      max = count;
  }

  return max;
};

/**
 * Count the highest number of
 * descendants a transaction may have.
 * @param {TX} tx
 * @returns {Number}
 */

Mempool.prototype.countDescendants = function countDescendants(tx) {
  var max = 0;
  var hash = tx.hash('hex');
  var i, count, next;

  for (i = 0; i < tx.outputs.length; i++) {
    next = this.isSpent(hash, i);

    if (!next)
      continue;

    count = 1;
    count += this.countDescendants(next.tx);

    if (count > max)
      max = count;
  }

  return max;
};

/**
 * Get all transaction ancestors.
 * @param {TX} tx
 * @returns {MempoolEntry[]}
 */

Mempool.prototype.getAncestors = function getAncestors(tx) {
  var self = this;
  var entries = [];

  (function traverse(tx) {
    var i, input, prev;

    for (i = 0; i < tx.inputs.length; i++) {
      input = tx.inputs[i];
      prev = self.getTX(input.prevout.hash);

      if (!prev)
        continue;

      entries.push(prev);
      traverse(prev);
    }
  })(tx);

  return entries;
};

/**
 * Get all a transaction descendants.
 * @param {TX} tx
 * @returns {MempoolEntry[]}
 */

Mempool.prototype.getDescendants = function getDescendants(tx) {
  var self = this;
  var entries = [];

  (function traverse(tx) {
    var hash = tx.hash('hex');
    var i, next;

    for (i = 0; i < tx.outputs.length; i++) {
      next = self.isSpent(hash, i);

      if (!next)
        continue;

      entries.push(next);
      traverse(next.tx);
    }
  })(tx);

  return entries;
};

/**
 * Find a unconfirmed transactions that
 * this transaction depends on.
 * @param {TX} tx
 * @returns {Hash[]}
 */

Mempool.prototype.getDepends = function getDepends(tx) {
  var prevout = tx.getPrevout();
  var depends = [];
  var i, hash;

  for (i = 0; i < prevout.length; i++) {
    hash = prevout[i].hash;
    if (this.hasTX(hash))
      depends.push(hash);
  }

  return depends;
};

/**
 * Store an orphaned transaction.
 * @param {TX} tx
 */

Mempool.prototype.storeOrphan = function storeOrphan(tx) {
  var missing = {};
  var i, hash, input, prev;

  if (tx.getWeight() > constants.tx.MAX_WEIGHT) {
    this.logger.debug('Ignoring large orphan: %s', tx.rhash);
    if (!tx.hasWitness())
      this.rejects.add(tx.hash());
    return;
  }

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];

    if (input.coin)
      continue;

    if (this.hasReject(input.prevout.hash)) {
      this.logger.debug('Not storing orphan %s (rejected parents).', tx.rhash);
      this.rejects.add(tx.hash());
      return;
    }
  }

  hash = tx.hash('hex');

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];

    if (input.coin)
      continue;

    missing[input.prevout.hash] = true;
  }

  missing = Object.keys(missing);

  assert(missing.length > 0);

  for (i = 0; i < missing.length; i++) {
    prev = missing[i];

    if (!this.waiting[prev])
      this.waiting[prev] = [];

    this.waiting[prev].push(hash);
  }

  this.orphans[hash] = tx.toExtended(true);
  this.totalOrphans++;

  this.logger.debug('Added orphan %s to mempool.', tx.rhash);

  this.emit('add orphan', tx);

  this.limitOrphans();

  return missing;
};

/**
 * Return the full balance of all unspents in the mempool
 * (not very useful in practice, only used for testing).
 * @returns {Amount}
 */

Mempool.prototype.getBalance = function getBalance() {
  var hashes = this.getSnapshot();
  var total = 0;
  var i, j, tx, hash, coin;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    tx = this.getTX(hash);

    if (!tx)
      continue;

    hash = tx.hash('hex');

    for (j = 0; j < tx.outputs.length; j++) {
      coin = this.getCoin(hash, j);
      if (coin)
        total += coin.value;
    }
  }

  return total;
};

/**
 * Retrieve _all_ transactions from the mempool.
 * @returns {TX[]}
 */

Mempool.prototype.getHistory = function getHistory() {
  var hashes = this.getSnapshot();
  var txs = [];
  var i, hash, tx;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    tx = this.getTX(hash);

    if (!tx)
      continue;

    txs.push(tx);
  }

  return txs;
};

/**
 * Retrieve an orphan transaction.
 * @param {Hash} hash
 * @returns {TX}
 */

Mempool.prototype.getOrphan = function getOrphan(hash) {
  var data = this.orphans[hash];
  var orphan;

  if (!data)
    return;

  try {
    orphan = TX.fromExtended(data, true);
  } catch (e) {
    delete this.orphans[hash];
    this.logger.warning('%s %s',
      'Warning: possible memory corruption.',
      'Orphan failed deserialization.');
    return;
  }

  return orphan;
};

/**
 * @param {Hash} hash
 * @returns {Boolean}
 */

Mempool.prototype.hasOrphan = function hasOrphan(hash) {
  return this.orphans[hash] != null;
};

/**
 * Potentially resolve any transactions
 * that redeem the passed-in transaction.
 * Deletes all orphan entries and
 * returns orphan hashes.
 * @param {TX} tx
 * @returns {Array} Resolved
 */

Mempool.prototype.resolveOrphans = function resolveOrphans(tx) {
  var hash = tx.hash('hex');
  var resolved = [];
  var hashes = this.waiting[hash];
  var i, orphanHash, orphan;

  if (!hashes)
    return resolved;

  for (i = 0; i < hashes.length; i++) {
    orphanHash = hashes[i];
    orphan = this.getOrphan(orphanHash);

    if (!orphan)
      continue;

    orphan.fillCoins(tx);

    if (orphan.hasCoins()) {
      this.totalOrphans--;
      delete this.orphans[orphanHash];
      resolved.push(orphan);
      continue;
    }

    this.orphans[orphanHash] = orphan.toExtended(true);
  }

  delete this.waiting[hash];

  return resolved;
};

/**
 * Remove a transaction from the mempool.
 * @param {TX|Hash} tx
 */

Mempool.prototype.removeOrphan = function removeOrphan(tx) {
  var i, j, hashes, prevout, prev, hash;

  if (typeof tx === 'string')
    tx = this.getOrphan(tx);

  if (!tx)
    return;

  hash = tx.hash('hex');
  prevout = tx.getPrevout();

  for (i = 0; i < prevout.length; i++) {
    prev = prevout[i];
    hashes = this.waiting[prev];

    if (!hashes)
      continue;

    j = hashes.indexOf(hash);
    if (j !== -1)
      hashes.splice(j, 1);

    if (hashes.length === 0) {
      delete this.waiting[prev];
      continue;
    }

    this.waiting[prev] = hashes;
  }

  delete this.orphans[hash];

  this.emit('remove orphan', tx);

  this.totalOrphans--;
};

/**
 * Fill transaction with all unspent _and spent_
 * coins. Similar to {@link Mempool#fillHistory}
 * except that it will also fill with coins
 * from the blockchain as well.
 * @param {TX} tx
 * @returns {Promise} - Returns {@link TX}.
 */

Mempool.prototype.fillAllHistory = function fillAllHistory(tx) {
  this.fillHistory(tx);

  if (tx.hasCoins())
    return Promise.resolve(tx);

  return this.chain.db.fillCoins(tx);
};

/**
 * Fill transaction with all unspent
 * coins. Similar to {@link Mempool#fillCoins}
 * except that it will also fill with coins
 * from the blockchain as well.
 * @param {TX} tx
 * @returns {Promise} - Returns {@link TX}.
 */

Mempool.prototype.fillAllCoins = co(function* fillAllCoins(tx) {
  var i, input, hash, index, coin;

  this.fillCoins(tx);

  if (tx.hasCoins())
    return tx;

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    hash = input.prevout.hash;
    index = input.prevout.index;

    if (this.isSpent(hash, index))
      continue;

    coin = yield this.chain.db.getCoin(hash, index);

    if (coin)
      input.coin = coin;
  }

  return tx;
});

/**
 * Get a snapshot of all transaction hashes in the mempool. Used
 * for generating INV packets in response to MEMPOOL packets.
 * @returns {Hash[]}
 */

Mempool.prototype.getSnapshot = function getSnapshot() {
  return Object.keys(this.tx);
};

/**
 * Check sequence locks on a transaction against the current tip.
 * @param {TX} tx
 * @param {LockFlags} flags
 * @returns {Promise} - Returns Boolean.
 */

Mempool.prototype.checkLocks = function checkLocks(tx, flags) {
  return this.chain.checkLocks(this.chain.tip, tx, flags);
};

/**
 * Test all of a transactions outpoints to see if they are doublespends.
 * Note that this will only test against the mempool spents, not the
 * blockchain's. The blockchain spents are not checked against because
 * the blockchain does not maintain a spent list. The transaction will
 * be seen as an orphan rather than a double spend.
 * @param {TX} tx
 * @returns {Promise} - Returns Boolean.
 */

Mempool.prototype.isDoubleSpend = function isDoubleSpend(tx) {
  var i, input, prevout;

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    prevout = input.prevout;
    if (this.isSpent(prevout.hash, prevout.index))
      return true;
  }

  return false;
};

/**
 * Calculate bitcoinj-style confidence.
 * @see http://bit.ly/1OVQwlO
 * @param {TX|Hash} hash
 * @returns {Promise} - Returns Number.
 */

Mempool.prototype.getConfidence = co(function* getConfidence(hash) {
  var tx, result;

  if (hash instanceof TX) {
    tx = hash;
    hash = hash.hash('hex');
  } else {
    tx = this.getTX(hash);
  }

  if (this.hasTX(hash))
    return constants.confidence.PENDING;

  if (tx && this.isDoubleSpend(tx))
    return constants.confidence.INCONFLICT;

  if (tx && tx.block) {
    result = yield this.chain.db.isMainChain(tx.block);
    if (result)
      return constants.confidence.BUILDING;
    return constants.confidence.DEAD;
  }

  result = yield this.chain.db.hasCoins(hash);

  if (result)
    return constants.confidence.BUILDING;

  return constants.confidence.UNKNOWN;
});

/**
 * Map a transaction to the mempool.
 * @private
 * @param {MempoolEntry} entry
 */

Mempool.prototype.trackEntry = function trackEntry(entry) {
  var tx = entry.tx;
  var hash = tx.hash('hex');
  var i, input, output, key, coin;

  this.tx[hash] = entry;

  if (this.options.indexAddress)
    this.indexTX.addTX(tx);

  assert(!tx.isCoinbase());

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    key = input.prevout.hash + input.prevout.index;

    assert(input.coin);

    if (this.options.indexAddress)
      this.coinIndex.removeCoin(input.prevout);

    this.spents[key] = entry;
  }

  if (this.options.indexAddress) {
    for (i = 0; i < tx.outputs.length; i++) {
      output = tx.outputs[i];

      if (output.script.isUnspendable())
        continue;

      coin = Coin.fromTX(tx, i);
      this.coinIndex.addCoin(coin);
    }
  }

  this.size += this.memUsage(tx);
  this.totalTX++;
};

/**
 * Unmap a transaction from the mempool.
 * @private
 * @param {MempoolEntry} entry
 */

Mempool.prototype.untrackEntry = function untrackEntry(entry) {
  var tx = entry.tx;
  var hash = tx.hash('hex');
  var i, input, output, key, coin;

  delete this.tx[hash];

  if (this.options.indexAddress)
    this.txIndex.removeTX(tx);

  assert(!tx.isCoinbase());

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    key = input.prevout.hash + input.prevout.index;

    if (this.options.indexAddress)
      this.coinIndex.removeCoin(input.prevout);

    delete this.spents[key];
  }

  if (this.options.indexAddress) {
    for (i = 0; i < tx.outputs.length; i++) {
      output = tx.outputs[i];

      if (output.script.isUnspendable())
        continue;

      coin = Coin.fromTX(tx, i);
      this.coinIndex.removeCoin(coin);
    }
  }

  this.size -= this.memUsage(tx);
  this.totalTX--;
};

/**
 * Recursively remove spenders of a transaction.
 * @private
 * @param {MempoolEntry} entry
 * @param {Boolean} limit
 * @returns {Promise}
 */

Mempool.prototype.removeSpenders = function removeSpenders(entry) {
  var tx = entry.tx;
  var hash = tx.hash('hex');
  var i, spender;

  for (i = 0; i < tx.outputs.length; i++) {
    spender = this.isSpent(hash, i);

    if (!spender)
      continue;

    this.removeUnchecked(spender, true);
  }
};

/**
 * Calculate the memory usage of a transaction.
 * Note that this only calculates the JS heap
 * size. Sizes of buffers are ignored (the v8
 * heap is what we care most about). All numbers
 * are based on the output of v8 heap snapshots
 * of TX objects.
 * @param {TX} tx
 * @returns {Number} Usage in bytes.
 */

Mempool.prototype.memUsage = function memUsage(tx) {
  var mem = 0;
  var i, j, input, output, coin, op;

  mem += 264; // tx
  mem += 80; // _hash
  mem += 80; // _raw

  mem += 32; // input array

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];

    mem += 144; // input

    if (input.coin) {
      coin = input.coin;
      mem += 144; // coin
      mem += 88; // coin hash
      mem += 40; // script
      mem += 80; // script raw buffer
      mem += 32; // script code array
      mem += coin.script.code.length * 40; // opcodes

      for (j = 0; j < coin.script.code.length; j++) {
        op = coin.script.code[j];
        if (op.data)
          mem += 80; // op buffers
      }
    }

    mem += 104; // prevout
    mem += 88; // prevout hash

    mem += 40; // script
    mem += 80; // script raw buffer
    mem += 32; // script code array
    mem += input.script.code.length * 40; // opcodes

    for (j = 0; j < input.script.code.length; j++) {
      op = input.script.code[j];
      if (op.data)
        mem += 80; // op buffers
    }

    mem += 96; // witness
    mem += 32; // witness items
    mem += input.witness.items.length * 80; // witness buffers
  }

  mem += 32; // output array

  for (i = 0; i < tx.outputs.length; i++) {
    output = tx.outputs[i];

    mem += 120; // output
    mem += 40; // script
    mem += 80; // script raw buffer
    mem += 32; // script code array
    mem += output.script.code.length * 40; // opcodes

    for (j = 0; j < output.script.code.length; j++) {
      op = output.script.code[j];
      if (op.data)
        mem += 80; // op buffers
    }
  }

  mem += 176; // mempool entry

  return mem;
};

/**
 * Calculate the memory usage of the entire mempool.
 * @see DynamicMemoryUsage()
 * @returns {Number} Usage in bytes.
 */

Mempool.prototype.getSize = function getSize() {
  return this.size;
};

/**
 * Address Index
 */

function AddressIndex(mempool) {
  this.mempool = mempool;
  this.map = {};
}

AddressIndex.prototype.getCoins = function getCoins(address) {
  var items = this.map[address];
  var out = [];
  var i, item, outpoint, coin;

  if (!items)
    return out;

  for (i = 0; i < items.length; i++) {
    item = items[i];
    outpoint = Outpoint.fromRaw(item);
    coin = this.mempool.getCoin(outpoint.hash, outpoint.index);
    assert(coin);
    out.push(coin);
  }

  return out;
};

AddressIndex.prototype.getTX = function getTX(address) {
  var items = this.map[address];
  var out = [];
  var i, hash, tx;

  if (!items)
    return out;

  for (i = 0; i < items.length; i++) {
    hash = items[i].toString('hex');
    tx = this.mempool.getTX(hash);
    assert(tx);
    out.push(tx);
  }

  return out;
};

AddressIndex.prototype.addTX = function addTX(tx) {
  var key = tx.hash('hex');
  var hashes = tx.getHashes('hex');
  var i, hash, items;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    items = this.map[hash];

    if (!items) {
      items = [];
      this.map[hash] = items;
    }

    utils.binaryInsert(items, tx.hash(), utils.cmp);
  }

  this.map[key] = hashes;
};

AddressIndex.prototype.removeTX = function removeTX(tx) {
  var key = tx.hash('hex');
  var hashes = this.map[key];
  var i, hash, items;

  if (!hashes)
    return;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    items = this.map[hash];

    if (!items)
      continue;

    utils.binaryRemove(items, tx.hash(), utils.cmp);

    if (items.length === 0)
      delete this.map[hash];
  }

  delete this.map[key];
};

AddressIndex.prototype.addCoin = function addCoin(coin) {
  var key = coin.hash + coin.index;
  var hash = coin.getHash('hex');
  var outpoint, items;

  if (!hash)
    return;

  items = this.map[hash];

  if (!items) {
    items = [];
    this.map[hash] = items;
  }

  outpoint = Outpoint(coin.hash, coin.index).toRaw();
  utils.binaryInsert(items, outpoint, utils.cmp);

  this.map[key] = hash;
};

AddressIndex.prototype.removeCoin = function removeCoin(coin) {
  var key = coin.hash + coin.index;
  var hash = this.map[key];
  var outpoint, items;

  if (!hash)
    return;

  items = this.map[hash];

  if (!items)
    return;

  outpoint = Outpoint(coin.hash, coin.index).toRaw();
  utils.binaryRemove(items, outpoint, utils.cmp);

  if (items.length === 0)
    delete this.map[hash];

  delete this.map[key];
};

/*
 * Expose
 */

module.exports = Mempool;
