/*!
 * witness.js - witness 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';

var assert = require('assert');
var BN = require('bn.js');
var util = require('../utils/util');
var Script = require('./script');
var common = require('./common');
var encoding = require('../utils/encoding');
var Opcode = require('./opcode');
var BufferReader = require('../utils/reader');
var StaticWriter = require('../utils/staticwriter');
var Address = require('../primitives/address');
var Stack = require('./stack');
var opcodes = common.opcodes;
var scriptTypes = common.types;
var STACK_FALSE = common.STACK_FALSE;
var STACK_NEGATE = common.STACK_NEGATE;

/**
 * Refers to the witness field of segregated witness transactions.
 * @alias module:script.Witness
 * @constructor
 * @param {Buffer[]|NakedWitness} items - Array of
 * stack items.
 * @property {Buffer[]} items
 * @property {Script?} redeem
 * @property {Number} length
 */

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

  this.items = [];

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

/**
 * Getter to retrieve witness vector length.
 * @name module:script.Witness#length_getter
 * @method
 * @private
 * @returns {Number}
 */

Witness.prototype.__defineGetter__('length', function() {
  return this.items.length;
});

/**
 * Setter to set witness vector length.
 * @name module:script.Witness#length_setter
 * @method
 * @private
 * @param {Number} value
 * @returns {Number}
 */

Witness.prototype.__defineSetter__('length', function(length) {
  return this.items.length = length;
});

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

Witness.prototype.fromOptions = function fromOptions(options) {
  var items;

  assert(options, 'Witness data is required.');

  items = options.items;

  if (!items)
    items = options;

  if (items)
    this.fromArray(items);

  return this;
};

/**
 * Instantiate witness from options.
 * @param {Object} options
 * @returns {Witness}
 */

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

/**
 * Convert witness to an array of buffers.
 * @returns {Buffer[]}
 */

Witness.prototype.toArray = function toArray() {
  return this.items.slice();
};

/**
 * Inject properties from an array of buffers.
 * @private
 * @param {Buffer[]} items
 */

Witness.prototype.fromArray = function fromArray(items) {
  assert(Array.isArray(items));
  this.items = items;
  return this;
};

/**
 * Insantiate witness from an array of buffers.
 * @param {Buffer[]} items
 * @returns {Witness}
 */

Witness.fromArray = function fromArray(items) {
  return new Witness().fromArray(items);
};

/**
 * Inspect a Witness object.
 * @returns {String} Human-readable script.
 */

Witness.prototype.inspect = function inspect() {
  return '<Witness: ' + this.toString() + '>';
};

/**
 * Convert a Witness object to a String.
 * @returns {String} Human-readable script.
 */

Witness.prototype.toString = function toString() {
  return common.formatStack(this.items);
};

/**
 * Format the witness object as bitcoind asm.
 * @param {Boolean?} decode - Attempt to decode hash types.
 * @returns {String} Human-readable script.
 */

Witness.prototype.toASM = function toASM(decode) {
  return common.formatStackASM(this.items, decode);
};

/**
 * Clone the witness object.
 * @returns {Witness} A clone of the current witness object.
 */

Witness.prototype.clone = function clone() {
  return new Witness().inject(this);
};

/**
 * Inject properties from witness.
 * Used for cloning.
 * @private
 * @param {Witness} witness
 * @returns {Witness}
 */

Witness.prototype.inject = function inject(witness) {
  this.items = witness.items.slice();
  return this;
};

/**
 * Convert the Witness to a Stack object.
 * This is usually done before executing
 * a witness program.
 * @returns {Stack}
 */

Witness.prototype.toStack = function toStack() {
  return new Stack(this.items.slice());
};

/**
 * "Guess" the type of the witness.
 * This method is not 100% reliable.
 * @returns {ScriptType}
 */

Witness.prototype.getInputType = function getInputType() {
  if (this.isPubkeyhashInput())
    return scriptTypes.WITNESSPUBKEYHASH;

  if (this.isScripthashInput())
    return scriptTypes.WITNESSSCRIPTHASH;

  return scriptTypes.NONSTANDARD;
};

/**
 * "Guess" the address of the witness.
 * This method is not 100% reliable.
 * @returns {Address|null}
 */

Witness.prototype.getInputAddress = function getInputAddress() {
  return Address.fromWitness(this);
};

/**
 * "Test" whether the witness is a pubkey input.
 * Always returns false.
 * @returns {Boolean}
 */

Witness.prototype.isPubkeyInput = function isPubkeyInput() {
  return false;
};

