/*!
 * stack.js - stack 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 common = require('./common');
const ScriptNum = require('./scriptnum');

/**
 * Represents the stack of a Script during execution.
 * @alias module:script.Stack
 * @constructor
 * @param {Buffer[]?} items - Stack items.
 * @property {Buffer[]} items - Stack items.
 * @property {Number} length - Size of stack.
 */

function Stack(items) {
  if (!(this instanceof Stack))
    return new Stack(items);

  this.items = items || [];
}

/*
 * Expose length setter and getter.
 */

Object.defineProperty(Stack.prototype, 'length', {
  get() {
    return this.items.length;
  },
  set(length) {
    this.items.length = length;
    return this.items.length;
  }
});

/**
 * Instantiate a key and value iterator.
 * @returns {StackIterator}
 */

Stack.prototype[Symbol.iterator] = function iterator() {
  return this.items[Symbol.iterator]();
};

/**
 * Instantiate a value-only iterator.
 * @returns {StackIterator}
 */

Stack.prototype.values = function values() {
  return this.items.values();
};

/**
 * Instantiate a key and value iterator.
 * @returns {StackIterator}
 */

Stack.prototype.entries = function entries() {
  return this.items.entries();
};

/**
 * Inspect the stack.
 * @returns {String} Human-readable stack.
 */

Stack.prototype.inspect = function inspect() {
  return `<Stack: ${this.toString()}>`;
};

/**
 * Convert the stack to a string.
 * @returns {String} Human-readable stack.
 */

Stack.prototype.toString = function toString() {
  const out = [];

  for (const item of this.items)
    out.push(item.toString('hex'));

  return out.join(' ');
};

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

Stack.prototype.toASM = function toASM(decode) {
  const out = [];

  for (const item of this.items)
    out.push(common.toASM(item, decode));

  return out.join(' ');
};

/**
 * Clone the stack.
 * @returns {Stack} Cloned stack.
 */

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

/**
 * Clear the stack.
 * @returns {Stack}
 */

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

/**
 * Get a stack item by index.
 * @param {Number} index
 * @returns {Buffer|null}
 */

Stack.prototype.get = function get(index) {
  if (index < 0)
    index += this.items.length;

  if (index < 0 || index >= this.items.length)
    return null;

  return this.items[index];
};

/**
 * Pop a stack item.
 * @see Array#pop
 * @returns {Buffer|null}
 */

Stack.prototype.pop = function pop() {
  const item = this.items.pop();
  return item || null;
};

/**
 * Shift a stack item.
 * @see Array#shift
 * @returns {Buffer|null}
 */

Stack.prototype.shift = function shift() {
  const item = this.items.shift();
  return item || null;
};

/**
 * Remove an item.
 * @param {Number} index
 * @returns {Buffer}
 */

Stack.prototype.remove = function remove(index) {
  if (index < 0)
    index += this.items.length;

  if (index < 0 || index >= this.items.length)
    return null;

  const items = this.items.splice(index, 1);

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

  return items[0];
};

/**
 * Set stack item at index.
 * @param {Number} index
 * @param {Buffer} value
 * @returns {Buffer}
 */

Stack.prototype.set = function set(index, item) {
  if (index < 0)
    index += this.items.length;

  assert(Buffer.isBuffer(item));
  assert(index >= 0 && index <= this.items.length);

  this.items[index] = item;

  return this;
};

/**
 * Push item onto stack.
 * @see Array#push
 * @param {Buffer} item
 * @returns {Number} Stack size.
 */

Stack.prototype.push = function push(item) {
  assert(Buffer.isBuffer(item));
  this.items.push(item);
  return this;
};

/**
 * Unshift item from stack.
 * @see Array#unshift
 * @param {Buffer} item
 * @returns {Number}
 */

Stack.prototype.unshift = function unshift(item) {
  assert(Buffer.isBuffer(item));
  this.items.unshift(item);
  return this;
};

/**
 * Insert an item.
 * @param {Number} index
 * @param {Buffer} item
 * @returns {Buffer}
 */

