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

'use strict';

const assert = require('assert');
const BN = require('../crypto/bn');
const consensus = require('../protocol/consensus');
const util = require('../utils/util');
const digest = require('../crypto/digest');
const encoding = require('../utils/encoding');
const BufferReader = require('../utils/reader');
const StaticWriter = require('../utils/staticwriter');
const Headers = require('../primitives/headers');
const InvItem = require('../primitives/invitem');
const ZERO = new BN(0);

/**
 * Represents an entry in the chain. Unlike
 * other bitcoin fullnodes, we store the
 * chainwork _with_ the entry in order to
 * avoid reading the entire chain index on
 * boot and recalculating the chainworks.
 * @alias module:blockchain.ChainEntry
 * @constructor
 * @param {Chain} chain
 * @param {Object} options
 * @param {ChainEntry} prev
 * @property {Hash} hash
 * @property {Number} version - Transaction version. Note that Bcoin reads
 * versions as unsigned even though they are signed at the protocol level.
 * This value will never be negative.
 * @property {Hash} prevBlock
 * @property {Hash} merkleRoot
 * @property {Number} time
 * @property {Number} bits
 * @property {Number} nonce
 * @property {Number} height
 * @property {BN} chainwork
 * @property {ReversedHash} rhash - Reversed block hash (uint256le).
 */