/**
 * "Guess" whether the witness is a pubkeyhash input.
 * This method is not 100% reliable.
 * @returns {Boolean}
 */

Witness.prototype.isPubkeyhashInput = function isPubkeyhashInput() {
  return this.items.length === 2
    && common.isSignatureEncoding(this.items[0])
    && common.isKeyEncoding(this.items[1]);
};

/**
 * "Test" whether the witness is a multisig input.
 * Always returns false.
 * @returns {Boolean}
 */

Witness.prototype.isMultisigInput = function isMultisigInput() {
  return false;
};

/**
 * "Guess" whether the witness is a scripthash input.
 * This method is not 100% reliable.
 * @returns {Boolean}
 */

Witness.prototype.isScripthashInput = function isScripthashInput() {
  return this.items.length > 0 && !this.isPubkeyhashInput();
};

/**
 * "Guess" whether the witness is an unknown/non-standard type.
 * This method is not 100% reliable.
 * @returns {Boolean}
 */

Witness.prototype.isUnknownInput = function isUnknownInput() {
  return this.getInputType() === scriptTypes.NONSTANDARD;
};

/**
 * Test the witness against a bloom filter.
 * @param {Bloom} filter
 * @returns {Boolean}
 */

Witness.prototype.test = function test(filter) {
  var i, chunk;

  for (i = 0; i < this.items.length; i++) {
    chunk = this.items[i];

    if (chunk.length === 0)
      continue;

    if (filter.test(chunk))
      return true;
  }

  return false;
};

/**
 * Grab and deserialize the redeem script from the witness.
 * @returns {Script} Redeem script.
 */

Witness.prototype.getRedeem = function getRedeem() {
  var redeem;

  if (this.items.length === 0)
    return;

  redeem = this.items[this.items.length - 1];

  if (!redeem)
    return;

  return Script.fromRaw(redeem);
};

/**
 * Does nothing currently.
 */

Witness.prototype.compile = function compile() {
  // NOP
};

/**
 * Find a data element in a witness.
 * @param {Buffer} data - Data element to match against.
 * @returns {Number} Index (`-1` if not present).
 */

Witness.prototype.indexOf = function indexOf(data) {
  return util.indexOf(this.items, data);
};

/**
 * Calculate size of the witness
 * excluding the varint size bytes.
 * @returns {Number}
 */

Witness.prototype.getSize = function getSize() {
  var size = 0;
  var i, item;

  for (i = 0; i < this.items.length; i++) {
    item = this.items[i];
    size += encoding.sizeVarBytes(item);
  }

  return size;
};

/**
 * Calculate size of the witness
 * including the varint size bytes.
 * @returns {Number}
 */

Witness.prototype.getVarSize = function getVarSize() {
  return encoding.sizeVarint(this.items.length) + this.getSize();
};

/**
 * Write witness to a buffer writer.
 * @param {BufferWriter} bw
 */

Witness.prototype.toWriter = function toWriter(bw) {
  var i;

  bw.writeVarint(this.items.length);

  for (i = 0; i < this.items.length; i++)
    bw.writeVarBytes(this.items[i]);

  return bw;
};

/**
 * Encode the witness to a Buffer.
 * @param {String} enc - Encoding, either `'hex'` or `null`.
 * @returns {Buffer|String} Serialized script.
 */

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

/**
 * Convert witness to a hex string.
 * @returns {String}
 */

Witness.prototype.toJSON = function toJSON() {
  return this.toRaw().toString('hex');
};

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

Witness.prototype.fromJSON = function fromJSON(json) {
  assert(typeof json === 'string', 'Witness must be a string.');
  return this.fromRaw(new Buffer(json, 'hex'));
};

/**
 * Insantiate witness from a hex string.
 * @param {String} json
 * @returns {Witness}
 */

Witness.fromJSON = function fromJSON(json) {
  return new Witness().fromJSON(json);
};

/**
 * Unshift an item onto the witness vector.
 * @param {Number|String|Buffer|BN} data
 * @returns {Number}
 */

Witness.prototype.unshift = function unshift(data) {
  return this.items.unshift(Witness.encodeItem(data));
};

/**
 * Push an item onto the witness vector.
 * @param {Number|String|Buffer|BN} data
 * @returns {Number}
 */

Witness.prototype.push = function push(data) {
  return this.items.push(Witness.encodeItem(data));
};

/**
 * Shift an item off the witness vector.
 * @returns {Buffer}
 */

Witness.prototype.shift = function shift() {
  return this.items.shift();
};

/**
 * Shift an item off the witness vector.
 * @returns {Buffer}
 */

