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

'use strict';

module.exports = TX;

var utils = require('../utils/utils');
var crypto = require('../crypto/crypto');
var assert = require('assert');
var constants = require('../protocol/constants');
var Network = require('../protocol/network');
var Script = require('../script/script');
var Stack = require('../script/stack');
var BufferWriter = require('../utils/writer');
var VerifyResult = utils.VerifyResult;
var Input = require('./input');
var Output = require('./output');
var Outpoint = require('./outpoint');
var Coin = require('./coin');
var InvItem = require('./invitem');
var workers = require('../workers/workers');
var BufferWriter = require('../utils/writer');
var BufferReader = require('../utils/reader');

/*
 * Constants
 */

var BAD_OKAY = 0;
var BAD_WITNESS = 1;
var BAD_P2SH = 2;
var BAD_NONSTD_P2WSH = 3;

/**
 * A static transaction object.
 * @exports TX
 * @constructor
 * @param {NakedTX} options - Transaction fields.
 * @property {String} type - "tx" (inv type).
 * @property {Number} version - Transaction version. Note that BCoin reads
 * versions as unsigned even though they are signed at the protocol level.
 * This value will never be negative.
 * @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 {Number} ts - Timestamp of the block the transaction
 * was included in (unix time).
 * @property {Hash|null} block - Hash of the block the transaction
 * was included in.
 * @property {Number} index - Transaction's index in the block tx vector.
 * @property {Number} ps - "Pending Since": The time at which the transaction
 * was first seen. Only non-zero on unconfirmed transactions.
 * @property {Number} height - Height of the block the
 * transaction was included in (-1 if unconfirmed).
 * @property {ReversedHash|null} rblock - Reversed block hash (uint256le).
 * @property {ReversedHash} rhash - Reversed transaction hash (uint256le).
 * @property {ReversedHash} rwhash - Reversed witness
 * transaction hash (uint256le).
 * @property {String} txid - Transaction ID.
 * @property {String} wtxid - Witness transaction ID (Same as txid if no
 * witness is present. All zeroes if coinbase).
 */

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

  this.version = 1;
  this.flag = 1;
  this.inputs = [];
  this.outputs = [];
  this.locktime = 0;

  this.ts = 0;
  this.block = null;
  this.index = -1;
  this.ps = utils.now();
  this.height = -1;
  this.mutable = false;

  this._hash = null;
  this._whash = null;

  this._raw = null;
  this._size = -1;
  this._witnessSize = -1;
  this._lastWitnessSize = 0;

  this._outputValue = -1;
  this._inputValue = -1;
  this._hashPrevouts = null;
  this._hashSequence = null;
  this._hashOutputs = null;

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

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

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

  assert(options, 'TX data is required.');

  if (options.version != null) {
    assert(utils.isNumber(options.version));
    this.version = options.version;
  }

  if (options.flag != null) {
    assert(utils.isNumber(options.flag));
    this.flag = options.flag;
  }

  if (options.inputs) {
    assert(Array.isArray(options.inputs));
    for (i = 0; i < options.inputs.length; i++)
      this.inputs.push(new Input(options.inputs[i]));
  }

  if (options.outputs) {
    assert(Array.isArray(options.outputs));
    for (i = 0; i < options.outputs.length; i++)
      this.outputs.push(new Output(options.outputs[i]));
  }

  if (options.locktime != null) {
    assert(utils.isNumber(options.locktime));
    this.locktime = options.locktime;
  }

  if (options.ts != null)
    assert(utils.isNumber(options.locktime));
    this.ts = options.ts;

  if (options.block !== undefined) {
    assert(options.block === null || typeof options.block === 'string');
    this.block = options.block;
  }

  if (options.index != null) {
    assert(utils.isNumber(options.index));
    this.index = options.index;
  }

  if (options.ps != null) {
    assert(utils.isNumber(options.ps));
    this.ps = options.ps;
  }

  if (options.height != null) {
    assert(utils.isNumber(options.height));
    this.height = options.height;
  }

  return this;
};

/**
 * Instantiate TX from options object.
 * @param {NakedTX} options
 * @returns {TX}
 */

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

/**
 * Clone the transaction.
 * @returns {TX}
 */

TX.prototype.clone = function clone() {
  return new TX(this);
};

/**
 * Set the block the transaction was included in.
 * @param {Block|MerkleBlock} block
 * @param {Number} index
 */

TX.prototype.setBlock = function setBlock(block, index) {
  this.ts = block.ts;
  this.block = block.hash('hex');
  this.height = block.height;
  this.index = index == null ? -1 : index;
};

/**
 * Remove all relevant block data from the transaction.
 */

TX.prototype.unsetBlock = function unsetBlock() {
  this.ts = 0;
  this.block = null;
  this.height = -1;
  this.index = -1;
};

/**
 * Hash the transaction with the non-witness serialization.
 * @param {String?} enc - Can be `'hex'` or `null`.
 * @returns {Hash|Buffer} hash
 */

TX.prototype.hash = function _hash(enc) {
  var hash = this._hash;

  if (!hash) {
    hash = crypto.hash256(this.toNormal());
    if (!this.mutable)
      this._hash = hash;
  }

  return enc === 'hex' ? hash.toString('hex') : hash;
};

/**
 * Hash the transaction with the witness
 * serialization, return the wtxid (normal
 * hash if no witness is present, all zeroes
 * if coinbase).
 * @param {String?} enc - Can be `'hex'` or `null`.
 * @returns {Hash|Buffer} hash
 */

TX.prototype.witnessHash = function witnessHash(enc) {
  var hash = this._whash;

  if (this.isCoinbase()) {
    return enc === 'hex'
      ? constants.NULL_HASH
      : utils.copy(constants.ZERO_HASH);
  }

  if (!this.hasWitness())
    return this.hash(enc);

  if (!hash) {
    hash = crypto.hash256(this.toWitness());
    if (!this.mutable)
      this._whash = hash;
  }

  return enc === 'hex' ? hash.toString('hex') : hash;
};

/**
 * Serialize the transaction. Note
 * that this is cached. This will use
 * the witness serialization if a
 * witness is present.
 * @returns {Buffer} Serialized transaction.
 */

TX.prototype.toRaw = function toRaw(writer) {
  var raw = this.getRaw();
  if (writer) {
    writer.writeBytes(raw);
    return writer;
  }
  return raw;
};

/**
 * Serialize the transaction without the
 * witness vector, regardless of whether it
 * is a witness transaction or not.
 * @returns {Buffer} Serialized transaction.
 */

TX.prototype.toNormal = function toNormal(writer) {
  var raw = this.getRaw();
  if (!TX.isWitness(raw)) {
    if (writer) {
      writer.writeBytes(raw);
      return writer;
    }
    return raw;
  }
  return this.frameNormal(writer);
};

/**
 * Serialize the transaction with the
 * witness vector. Will use normal
 * serialization if witness vector is empty.
 * @returns {Buffer} Serialized transaction.
 */

