/*!
 * minerblock.js - miner block object for bcoin (because we can)
 * Copyright (c) 2014-2015, Fedor Indutny (MIT License)
 * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var BN = require('bn.js');
var util = require('../utils/util');
var co = require('../utils/co');
var StaticWriter = require('../utils/staticwriter');
var Network = require('../protocol/network');
var TX = require('../primitives/tx');
var Block = require('../primitives/block');
var Input = require('../primitives/input');
var Output = require('../primitives/output');
var mine = require('./mine');
var workerPool = require('../workers/workerpool').pool;
var consensus = require('../protocol/consensus');
var policy = require('../protocol/policy');
var encoding = require('../utils/encoding');

/**
 * MinerBlock (block attempt)
 * @alias module:mining.MinerBlock
 * @constructor
 * @param {Object} options
 * @param {ChainEntry} options.tip
 * @param {Number} options.height
 * @param {Number} options.target - Compact form.
 * @param {Base58Address} options.address - Payout address.
 * @param {Boolean} options.witness - Allow witness
 * transactions, mine a witness block.
 * @param {String} options.coinbaseFlags
 * @property {Block} block
 * @property {TX} coinbase
 * @property {BN} hashes - Number of hashes attempted.
 * @property {Number} rate - Hash rate.
 * @emits MinerBlock#status
 */

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

  EventEmitter.call(this);

  this.network = Network.get(options.network);
  this.tip = options.tip;
  this.version = options.version;
  this.height = options.tip.height + 1;
  this.ts = options.ts;
  this.bits = options.bits;
  this.target = consensus.fromCompact(this.bits).toArrayLike(Buffer, 'le', 32);
  this.locktime = options.locktime;
  this.flags = options.flags;
  this.nonce1 = 0;
  this.nonce2 = 0;
  this.iterations = 0;
  this.coinbaseFlags = options.coinbaseFlags;
  this.witness = options.witness;
  this.address = options.address;
  this.reward = consensus.getReward(this.height, this.network.halvingInterval);

  this.destroyed = false;
  this.committed = false;

  this.sigops = options.sigops;
  this.weight = options.weight;
  this.fees = 0;
  this.items = [];

  this.coinbase = new TX();
  this.coinbase.mutable = true;

  this.block = new Block();
  this.block.mutable = true;

  this._init();
}

util.inherits(MinerBlock, EventEmitter);

/**
 * Nonce range interval.
 * @const {Number}
 * @default
 */

MinerBlock.INTERVAL = 0xffffffff / 1500 | 0;

/**
 * Calculate number of hashes.
 * @returns {Number}
 */

MinerBlock.prototype.getHashes = function() {
  return this.iterations * 0xffffffff + this.block.nonce;
};

/**
 * Calculate hashrate.
 * @returns {Number}
 */

MinerBlock.prototype.getRate = function() {
  return (this.block.nonce / (util.now() - this.begin)) | 0;
};

/**
 * Initialize the block.
 * @private
 */

MinerBlock.prototype._init = function _init() {
  var scale = consensus.WITNESS_SCALE_FACTOR;
  var hash = encoding.ZERO_HASH;
  var block = this.block;
  var cb = this.coinbase;
  var weight = 0;
  var sigops = 0;
  var input, output, commit, padding;

  assert(this.coinbaseFlags.length <= 20);

  // Setup our block.
  block.version = this.version;
  block.prevBlock = this.tip.hash;
  block.merkleRoot = encoding.NULL_HASH;
  block.ts = this.ts;
  block.bits = this.bits;
  block.nonce = 0;

  // Coinbase input.
  input = new Input();

  // Height (required in v2+ blocks)
  input.script.set(0, new BN(this.height));

  // Let the world know this little
  // miner succeeded.
  input.script.set(1, encoding.ZERO_HASH160);

  // Smaller nonce for good measure.
  input.script.set(2, util.nonce().slice(0, 4));

  // extraNonce - incremented when
  // the nonce overflows.
  input.script.set(3, this.extraNonce());

  input.script.compile();

  // Set up the witness nonce.
  if (this.witness) {
    input.witness.set(0, block.createWitnessNonce());
    input.witness.compile();
  }

  cb.inputs.push(input);

  // Reward output.
  output = new Output();
  output.script.fromPubkeyhash(encoding.ZERO_HASH160);

  cb.outputs.push(output);

  // If we're using segwit, we
  // need to set up the commitment.
  if (this.witness) {
    // Commitment output.
    commit = new Output();
    commit.script.fromCommitment(hash);
    cb.outputs.push(commit);
  }

  block.txs.push(cb);

  // Initialize weight.
  weight = block.getWeight();

  // 4 extra bytes for varint tx count.
  weight += 4 * scale;

  // Padding for the CB height (constant size).
  padding = 5 - input.script.code[0].getSize();
  assert(padding >= 0);

  weight += padding * scale;

  // Reserved size.
  // Without segwit:
  //   Block weight = 840
  //   Block stripped size = 210
  //   Block size = 210
  //   CB weight = 500
  //   CB stripped size = 125
  //   CB size = 125
  //   Sigops cost = 4
  // With segwit:
  //   Block weight = 1064
  //   Block stripped size = 257
  //   Block size = 293
  //   CB weight = 724
  //   CB stripped size = 172
  //   CB size = 208
  //   Sigops cost = 4
  if (!this.witness) {
    assert.equal(weight, 840);
    assert.equal(block.getBaseSize() + 4 + padding, 210);
    assert.equal(block.getSize() + 4 + padding, 210);
    assert.equal(cb.getWeight() + padding * scale, 500);
    assert.equal(cb.getBaseSize() + padding, 125);
    assert.equal(cb.getSize() + padding, 125);
  } else {
    assert.equal(weight, 1064);
    assert.equal(block.getBaseSize() + 4 + padding, 257);
    assert.equal(block.getSize() + 4 + padding, 293);
    assert.equal(cb.getWeight() + padding * scale, 724);
    assert.equal(cb.getBaseSize() + padding, 172);
    assert.equal(cb.getSize() + padding, 208);
  }

  // Initialize sigops weight.
  sigops = 4;

  // Setup coinbase flags (variable size).
  input.script.set(1, this.coinbaseFlags);
  input.script.compile();

  // Setup output script (variable size).
  output.script.clear();
  output.script.fromAddress(this.address);

  // Update commitments.
  this.refresh();

  // Ensure the variable size
  // stuff didn't break anything.
  assert(block.getWeight() <= weight,
    'Coinbase exceeds reserved size!');

  assert(cb.getSigopsCost(null, this.flags) <= sigops,
    'Coinbase exceeds reserved sigops!');

  assert(this.weight >= weight,
    'Coinbase exceeds reserved size!');

  assert(this.sigops >= sigops,
    'Coinbase exceeds reserved sigops!');
};

