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

'use strict';

var utils = require('../utils/utils');
var co = require('../utils/co');
var assert = require('assert');
var AsyncObject = require('../utils/async');
var MinerBlock = require('./minerblock');
var Address = require('../primitives/address');
var Workers = require('../workers/workers');
var time = require('../net/timedata');

/**
 * A bitcoin miner (supports mining witness blocks).
 * @exports Miner
 * @constructor
 * @param {Object} options
 * @param {Base58Address} options.address - Payout address.
 * @param {String?} [options.coinbaseFlags="mined by bcoin"]
 * @property {Boolean} running
 * @property {Boolean} loaded
 * @emits Miner#block
 * @emits Miner#status
 */

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

  AsyncObject.call(this);

  if (!options)
    options = {};

  this.options = options;
  this.address = Address(this.options.address);
  this.coinbaseFlags = this.options.coinbaseFlags || 'mined by bcoin';
  this.version = null;

  if (typeof this.coinbaseFlags === 'string')
    this.coinbaseFlags = new Buffer(this.coinbaseFlags, 'utf8');

  this.pool = options.pool;
  this.chain = options.chain;
  this.logger = options.logger || this.chain.logger;
  this.mempool = options.mempool;
  this.fees = this.mempool ? this.mempool.fees : options.fees;

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

  this.network = this.chain.network;
  this.running = false;
  this.timeout = null;
  this.attempt = null;
  this.workerPool = null;

  this._init();
}

utils.inherits(Miner, AsyncObject);

/**
 * Initialize the miner.
 * @private
 */

Miner.prototype._init = function _init() {
  var self = this;

  if (this.mempool) {
    this.mempool.on('tx', function(tx) {
      if (!self.running)
        return;
      if (self.attempt)
        self.attempt.addTX(tx.clone());
    });
  } else if (this.pool) {
    this.pool.on('tx', function(tx) {
      if (!self.running)
        return;
      if (self.attempt)
        self.attempt.addTX(tx.clone());
    });
  }

  this.chain.on('tip', function(tip) {
    if (!self.running)
      return;
    self.stop();
    setTimeout(function() {
      self.start();
    }, self.network.type === 'regtest' ? 100 : 5000);
  });

  this.on('block', function(block) {
    // Emit the block hex as a failsafe (in case we can't send it)
    self.logger.info('Found block: %d (%s).', block.height, block.rhash);
    self.logger.debug('Raw: %s', block.toRaw().toString('hex'));
  });

  this.on('status', function(stat) {
    self.logger.info(
      'Miner: hashrate=%dkhs hashes=%d target=%d height=%d best=%s',
      stat.hashrate / 1000 | 0,
      stat.hashes,
      stat.target,
      stat.height,
      stat.best);
  });

  if (Workers.enabled) {
    this.workerPool = new Workers({
      size: 1,
      timeout: -1
    });

    this.workerPool.on('error', function(err) {
      self.emit('error', err);
    });

    this.workerPool.on('status', function(stat) {
      self.emit('status', stat);
    });
  }
};

/**
 * Open the miner, wait for the chain and mempool to load.
 * @alias Miner#open
 * @returns {Promise}
 */

Miner.prototype._open = co(function* open() {
  if (this.mempool)
    yield this.mempool.open();
  else
    yield this.chain.open();

  this.logger.info('Miner loaded (flags=%s).',
    this.coinbaseFlags.toString('utf8'));
});

/**
 * Close the miner.
 * @alias Miner#close
 * @returns {Promise}
 */

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

/**
 * Start mining.
 * @param {Number?} version - Custom block version.
 */

Miner.prototype.start = co(function* start() {
  var self = this;
  var attempt, block;

  this.stop();

  this.running = true;

  // Create a new block and start hashing
  try {
    attempt = yield this.createBlock();
  } catch (e) {
    this.emit('error', e);
    return;
  }

  if (!this.running)
    return;

  this.attempt = attempt;

  attempt.on('status', function(status) {
    self.emit('status', status);
  });

  try {
    block = yield attempt.mineAsync();
  } catch (e) {
    if (!this.running)
      return;
    this.emit('error', e);
    return this.start();
  }

  // Add our block to the chain
  try {
    yield this.chain.add(block);
  } catch (err) {
    if (err.type === 'VerifyError')
      this.logger.warning('%s could not be added to chain.', block.rhash);
    this.emit('error', err);
    this.start();
    return;
  }

  // Emit our newly found block
  this.emit('block', block);

  // `tip` will now be emitted by chain
  // and the whole process starts over.
});

/**
 * Stop mining.
 */

Miner.prototype.stop = function stop() {
  if (!this.running)
    return;

  this.running = false;

  if (this.attempt) {
    this.attempt.destroy();
    this.attempt = null;
  }

  if (this.workerPool)
    this.workerPool.destroy();
};

/**
 * Create a block "attempt".
 * @param {Number?} version - Custom block version.
 * @returns {Promise} - Returns {@link MinerBlock}.
 */

Miner.prototype.createBlock = co(function* createBlock(tip) {
  var i, ts, attempt, txs, tx, target, version;

  if (!this.loaded)
    yield this.open();

  if (!tip)
    tip = this.chain.tip;

  assert(tip);

  ts = Math.max(time.now(), tip.ts + 1);

  // Find target
  target = yield this.chain.getTargetAsync(ts, tip);

  if (this.version != null) {
    version = this.version;
  } else {
    // Calculate version with versionbits
    version = yield this.chain.computeBlockVersion(tip);
  }

  attempt = new MinerBlock({
    workerPool: this.workerPool,
    tip: tip,
    version: version,
    target: target,
    address: this.address,
    coinbaseFlags: this.coinbaseFlags,
    witness: this.chain.segwitActive,
    network: this.network
  });

  if (!this.mempool)
    return attempt;

  txs = this.mempool.getHistory();

  for (i = 0; i < txs.length; i++) {
    tx = txs[i];
    attempt.addTX(tx);
  }

  return attempt;
});

/**
 * Mine a single block.
 * @param {Number?} version - Custom block version.
 * @returns {Promise} - Returns [{@link Block}].
 */

Miner.prototype.mineBlock = co(function* mineBlock(tip) {
  // Create a new block and start hashing
  var attempt = yield this.createBlock(tip);
  return yield attempt.mineAsync();
});

/*
 * Expose
 */

module.exports = Miner;