TX.prototype.toWitness = function toWitness(writer) {
  return this.toRaw(writer);
};

/**
 * Serialize the transaction. Note
 * that this is cached. This will use
 * the witness serialization if a
 * witness is present.
 * @returns {Buffer} Serialized transaction.
 */

TX.prototype.getRaw = function getRaw() {
  var raw;

  if (this._raw) {
    assert(this._size > 0);
    assert(this._witnessSize >= 0);
    this._lastWitnessSize = this._witnessSize;
    return this._raw;
  }

  if (this.hasWitness())
    raw = this.frameWitness();
  else
    raw = this.frameNormal();

  if (!this.mutable) {
    this._raw = raw;
    this._size = raw.length;
    this._witnessSize = this._lastWitnessSize;
  }

  return raw;
};

/**
 * Calculate real size and size of the witness bytes.
 * @returns {Object} Contains `size` and `witnessSize`.
 */

TX.prototype.getSizes = function getSizes() {
  var writer;

  if (this.mutable) {
    assert(!this._raw);
    writer = new BufferWriter();
    this.toRaw(writer);
    return {
      size: writer.written,
      witnessSize: this._lastWitnessSize
    };
  }

  this.getRaw();

  return {
    size: this._size,
    witnessSize: this._witnessSize
  };
};

/**
 * Calculate the virtual size of the transaction.
 * Note that this is cached.
 * @returns {Number} vsize
 */

TX.prototype.getVirtualSize = function getVirtualSize() {
  var scale = constants.WITNESS_SCALE_FACTOR;
  return (this.getWeight() + scale - 1) / scale | 0;
};

/**
 * Calculate the weight of the transaction.
 * Note that this is cached.
 * @returns {Number} weight
 */

TX.prototype.getWeight = function getWeight() {
  var sizes = this.getSizes();
  var base = sizes.size - sizes.witnessSize;
  return base * (constants.WITNESS_SCALE_FACTOR - 1) + sizes.size;
};

/**
 * Calculate the real size of the transaction
 * with the witness included.
 * @returns {Number} size
 */

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

/**
 * Calculate the size of the transaction
 * without the witness.
 * with the witness included.
 * @returns {Number} size
 */

TX.prototype.getBaseSize = function getBaseSize() {
  var sizes = this.getSizes();
  return sizes.size - sizes.witnessSize;
};

/**
 * Test whether the transaction has a non-empty witness.
 * @returns {Boolean}
 */

TX.prototype.hasWitness = function hasWitness() {
  var i;

  if (this._witnessSize !== -1)
    return this._witnessSize !== 0;

  for (i = 0; i < this.inputs.length; i++) {
    if (this.inputs[i].witness.items.length > 0)
      return true;
  }

  return false;
};

/**
 * Get the signature hash of the transaction for signing verifying.
 * @param {Number} index - Index of input being signed/verified.
 * @param {Script} prev - Previous output script or redeem script
 * (in the case of witnesspubkeyhash, this should be the generated
 * p2pkh script).
 * @param {SighashType} type - Sighash type.
 * @param {Number} version - Sighash version (0=legacy, 1=segwit).
 * @returns {Buffer} Signature hash.
 */

TX.prototype.signatureHash = function signatureHash(index, prev, type, version) {
  if (typeof index !== 'number')
    index = this.inputs.indexOf(index);

  if (typeof type === 'string')
    type = constants.hashType[type.toUpperCase()];

  assert(index >= 0 && index < this.inputs.length);
  assert(prev instanceof Script);

  // Traditional sighashing
  if (version === 0)
    return this.signatureHashV0(index, prev, type);

  // Segwit sighashing
  if (version === 1)
    return this.signatureHashV1(index, prev, type);

  assert(false, 'Unknown sighash version.');
};

/**
 * Legacy sighashing -- O(n^2).
 * @private
 * @param {Number} index
 * @param {Script} prev
 * @param {SighashType} type
 * @returns {Buffer}
 */

TX.prototype.signatureHashV0 = function signatureHashV0(index, prev, type) {
  var i, input, output;
  var p = new BufferWriter();
  var hashType = type & 0x1f;

  if (hashType === constants.hashType.SINGLE) {
    // Bitcoind used to return 1 as an error code:
    // it ended up being treated like a hash.
    if (index >= this.outputs.length)
      return utils.copy(constants.ONE_HASH);
  }

  // Remove all code separators.
  prev = prev.removeSeparators();

  p.writeU32(this.version);

  if (type & constants.hashType.ANYONECANPAY) {
    p.writeVarint(1);

    // Serialize only the current
    // input if ANYONECANPAY.
    input = this.inputs[index];

    // Outpoint.
    p.writeHash(input.prevout.hash);
    p.writeU32(input.prevout.index);

    // Replace script with previous
    // output script if current index.
    p.writeVarBytes(prev.toRaw());
    p.writeU32(input.sequence);
  } else {
    p.writeVarint(this.inputs.length);
    for (i = 0; i < this.inputs.length; i++) {
      input = this.inputs[i];

      // Outpoint.
      p.writeHash(input.prevout.hash);
      p.writeU32(input.prevout.index);

      // Replace script with previous
      // output script if current index.
      if (i === index) {
        p.writeVarBytes(prev.toRaw());
        p.writeU32(input.sequence);
        continue;
      }

      // Script is null.
      p.writeVarint(0);

      // Sequences are 0 if NONE or SINGLE.
      if (hashType === constants.hashType.NONE
          || hashType === constants.hashType.SINGLE) {
        p.writeU32(0);
      } else {
        p.writeU32(input.sequence);
      }
    }
  }

  if (hashType === constants.hashType.NONE) {
    // No outputs if NONE.
    p.writeVarint(0);
  } else if (hashType === constants.hashType.SINGLE) {
    // Drop all outputs after the
    // current input index if SINGLE.
    p.writeVarint(index + 1);

    for (i = 0; i < index + 1; i++) {
      output = this.outputs[i];

      // Regular serialization if
      // at current input index.
      if (i === index) {
        p.write64(output.value);
        p.writeVarBytes(output.script.toRaw());
        continue;
      }

      // Null all outputs not at
      // current input index.
      p.write64(-1);
      p.writeVarint(0);
    }
  } else {
    // Regular output serialization if ALL.
    p.writeVarint(this.outputs.length);

    for (i = 0; i < this.outputs.length; i++) {
      output = this.outputs[i];
      p.write64(output.value);
      p.writeVarBytes(output.script.toRaw());
    }
  }

  p.writeU32(this.locktime);

  // Append the hash type.
  p.writeU32(type);

  return crypto.hash256(p.render());
};

/**
 * Witness sighashing -- O(n).
 * @private
 * @param {Number} index
 * @param {Script} prev
 * @param {SighashType} type
 * @returns {Buffer}
 */

