/*!
 * bip152.js - compact block object for bcoin
 * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var utils = require('../utils/utils');
var BufferReader = require('../utils/reader');
var BufferWriter = require('../utils/writer');
var co = require('../utils/co');
var crypto = require('../crypto/crypto');
var assert = require('assert');
var constants = require('../protocol/constants');
var siphash = require('../crypto/siphash');
var AbstractBlock = require('../primitives/abstractblock');
var TX = require('../primitives/tx');
var Block = require('../primitives/block');

/**
 * Represents a compact block (bip152): `cmpctblock` packet.
 * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki
 * @exports CompactBlock
 * @constructor
 * @extends AbstractBlock
 * @param {NakedBlock} options
 * @property {TX[]} available - Available transaction vector.
 */

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

  AbstractBlock.call(this, options);

  this.keyNonce = null;
  this.ids = [];
  this.ptx = [];

  this.available = [];
  this.idMap = {};
  this.count = 0;
  this.sipKey = null;
  this.timeout = null;
  this.callback = null;

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

utils.inherits(CompactBlock, AbstractBlock);

CompactBlock.prototype._verify = function _verify(ret) {
  return this.verifyHeaders(ret);
};

CompactBlock.prototype.fromOptions = function fromOptions(options) {
  assert(Buffer.isBuffer(options.keyNonce));
  assert(Array.isArray(options.ids));
  assert(Array.isArray(options.ptx));

  this.keyNonce = options.keyNonce;
  this.ids = options.ids;
  this.ptx = options.ptx;

  if (options.available)
    this.available = options.available;

  if (options.idMap)
    this.idMap = options.idMap;

  if (options.count)
    this.count = options.count;

  this.sipKey = options.sipKey;

  this.initKey();
  this.init();

  return this;
};

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

CompactBlock.prototype.fromRaw = function fromRaw(data) {
  var p = new BufferReader(data);
  var i, count, index, tx;

  this.version = p.readU32(); // Technically signed
  this.prevBlock = p.readHash('hex');
  this.merkleRoot = p.readHash('hex');
  this.ts = p.readU32();
  this.bits = p.readU32();
  this.nonce = p.readU32();

  this.keyNonce = p.readBytes(8);

  this.initKey();

  count = p.readVarint();

  this.totalTX += count;

  for (i = 0; i < count; i++)
    this.ids.push(p.readU32() + p.readU16() * 0x100000000);

  count = p.readVarint();

  this.totalTX += count;

  for (i = 0; i < count; i++) {
    index = p.readVarint();
    assert(index <= 0xffff);
    assert(index < this.totalTX);
    tx = TX.fromRaw(p);
    this.ptx.push([index, tx]);
  }

  this.init();

  return this;
};

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

CompactBlock.prototype.toRaw = function toRaw(writer) {
  return this.frame(true, writer);
};

CompactBlock.prototype.toNormal = function toNormal(writer) {
  return this.frame(false, writer);
};

CompactBlock.prototype.frame = function frame(witness, writer) {
  var p = BufferWriter(writer);
  var i, id, lo, hi, ptx;

  p.writeU32(this.version);
  p.writeHash(this.prevBlock);
  p.writeHash(this.merkleRoot);
  p.writeU32(this.ts);
  p.writeU32(this.bits);
  p.writeU32(this.nonce);

  p.writeBytes(this.keyNonce);

  p.writeVarint(this.ids.length);

  for (i = 0; i < this.ids.length; i++) {
    id = this.ids[i];
    lo = id % 0x100000000;
    hi = (id - lo) / 0x100000000;
    hi &= 0xffff;
    p.writeU32(lo);
    p.writeU16(hi);
  }

  p.writeVarint(this.ptx.length);

  for (i = 0; i < this.ptx.length; i++) {
    ptx = this.ptx[i];
    p.writeVarint(ptx[0]);
    if (witness)
      ptx[1].toRaw(p);
    else
      ptx[1].toNormal(p);
  }

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

  return p;
};

CompactBlock.prototype.toRequest = function toRequest() {
  return TXRequest.fromCompact(this);
};

