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

'use strict';

var assert = require('assert');
var BufferReader = require('../utils/reader');
var StaticWriter = require('../utils/staticwriter');
var encoding = require('../utils/encoding');
var Output = require('../primitives/output');
var Coins = require('./coins');
var compressor = require('./compress');
var compress = compressor.compress;
var decompress = compressor.decompress;

/**
 * UndoCoins
 * Coins need to be resurrected from somewhere
 * during a reorg. The undo coins store all
 * spent coins in a single record per block
 * (in a compressed format).
 * @alias module:coins.UndoCoins
 * @constructor
 * @property {UndoCoin[]} items
 */

function UndoCoins() {
  if (!(this instanceof UndoCoins))
    return new UndoCoins();

  this.items = [];
}

/**
 * Push coin entry onto undo coin array.
 * @param {CoinEntry}
 */

UndoCoins.prototype.push = function push(entry) {
  var undo = new UndoCoin();
  undo.entry = entry;
  this.items.push(undo);
};

/**
 * Calculate undo coins size.
 * @returns {Number}
 */

UndoCoins.prototype.getSize = function getSize() {
  var size = 0;
  var i, coin;

  size += 4;

  for (i = 0; i < this.items.length; i++) {
    coin = this.items[i];
    size += coin.getSize();
  }

  return size;
};

/**
 * Serialize all undo coins.
 * @returns {Buffer}
 */

UndoCoins.prototype.toRaw = function toRaw() {
  var size = this.getSize();
  var bw = new StaticWriter(size);
  var i, coin;

  bw.writeU32(this.items.length);

  for (i = 0; i < this.items.length; i++) {
    coin = this.items[i];
    coin.toWriter(bw);
  }

  return bw.render();
};

/**
 * Inject properties from serialized data.
 * @private
 * @param {Buffer} data
 * @returns {UndoCoins}
 */

UndoCoins.prototype.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data);
  var count = br.readU32();
  var i;

  for (i = 0; i < count; i++)
    this.items.push(UndoCoin.fromReader(br));

  return this;
};

/**
 * Instantiate undo coins from serialized data.
 * @param {Buffer} data
 * @returns {UndoCoins}
 */

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

/**
 * Test whether the undo coins have any members.
 * @returns {Boolean}
 */

UndoCoins.prototype.isEmpty = function isEmpty() {
  return this.items.length === 0;
};

/**
 * Render the undo coins.
 * @returns {Buffer}
 */

UndoCoins.prototype.commit = function commit() {
  var raw = this.toRaw();
  this.items.length = 0;
  return raw;
};

/**
 * Retrieve the last undo coin.
 * @returns {UndoCoin}
 */

UndoCoins.prototype.top = function top() {
  return this.items[this.items.length - 1];
};

/**
 * Re-apply undo coins to a view, effectively unspending them.
 * @param {CoinView} view
 * @param {Outpoint} outpoint
 */

UndoCoins.prototype.apply = function apply(view, outpoint) {
  var undo = this.items.pop();
  var hash = outpoint.hash;
  var index = outpoint.index;
  var coins;

  assert(undo);

  if (undo.height !== -1) {
    coins = new Coins();

    assert(!view.map[hash]);
    view.map[hash] = coins;

    coins.hash = hash;
    coins.coinbase = undo.coinbase;
    coins.height = undo.height;
    coins.version = undo.version;
  } else {
    coins = view.map[hash];
    assert(coins);
  }

  coins.addOutput(index, undo.toOutput());

  assert(coins.has(index));
};

/**
 * UndoCoin
 * @alias module:coins.UndoCoin
 * @constructor
 * @property {CoinEntry|null} entry
 * @property {Output|null} output
 * @property {Number} version
 * @property {Number} height
 * @property {Boolean} coinbase
 */

function UndoCoin() {
  this.entry = null;
  this.output = null;
  this.version = -1;
  this.height = -1;
  this.coinbase = false;
}

/**
 * Convert undo coin to an output.
 * @returns {Output}
 */

UndoCoin.prototype.toOutput = function toOutput() {
  if (!this.output) {
    assert(this.entry);
    return this.entry.toOutput();
  }
  return this.output;
};

/**
 * Calculate undo coin size.
 * @returns {Number}
 */

UndoCoin.prototype.getSize = function getSize() {
  var height = this.height;
  var size = 0;

  if (height === -1)
    height = 0;

  size += encoding.sizeVarint(height * 2 + (this.coinbase ? 1 : 0));

  if (this.height !== -1)
    size += encoding.sizeVarint(this.version);

  if (this.entry) {
    // Cached from spend.
    size += this.entry.getSize();
  } else {
    size += compress.size(this.output);
  }

  return size;
};

/**
 * Write the undo coin to a buffer writer.
 * @param {BufferWriter} bw
 */

UndoCoin.prototype.toWriter = function toWriter(bw) {
  var height = this.height;

  assert(height !== 0);

  if (height === -1)
    height = 0;

  bw.writeVarint(height * 2 + (this.coinbase ? 1 : 0));

  if (this.height !== -1) {
    assert(this.version !== -1);
    bw.writeVarint(this.version);
  }

  if (this.entry) {
    // Cached from spend.
    this.entry.toWriter(bw);
  } else {
    compress.output(this.output, bw);
  }

  return bw;
};

/**
 * Serialize the undo coin.
 * @returns {Buffer}
 */

UndoCoin.prototype.toRaw = function toRaw() {
  var size = this.getSize();
  return this.toWriter(new StaticWriter(size)).render();
};

/**
 * Inject properties from buffer reader.
 * @private
 * @param {BufferReader} br
 * @returns {UndoCoin}
 */

UndoCoin.prototype.fromReader = function fromReader(br) {
  var code = br.readVarint();

  this.output = new Output();

  this.height = code / 2 | 0;

  if (this.height === 0)
    this.height = -1;

  this.coinbase = (code & 1) !== 0;

  if (this.height !== -1)
    this.version = br.readVarint();

  decompress.output(this.output, br);

  return this;
};

/**
 * Inject properties from serialized data.
 * @private
 * @param {Buffer} data
 * @returns {UndoCoin}
 */

UndoCoin.prototype.fromRaw = function fromRaw(data) {
  return this.fromReader(new BufferReader(data));
};

/**
 * Instantiate undo coin from serialized data.
 * @param {Buffer} data
 * @returns {UndoCoin}
 */

UndoCoin.fromReader = function fromReader(br) {
  return new UndoCoin().fromReader(br);
};

/**
 * Instantiate undo coin from serialized data.
 * @param {Buffer} data
 * @returns {UndoCoin}
 */

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

/*
 * Expose
 */

exports = UndoCoins;
exports.UndoCoins = UndoCoins;
exports.UndoCoin = UndoCoin;

module.exports = exports;