TX.prototype.signatureHashV1 = function signatureHashV1(index, prev, type) {
  var p = new BufferWriter();
  var i, hashPrevouts, hashSequence, hashOutputs;

  if (!(type & constants.hashType.ANYONECANPAY)) {
    if (this._hashPrevouts) {
      hashPrevouts = this._hashPrevouts;
    } else {
      hashPrevouts = new BufferWriter();
      for (i = 0; i < this.inputs.length; i++)
        this.inputs[i].prevout.toRaw(hashPrevouts);
      hashPrevouts = crypto.hash256(hashPrevouts.render());
      if (!this.mutable)
        this._hashPrevouts = hashPrevouts;
    }
  } else {
    hashPrevouts = utils.copy(constants.ZERO_HASH);
  }

  if (!(type & constants.hashType.ANYONECANPAY)
      && (type & 0x1f) !== constants.hashType.SINGLE
      && (type & 0x1f) !== constants.hashType.NONE) {
    if (this._hashSequence) {
      hashSequence = this._hashSequence;
    } else {
      hashSequence = new BufferWriter();
      for (i = 0; i < this.inputs.length; i++)
        hashSequence.writeU32(this.inputs[i].sequence);
      hashSequence = crypto.hash256(hashSequence.render());
      if (!this.mutable)
        this._hashSequence = hashSequence;
    }
  } else {
    hashSequence = utils.copy(constants.ZERO_HASH);
  }

  if ((type & 0x1f) !== constants.hashType.SINGLE
      && (type & 0x1f) !== constants.hashType.NONE) {
    if (this._hashOutputs) {
      hashOutputs = this._hashOutputs;
    } else {
      hashOutputs = new BufferWriter();
      for (i = 0; i < this.outputs.length; i++)
        this.outputs[i].toRaw(hashOutputs);
      hashOutputs = crypto.hash256(hashOutputs.render());
      if (!this.mutable)
        this._hashOutputs = hashOutputs;
    }
  } else if ((type & 0x1f) === constants.hashType.SINGLE && index < this.outputs.length) {
    hashOutputs = this.outputs[index].toRaw();
    hashOutputs = crypto.hash256(hashOutputs);
  } else {
    hashOutputs = utils.copy(constants.ZERO_HASH);
  }

  p.writeU32(this.version);
  p.writeBytes(hashPrevouts);
  p.writeBytes(hashSequence);
  p.writeHash(this.inputs[index].prevout.hash);
  p.writeU32(this.inputs[index].prevout.index);
  p.writeVarBytes(prev.toRaw());
  p.write64(this.inputs[index].coin.value);
  p.writeU32(this.inputs[index].sequence);
  p.writeBytes(hashOutputs);
  p.writeU32(this.locktime);
  p.writeU32(type);

  return crypto.hash256(p.render());
};

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

TX.prototype.verify = function verify(flags) {
  var i;

  if (this.inputs.length === 0)
    return false;

  if (this.isCoinbase())
    return true;

  for (i = 0; i < this.inputs.length; i++) {
    if (!this.verifyInput(i, flags))
      return false;
  }

  return true;
};

/**
 * Verify a transaction input.
 * @param {Number} index - Index of output being
 * verified.
 * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
 * @returns {Boolean} Whether the input is valid.
 */

TX.prototype.verifyInput = function verifyInput(index, flags) {
  var input;

  if (typeof index === 'object')
    index = this.inputs.indexOf(index);

  input = this.inputs[index];

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

  if (!input.coin)
    return false;

  try {
    Script.verify(
      input.script,
      input.witness,
      input.coin.script,
      this,
      index,
      flags
    );
  } catch (e) {
    if (e.type === 'ScriptError')
      return false;
    throw e;
  }

  return true;
};

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

TX.prototype.verifyAsync = function verifyAsync(flags) {
  if (this.inputs.length === 0)
    return Promise.resolve(false);

  if (this.isCoinbase())
    return Promise.resolve(true);

  return workers.pool.verify(this, flags);
};

/**
 * Verify a transaction input asynchronously.
 * @param {Number} index - Index of output being
 * verified.
 * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS]
 * @returns {Boolean} Whether the input is valid.
 */

TX.prototype.verifyInputAsync = function verifyInputAsync(index, flags) {
  var input;

  if (typeof index === 'object')
    index = this.inputs.indexOf(index);

  input = this.inputs[index];

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

  return workers.pool.verifyInput(this, index, flags);
};

/**
 * Test whether the transaction is a coinbase
 * by examining the inputs.
 * @returns {Boolean}
 */

TX.prototype.isCoinbase = function isCoinbase() {
  return this.inputs.length === 1 && this.inputs[0].prevout.isNull();
};

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

TX.prototype.getFee = function getFee() {
  if (!this.hasCoins())
    return 0;

  return this.getInputValue() - this.getOutputValue();
};

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

TX.prototype.getInputValue = function getInputValue() {
  var total = 0;
  var i;

  if (this._inputValue !== -1)
    return this._inputValue;

  if (!this.hasCoins())
    return total;

  for (i = 0; i < this.inputs.length; i++)
    total += this.inputs[i].coin.value;

  if (!this.mutable)
    this._inputValue = total;

  return total;
};

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

TX.prototype.getOutputValue = function getOutputValue() {
  var total = 0;
  var i;

  if (this._outputValue !== -1)
    return this._outputValue;

  for (i = 0; i < this.outputs.length; i++)
    total += this.outputs[i].value;

  if (!this.mutable)
    this._outputValue = total;

  return total;
};

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

TX.prototype.getInputAddresses = function getInputAddresses() {
  var table = {};
  var addresses = [];
  var i, address, hash;

  for (i = 0; i < this.inputs.length; i++) {
    address = this.inputs[i].getAddress();
    if (address) {
      hash = address.getHash('hex');
      if (!table[hash]) {
        table[hash] = true;
        addresses.push(address);
      }
    }
  }

  addresses.table = table;

  return addresses;
};

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

TX.prototype.getOutputAddresses = function getOutputAddresses() {
  var table = {};
  var addresses = [];
  var i, address, hash;

  for (i = 0; i < this.outputs.length; i++) {
    address = this.outputs[i].getAddress();
    if (address) {
      hash = address.getHash('hex');
      if (!table[hash]) {
        table[hash] = true;
        addresses.push(address);
      }
    }
  }

  addresses.table = table;

  return addresses;
};

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

TX.prototype.getAddresses = function getAddresses() {
  var input = this.getInputAddresses();
  var output = this.getOutputAddresses();
  var i, hash;

  for (i = 0; i < output.length; i++) {
    hash = output[i].getHash('hex');
    if (!input.table[hash]) {
      input.table[hash] = true;
      input.push(output[i]);
    }
  }

  return input;
};

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

TX.prototype.getInputHashes = function getInputHashes(enc) {
  var input = this.getInputAddresses();
  var i;

  if (enc === 'hex')
    return Object.keys(input.table);

  for (i = 0; i < input.length; i++)
    input[i] = input[i].getHash();

  return input;
};

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