CompactBlock.prototype.fillMempool = function fillMempool(mempool) {
  var have = {};
  var hashes = mempool.getSnapshot();
  var i, id, index, hash, tx;

  for (i = 0; i < hashes.length; i++) {
    hash = hashes[i];
    id = this.sid(hash);
    index = this.idMap[id];

    if (index == null)
      continue;

    if (have[index]) {
      // Siphash collision, just request it.
      this.available[index] = null;
      this.count--;
      continue;
    }

    tx = mempool.getTX(hash);

    if (!tx)
      continue;

    this.available[index] = tx;
    have[index] = true;
    this.count++;

    // We actually may have a siphash collision
    // here, but exit early anyway for perf.
    if (this.count === this.totalTX)
      return true;
  }

  return false;
};

CompactBlock.prototype.fillMissing = function fillMissing(res) {
  var offset = 0;
  var i;

  for (i = 0; i < this.available.length; i++) {
    if (this.available[i])
      continue;

    if (offset >= res.txs.length)
      return false;

    this.available[i] = res.txs[offset++];
  }

  return offset === res.txs.length;
};

CompactBlock.prototype.sid = function sid(hash) {
  if (typeof hash === 'string')
    hash = new Buffer(hash, 'hex');

  hash = siphash(hash, this.sipKey);

  return hash.readUInt32LE(0, true)
    + hash.readUInt16LE(4, true)
    * 0x100000000;
};

CompactBlock.prototype.hasIndex = function hasIndex(index) {
  return this.available[index] != null;
};

CompactBlock.prototype.initKey = function initKey() {
  var data = new Buffer(88);
  var hash;

  this.abbr().copy(data, 0);
  this.keyNonce.copy(data, 80);

  hash = crypto.sha256(data);

  this.sipKey = hash.slice(0, 16);
};

CompactBlock.prototype.init = function init() {
  var i, last, ptx, offset, id;

  if (this.totalTX === 0)
    throw new Error('Empty vectors.');

  if (this.totalTX > constants.block.MAX_SIZE / 10)
    throw new Error('Compact block too big.');

  // No sparse arrays here, v8.
  for (i = 0; i < this.totalTX; i++)
    this.available.push(null);

  last = -1;

  for (i = 0; i < this.ptx.length; i++) {
    ptx = this.ptx[i];
    assert(ptx);
    last += ptx[0] + 1;
    assert(last <= 0xffff);
    assert(last <= this.ids.length + i);
    this.available[last] = ptx[1];
    this.count++;
  }

  offset = 0;

  for (i = 0; i < this.ids.length; i++) {
    while (this.available[i + offset])
      offset++;

    id = this.ids[i];

    // Fails on siphash collision
    assert(!this.idMap[id], 'Siphash collision.');

    this.idMap[id] = i + offset;

    // We're supposed to fail here if there's
    // more than 12 hash collisions, but we
    // don't have lowlevel access to our hash
    // table. Hopefully we don't get hashdos'd.
  }
};

CompactBlock.prototype.toBlock = function toBlock() {
  var block = new Block();
  var i, tx;

  block.version = this.version;
  block.prevBlock = this.prevBlock;
  block.merkleRoot = this.merkleRoot;
  block.ts = this.ts;
  block.bits = this.bits;
  block.nonce = this.nonce;
  block.totalTX = this.totalTX;
  block.txs = new Array(this.ptx.length);
  block._hash = this._hash;

  for (i = 0; i < this.available.length; i++) {
    tx = this.available[i];
    assert(tx, 'Compact block is not full.');
    tx.setBlock(block, i);
    block.txs[i] = tx;
  }

  return block;
};

CompactBlock.prototype.fromBlock = function fromBlock(block, nonce) {
  var i, tx, id;

  this.version = block.version;
  this.prevBlock = block.prevBlock;
  this.merkleRoot = block.merkleRoot;
  this.ts = block.ts;
  this.bits = block.bits;
  this.nonce = block.nonce;
  this.totalTX = block.totalTX;

  if (!nonce)
    nonce = utils.nonce(true);

  this.keyNonce = nonce;

  this.initKey();

  for (i = 1; i < block.txs.length; i++) {
    tx = block.txs[i];
    id = this.sid(tx.hash());
    this.ids.push(id);
  }

  this.ptx.push([0, block.txs[0]]);

  return this;
};

CompactBlock.fromBlock = function fromBlock(block, nonce) {
  return new CompactBlock().fromBlock(block, nonce);
};

CompactBlock.prototype.wait = function wait(time) {
  var self = this;
  return new Promise(function(resolve, reject) {
    self._wait(time, co.wrap(resolve, reject));
  });
};