function ChainEntry(chain, options, prev) {
  if (!(this instanceof ChainEntry))
    return new ChainEntry(chain, options, prev);

  this.chain = chain;
  this.hash = encoding.NULL_HASH;
  this.version = 1;
  this.prevBlock = encoding.NULL_HASH;
  this.merkleRoot = encoding.NULL_HASH;
  this.time = 0;
  this.bits = 0;
  this.nonce = 0;
  this.height = 0;
  this.chainwork = ZERO;

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

/**
 * The max chainwork (1 << 256).
 * @const {BN}
 */

ChainEntry.MAX_CHAINWORK = new BN(1).ushln(256);

/**
 * Size of set to pick median time from.
 * @const {Number}
 * @default
 */

ChainEntry.MEDIAN_TIMESPAN = 11;

/**
 * Inject properties from options.
 * @private
 * @param {Object} options
 * @param {ChainEntry} prev - Previous entry.
 */

ChainEntry.prototype.fromOptions = function fromOptions(options, prev) {
  assert(options, 'Block data is required.');
  assert(typeof options.hash === 'string');
  assert(util.isU32(options.version));
  assert(typeof options.prevBlock === 'string');
  assert(typeof options.merkleRoot === 'string');
  assert(util.isU32(options.time));
  assert(util.isU32(options.bits));
  assert(util.isU32(options.nonce));
  assert(util.isU32(options.height));
  assert(!options.chainwork || BN.isBN(options.chainwork));

  this.hash = options.hash;
  this.version = options.version;
  this.prevBlock = options.prevBlock;
  this.merkleRoot = options.merkleRoot;
  this.time = options.time;
  this.bits = options.bits;
  this.nonce = options.nonce;
  this.height = options.height;
  this.chainwork = options.chainwork || ZERO;

  if (!this.chainwork)
    this.chainwork = this.getChainwork(prev);

  return this;
};

/**
 * Instantiate chainentry from options.
 * @param {Chain} chain
 * @param {Object} options
 * @param {ChainEntry} prev - Previous entry.
 * @returns {ChainEntry}
 */

ChainEntry.fromOptions = function fromOptions(chain, options, prev) {
  return new ChainEntry(chain).fromOptions(options, prev);
};

/**
 * Calculate the proof: (1 << 256) / (target + 1)
 * @returns {BN} proof
 */

ChainEntry.prototype.getProof = function getProof() {
  const target = consensus.fromCompact(this.bits);

  if (target.isNeg() || target.cmpn(0) === 0)
    return new BN(0);

  return ChainEntry.MAX_CHAINWORK.div(target.iaddn(1));
};

/**
 * Calculate the chainwork by
 * adding proof to previous chainwork.
 * @returns {BN} chainwork
 */

ChainEntry.prototype.getChainwork = function getChainwork(prev) {
  const proof = this.getProof();

  if (!prev)
    return proof;

  return proof.iadd(prev.chainwork);
};

/**
 * Test against the genesis block.
 * @returns {Boolean}
 */

ChainEntry.prototype.isGenesis = function isGenesis() {
  return this.hash === this.chain.network.genesis.hash;
};

/**
 * Test whether the entry is in the main chain.
 * @method
 * @returns {Promise} - Return Boolean.
 */

ChainEntry.prototype.isMainChain = async function isMainChain() {
  if (this.hash === this.chain.tip.hash
      || this.hash === this.chain.network.genesis.hash) {
    return true;
  }

  const entry = this.chain.db.getCache(this.height);

  if (entry) {
    if (entry.hash === this.hash)
      return true;
    return false;
  }

  if (await this.chain.db.getNextHash(this.hash))
    return true;

  return false;
};

/**
 * Get ancestor by `height`.
 * @method
 * @param {Number} height
 * @returns {Promise} - Returns ChainEntry[].
 */

ChainEntry.prototype.getAncestor = async function getAncestor(height) {
  if (height < 0)
    return null;

  assert(height >= 0);
  assert(height <= this.height);

  if (await this.isMainChain())
    return await this.chain.db.getEntry(height);

  let entry = this;
  while (entry.height !== height) {
    entry = await entry.getPrevious();
    assert(entry);
  }

  return entry;
};

/**
 * Get previous entry.
 * @returns {Promise} - Returns ChainEntry.
 */

ChainEntry.prototype.getPrevious = function getPrevious() {
  return this.chain.db.getEntry(this.prevBlock);
};

/**
 * Get previous cached entry.
 * @returns {ChainEntry|null}
 */

ChainEntry.prototype.getPrevCache = function getPrevCache() {
  return this.chain.db.getCache(this.prevBlock);
};

/**
 * Get next entry.
 * @method
 * @returns {Promise} - Returns ChainEntry.
 */

ChainEntry.prototype.getNext = async function getNext() {
  const hash = await this.chain.db.getNextHash(this.hash);

  if (!hash)
    return null;

  return await this.chain.db.getEntry(hash);
};

/**
 * Get next entry.
 * @method
 * @returns {Promise} - Returns ChainEntry.
 */

ChainEntry.prototype.getNextEntry = async function getNextEntry() {
  const entry = await this.chain.db.getEntry(this.height + 1);

  if (!entry)
    return null;

  // Not on main chain.
  if (entry.prevBlock !== this.hash)
    return null;

  return entry;
};

/**
 * Calculate median time past.
 * @method
 * @param {Number?} time
 * @returns {Promise} - Returns Number.
 */

ChainEntry.prototype.getMedianTime = async function getMedianTime(time) {
  let timespan = ChainEntry.MEDIAN_TIMESPAN;
  const median = [];

  // In case we ever want to check
  // the MTP of the _current_ block
  // (necessary for BIP148).
  if (time != null) {
    median.push(time);
    timespan -= 1;
  }

  let entry = this;
  for (let i = 0; i < timespan && entry; i++) {
    median.push(entry.time);

    const cache = entry.getPrevCache();

    if (cache) {
      entry = cache;
      continue;
    }

    entry = await entry.getPrevious();
  }

  median.sort(cmp);

  return median[median.length >>> 1];
};

/**
 * Test whether the entry is potentially
 * an ancestor of a checkpoint.
 * @returns {Boolean}
 */

ChainEntry.prototype.isHistorical = function isHistorical() {
  if (this.chain.options.checkpoints) {
    if (this.height + 1 <= this.chain.network.lastCheckpoint)
      return true;
  }
  return false;
};

/**
 * Test whether the entry contains an unknown version bit.
 * @returns {Boolean}
 */

ChainEntry.prototype.hasUnknown = function hasUnknown() {
  const bits = this.version & consensus.VERSION_TOP_MASK;
  const topBits = consensus.VERSION_TOP_BITS;

  if ((bits >>> 0) !== topBits)
    return false;

  return (this.version & this.chain.network.unknownBits) !== 0;
};

/**
 * Test whether the entry contains a version bit.
 * @param {Number} bit
 * @returns {Boolean}
 */

ChainEntry.prototype.hasBit = function hasBit(bit) {
  return consensus.hasBit(this.version, bit);
};

/**
 * Get little-endian block hash.
 * @returns {Hash}
 */

ChainEntry.prototype.rhash = function rhash() {
  return util.revHex(this.hash);
};

/**
 * Inject properties from block.
 * @private
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} prev - Previous entry.
 */

ChainEntry.prototype.fromBlock = function fromBlock(block, prev) {
  this.hash = block.hash('hex');
  this.version = block.version;
  this.prevBlock = block.prevBlock;
  this.merkleRoot = block.merkleRoot;
  this.time = block.time;
  this.bits = block.bits;
  this.nonce = block.nonce;
  this.height = prev ? prev.height + 1: 0;
  this.chainwork = this.getChainwork(prev);
  return this;
};

/**
 * Instantiate chainentry from block.
 * @param {Chain} chain
 * @param {Block|MerkleBlock} block
 * @param {ChainEntry} prev - Previous entry.
 * @returns {ChainEntry}
 */

ChainEntry.fromBlock = function fromBlock(chain, block, prev) {
  return new ChainEntry(chain).fromBlock(block, prev);
};

/**
 * Serialize the entry to internal database format.
 * @returns {Buffer}
 */

ChainEntry.prototype.toRaw = function toRaw() {
  const bw = new StaticWriter(116);

  bw.writeU32(this.version);
  bw.writeHash(this.prevBlock);
  bw.writeHash(this.merkleRoot);
  bw.writeU32(this.time);
  bw.writeU32(this.bits);
  bw.writeU32(this.nonce);
  bw.writeU32(this.height);
  bw.writeBytes(this.chainwork.toArrayLike(Buffer, 'le', 32));

  return bw.render();
};

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

ChainEntry.prototype.fromRaw = function fromRaw(data) {
  const br = new BufferReader(data, true);
  const hash = digest.hash256(br.readBytes(80));

  br.seek(-80);

  this.hash = hash.toString('hex');
  this.version = br.readU32();
  this.prevBlock = br.readHash('hex');
  this.merkleRoot = br.readHash('hex');
  this.time = br.readU32();
  this.bits = br.readU32();
  this.nonce = br.readU32();
  this.height = br.readU32();
  this.chainwork = new BN(br.readBytes(32), 'le');

  return this;
};

/**
 * Deserialize the entry.
 * @param {Chain} chain
 * @param {Buffer} data
 * @returns {ChainEntry}
 */

ChainEntry.fromRaw = function fromRaw(chain, data) {
  return new ChainEntry(chain).fromRaw(data);
};

/**
 * Serialize the entry to an object more
 * suitable for JSON serialization.
 * @returns {Object}
 */

ChainEntry.prototype.toJSON = function toJSON() {
  return {
    hash: util.revHex(this.hash),
    version: this.version,
    prevBlock: util.revHex(this.prevBlock),
    merkleRoot: util.revHex(this.merkleRoot),
    time: this.time,
    bits: this.bits,
    nonce: this.nonce,
    height: this.height,
    chainwork: this.chainwork.toString('hex', 64)
  };
};

/**
 * Inject properties from json object.
 * @private
 * @param {Object} json
 */

ChainEntry.prototype.fromJSON = function fromJSON(json) {
  assert(json, 'Block data is required.');
  assert(typeof json.hash === 'string');
  assert(util.isU32(json.version));
  assert(typeof json.prevBlock === 'string');
  assert(typeof json.merkleRoot === 'string');
  assert(util.isU32(json.time));
  assert(util.isU32(json.bits));
  assert(util.isU32(json.nonce));
  assert(typeof json.chainwork === 'string');

  this.hash = util.revHex(json.hash);
  this.version = json.version;
  this.prevBlock = util.revHex(json.prevBlock);
  this.merkleRoot = util.revHex(json.merkleRoot);
  this.time = json.time;
  this.bits = json.bits;
  this.nonce = json.nonce;
  this.height = json.height;
  this.chainwork = new BN(json.chainwork, 'hex');

  return this;
};

/**
 * Instantiate block from jsonified object.
 * @param {Chain} chain
 * @param {Object} json
 * @returns {ChainEntry}
 */

ChainEntry.fromJSON = function fromJSON(chain, json) {
  return new ChainEntry(chain).fromJSON(json);
};

/**
 * Convert the entry to a headers object.
 * @returns {Headers}
 */

ChainEntry.prototype.toHeaders = function toHeaders() {
  return Headers.fromEntry(this);
};

/**
 * Convert the entry to an inv item.
 * @returns {InvItem}
 */

ChainEntry.prototype.toInv = function toInv() {
  return new InvItem(InvItem.types.BLOCK, this.hash);
};

/**
 * Return a more user-friendly object.
 * @returns {Object}
 */

ChainEntry.prototype.inspect = function inspect() {
  const json = this.toJSON();
  json.version = util.hex32(json.version);
  return json;
};

/**
 * Test whether an object is a {@link ChainEntry}.
 * @param {Object} obj
 * @returns {Boolean}
 */

ChainEntry.isChainEntry = function isChainEntry(obj) {
  return obj
    && BN.isBN(obj.chainwork)
    && typeof obj.getMedianTime === 'function';
};

/*
 * Helpers
 */

function cmp(a, b) {
  return a - b;
}

/*
 * Expose
 */

module.exports = ChainEntry;