TX.prototype.getOutputHashes = function getOutputHashes(enc) {
  var output = this.getOutputAddresses();
  var i;

  if (enc === 'hex')
    return Object.keys(output.table);

  for (i = 0; i < output.length; i++)
    output[i] = output[i].getHash();

  return output;
};

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

TX.prototype.getHashes = function getHashes(enc) {
  var hashes = this.getAddresses();
  var i;

  if (enc === 'hex')
    return Object.keys(hashes.table);

  for (i = 0; i < hashes.length; i++)
    hashes[i] = hashes[i].getHash();

  return hashes;
};

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

TX.prototype.hasCoins = function hasCoins() {
  var i;

  if (this.inputs.length === 0)
    return false;

  for (i = 0; i < this.inputs.length; i++) {
    if (!this.inputs[i].coin)
      return false;
  }

  return true;
};

/**
 * Attempt to connect coins to prevouts.
 * @param {Coin|TX|Coin[]|TX[]} coins
 * @returns {Boolean} Whether the transaction is now completely filled.
 */

TX.prototype.fillCoins = function fillCoins(coins) {
  var result = true;
  var i, input, hash, index, map, coin;

  if ((coins instanceof Coin)
      || (coins instanceof TX)) {
    coins = [coins];
  }

  if (Array.isArray(coins)) {
    map = {};
    for (i = 0; i < coins.length; i++) {
      coin = coins[i];
      if (coin instanceof TX) {
        map[coin.hash('hex')] = coin;
      } else if (coin instanceof Coin) {
        assert(typeof coin.hash === 'string');
        assert(typeof coin.index === 'number');
        map[coin.hash + coin.index] = coin;
      } else {
        assert(false, 'Non-coin object passed to fillCoins.');
      }
    }
    coins = map;
  }

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

    if (input.coin)
      continue;

    coin = coins[hash];

    if (coin) {
      input.coin = Coin.fromTX(coin, index);
      continue;
    }

    coin = coins[hash + index];

    if (coin) {
      input.coin = coin;
      continue;
    }

    result = false;
  }

  return result;
};

/**
 * Check finality of transaction by examining nLockTime and nSequences.
 * @example
 * tx.isFinal(network.height + 1, utils.now());
 * @param {Number} height - Height at which to test. This
 * is usually the chain height, or the chain height + 1
 * when the transaction entered the mempool.
 * @param {Number} ts - Time at which to test. This is
 * usually the chain tip's parent's median time, or the
 * time at which the transaction entered the mempool. If
 * MEDIAN_TIME_PAST is enabled this will be the median
 * time of the chain tip's previous entry's median time.
 * @returns {Boolean}
 */

TX.prototype.isFinal = function isFinal(height, ts) {
  var threshold = constants.LOCKTIME_THRESHOLD;
  var i;

  if (this.locktime === 0)
    return true;

  if (this.locktime < (this.locktime < threshold ? height : ts))
    return true;

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

  return true;
};

/**
 * Calculate legacy (inaccurate) sigop count.
 * @returns {Number} sigop count
 */

TX.prototype.getLegacySigops = function getLegacySigops() {
  var total = 0;
  var i;

  for (i = 0; i < this.inputs.length; i++)
    total += this.inputs[i].script.getSigops(false);

  for (i = 0; i < this.outputs.length; i++)
    total += this.outputs[i].script.getSigops(false);

  return total;
};

/**
 * Calculate accurate sigop count, taking into account redeem scripts.
 * @returns {Number} sigop count
 */

TX.prototype.getScripthashSigops = function getScripthashSigops() {
  var total = 0;
  var i, input;

  if (this.isCoinbase())
    return 0;

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

    if (!input.coin)
      continue;

    if (input.coin.script.isScripthash())
      total += input.coin.script.getScripthashSigops(input.script);
  }

  return total;
};

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

TX.prototype.getSigopsWeight = function getSigopsWeight(flags) {
  var weight = this.getLegacySigops() * constants.WITNESS_SCALE_FACTOR;
  var input, i;

  if (flags == null)
    flags = constants.flags.STANDARD_VERIFY_FLAGS;

  if (this.isCoinbase())
    return weight;

  if (flags & constants.flags.VERIFY_P2SH)
    weight += this.getScripthashSigops() * constants.WITNESS_SCALE_FACTOR;

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

    if (!input.coin)
      continue;

    weight += Script.getWitnessSigops(
      input.script,
      input.coin.script,
      input.witness,
      flags);
  }

  return weight;
};

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

TX.prototype.getSigops = function getSigops(flags) {
  var scale = constants.WITNESS_SCALE_FACTOR;

  if (flags == null)
    flags = constants.flags.STANDARD_VERIFY_FLAGS;

  return (this.getSigopsWeight(flags) + scale - 1) / scale | 0;
};

/**
 * Non-contextual sanity checks for the transaction.
 * Will mostly verify coin and output values.
 * @see CheckTransaction()
 * @param {Object?} ret - Return object, may be
 * set with properties `reason` and `score`.
 * @returns {Boolean} sane
 */

TX.prototype.isSane = function isSane(ret) {
  var prevout = {};
  var total = 0;
  var i, input, output, size, key;

  if (!ret)
    ret = new VerifyResult();

  if (this.inputs.length === 0) {
    ret.reason = 'bad-txns-vin-empty';
    ret.score = 100;
    return false;
  }

  if (this.outputs.length === 0) {
    ret.reason = 'bad-txns-vout-empty';
    ret.score = 100;
    return false;
  }

  if (this.getBaseSize() > constants.block.MAX_SIZE) {
    ret.reason = 'bad-txns-oversize';
    ret.score = 100;
    return false;
  }

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

    if (output.value < 0) {
      ret.reason = 'bad-txns-vout-negative';
      ret.score = 100;
      return false;
    }

    if (output.value > constants.MAX_MONEY) {
      ret.reason = 'bad-txns-vout-toolarge';
      ret.score = 100;
      return false;
    }

    total += output.value;

    if (total < 0 || total > constants.MAX_MONEY) {
      ret.reason = 'bad-txns-txouttotal-toolarge';
      ret.score = 100;
      return false;
    }
  }

  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    key = input.prevout.hash + input.prevout.index;
    if (prevout[key]) {
      ret.reason = 'bad-txns-inputs-duplicate';
      ret.score = 100;
      return false;
    }
    prevout[key] = true;
  }

  if (this.isCoinbase()) {
    size = this.inputs[0].script.getSize();
    if (size < 2 || size > 100) {
      ret.reason = 'bad-cb-length';
      ret.score = 100;
      return false;
    }
  } else {
    for (i = 0; i < this.inputs.length; i++) {
      input = this.inputs[i];
      if (input.prevout.isNull()) {
        ret.reason = 'bad-txns-prevout-null';
        ret.score = 10;
        return false;
      }
    }
  }

  return true;
};

/**
 * Non-contextual checks to determine whether the
 * transaction has all standard output script
 * types and standard input script size with only
 * pushdatas in the code.
 * Will mostly verify coin and output values.
 * @see IsStandardTx()
 * @param {Object?} ret - Return object, may be
 * set with properties `reason` and `score`.
 * @returns {Boolean}
 */