Stack.prototype.insert = function insert(index, item) {
  if (index < 0)
    index += this.items.length;

  assert(Buffer.isBuffer(item));
  assert(index >= 0 && index <= this.items.length);

  this.items.splice(index, 0, item);

  return this;
};

/**
 * Erase stack items.
 * @param {Number} start
 * @param {Number} end
 * @returns {Buffer[]}
 */

Stack.prototype.erase = function erase(start, end) {
  if (start < 0)
    start = this.items.length + start;

  if (end < 0)
    end = this.items.length + end;

  this.items.splice(start, end - start);
};

/**
 * Swap stack values.
 * @param {Number} i1 - Index 1.
 * @param {Number} i2 - Index 2.
 */

Stack.prototype.swap = function swap(i1, i2) {
  if (i1 < 0)
    i1 = this.items.length + i1;

  if (i2 < 0)
    i2 = this.items.length + i2;

  const v1 = this.items[i1];
  const v2 = this.items[i2];

  this.items[i1] = v2;
  this.items[i2] = v1;
};

/*
 * Data
 */

Stack.prototype.getData = function getData(index) {
  return this.get(index);
};

Stack.prototype.popData = function popData() {
  return this.pop();
};

Stack.prototype.shiftData = function shiftData() {
  return this.shift();
};

Stack.prototype.removeData = function removeData(index) {
  return this.remove(index);
};

Stack.prototype.setData = function setData(index, data) {
  return this.set(index, data);
};

Stack.prototype.pushData = function pushData(data) {
  return this.push(data);
};

Stack.prototype.unshiftData = function unshiftData(data) {
  return this.unshift(data);
};

Stack.prototype.insertData = function insertData(index, data) {
  return this.insert(index, data);
};

/*
 * Length
 */

Stack.prototype.getLength = function getLength(index) {
  const item = this.get(index);
  return item ? item.length : -1;
};

/*
 * String
 */

Stack.prototype.getString = function getString(index, enc) {
  const item = this.get(index);
  return item ? Stack.toString(item, enc) : null;
};

Stack.prototype.popString = function popString(enc) {
  const item = this.pop();
  return item ? Stack.toString(item, enc) : null;
};

Stack.prototype.shiftString = function shiftString(enc) {
  const item = this.shift();
  return item ? Stack.toString(item, enc) : null;
};

Stack.prototype.removeString = function removeString(index, enc) {
  const item = this.remove(index);
  return item ? Stack.toString(item, enc) : null;
};

Stack.prototype.setString = function setString(index, str, enc) {
  return this.set(index, Stack.fromString(str, enc));
};

Stack.prototype.pushString = function pushString(str, enc) {
  return this.push(Stack.fromString(str, enc));
};

Stack.prototype.unshiftString = function unshiftString(str, enc) {
  return this.unshift(Stack.fromString(str, enc));
};

Stack.prototype.insertString = function insertString(index, str, enc) {
  return this.insert(index, Stack.fromString(str, enc));
};

/*
 * Num
 */

Stack.prototype.getNum = function getNum(index, minimal, limit) {
  const item = this.get(index);
  return item ? Stack.toNum(item, minimal, limit) : null;
};

Stack.prototype.popNum = function popNum(minimal, limit) {
  const item = this.pop();
  return item ? Stack.toNum(item, minimal, limit) : null;
};

Stack.prototype.shiftNum = function shiftNum(minimal, limit) {
  const item = this.shift();
  return item ? Stack.toNum(item, minimal, limit) : null;
};

Stack.prototype.removeNum = function removeNum(index, minimal, limit) {
  const item = this.remove(index);
  return item ? Stack.toNum(item, minimal, limit) : null;
};

Stack.prototype.setNum = function setNum(index, num) {
  return this.set(index, Stack.fromNum(num));
};

Stack.prototype.pushNum = function pushNum(num) {
  return this.push(Stack.fromNum(num));
};

Stack.prototype.unshiftNum = function unshiftNum(num) {
  return this.unshift(Stack.fromNum(num));
};

