text: 'use strict';
  
  var _ = require('lodash');
  var BlockHeader = require('./blockheader');
  var BufferUtil = require('../util/buffer');
  var BufferReader = require('../encoding/bufferreader');
  var BufferWriter = require('../encoding/bufferwriter');
  var Hash = require('../crypto/hash');
  var JSUtil = require('../util/js');
  var Transaction = require('../transaction');
  var $ = require('../util/preconditions');
  
  /**
   * Instantiate a MerkleBlock from a Buffer, JSON object, or Object with
   * the properties of the Block
   *
   * @param {*} - A Buffer, JSON string, or Object representing a MerkleBlock
   * @returns {MerkleBlock}
   * @constructor
   */
  function MerkleBlock(arg) {
    /* jshint maxstatements: 18 */
  
    if (!(this instanceof MerkleBlock)) {
      return new MerkleBlock(arg);
    }
  
    var info = {};
    if (BufferUtil.isBuffer(arg)) {
      info = MerkleBlock._fromBufferReader(BufferReader(arg));
    } else if (_.isObject(arg)) {
      var header;
      if(arg.header instanceof BlockHeader) {
        header = arg.header;
      } else {
        header = BlockHeader.fromObject(arg.header);
      }
      info = {
        /**
         * @name MerkleBlock#header
         * @type {BlockHeader}
         */
        header: header,
        /**
         * @name MerkleBlock#numTransactions
         * @type {Number}
         */
        numTransactions: arg.numTransactions,
        /**
         * @name MerkleBlock#hashes
         * @type {String[]}
         */
        hashes: arg.hashes,
        /**
         * @name MerkleBlock#flags
         * @type {Number[]}
         */
        flags: arg.flags
      };
    } else {
      throw new TypeError('Unrecognized argument for MerkleBlock');
    }
    _.extend(this,info);
    this._flagBitsUsed = 0;
    this._hashesUsed = 0;
    return this;
  }
  
  /**
   * @param {Buffer} - MerkleBlock data in a Buffer object
   * @returns {MerkleBlock} - A MerkleBlock object
   */
  MerkleBlock.fromBuffer = function fromBuffer(buf) {
    return MerkleBlock.fromBufferReader(BufferReader(buf));
  };
  
  /**
   * @param {BufferReader} - MerkleBlock data in a BufferReader object
   * @returns {MerkleBlock} - A MerkleBlock object
   */
  MerkleBlock.fromBufferReader = function fromBufferReader(br) {
    return new MerkleBlock(MerkleBlock._fromBufferReader(br));
  };
  
  /**
   * @returns {Buffer} - A buffer of the block
   */
  MerkleBlock.prototype.toBuffer = function toBuffer() {
    return this.toBufferWriter().concat();
  };
  
  /**
   * @param {BufferWriter} - An existing instance of BufferWriter
   * @returns {BufferWriter} - An instance of BufferWriter representation of the MerkleBlock
   */
  MerkleBlock.prototype.toBufferWriter = function toBufferWriter(bw) {
    if (!bw) {
      bw = new BufferWriter();
    }
    bw.write(this.header.toBuffer());
    bw.writeUInt32LE(this.numTransactions);
    bw.writeVarintNum(this.hashes.length);
    for (var i = 0; i < this.hashes.length; i++) {
      bw.write(Buffer.from(this.hashes[i], 'hex'));
    }
    bw.writeVarintNum(this.flags.length);
    for (i = 0; i < this.flags.length; i++) {
      bw.writeUInt8(this.flags[i]);
    }
    return bw;
  };
  
  /**
   * @returns {Object} - A plain object with the MerkleBlock properties
   */
  MerkleBlock.prototype.toObject = MerkleBlock.prototype.toJSON = function toObject() {
    return {
      header: this.header.toObject(),
      numTransactions: this.numTransactions,
      hashes: this.hashes,
      flags: this.flags
    };
  };
  
  /**
   * Verify that the MerkleBlock is valid
   * @returns {Boolean} - True/False whether this MerkleBlock is Valid
   */
  MerkleBlock.prototype.validMerkleTree = function validMerkleTree() {
    $.checkState(_.isArray(this.flags), 'MerkleBlock flags is not an array');
    $.checkState(_.isArray(this.hashes), 'MerkleBlock hashes is not an array');
  
    // Can't have more hashes than numTransactions
    if(this.hashes.length > this.numTransactions) {
      return false;
    }
  
    // Can't have more flag bits than num hashes
    if(this.flags.length * 8 < this.hashes.length) {
      return false;
    }
  
    var height = this._calcTreeHeight();
    var opts = { hashesUsed: 0, flagBitsUsed: 0 };
    var root = this._traverseMerkleTree(height, 0, opts);
    if(opts.hashesUsed !== this.hashes.length) {
      return false;
    }
    return BufferUtil.equals(root, this.header.merkleRoot);
  };
  
  /**
   * Traverse a the tree in this MerkleBlock, validating it along the way
   * Modeled after Bitcoin Core merkleblock.cpp TraverseAndExtract()
   * @param {Number} - depth - Current height
   * @param {Number} - pos - Current position in the tree
   * @param {Object} - opts - Object with values that need to be mutated throughout the traversal
   * @param {Number} - opts.flagBitsUsed - Number of flag bits used, should start at 0
   * @param {Number} - opts.hashesUsed - Number of hashes used, should start at 0
   * @param {Array} - opts.txs - Will finish populated by transactions found during traversal
   * @returns {Buffer|null} - Buffer containing the Merkle Hash for that height
   * @private
   */
  MerkleBlock.prototype._traverseMerkleTree = function traverseMerkleTree(depth, pos, opts) {
    /* jshint maxcomplexity:  12*/
    /* jshint maxstatements: 20 */
  
    opts = opts || {};
    opts.txs = opts.txs || [];
    opts.flagBitsUsed = opts.flagBitsUsed || 0;
    opts.hashesUsed = opts.hashesUsed || 0;
  
    if(opts.flagBitsUsed > this.flags.length * 8) {
      return null;
    }
    var isParentOfMatch = (this.flags[opts.flagBitsUsed >> 3] >>> (opts.flagBitsUsed++ & 7)) & 1;
    if(depth === 0 || !isParentOfMatch) {
      if(opts.hashesUsed >= this.hashes.length) {
        return null;
      }
      var hash = this.hashes[opts.hashesUsed++];
      if(depth === 0 && isParentOfMatch) {
        opts.txs.push(hash);
      }
      return Buffer.from(hash, 'hex');
    } else {
      var left = this._traverseMerkleTree(depth-1, pos*2, opts);
      var right = left;
      if(pos*2+1 < this._calcTreeWidth(depth-1)) {
        right = this._traverseMerkleTree(depth-1, pos*2+1, opts);
      }
      return Hash.sha256sha256(new Buffer.concat([left, right]));
    }
  };
  
  /** Calculates the width of a merkle tree at a given height.
   *  Modeled after Bitcoin Core merkleblock.h CalcTreeWidth()
   * @param {Number} - Height at which we want the tree width
   * @returns {Number} - Width of the tree at a given height
   * @private
   */
  MerkleBlock.prototype._calcTreeWidth = function calcTreeWidth(height) {
    return (this.numTransactions + (1 << height) - 1) >> height;
  };
  
  /** Calculates the height of the merkle tree in this MerkleBlock
   * @param {Number} - Height at which we want the tree width
   * @returns {Number} - Height of the merkle tree in this MerkleBlock
   * @private
   */
  MerkleBlock.prototype._calcTreeHeight = function calcTreeHeight() {
    var height = 0;
    while (this._calcTreeWidth(height) > 1) {
      height++;
    }
    return height;
  };
  
  /**
   * @param {Transaction|String} - Transaction or Transaction ID Hash
   * @returns {Boolean} - return true/false if this MerkleBlock has the TX or not
   * @private
   */
  MerkleBlock.prototype.hasTransaction = function hasTransaction(tx) {
    $.checkArgument(!_.isUndefined(tx), 'tx cannot be undefined');
    $.checkArgument(tx instanceof Transaction || typeof tx === 'string',
        'Invalid tx given, tx must be a "string" or "Transaction"');
  
    var hash = tx;
    if(tx instanceof Transaction) {
      // We need to reverse the id hash for the lookup
      hash = BufferUtil.reverse(Buffer.from(tx.id, 'hex')).toString('hex');
    }
  
    var txs = [];
    var height = this._calcTreeHeight();
    this._traverseMerkleTree(height, 0, { txs: txs });
    return txs.indexOf(hash) !== -1;
  };
  
  /**
   * @param {Buffer} - MerkleBlock data
   * @returns {Object} - An Object representing merkleblock data
   * @private
   */
  MerkleBlock._fromBufferReader = function _fromBufferReader(br) {
    $.checkState(!br.finished(), 'No merkleblock data received');
    var info = {};
    info.header = BlockHeader.fromBufferReader(br);
    info.numTransactions = br.readUInt32LE();
    var numHashes = br.readVarintNum();
    info.hashes = [];
    for (var i = 0; i < numHashes; i++) {
      info.hashes.push(br.read(32).toString('hex'));
    }
    var numFlags = br.readVarintNum();
    info.flags = [];
    for (i = 0; i < numFlags; i++) {
      info.flags.push(br.readUInt8());
    }
    return info;
  };
  
  /**
   * @param {Object} - A plain JavaScript object
   * @returns {Block} - An instance of block
   */
  MerkleBlock.fromObject = function fromObject(obj) {
    return new MerkleBlock(obj);
  };
  
  module.exports = MerkleBlock;
  