TX.prototype.isStandard = function isStandard(ret) {
  var i, input, output;
  var nulldata = 0;

  if (!ret)
    ret = new VerifyResult();

  if (this.version < 1 || this.version > constants.tx.MAX_VERSION) {
    ret.reason = 'version';
    ret.score = 0;
    return false;
  }

  if (this.getWeight() >= constants.tx.MAX_WEIGHT) {
    ret.reason = 'tx-size';
    ret.score = 0;
    return false;
  }

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

    if (input.script.getSize() > 1650) {
      ret.reason = 'scriptsig-size';
      ret.score = 0;
      return false;
    }

    if (!input.script.isPushOnly()) {
      ret.reason = 'scriptsig-not-pushonly';
      ret.score = 0;
      return false;
    }
  }

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

    if (!output.script.isStandard()) {
      ret.reason = 'scriptpubkey';
      ret.score = 0;
      return false;
    }

    if (output.script.isNulldata()) {
      nulldata++;
      continue;
    }

    if (output.script.isMultisig() && !constants.tx.BARE_MULTISIG) {
      ret.reason = 'bare-multisig';
      ret.score = 0;
      return false;
    }

    if (output.isDust(constants.tx.MIN_RELAY)) {
      ret.reason = 'dust';
      ret.score = 0;
      return false;
    }
  }

  if (nulldata > 1) {
    ret.reason = 'multi-op-return';
    ret.score = 0;
    return false;
  }

  return true;
};

/**
 * Perform contextual checks to verify coin and input
 * script standardness (including the redeem script).
 * @see AreInputsStandard()
 * @param {VerifyFlags?} flags
 * @returns {Boolean}
 */

TX.prototype.hasStandardInputs = function hasStandardInputs() {
  var maxSigops = constants.script.MAX_SCRIPTHASH_SIGOPS;
  var VERIFY_NONE = constants.flags.VERIFY_NONE;
  var i, input, stack, redeem;

  if (this.isCoinbase())
    return true;

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

    if (!input.coin)
      return false;

    if (input.coin.script.isUnknown())
      return false;

    if (input.coin.script.isScripthash()) {
      stack = new Stack();

      try {
        input.script.execute(stack, VERIFY_NONE, this, i, 0);
      } catch (e) {
        return false;
      }

      if (stack.length === 0)
        return false;

      redeem = stack.getRedeem();

      if (!redeem)
        return false;

      if (redeem.getSigops(true) > maxSigops)
        return false;
    }
  }

  return true;
};

/**
 * Perform contextual checks to verify coin and witness standardness.
 * @see IsBadWitness()
 * @returns {Boolean}
 */

TX.prototype.hasStandardWitness = function hasStandardWitness(ret) {
  var result;

  if (!ret)
    ret = new VerifyResult();

  result = this.getWitnessStandard();

  switch (result) {
    case BAD_WITNESS:
      ret.reason = 'bad-witness';
      ret.score = 100;
      return false;
    case BAD_P2SH:
      ret.reason = 'bad-P2SH-scriptSig';
      ret.score = 100;
      return false;
    case BAD_NONSTD_P2WSH:
      ret.reason = 'bad-witness-nonstandard';
      ret.score = 0;
      return false;
  }

  return true;
};

/**
 * Perform contextual checks to verify coin and witness standardness.
 * @private
 * @see IsBadWitness()
 * @returns {Boolean}
 */

TX.prototype.getWitnessStandard = function getWitnessStandard() {
  var ret = BAD_OKAY;
  var i, j, input, prev, hash, redeem, m, n;

  if (!this.hasWitness())
    return ret;

  if (this.isCoinbase())
    return ret;

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

    if (!input.coin)
      continue;

    if (input.witness.length === 0)
      continue;

    prev = input.coin.script;

    if (prev.isScripthash()) {
      prev = input.script.getRedeem();
      if (!prev)
        return BAD_P2SH;
    }

    if (!prev.isProgram())
      return BAD_WITNESS;

    if (prev.isWitnessPubkeyhash()) {
      if (input.witness.length !== 2)
        return BAD_WITNESS;

      if (input.witness.get(0).length > 73)
        return BAD_WITNESS;

      hash = crypto.hash160(input.witness.get(1));

      if (!utils.equal(hash, prev.get(1)))
        return BAD_WITNESS;

      continue;
    }

    if (!prev.isWitnessScripthash()) {
      // Unknown program type,
      // let it through for now.
      continue;
    }

    redeem = input.witness.get(input.witness.length - 1);

    if (redeem.length > constants.script.MAX_SIZE)
      return BAD_WITNESS;

    if (redeem.length > constants.script.MAX_P2WSH_SIZE)
      ret = BAD_NONSTD_P2WSH;

    hash = crypto.sha256(redeem);

    if (!utils.equal(hash, prev.get(1)))
      return BAD_WITNESS;

    // Based on Johnson Lau's calculations:
    if (input.witness.length - 1 > 604)
      return BAD_WITNESS;

    if (input.witness.length - 1 > constants.script.MAX_P2WSH_STACK)
      ret = BAD_NONSTD_P2WSH;

    for (j = 0; j < input.witness.length; j++) {
      if (input.witness.get(j).length > constants.script.MAX_PUSH)
        return BAD_WITNESS;

      if (input.witness.get(j).length > constants.script.MAX_P2WSH_PUSH)
        ret = BAD_NONSTD_P2WSH;
    }

    redeem = new Script(redeem);

    if (redeem.isPubkey()) {
      if (input.witness.length - 1 !== 1)
        return BAD_WITNESS;

      if (input.witness.get(0).length > 73)
        return BAD_WITNESS;

      continue;
    }

    if (redeem.isPubkeyhash()) {
      if (input.witness.length - 1 !== 2)
        return BAD_WITNESS;

      if (input.witness.get(0).length > 73)
        return BAD_WITNESS;

      continue;
    }

    if (redeem.isMultisig()) {
      m = redeem.getSmall(0);
      n = redeem.getSmall(redeem.length - 2);

      if (input.witness.length - 1 !== m + 1)
        return BAD_WITNESS;

      if (input.witness.get(0).length !== 0)
        return BAD_WITNESS;

      for (j = 1; j < input.witness.length - 1; j++) {
        if (input.witness.get(i).length > 73)
          return BAD_WITNESS;
      }
    }
  }

  return ret;
};

/**
 * 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}
 */