Witness.prototype.pop = function push(data) {
  return this.items.pop();
};

/**
 * Remove an item from the witness vector.
 * @param {Number} index
 * @returns {Buffer}
 */

Witness.prototype.remove = function remove(i) {
  return this.items.splice(i, 1)[0];
};

/**
 * Insert an item into the witness vector.
 * @param {Number} index
 * @param {Number|String|Buffer|BN} data
 */

Witness.prototype.insert = function insert(i, data) {
  assert(i <= this.items.length, 'Index out of bounds.');
  this.items.splice(i, 0, Witness.encodeItem(data))[0];
};

/**
 * Get an item from the witness vector.
 * @param {Number} index
 * @returns {Buffer}
 */

Witness.prototype.get = function get(i) {
  return this.items[i];
};

/**
 * Get a small int (0-16) from the witness vector.
 * @param {Number} index
 * @returns {Number} `-1` on non-existent.
 */

Witness.prototype.getSmall = function getSmall(i) {
  var item = this.items[i];
  if (!item || item.length > 1)
    return -1;
  if (item.length === 0)
    return 0;
  if (!(item[0] >= 1 && item[1] <= 16))
    return -1;
  return item[0];
};

/**
 * Get a number from the witness vector.
 * @param {Number} index
 * @returns {BN}
 */

Witness.prototype.getNumber = function getNumber(i) {
  var item = this.items[i];
  if (!item || item.length > 5)
    return;
  return common.num(item, common.flags.VERIFY_NONE, 5);
};

/**
 * Get a string from the witness vector.
 * @param {Number} index
 * @returns {String}
 */

Witness.prototype.getString = function getString(i) {
  var item = this.items[i];
  if (!item)
    return;
  return item.toString('utf8');
};

/**
 * Clear the witness items.
 */

Witness.prototype.clear = function clear() {
  this.items.length = 0;
};

/**
 * Set an item in the witness vector.
 * @param {Number} index
 * @param {Number|String|Buffer|BN} data
 */

Witness.prototype.set = function set(i, data) {
  assert(i <= this.items.length, 'Index out of bounds.');
  this.items[i] = Witness.encodeItem(data);
};

/**
 * Encode a witness item.
 * @param {Number|String|Buffer|BN} data
 * @returns {Buffer}
 */

Witness.encodeItem = function encodeItem(data) {
  if (data instanceof Opcode)
    data = data.data || data.value;

  if (typeof data === 'number') {
    if (data === opcodes.OP_1NEGATE)
      return STACK_NEGATE;

    if (data === opcodes.OP_0)
      return STACK_FALSE;

    if (data >= opcodes.OP_1 && data <= opcodes.OP_16)
      return new Buffer([data - 0x50]);

    throw new Error('Non-push opcode in witness.');
  }

  if (BN.isBN(data))
    return common.array(data);

  if (typeof data === 'string')
    return new Buffer(data, 'utf8');

  return data;
};

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

Witness.prototype.fromReader = function fromReader(br) {
  var chunkCount = br.readVarint();
  var i;

  for (i = 0; i < chunkCount; i++)
    this.items.push(br.readVarBytes());

  return this;
};

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

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

/**
 * Create a witness from a buffer reader.
 * @param {BufferReader} br
 */

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

/**
 * Create a witness from a serialized buffer.
 * @param {Buffer|String} data - Serialized witness.
 * @param {String?} enc - Either `"hex"` or `null`.
 * @returns {Witness}
 */

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

/**
 * Inject items from string.
 * @private
 * @param {String|String[]} items
 */

Witness.prototype.fromString = function fromString(items) {
  var i;

  if (!Array.isArray(items)) {
    assert(typeof items === 'string');

    items = items.trim();

    if (items.length === 0)
      return this;

    items = items.split(/\s+/);
  }

  for (i = 0; i < items.length; i++)
    this.items.push(new Buffer(items[i], 'hex'));

  return this;
};

/**
 * Parse a test script/array
 * string into a witness object. _Must_
 * contain only stack items (no non-push
 * opcodes).
 * @param {String|String[]} items - Script string.
 * @returns {Witness}
 * @throws Parse error.
 */

Witness.fromString = function fromString(items) {
  return new Witness().fromString(items);
};

/**
 * Test an object to see if it is a Witness.
 * @param {Object} obj
 * @returns {Boolean}
 */

Witness.isWitness = function isWitness(obj) {
  return obj
    && Array.isArray(obj.items)
    && typeof obj.toStack === 'function';
};

/*
 * Expose
 */

module.exports = Witness;
