'use strict';

var _ = require('lodash');
var BlockHeader = require('./blockheader');
var BN = require('./crypto/bn');
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 Varint = require('./encoding/varint');

/**
 * Instantiate a Block from a Buffer, JSON object, or Object with
 * the properties of the Block
 *
 * @param {*} - A Buffer, JSON string, or Object
 * @returns {Block}
 * @constructor
 */
function Block(arg) {
  if (!(this instanceof Block)) {
    return new Block(arg);
  }
  _.extend(this, Block._from(arg));
  return this;
}

// https://github.com/bitcoin/bitcoin/blob/b5fa132329f0377d787a4a21c1686609c2bfaece/src/primitives/block.h#L14
Block.MAX_BLOCK_SIZE = 1000000;

/**
 * @param {*} - A Buffer, JSON string or Object
 * @returns {Object} - An object representing block data
 * @throws {TypeError} - If the argument was not recognized
 * @private
 */
Block._from = function _from(arg) {
  var info = {};
  if (BufferUtil.isBuffer(arg)) {
    info = Block._fromBufferReader(BufferReader(arg));
  } else if (JSUtil.isValidJSON(arg)) {
    info = Block._fromJSON(arg);
  } else if (_.isObject(arg)) {
    info = {
      /**
       * @name Block#magicnum
       * @type number
       */
      magicnum: arg.magicnum,
      /**
       * @name Block#size
       * @type number
       */
      size: arg.size,
      /**
       * @name Block#header
       * @type {BlockHeader}
       */
      header: arg.header,
      txsvi: arg.txsvi,
      /**
       * @name Block#txs
       * @type {Transaction[]}
       */
      txs: arg.txs
    };
  } else {
    throw new TypeError('Unrecognized argument for Block');
  }
  return info;
};

/**
 * @param {String|Object} - A JSON string or object
 * @returns {Object} - An object representing block data
 * @private
 */
Block._fromJSON = function _fromJSON(data) {
  if (JSUtil.isValidJSON(data)) {
    data = JSON.parse(data);
  }
  var txs = [];
  data.txs.forEach(function(tx) {
    txs.push(Transaction().fromJSON(tx));
  });
  var info = {
    magicnum: data.magicnum,
    size: data.size,
    header: BlockHeader.fromJSON(data.header),
    txsvi: Varint().fromString(data.txsvi),
    txs: txs
  };
  return info;
};

/**
 * @param {String|Object} - A JSON string or object
 * @returns {Block} - An instance of block
 */
Block.fromJSON = function fromJSON(json) {
  var info = Block._fromJSON(json);
  return new Block(info);
};

/**
 * @param {BufferReader} - Block data
 * @returns {Object} - An object representing the block data
 * @private
 */
Block._fromBufferReader = function _fromBufferReader(br) {
  var info = {};
  info.magicnum = br.readUInt32LE();
  info.size = br.readUInt32LE();
  info.header = BlockHeader.fromBufferReader(br);
  info.txsvi = Varint(br.readVarintBuf());
  var txslen = info.txsvi.toNumber();
  info.txs = [];
  for (var i = 0; i < txslen; i++) {
    info.txs.push(Transaction().fromBufferReader(br));
  }
  return info;
};

/**
 * @param {BufferReader} - A buffer reader of the block
 * @returns {Block} - An instance of block
 */
Block.fromBufferReader = function fromBufferReader(br) {
  var info = Block._fromBufferReader(br);
  return new Block(info);
};

/**
 * @param {Buffer} - A buffer of the block
 * @returns {Block} - An instance of block
 */
Block.fromBuffer = function fromBuffer(buf) {
  return Block.fromBufferReader(BufferReader(buf));
};

/**
 * @param {String} - str - A hex encoded string of the block
 * @returns {Block} - A hex encoded string of the block
 */
Block.fromString = function fromString(str) {
  var buf = new Buffer(str, 'hex');
  return Block.fromBuffer(buf);
};

/**
 * @param {Binary} - Raw block binary data or buffer
 * @returns {Block} - An instance of block
 */
Block.fromRawBlock = function fromRawBlock(data) {
  if (!BufferUtil.isBuffer(data)) {
    data = new Buffer(data, 'binary');
  }
  var br = BufferReader(data);
  var info = Block._fromBufferReader(br);
  return new Block(info);
};