TX.prototype.checkInputs = function checkInputs(spendHeight, ret) {
  var total = 0;
  var i, input, coin, fee, value;

  if (!ret)
    ret = new VerifyResult();

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

    if (!coin) {
      // Note: don't trigger dos score here.
      ret.reason = 'bad-txns-inputs-missingorspent';
      ret.score = 0;
      return false;
    }

    if (coin.coinbase && spendHeight != null) {
      if (spendHeight - coin.height < constants.tx.COINBASE_MATURITY) {
        ret.reason = 'bad-txns-premature-spend-of-coinbase';
        ret.score = 0;
        return false;
      }
    }

    if (coin.value < 0 || coin.value > constants.MAX_MONEY) {
      ret.reason = 'bad-txns-inputvalues-outofrange';
      ret.score = 100;
      return false;
    }

    total += coin.value;

    if (total < 0 || total > constants.MAX_MONEY) {
      ret.reason = 'bad-txns-inputvalues-outofrange';
      ret.score = 100;
      return false;
    }
  }

  // Overflows already checked in `isSane()`.
  value = this.getOutputValue();

  if (total < value) {
    ret.reason = 'bad-txns-in-belowout';
    ret.score = 100;
    return false;
  }

  fee = total - value;

  if (fee < 0) {
    ret.reason = 'bad-txns-fee-negative';
    ret.score = 100;
    return false;
  }

  if (fee > constants.MAX_MONEY) {
    ret.reason = 'bad-txns-fee-outofrange';
    ret.score = 100;
    return false;
  }

  return true;
};

/**
 * Estimate the max possible size of transaction once the
 * inputs are scripted. If the transaction is non-mutable,
 * this will just return the virtual size.
 * @returns {Number} size
 */

TX.prototype.maxSize = function maxSize() {
  return this.getVirtualSize();
};

/**
 * Calculate the modified size of the transaction. This
 * is used in the mempool for calculating priority.
 * @param {Number?} size - The size to modify. If not present,
 * virtual size will be used.
 * @returns {Number} Modified size.
 */

TX.prototype.getModifiedSize = function getModifiedSize(size) {
  var i, offset;

  if (size == null)
    size = this.maxSize();

  for (i = 0; i < this.inputs.length; i++) {
    offset = 41 + Math.min(110, this.inputs[i].script.getSize());
    if (size > offset)
      size -= offset;
  }

  return size;
};

/**
 * Calculate the transaction priority.
 * @param {Number?} height - If not present, tx height
 * or network height will be used.
 * @param {Number?} size - Size to calculate priority
 * based on. If not present, virtual size will be used.
 * @returns {Number}
 */

TX.prototype.getPriority = function getPriority(height, size) {
  var sum = 0;
  var i, input, age;

  if (this.isCoinbase())
    return sum;

  if (height == null) {
    height = this.height;
    if (height === -1)
      height = Network.primary.height;
  }

  if (size == null)
    size = this.maxSize();

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

    if (!input.coin)
      continue;

    if (input.coin.height === -1)
      continue;

    if (input.coin.height <= height) {
      age = height - input.coin.height;
      sum += input.coin.value * age;
    }
  }

  return Math.floor(sum / size);
};

/**
 * Calculate the transaction's on-chain value.
 * @param {Number?} height
 * @returns {Number}
 */

TX.prototype.getChainValue = function getChainValue(height) {
  var value = 0;
  var i, input;

  if (this.isCoinbase())
    return value;

  if (height == null)
    height = Infinity;

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

    if (!input.coin)
      continue;

    if (input.coin.height === -1)
      continue;

    if (input.coin.height <= height)
      value += input.coin.value;
  }

  return value;
};

/**
 * Determine whether the transaction is above the
 * free threshold in priority. A transaction which
 * passed this test is most likely relayable
 * without a fee.
 * @param {Number?} height - If not present, tx
 * height or network height will be used.
 * @param {Number?} size - If not present, modified
 * size will be calculated and used.
 * @returns {Boolean}
 */

TX.prototype.isFree = function isFree(height, size) {
  var priority = this.getPriority(height, size);
  return priority > constants.tx.FREE_THRESHOLD;
};

/**
 * Calculate minimum fee in order for the transaction
 * to be relayable (not the constant min relay fee).
 * @param {Number?} size - If not present, max size
 * estimation will be calculated and used.
 * @param {Rate?} rate - Rate of satoshi per kB.
 * @returns {Amount} fee
 */

TX.prototype.getMinFee = function getMinFee(size, rate) {
  if (size == null)
    size = this.maxSize();

  return TX.getMinFee(size, rate);
};

/**
 * Calculate the minimum fee in order for the transaction
 * to be relayable, but _round to the nearest kilobyte
 * when taking into account size.
 * @param {Number?} size - If not present, max size
 * estimation will be calculated and used.
 * @param {Rate?} rate - Rate of satoshi per kB.
 * @returns {Amount} fee
 */

TX.prototype.getRoundFee = function getRoundFee(size, rate) {
  if (size == null)
    size = this.maxSize();

  return TX.getRoundFee(size, rate);
};

/**
 * Calculate the transaction's rate based on size
 * and fees. Size will be calculated if not present.
 * @param {Number?} size
 * @returns {Rate}
 */

TX.prototype.getRate = function getRate(size) {
  if (size == null)
    size = this.maxSize();

  return TX.getRate(size, this.getFee());
};

/**
 * Calculate current number of transaction confirmations.
 * @param {Number?} height - Current chain height. If not
 * present, network chain height will be used.
 * @returns {Number} confirmations
 */

TX.prototype.getConfirmations = function getConfirmations(height) {
  if (height == null)
    height = Network.primary.height;

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

  if (height < this.height)
    return 1;

  return height - this.height + 1;
};

/**
 * Get all unique outpoint hashes.
 * @returns {Hash[]} Outpoint hashes.
 */

TX.prototype.getPrevout = function getPrevout() {
  var prevout = {};
  var i, input;

  if (this.isCoinbase())
    return [];

  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    prevout[input.prevout.hash] = true;
  }

  return Object.keys(prevout);
};

/**
 * Test a transaction against a bloom filter using
 * the BIP37 matching algorithm. Note that this may
 * update the filter depending on what the `update`
 * value is.
 * @see "Filter matching algorithm":
 * @see https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki
 * @param {Bloom} filter
 * @returns {Boolean} True if the transaction matched.
 */

TX.prototype.isWatched = function isWatched(filter) {
  var found = false;
  var i, input, output, outpoint;

  if (!filter)
    return false;

  // 1. Test the tx hash
  if (filter.test(this.hash()))
    found = true;

  // 2. Test data elements in output scripts
  //    (may need to update filter on match)
  for (i = 0; i < this.outputs.length; i++) {
    output = this.outputs[i];
    // Test the output script
    if (output.script.test(filter)) {
      if (filter.update === constants.filterFlags.ALL) {
        outpoint = Outpoint.fromTX(this, i);
        filter.add(outpoint.toRaw());
      } else if (filter.update === constants.filterFlags.PUBKEY_ONLY) {
        if (output.script.isPubkey() || output.script.isMultisig()) {
          outpoint = Outpoint.fromTX(this, i);
          filter.add(outpoint.toRaw());
        }
      }
      found = true;
    }
  }

  if (found)
    return found;

  // 3. Test prev_out structure
  // 4. Test data elements in input scripts
  for (i = 0; i < this.inputs.length; i++) {
    input = this.inputs[i];
    outpoint = input.prevout.toRaw();

    // Test the COutPoint structure
    if (filter.test(outpoint))
      return true;

    // Test the input script
    if (input.script.test(filter))
      return true;

    // Test the witness
    // if (input.witness.test(filter))
    //   return true;
  }

  // 5. No match
  return false;
};