Stack.prototype.insertNum = function insertNum(index, num) {
  return this.insert(index, Stack.fromNum(num));
};

/*
 * Int
 */

Stack.prototype.getInt = function getInt(index, minimal, limit) {
  const item = this.get(index);
  return item ? Stack.toInt(item, minimal, limit) : -1;
};

Stack.prototype.popInt = function popInt(minimal, limit) {
  const item = this.pop();
  return item ? Stack.toInt(item, minimal, limit) : -1;
};

Stack.prototype.shiftInt = function shiftInt(minimal, limit) {
  const item = this.shift();
  return item ? Stack.toInt(item, minimal, limit) : -1;
};

Stack.prototype.removeInt = function removeInt(index, minimal, limit) {
  const item = this.remove(index);
  return item ? Stack.toInt(item, minimal, limit) : -1;
};

Stack.prototype.setInt = function setInt(index, num) {
  return this.set(index, Stack.fromInt(num));
};

Stack.prototype.pushInt = function pushInt(num) {
  return this.push(Stack.fromInt(num));
};

Stack.prototype.unshiftInt = function unshiftInt(num) {
  return this.unshift(Stack.fromInt(num));
};

Stack.prototype.insertInt = function insertInt(index, num) {
  return this.insert(index, Stack.fromInt(num));
};

/*
 * Bool
 */

Stack.prototype.getBool = function getBool(index) {
  const item = this.get(index);
  return item ? Stack.toBool(item) : false;
};

Stack.prototype.popBool = function popBool() {
  const item = this.pop();
  return item ? Stack.toBool(item) : false;
};

Stack.prototype.shiftBool = function shiftBool() {
  const item = this.shift();
  return item ? Stack.toBool(item) : false;
};

Stack.prototype.removeBool = function removeBool(index) {
  const item = this.remove(index);
  return item ? Stack.toBool(item) : false;
};

Stack.prototype.setBool = function setBool(index, value) {
  return this.set(index, Stack.fromBool(value));
};

Stack.prototype.pushBool = function pushBool(value) {
  return this.push(Stack.fromBool(value));
};

Stack.prototype.unshiftBool = function unshiftBool(value) {
  return this.unshift(Stack.fromBool(value));
};

Stack.prototype.insertBool = function insertBool(index, value) {
  return this.insert(index, Stack.fromBool(value));
};

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

Stack.isStack = function isStack(obj) {
  return obj instanceof Stack;
};

/*
 * Encoding
 */

Stack.toString = function toString(item, enc) {
  assert(Buffer.isBuffer(item));
  return item.toString(enc || 'utf8');
};

Stack.fromString = function fromString(str, enc) {
  assert(typeof str === 'string');
  return Buffer.from(str, enc || 'utf8');
};

Stack.toNum = function toNum(item, minimal, limit) {
  assert(Buffer.isBuffer(item));
  return ScriptNum.decode(item, minimal, limit);
};

Stack.fromNum = function fromNum(num) {
  assert(ScriptNum.isScriptNum(num));
  return num.encode();
};

Stack.toInt = function toInt(item, minimal, limit) {
  assert(Buffer.isBuffer(item));

  const num = Stack.toNum(item, minimal, limit);

  if (!num)
    return -1;

  return num.getInt();
};

Stack.fromInt = function fromInt(int) {
  assert(typeof int === 'number');

  if (int >= -1 && int <= 16)
    return common.small[int + 1];

  const num = ScriptNum.fromNumber(int);

  return Stack.fromNum(num);
};

Stack.toBool = function toBool(item) {
  assert(Buffer.isBuffer(item));

  for (let i = 0; i < item.length; i++) {
    if (item[i] !== 0) {
      // Cannot be negative zero
      if (i === item.length - 1 && item[i] === 0x80)
        return false;
      return true;
    }
  }

  return false;
};

Stack.fromBool = function fromBool(value) {
  assert(typeof value === 'boolean');
  return Stack.fromInt(value ? 1 : 0);
};

/*
 * Expose
 */

module.exports = Stack;