CompactBlock.prototype._wait = function wait(time, callback) {
  var self = this;
  assert(this.timeout == null);
  this.callback = callback;
  this.timeout = setTimeout(function() {
    self.complete(new Error('Timed out.'));
  }, time);
};

CompactBlock.prototype.complete = function complete(err) {
  if (this.timeout != null) {
    clearTimeout(this.timeout);
    this.timeout = null;
    this.callback(err);
  }
};

CompactBlock.prototype.destroy = function destroy() {
  if (this.timeout != null) {
    clearTimeout(this.timeout);
    this.timeout = null;
  }
};

/**
 * Represents a BlockTransactionsRequest (bip152): `getblocktxn` packet.
 * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki
 * @constructor
 */

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

  this.hash = null;
  this.indexes = [];

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

TXRequest.prototype.fromOptions = function fromOptions(options) {
  this.hash = options.hash;

  if (options.indexes)
    this.indexes = options.indexes;

  return this;
};

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

TXRequest.prototype.fromCompact = function fromCompact(block) {
  var i;

  this.hash = block.hash('hex');

  for (i = 0; i < block.available.length; i++) {
    if (!block.available[i])
      this.indexes.push(i);
  }

  return this;
};

TXRequest.fromCompact = function fromCompact(block) {
  return new TXRequest().fromCompact(block);
};

TXRequest.prototype.fromRaw = function fromRaw(data) {
  var p = BufferReader(data);
  var i, count, index, offset;

  this.hash = p.readHash('hex');

  count = p.readVarint();

  for (i = 0; i < count; i++) {
    index = p.readVarint();
    assert(index <= 0xffff);
    this.indexes.push(index);
  }

  offset = 0;

  for (i = 0; i < count; i++) {
    index = this.indexes[i];
    index += offset;
    assert(index <= 0xffff);
    this.indexes[i] = index;
    offset = index + 1;
  }

  return this;
};

TXRequest.fromRaw = function fromRaw(data) {
  return new TXRequest().fromRaw(data);
};

TXRequest.prototype.toRaw = function toRaw(writer) {
  var p = BufferWriter(writer);
  var i, index;

  p.writeHash(this.hash);

  p.writeVarint(this.indexes.length);

  for (i = 0; i < this.indexes.length; i++) {
    index = this.indexes[i] - (i === 0 ? 0 : this.indexes[i - 1] + 1);
    p.writeVarint(index);
  }

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

  return p;
};

/**
 * Represents BlockTransactions (bip152): `blocktxn` packet.
 * @see https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki
 * @constructor
 */

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

  this.hash = null;
  this.txs = [];

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

TXResponse.prototype.fromOptions = function fromOptions(options) {
  this.hash = options.hash;

  if (options.txs)
    this.txs = options.txs;

  return this;
};

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

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

  this.hash = p.readHash('hex');

  count = p.readVarint();

  for (i = 0; i < count; i++)
    this.txs.push(TX.fromRaw(p));

  return this;
};

TXResponse.fromRaw = function fromRaw(data) {
  return new TXResponse().fromRaw(data);
};

TXResponse.prototype.fromBlock = function fromBlock(block, req) {
  var i, index;

  this.hash = req.hash;

  for (i = 0; i < req.indexes.length; i++) {
    index = req.indexes[i];
    if (index >= block.txs.length)
      return this;
    this.txs.push(block.txs[index]);
  }

  return this;
};

TXResponse.fromBlock = function fromBlock(block, req) {
  return new TXResponse().fromBlock(block, req);
};

TXResponse.prototype.toRaw = function toRaw(writer) {
  return this.frame(true, writer);
};

TXResponse.prototype.toNormal = function toNormal(writer) {
  return this.frame(false, writer);
};

TXResponse.prototype.frame = function frame(witness, writer) {
  var p = BufferWriter(writer);
  var i, tx;

  p.writeHash(this.hash);

  p.writeVarint(this.txs.length);

  for (i = 0; i < this.txs.length; i++) {
    tx = this.txs[i];
    if (witness)
      tx.toRaw(p);
    else
      tx.toNormal(p);
  }

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

  return p;
};

/*
 * Expose
 */

exports.CompactBlock = CompactBlock;
exports.TXRequest = TXRequest;
exports.TXResponse = TXResponse;