/**
 * @returns {Object} - A plain object with the block properties
 */
Block.prototype.toObject = function toObject() {
  var txs = [];
  this.txs.forEach(function(tx) {
    txs.push(tx.toObject());
  });
  return {
    magicnum: this.magicnum,
    size: this.size,
    header: this.header.toObject(),
    txsvi: this.txsvi.toString(),
    txs: txs
  };
};

/**
 * @returns {String} - A JSON string
 */
Block.prototype.toJSON = function toJSON() {
  return JSON.stringify(this.toObject());
};

/**
 * @returns {Buffer} - A buffer of the block
 */
Block.prototype.toBuffer = function toBuffer() {
  return this.toBufferWriter().concat();
};

/**
 * @returns {String} - A hex encoded string of the block
 */
Block.prototype.toString = function toString() {
  return this.toBuffer().toString('hex');
};

/**
 * @param {BufferWriter} - An existing instance of BufferWriter
 * @returns {BufferWriter} - An instance of BufferWriter representation of the Block
 */
Block.prototype.toBufferWriter = function toBufferWriter(bw) {
  if (!bw) {
    bw = new BufferWriter();
  }
  bw.writeUInt32LE(this.magicnum);
  bw.writeUInt32LE(this.size);
  bw.write(this.header.toBuffer());
  bw.write(this.txsvi.buf);
  var txslen = this.txsvi.toNumber();
  for (var i = 0; i < txslen; i++) {
    this.txs[i].toBufferWriter(bw);
  }
  return bw;
};

/**
 * Will iterate through each transaction and return an array of hashes
 * @returns {Array} - An array with transaction hashes
 */
Block.prototype.getTransactionHashes = function getTransactionHashes() {
  var hashes = [];
  if (this.txs.length === 0) {
    return [Block.Values.NULL_HASH];
  }
  for (var t = 0; t < this.txs.length; t++) {
    hashes.push(this.txs[t]._getHash());
  }
  return hashes;
};

/**
 * Will build a merkle tree of all the transactions, ultimately arriving at
 * a single point, the merkle root.
 * @link https://en.bitcoin.it/wiki/Protocol_specification#Merkle_Trees
 * @returns {Array} - An array with each level of the tree after the other.
 */
Block.prototype.getMerkleTree = function getMerkleTree() {

  var tree = this.getTransactionHashes();

  var j = 0;
  for (var size = this.txs.length; size > 1; size = Math.floor((size + 1) / 2)) {
    for (var i = 0; i < size; i += 2) {
      var i2 = Math.min(i + 1, size - 1);
      var buf = Buffer.concat([tree[j + i], tree[j + i2]]);
      tree.push(Hash.sha256sha256(buf));
    }
    j += size;
  }

  return tree;
};

/**
 * Calculates the merkleRoot from the transactions.
 * @returns {Buffer} - A buffer of the merkle root hash
 */
Block.prototype.getMerkleRoot = function getMerkleRoot() {
  var tree = this.getMerkleTree();
  return tree[tree.length - 1];
};

/**
 * Verifies that the transactions in the block match the header merkle root
 * @returns {Boolean} - If the merkle roots match
 */
Block.prototype.validMerkleRoot = function validMerkleRoot() {

  var h = new BN(this.header.merkleRoot.toString('hex'), 'hex');
  var c = new BN(this.getMerkleRoot().toString('hex'), 'hex');

  if (h.cmp(c) !== 0) {
    return false;
  }

  return true;
};

/**
 * @returns {Buffer} - The little endian hash buffer of the header
 */
Block.prototype._getHash = function() {
  return this.header._getHash();
};

var idProperty = {
  configurable: false,
  writeable: false,
  /**
   * @returns {string} - The big endian hash buffer of the header
   */
  get: function() {
    if (!this._id) {
      this._id = this.header.id;
    }
    return this._id;
  },
  set: _.noop
};
Object.defineProperty(Block.prototype, 'id', idProperty);
Object.defineProperty(Block.prototype, 'hash', idProperty);

/**
 * @returns {String} - A string formated for the console
 */
Block.prototype.inspect = function inspect() {
  return '<Block ' + this.id + '>';
};

Block.Values = {
  NULL_HASH: new Buffer('0000000000000000000000000000000000000000000000000000000000000000', 'hex')
};

module.exports = Block;