/**
 * Update coinbase, witness
 * commitment, and merkle root.
 */

MinerBlock.prototype.refresh = function refresh() {
  // Update coinbase.
  this.updateCoinbase();

  // Witness commitment.
  if (this.witness)
    this.updateCommitment();

  // Create our merkle root.
  this.updateMerkle();
};

/**
 * Update the commitment output for segwit.
 */

MinerBlock.prototype.updateCommitment = function updateCommitment() {
  var output = this.coinbase.outputs[1];
  var hash;

  // Recalculate witness merkle root.
  hash = this.block.createCommitmentHash();

  // Update commitment.
  output.script.clear();
  output.script.fromCommitment(hash);
};

/**
 * Update the extra nonce and coinbase reward.
 */

MinerBlock.prototype.updateCoinbase = function updateCoinbase() {
  var input = this.coinbase.inputs[0];
  var output = this.coinbase.outputs[0];

  // Update extra nonce.
  input.script.set(3, this.extraNonce());
  input.script.compile();

  // Update reward.
  output.value = this.reward + this.fees;
};

/**
 * Increment the extraNonce.
 */

MinerBlock.prototype.updateNonce = function updateNonce() {
  // Overflow the nonce and increment the extraNonce.
  this.block.nonce = 0;
  this.nonce1++;

  // Wrap at 4 bytes.
  if (this.nonce1 === 0xffffffff) {
    this.nonce1 = 0;
    this.nonce2++;
  }

  // We incremented the extraNonce, need to update coinbase.
  this.updateCoinbase();

  // We changed the coinbase, need to update merkleRoot.
  this.updateMerkle();
};

/**
 * Rebuild the merkle tree and update merkle root.
 */

MinerBlock.prototype.updateMerkle = function updateMerkle() {
  // Recalculate merkle root.
  this.block.merkleRoot = this.block.createMerkleRoot('hex');
};

/**
 * Render extraNonce.
 * @returns {Buffer}
 */

MinerBlock.prototype.extraNonce = function extraNonce() {
  var bw = new StaticWriter(8);
  bw.writeU32BE(this.nonce1);
  bw.writeU32BE(this.nonce2);
  return bw.render();
};

/**
 * Add a transaction to the block. Rebuilds the merkle tree,
 * updates coinbase and commitment.
 * @param {TX} tx
 * @returns {Boolean} Whether the transaction was successfully added.
 */

MinerBlock.prototype.addTX = function addTX(tx, view) {
  var hash = tx.hash('hex');
  var item, weight, sigops;

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

  if (this.block.hasTX(hash))
    return false;

  item = BlockEntry.fromTX(tx, view, this);
  weight = item.tx.getWeight();
  sigops = item.sigops;

  if (!tx.isFinal(this.height, this.locktime))
    return false;

  if (this.weight + weight > consensus.MAX_BLOCK_WEIGHT)
    return false;

  if (this.sigops + sigops > consensus.MAX_BLOCK_SIGOPS_COST)
    return false;

  if (!this.witness && tx.hasWitness())
    return false;

  this.weight += weight;
  this.sigops += sigops;
  this.fees += item.fee;

  // Add the tx to our block
  this.block.txs.push(tx);
  this.items.push(item);

  // Update commitments.
  this.refresh();

  return true;
};

/**
 * Hash until the nonce overflows.
 * @returns {Boolean} Whether the nonce was found.
 */