TX.prototype.__defineGetter__('rblock', function() {
  return this.block
    ? utils.revHex(this.block)
    : null;
});

TX.prototype.__defineGetter__('rhash', function() {
  return utils.revHex(this.hash('hex'));
});

TX.prototype.__defineGetter__('rwhash', function() {
  return utils.revHex(this.witnessHash('hex'));
});

TX.prototype.__defineGetter__('txid', function() {
  return this.rhash;
});

TX.prototype.__defineGetter__('wtxid', function() {
  return this.rwhash;
});

/**
 * Convert the tx to an inv item.
 * @returns {InvItem}
 */

TX.prototype.toInv = function toInv() {
  return new InvItem(constants.inv.TX, this.hash('hex'));
};

/**
 * Calculate minimum fee based on rate and size.
 * @param {Number?} size
 * @param {Rate?} rate - Rate of satoshi per kB.
 * @returns {Amount} fee
 */

TX.getMinFee = function getMinFee(size, rate) {
  var fee;

  if (rate == null)
    rate = constants.tx.MIN_RELAY;

  fee = Math.floor(rate * size / 1000);

  if (fee === 0 && rate > 0)
    fee = rate;

  return fee;
};

/**
 * Calculate the minimum fee in order for the transaction
 * to be relayable, but _round to the nearest kilobyte
 * when taking into account size.
 * @param {Number?} size
 * @param {Rate?} rate - Rate of satoshi per kB.
 * @returns {Amount} fee
 */

TX.getRoundFee = function getRoundFee(size, rate) {
  var fee;

  if (rate == null)
    rate = constants.tx.MIN_RELAY;

  fee = rate * Math.ceil(size / 1000);

  if (fee === 0 && rate > 0)
    fee = rate;

  return fee;
};

/**
 * Calculate a fee rate based on size and fees.
 * @param {Number} size
 * @param {Amount} fee
 * @returns {Rate}
 */

TX.getRate = function getRate(size, fee) {
  return Math.floor(fee * 1000 / size);
};

/**
 * Inspect the transaction and return a more
 * user-friendly representation of the data.
 * @returns {Object}
 */

TX.prototype.inspect = function inspect() {
  return {
    type: 'tx',
    hash: this.rhash,
    witnessHash: this.rwhash,
    size: this.getSize(),
    virtualSize: this.maxSize(),
    height: this.height,
    value: utils.btc(this.getOutputValue()),
    fee: utils.btc(this.getFee()),
    minFee: utils.btc(this.getMinFee()),
    rate: this.getRate(), // Rate can sometimes exceed 53 bits in testing
    confirmations: this.getConfirmations(),
    priority: this.getPriority(),
    date: utils.date(this.ts || this.ps),
    block: this.block ? utils.revHex(this.block) : null,
    ts: this.ts,
    ps: this.ps,
    index: this.index,
    version: this.version,
    flag: this.flag,
    inputs: this.inputs,
    outputs: this.outputs,
    locktime: this.locktime
  };
};

/**
 * Convert the transaction to an object suitable
 * for JSON serialization. Note that the hashes
 * will be reversed to abide by bitcoind's legacy
 * of little-endian uint256s.
 * @returns {Object}
 */

TX.prototype.toJSON = function toJSON() {
  return {
    type: 'tx',
    hash: utils.revHex(this.hash('hex')),
    witnessHash: utils.revHex(this.witnessHash('hex')),
    height: this.height,
    block: this.block ? utils.revHex(this.block) : null,
    ts: this.ts,
    ps: this.ps,
    index: this.index,
    fee: utils.btc(this.getFee()),
    confirmations: this.getConfirmations(),
    version: this.version,
    flag: this.flag,
    inputs: this.inputs.map(function(input) {
      return input.toJSON();
    }),
    outputs: this.outputs.map(function(output) {
      return output.toJSON();
    }),
    locktime: this.locktime
  };
};

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

TX.prototype.fromJSON = function fromJSON(json) {
  var i, input, output;

  assert(json, 'TX data is required.');
  assert.equal(json.type, 'tx');
  assert(utils.isNumber(json.version));
  assert(utils.isNumber(json.flag));
  assert(Array.isArray(json.inputs));
  assert(Array.isArray(json.outputs));
  assert(utils.isNumber(json.locktime));
  assert(!json.block || typeof json.block === 'string');
  assert(utils.isNumber(json.height));
  assert(utils.isNumber(json.ts));
  assert(utils.isNumber(json.ps));
  assert(utils.isNumber(json.index));

  this.block = json.block ? utils.revHex(json.block) : null;
  this.height = json.height;
  this.ts = json.ts;
  this.ps = json.ps;
  this.index = json.index;

  this.version = json.version;

  this.flag = json.flag;

  for (i = 0; i < json.inputs.length; i++) {
    input = json.inputs[i];
    this.inputs.push(Input.fromJSON(input));
  }

  for (i = 0; i < json.outputs.length; i++) {
    output = json.outputs[i];
    this.outputs.push(Output.fromJSON(output));
  }

  this.locktime = json.locktime;

  return this;
};

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

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

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

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

/**
 * Inject properties from serialized data.
 * @private
 * @param {Buffer|BufferReader} data
 */

TX.prototype.fromRaw = function fromRaw(data) {
  var p, i, count;

  if (TX.isWitness(data))
    return this.fromWitness(data);

  p = BufferReader(data);
  p.start();

  this.version = p.readU32(); // Technically signed

  count = p.readVarint();

  for (i = 0; i < count; i++)
    this.inputs.push(Input.fromRaw(p));

  count = p.readVarint();

  for (i = 0; i < count; i++)
    this.outputs.push(Output.fromRaw(p));

  this.locktime = p.readU32();

  if (!this.mutable) {
    this._raw = p.endData();
    this._size = this._raw.length;
    this._witnessSize = 0;
  } else {
    p.end();
  }

  return this;
};

/**
 * Inject properties from serialized
 * data (witness serialization).
 * @private
 * @param {Buffer|BufferReader} data
 */

TX.prototype.fromWitness = function fromWitness(data) {
  var p = BufferReader(data);
  var flag = 0;
  var witnessSize = 0;
  var hasWitness = false;
  var i, count, input;

  p.start();

  this.version = p.readU32(); // Technically signed

  assert(p.readU8() === 0, 'Non-zero marker.');

  flag = p.readU8();

  assert(flag !== 0, 'Flag byte is zero.');

  this.flag = flag;

  count = p.readVarint();

  for (i = 0; i < count; i++)
    this.inputs.push(Input.fromRaw(p));

  count = p.readVarint();

  for (i = 0; i < count; i++)
    this.outputs.push(Output.fromRaw(p));

  if (flag & 1) {
    flag ^= 1;

    p.start();

    for (i = 0; i < this.inputs.length; i++) {
      input = this.inputs[i];
      input.witness.fromRaw(p);
      if (input.witness.items.length > 0)
        hasWitness = true;
    }

    witnessSize = p.end() + 2;
  }

  if (flag !== 0)
    throw new Error('Unknown witness flag.');

  // We'll never be able to reserialize
  // this to get the regular txid, and
  // there's no way it's valid anyway.
  if (this.inputs.length === 0 && this.outputs.length !== 0)
    throw new Error('Zero input witness tx.');

  this.locktime = p.readU32();

  if (!this.mutable && hasWitness) {
    this._raw = p.endData();
    this._size = this._raw.length;
    this._witnessSize = witnessSize;
  } else {
    p.end();
  }

  return this;
};

/**
 * Serialize transaction without witness.
 * @private
 * @param {BufferWriter?} writer - A buffer writer to continue writing from.
 * @returns {Buffer} Returns a BufferWriter if `writer` was passed in.
 */

TX.prototype.frameNormal = function frameNormal(writer) {
  var p = BufferWriter(writer);
  var i;

  if (this.inputs.length === 0 && this.outputs.length !== 0)
    throw new Error('Cannot serialize zero-input tx.');

  p.writeU32(this.version);

  p.writeVarint(this.inputs.length);

  for (i = 0; i < this.inputs.length; i++)
    this.inputs[i].toRaw(p);

  p.writeVarint(this.outputs.length);

  for (i = 0; i < this.outputs.length; i++)
    this.outputs[i].toRaw(p);

  p.writeU32(this.locktime);

  if (!writer)
    p = p.render();

  this._lastWitnessSize = 0;

  return p;
};

/**
 * Serialize transaction with witness. Calculates the witness
 * size as it is framing (exposed on return value as `_witnessSize`).
 * @private
 * @param {BufferWriter?} writer - A buffer writer to continue writing from.
 * @returns {Buffer} Returns a BufferWriter if `writer` was passed in.
 */

TX.prototype.frameWitness = function frameWitness(writer) {
  var p = BufferWriter(writer);
  var witnessSize = 0;
  var i, start;

  if (this.inputs.length === 0 && this.outputs.length !== 0)
    throw new Error('Cannot serialize zero-input tx.');

  p.writeU32(this.version);
  p.writeU8(0);
  p.writeU8(this.flag);

  p.writeVarint(this.inputs.length);

  for (i = 0; i < this.inputs.length; i++)
    this.inputs[i].toRaw(p);

  p.writeVarint(this.outputs.length);

  for (i = 0; i < this.outputs.length; i++)
    this.outputs[i].toRaw(p);

  start = p.written;

  for (i = 0; i < this.inputs.length; i++)
    this.inputs[i].witness.toRaw(p);

  witnessSize += p.written - start;

  p.writeU32(this.locktime);

  if (witnessSize === this.inputs.length)
    throw new Error('Cannot serialize empty-witness tx.');

  this._lastWitnessSize = witnessSize + 2;

  if (!writer)
    p = p.render();

  return p;
};

/**
 * Test whether data is a witness transaction.
 * @param {Buffer|BufferReader} data
 * @returns {Boolean}
 */

TX.isWitness = function isWitness(data) {
  if (Buffer.isBuffer(data)) {
    if (data.length < 6)
      return false;

    return data[4] === 0 && data[5] !== 0;
  }

  if (data.left() < 6)
    return false;

  return data.data[data.offset + 4] === 0
    && data.data[data.offset + 5] !== 0;
};

/**
 * Serialize a transaction to BCoin "extended format".
 * This is the serialization format BCoin uses internally
 * to store transactions in the database. The extended
 * serialization includes the height, block hash, index,
 * timestamp, pending-since time, and optionally a vector
 * for the serialized coins.
 * @param {Boolean?} saveCoins - Whether to serialize the coins.
 * @param {String?} enc - One of `"hex"` or `null`.
 * @returns {Buffer}
 */

TX.prototype.toExtended = function toExtended(saveCoins, writer) {
  var height = this.height;
  var index = this.index;
  var p = BufferWriter(writer);
  var i, input;

  if (height === -1)
    height = 0x7fffffff;

  if (index === -1)
    index = 0x7fffffff;

  this.toRaw(p);

  p.writeU32(height);
  p.writeHash(this.block || constants.ZERO_HASH);
  p.writeU32(index);
  p.writeU32(this.ts);
  p.writeU32(this.ps);

  if (saveCoins) {
    p.writeVarint(this.inputs.length);
    for (i = 0; i < this.inputs.length; i++) {
      input = this.inputs[i];

      if (!input.coin) {
        p.writeVarint(0);
        continue;
      }

      p.writeVarBytes(input.coin.toRaw());
    }
  }

  if (!writer)
    p = p.render();

  return p;
};

/**
 * Inject properties from "extended" serialization format.
 * @param {Buffer} data
 * @param {Boolean?} saveCoins - If true, the function will
 * attempt to parse the coins.
 * @param {String?} enc - One of `"hex"` or `null`.
 */

TX.prototype.fromExtended = function fromExtended(data, saveCoins) {
  var p = BufferReader(data);
  var i, coinCount, coin;

  this.fromRaw(p);

  this.height = p.readU32();
  this.block = p.readHash('hex');
  this.index = p.readU32();
  this.ts = p.readU32();
  this.ps = p.readU32();

  if (this.block === constants.NULL_HASH)
    this.block = null;

  if (this.height === 0x7fffffff)
    this.height = -1;

  if (this.index === 0x7fffffff)
    this.index = -1;

  if (saveCoins) {
    coinCount = p.readVarint();
    for (i = 0; i < coinCount; i++) {
      coin = p.readVarBytes();
      if (coin.length === 0)
        continue;
      coin = Coin.fromRaw(coin);
      coin.hash = this.inputs[i].prevout.hash;
      coin.index = this.inputs[i].prevout.index;
      this.inputs[i].coin = coin;
    }
  }

  return this;
};

/**
 * Instantiate a transaction from a Buffer
 * in "extended" serialization format.
 * @param {Buffer} data
 * @param {Boolean?} saveCoins - If true, the function will
 * attempt to parse the coins.
 * @param {String?} enc - One of `"hex"` or `null`.
 * @returns {TX}
 */

TX.fromExtended = function fromExtended(data, saveCoins, enc) {
  if (typeof saveCoins === 'string') {
    enc = saveCoins;
    saveCoins = false;
  }

  if (typeof data === 'string')
    data = new Buffer(data, enc);

  return new TX().fromExtended(data, saveCoins);
};

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

TX.isTX = function isTX(obj) {
  return obj
    && Array.isArray(obj.inputs)
    && typeof obj.locktime === 'number'
    && typeof obj.witnessHash === 'function';
};

/*
 * Expose
 */

module.exports = TX;