MinerBlock.prototype.findNonce = function findNonce() {
  var block = this.block;
  var target = this.target;
  var data = block.abbr();
  var interval = MinerBlock.INTERVAL;
  var min = 0;
  var max = interval;
  var nonce;

  while (max <= 0xffffffff) {
    nonce = mine(data, target, min, max);

    if (nonce !== -1)
      break;

    block.nonce = max;

    min += interval;
    max += interval;

    this.sendStatus();
  }

  return nonce;
};

/**
 * Hash until the nonce overflows.
 * @method
 * @returns {Promise} - Returns Boolean.
 */

MinerBlock.prototype.findNonceAsync = co(function* findNonceAsync() {
  var block = this.block;
  var target = this.target;
  var interval = MinerBlock.INTERVAL;
  var min = 0;
  var max = interval;
  var data, nonce;

  while (max <= 0xffffffff) {
    data = block.abbr();
    nonce = yield workerPool.mine(data, target, min, max);

    if (nonce !== -1)
      break;

    if (this.destroyed)
      return nonce;

    block.nonce = max;

    min += interval;
    max += interval;

    this.sendStatus();
  }

  return nonce;
});

/**
 * Mine synchronously until the block is found.
 * @returns {Block}
 */

MinerBlock.prototype.mine = function mine() {
  var nonce;

  // Track how long we've been at it.
  this.begin = util.now();

  for (;;) {
    nonce = this.findNonce();

    if (nonce !== -1)
      break;

    this.iterate();
  }

  this.commit(nonce);

  return this.block;
};

/**
 * Mine asynchronously until the block is found.
 * @method
 * @returns {Promise} - Returns {@link Block}.
 */

MinerBlock.prototype.mineAsync = co(function* mineAsync() {
  var nonce;

  // Track how long we've been at it.
  this.begin = util.now();

  for (;;) {
    nonce = yield this.findNonceAsync();

    if (nonce !== -1)
      break;

    if (this.destroyed)
      return;

    this.iterate();
  }

  this.commit(nonce);

  return this.block;
});

/**
 * Increment extraNonce, rebuild merkletree.
 */

MinerBlock.prototype.iterate = function iterate() {
  // Keep track of our iterations.
  this.iterations++;

  // Send progress report.
  this.sendStatus();

  // Overflow the nonce and increment the extraNonce.
  this.updateNonce();
};

/**
 * Commit and finalize mined block.
 * @returns {Block}
 */

MinerBlock.prototype.commit = function commit(nonce) {
  assert(!this.committed, 'Block is already committed.');
  this.committed = true;
  this.block.nonce = nonce;
  this.block.mutable = false;
  this.coinbase.mutable = false;
  return this.block;
};

/**
 * Send a progress report (emits `status`).
 */

MinerBlock.prototype.sendStatus = function sendStatus() {
  this.emit('status', {
    block: this.block,
    target: this.block.bits,
    hashes: this.getHashes(),
    hashrate: this.getRate(),
    height: this.height,
    best: util.revHex(this.tip.hash)
  });
};

/**
 * Destroy the minerblock. Stop mining.
 */

MinerBlock.prototype.destroy = function destroy() {
  this.destroyed = true;
};

/**
 * BlockEntry
 * @alias module:mining.BlockEntry
 * @constructor
 * @param {TX} tx
 * @property {TX} tx
 * @property {Hash} hash
 * @property {Amount} fee
 * @property {Rate} rate
 * @property {Number} priority
 * @property {Boolean} free
 * @property {Sigops} sigops
 * @property {Number} depCount
 */

function BlockEntry(tx) {
  this.tx = tx;
  this.hash = tx.hash('hex');
  this.fee = 0;
  this.rate = 0;
  this.priority = 0;
  this.free = false;
  this.sigops = 0;
  this.descRate = 0;
  this.depCount = 0;
}

/**
 * Instantiate block entry from transaction.
 * @param {TX} tx
 * @param {CoinView} view
 * @param {MinerBlock} attempt
 * @returns {BlockEntry}
 */

BlockEntry.fromTX = function fromTX(tx, view, attempt) {
  var item = new BlockEntry(tx);
  item.fee = tx.getFee(view);
  item.rate = tx.getRate(view);
  item.priority = tx.getPriority(view, attempt.height);
  item.free = false;
  item.sigops = tx.getSigopsCost(view, attempt.flags);
  item.descRate = item.rate;
  return item;
};

/**
 * Instantiate block entry from mempool entry.
 * @param {MempoolEntry} entry
 * @param {MinerBlock} attempt
 * @returns {BlockEntry}
 */

BlockEntry.fromEntry = function fromEntry(entry, attempt) {
  var item = new BlockEntry(entry.tx);
  item.fee = entry.getFee();
  item.rate = entry.getRate();
  item.priority = entry.getPriority(attempt.height);
  item.free = item.fee < policy.getMinFee(entry.size);
  item.sigops = entry.sigops;
  item.descRate = entry.getDescRate();
  return item;
};

/*
 * Expose
 */

exports = MinerBlock;
exports.MinerBlock = MinerBlock;
exports.BlockEntry = BlockEntry;

module.exports = exports;
