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

'use strict';

/**
 * @module workers/packets
 */

var assert = require('assert');
var util = require('../utils/util');
var BufferReader = require('../utils/reader');
var encoding = require('../utils/encoding');
var Script = require('../script/script');
var Witness = require('../script/witness');
var Output = require('../primitives/output');

/*
 * Constants
 */

var packetTypes = {
  EVENT: 0,
  LOG: 1,
  ERROR: 2,
  ERRORRESULT: 3,
  VERIFY: 4,
  VERIFYRESULT: 5,
  SIGN: 6,
  SIGNRESULT: 7,
  VERIFYINPUT: 8,
  VERIFYINPUTRESULT: 9,
  SIGNINPUT: 10,
  SIGNINPUTRESULT: 11,
  ECVERIFY: 12,
  ECVERIFYRESULT: 13,
  ECSIGN: 14,
  ECSIGNRESULT: 15,
  MINE: 16,
  MINERESULT: 17,
  SCRYPT: 18,
  SCRYPTRESULT: 19
};

/**
 * Packet
 * @constructor
 */

function Packet() {
  this.id = ++Packet.id >>> 0;
}

Packet.id = 0;

Packet.prototype.cmd = -1;

Packet.prototype.toWriter = function toWriter() {
  throw new Error('Abstract method.');
};

/**
 * EventPacket
 * @constructor
 */

function EventPacket(items) {
  Packet.call(this);
  this.items = items || [];
  this.json = JSON.stringify(this.items);
}

util.inherits(EventPacket, Packet);

EventPacket.prototype.cmd = packetTypes.EVENT;

EventPacket.prototype.getSize = function getSize() {
  return encoding.sizeVarString(this.json, 'utf8');
};

EventPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarString(this.json, 'utf8');
};

EventPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new EventPacket();
  packet.items = JSON.parse(br.readVarString('utf8'));
  return packet;
};

/**
 * LogPacket
 * @constructor
 */

function LogPacket(text) {
  Packet.call(this);
  this.text = text || '';
}

util.inherits(LogPacket, Packet);

LogPacket.prototype.cmd = packetTypes.LOG;

LogPacket.prototype.getSize = function getSize() {
  return encoding.sizeVarString(this.text, 'utf8');
};

LogPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarString(this.text, 'utf8');
};

LogPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new LogPacket();
  packet.text = br.readVarString('utf8');
  return packet;
};

/**
 * ErrorPacket
 * @constructor
 */

function ErrorPacket(error) {
  Packet.call(this);
  this.error = error || new Error();
}

util.inherits(ErrorPacket, Packet);

ErrorPacket.prototype.cmd = packetTypes.ERROR;

ErrorPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += encoding.sizeVarString(this.error.message + '', 'utf8');
  size += encoding.sizeVarString(this.error.stack + '', 'utf8');
  size += encoding.sizeVarString((this.error.type || ''), 'utf8');
  return size;
};

ErrorPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarString(this.error.message + '', 'utf8');
  bw.writeVarString(this.error.stack + '', 'utf8');
  bw.writeVarString((this.error.type || ''), 'utf8');
};

ErrorPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ErrorPacket();
  packet.error.message = br.readVarString('utf8');
  packet.error.stack = br.readVarString('utf8');
  packet.error.type = br.readVarString('utf8');
  return packet;
};

/**
 * ErrorResultPacket
 * @constructor
 */

function ErrorResultPacket(error) {
  Packet.call(this);
  this.error = error || new Error();
}

util.inherits(ErrorResultPacket, Packet);

ErrorResultPacket.prototype.cmd = packetTypes.ERRORRESULT;

ErrorResultPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += encoding.sizeVarString(this.error.message + '', 'utf8');
  size += encoding.sizeVarString(this.error.stack + '', 'utf8');
  size += encoding.sizeVarString((this.error.type || ''), 'utf8');
  return size;
};

ErrorResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarString(this.error.message + '', 'utf8');
  bw.writeVarString(this.error.stack + '', 'utf8');
  bw.writeVarString((this.error.type || ''), 'utf8');
};

ErrorResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ErrorResultPacket();
  packet.error.message = br.readVarString('utf8');
  packet.error.stack = br.readVarString('utf8');
  packet.error.type = br.readVarString('utf8');
  return packet;
};

/**
 * VerifyPacket
 * @constructor
 */

function VerifyPacket(tx, view, flags) {
  Packet.call(this);
  this.tx = tx || null;
  this.view = view || null;
  this.flags = flags != null ? flags : null;
}

util.inherits(VerifyPacket, Packet);

VerifyPacket.prototype.cmd = packetTypes.VERIFY;

VerifyPacket.prototype.getSize = function() {
  return this.tx.getSize() + this.view.getFastSize(this.tx) + 4;
};

VerifyPacket.prototype.toWriter = function(bw) {
  this.tx.toWriter(bw);
  this.view.toFast(bw, this.tx);
  bw.write32(this.flags != null ? this.flags : -1);
};

VerifyPacket.fromRaw = function fromRaw(TX, CoinView, data) {
  var br = new BufferReader(data, true);
  var packet = new VerifyPacket();

  packet.tx = TX.fromReader(br);
  packet.view = CoinView.fromFast(br, packet.tx);

  packet.flags = br.read32();

  if (packet.flags === -1)
    packet.flags = null;

  return packet;
};

/**
 * VerifyResultPacket
 * @constructor
 */

function VerifyResultPacket(value) {
  Packet.call(this);
  this.value = value;
}

util.inherits(VerifyResultPacket, Packet);

VerifyResultPacket.prototype.cmd = packetTypes.VERIFYRESULT;

VerifyResultPacket.prototype.getSize = function getSize() {
  return 1;
};

VerifyResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeU8(this.value ? 1 : 0);
};

VerifyResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new VerifyResultPacket();
  packet.value = br.readU8() === 1;
  return packet;
};

/**
 * SignPacket
 * @constructor
 */

function SignPacket(tx, rings, type) {
  Packet.call(this);
  this.tx = tx || null;
  this.rings = rings || [];
  this.type = type != null ? type : 1;
}

util.inherits(SignPacket, Packet);

SignPacket.prototype.cmd = packetTypes.SIGN;

SignPacket.prototype.getSize = function getSize() {
  var size = 0;
  var i, ring;

  size += this.tx.getSize();
  size += this.tx.view.getFastSize(this.tx);
  size += encoding.sizeVarint(this.rings.length);

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

  size += 1;

  return size;
};

SignPacket.prototype.toWriter = function toWriter(bw) {
  var i, ring;

  this.tx.toWriter(bw);
  this.tx.view.toFast(bw, this.tx);

  bw.writeVarint(this.rings.length);

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

  bw.writeU8(this.type);
};

SignPacket.fromRaw = function fromRaw(MTX, KeyRing, data) {
  var br = new BufferReader(data, true);
  var packet = new SignPacket();
  var i, count, ring;

  packet.tx = MTX.fromReader(br);
  packet.tx.view.fromFast(br, packet.tx);

  count = br.readVarint();

  for (i = 0; i < count; i++) {
    ring = KeyRing.fromReader(br);
    packet.rings.push(ring);
  }

  packet.type = br.readU8();

  return packet;
};

/**
 * SignResultPacket
 * @constructor
 */

function SignResultPacket(total, witness, script) {
  Packet.call(this);
  this.total = total || 0;
  this.script = script || [];
  this.witness = witness || [];
}

util.inherits(SignResultPacket, Packet);

SignResultPacket.prototype.cmd = packetTypes.SIGNRESULT;

SignResultPacket.fromTX = function fromTX(tx, total) {
  var packet = new SignResultPacket(total);
  var i, input;

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    packet.script.push(input.script);
    packet.witness.push(input.witness);
  }

  return packet;
};

SignResultPacket.prototype.getSize = function getSize() {
  var size = 0;
  var i, script, witness;

  size += encoding.sizeVarint(this.total);
  size += encoding.sizeVarint(this.script.length);

  for (i = 0; i < this.script.length; i++) {
    script = this.script[i];
    witness = this.witness[i];
    size += script.getVarSize();
    size += witness.getVarSize();
  }

  return size;
};

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

  assert(this.script.length === this.witness.length);

  bw.writeVarint(this.total);
  bw.writeVarint(this.script.length);

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

SignResultPacket.prototype.inject = function inject(tx) {
  var i, input;

  assert(this.script.length === tx.inputs.length);
  assert(this.witness.length === tx.inputs.length);

  for (i = 0; i < tx.inputs.length; i++) {
    input = tx.inputs[i];
    input.script = this.script[i];
    input.witness = this.witness[i];
  }
};

SignResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new SignResultPacket();
  var i, count;

  packet.total = br.readVarint();

  count = br.readVarint();

  for (i = 0; i < count; i++) {
    packet.script.push(Script.fromReader(br));
    packet.witness.push(Witness.fromReader(br));
  }

  return packet;
};

/**
 * VerifyInputPacket
 * @constructor
 */

function VerifyInputPacket(tx, index, coin, flags) {
  Packet.call(this);
  this.tx = tx || null;
  this.index = index;
  this.coin = coin || null;
  this.flags = flags != null ? flags : null;
}

util.inherits(VerifyInputPacket, Packet);

VerifyInputPacket.prototype.cmd = packetTypes.VERIFYINPUT;

VerifyInputPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += this.tx.getSize();
  size += encoding.sizeVarint(this.index);
  size += encoding.sizeVarint(this.coin.value);
  size += this.coin.script.getVarSize();
  size += 4;
  return size;
};

VerifyInputPacket.prototype.toWriter = function toWriter(bw) {
  this.tx.toWriter(bw);
  bw.writeVarint(this.index);
  bw.writeVarint(this.coin.value);
  this.coin.script.toWriter(bw);
  bw.write32(this.flags != null ? this.flags : -1);
};

VerifyInputPacket.fromRaw = function fromRaw(TX, data) {
  var br = new BufferReader(data, true);
  var packet = new VerifyInputPacket();

  packet.tx = TX.fromReader(br);
  packet.index = br.readVarint();

  packet.coin = new Output();
  packet.coin.value = br.readVarint();
  packet.coin.script.fromReader(br);

  packet.flags = br.read32();

  if (packet.flags === -1)
    packet.flags = null;

  return packet;
};

/**
 * VerifyInputResultPacket
 * @constructor
 */

function VerifyInputResultPacket(value) {
  Packet.call(this);
  this.value = value;
}

util.inherits(VerifyInputResultPacket, Packet);

VerifyInputResultPacket.prototype.cmd = packetTypes.VERIFYINPUTRESULT;

VerifyInputResultPacket.prototype.getSize = function getSize() {
  return 1;
};

VerifyInputResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeU8(this.value ? 1 : 0);
};

VerifyInputResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new VerifyInputResultPacket();
  packet.value = br.readU8() === 1;
  return packet;
};

/**
 * SignInputPacket
 * @constructor
 */

function SignInputPacket(tx, index, coin, ring, type) {
  Packet.call(this);
  this.tx = tx || null;
  this.index = index;
  this.coin = coin || null;
  this.ring = ring || null;
  this.type = type != null ? type : 1;
}

util.inherits(SignInputPacket, Packet);

SignInputPacket.prototype.cmd = packetTypes.SIGNINPUT;

SignInputPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += this.tx.getSize();
  size += encoding.sizeVarint(this.index);
  size += encoding.sizeVarint(this.coin.value);
  size += this.coin.script.getVarSize();
  size += this.ring.getSize();
  size += 1;
  return size;
};

SignInputPacket.prototype.toWriter = function toWriter(bw) {
  this.tx.toWriter(bw);
  bw.writeVarint(this.index);
  bw.writeVarint(this.coin.value);
  this.coin.script.toWriter(bw);
  this.ring.toWriter(bw);
  bw.writeU8(this.type);
};

SignInputPacket.fromRaw = function fromRaw(MTX, KeyRing, data) {
  var br = new BufferReader(data, true);
  var packet = new SignInputPacket();

  packet.tx = MTX.fromReader(br);
  packet.index = br.readVarint();

  packet.coin = new Output();
  packet.coin.value = br.readVarint();
  packet.coin.script.fromReader(br);

  packet.ring = KeyRing.fromReader(br);
  packet.type = br.readU8();

  return packet;
};

/**
 * SignInputResultPacket
 * @constructor
 */

function SignInputResultPacket(value, witness, script) {
  Packet.call(this);
  this.value = value || false;
  this.script = script || null;
  this.witness = witness || null;
}

util.inherits(SignInputResultPacket, Packet);

SignInputResultPacket.prototype.cmd = packetTypes.SIGNINPUTRESULT;

SignInputResultPacket.fromTX = function fromTX(tx, i, value) {
  var packet = new SignInputResultPacket(value);
  var input = tx.inputs[i];

  assert(input);

  packet.script = input.script;
  packet.witness = input.witness;

  return packet;
};

SignInputResultPacket.prototype.getSize = function getSize() {
  return 1 + this.script.getVarSize() + this.witness.getVarSize();
};

SignInputResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeU8(this.value ? 1 : 0);
  this.script.toWriter(bw);
  this.witness.toWriter(bw);
};

SignInputResultPacket.prototype.inject = function inject(tx, i) {
  var input = tx.inputs[i];
  assert(input);
  input.script = this.script;
  input.witness = this.witness;
};

SignInputResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new SignInputResultPacket();
  packet.value = br.readU8() === 1;
  packet.script = Script.fromReader(br);
  packet.witness = Witness.fromReader(br);
  return packet;
};


/**
 * ECVerifyPacket
 * @constructor
 */

function ECVerifyPacket(msg, sig, key) {
  Packet.call(this);
  this.msg = msg || null;
  this.sig = sig || null;
  this.key = key || null;
}

util.inherits(ECVerifyPacket, Packet);

ECVerifyPacket.prototype.cmd = packetTypes.ECVERIFY;

ECVerifyPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += encoding.sizeVarBytes(this.msg);
  size += encoding.sizeVarBytes(this.sig);
  size += encoding.sizeVarBytes(this.key);
  return size;
};

ECVerifyPacket.prototype.toWriter = function(bw) {
  bw.writeVarBytes(this.msg);
  bw.writeVarBytes(this.sig);
  bw.writeVarBytes(this.key);
};

ECVerifyPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ECVerifyPacket();
  packet.msg = br.readVarBytes();
  packet.sig = br.readVarBytes();
  packet.key = br.readVarBytes();
  return packet;
};

/**
 * ECVerifyResultPacket
 * @constructor
 */

function ECVerifyResultPacket(value) {
  Packet.call(this);
  this.value = value;
}

util.inherits(ECVerifyResultPacket, Packet);

ECVerifyResultPacket.prototype.cmd = packetTypes.ECVERIFYRESULT;

ECVerifyResultPacket.prototype.getSize = function getSize() {
  return 1;
};

ECVerifyResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeU8(this.value ? 1 : 0);
};

ECVerifyResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ECVerifyResultPacket();
  packet.value = br.readU8() === 1;
  return packet;
};

/**
 * ECSignPacket
 * @constructor
 */

function ECSignPacket(msg, key) {
  Packet.call(this);
  this.msg = msg || null;
  this.key = key || null;
}

util.inherits(ECSignPacket, Packet);

ECSignPacket.prototype.cmd = packetTypes.ECSIGN;

ECSignPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += encoding.sizeVarBytes(this.msg);
  size += encoding.sizeVarBytes(this.key);
  return size;
};

ECSignPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarBytes(this.msg);
  bw.writeVarBytes(this.key);
};

ECSignPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ECSignPacket();
  packet.msg = br.readVarBytes();
  packet.key = br.readVarBytes();
  return packet;
};

/**
 * ECSignResultPacket
 * @constructor
 */

function ECSignResultPacket(sig) {
  Packet.call(this);
  this.sig = sig;
}

util.inherits(ECSignResultPacket, Packet);

ECSignResultPacket.prototype.cmd = packetTypes.ECSIGNRESULT;

ECSignResultPacket.prototype.getSize = function getSize() {
  return encoding.sizeVarBytes(this.sig);
};

ECSignResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarBytes(this.sig);
};

ECSignResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ECSignResultPacket();
  packet.sig = br.readVarBytes();
  return packet;
};

/**
 * MinePacket
 * @constructor
 */

function MinePacket(data, target, min, max) {
  Packet.call(this);
  this.data = data || null;
  this.target = target || null;
  this.min = min != null ? min : -1;
  this.max = max != null ? max : -1;
}

util.inherits(MinePacket, Packet);

MinePacket.prototype.cmd = packetTypes.MINE;

MinePacket.prototype.getSize = function getSize() {
  return 120;
};

MinePacket.prototype.toWriter = function toWriter(bw) {
  bw.writeBytes(this.data);
  bw.writeBytes(this.target);
  bw.writeU32(this.min);
  bw.writeU32(this.max);
};

MinePacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new MinePacket();
  packet.data = br.readBytes(80);
  packet.target = br.readBytes(32);
  packet.min = br.readU32();
  packet.max = br.readU32();
  return packet;
};

/**
 * MineResultPacket
 * @constructor
 */

function MineResultPacket(nonce) {
  Packet.call(this);
  this.nonce = nonce != null ? nonce : -1;
}

util.inherits(MineResultPacket, Packet);

MineResultPacket.prototype.cmd = packetTypes.MINERESULT;

MineResultPacket.prototype.getSize = function getSize() {
  return 4;
};

MineResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeU32(this.nonce);
};

MineResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new MineResultPacket();
  packet.nonce = br.readU32();
  if ((packet.nonce >> 0) === -1)
    packet.nonce = -1;
  return packet;
};

/**
 * ScryptPacket
 * @constructor
 */

function ScryptPacket(passwd, salt, N, r, p, len) {
  Packet.call(this);
  this.passwd = passwd || null;
  this.salt = salt || null;
  this.N = N != null ? N : -1;
  this.r = r != null ? r : -1;
  this.p = p != null ? p : -1;
  this.len = len != null ? len : -1;
}

util.inherits(ScryptPacket, Packet);

ScryptPacket.prototype.cmd = packetTypes.SCRYPT;

ScryptPacket.prototype.getSize = function getSize() {
  var size = 0;
  size += encoding.sizeVarBytes(this.passwd);
  size += encoding.sizeVarBytes(this.salt);
  size += 16;
  return size;
};

ScryptPacket.prototype.toWriter = function(bw) {
  bw.writeVarBytes(this.passwd);
  bw.writeVarBytes(this.salt);
  bw.writeU32(this.N);
  bw.writeU32(this.r);
  bw.writeU32(this.p);
  bw.writeU32(this.len);
};

ScryptPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ScryptPacket();
  packet.passwd = br.readVarBytes();
  packet.salt = br.readVarBytes();
  packet.N = br.readU32();
  packet.r = br.readU32();
  packet.p = br.readU32();
  packet.len = br.readU32();
  return packet;
};

/**
 * ScryptResultPacket
 * @constructor
 */

function ScryptResultPacket(key) {
  Packet.call(this);
  this.key = key || null;
}

util.inherits(ScryptResultPacket, Packet);

ScryptResultPacket.prototype.cmd = packetTypes.SCRYPTRESULT;

ScryptResultPacket.prototype.getSize = function getSize() {
  return encoding.sizeVarBytes(this.key);
};

ScryptResultPacket.prototype.toWriter = function toWriter(bw) {
  bw.writeVarBytes(this.key);
};

ScryptResultPacket.fromRaw = function fromRaw(data) {
  var br = new BufferReader(data, true);
  var packet = new ScryptResultPacket();
  packet.key = br.readVarBytes();
  return packet;
};

/*
 * Expose
 */

exports.types = packetTypes;
exports.EventPacket = EventPacket;
exports.LogPacket = LogPacket;
exports.ErrorPacket = ErrorPacket;
exports.ErrorResultPacket = ErrorResultPacket;
exports.VerifyPacket = VerifyPacket;
exports.VerifyResultPacket = VerifyResultPacket;
exports.SignPacket = SignPacket;
exports.SignResultPacket = SignResultPacket;
exports.VerifyInputPacket = VerifyInputPacket;
exports.VerifyInputResultPacket = VerifyInputResultPacket;
exports.SignInputPacket = SignInputPacket;
exports.SignInputResultPacket = SignInputResultPacket;
exports.ECVerifyPacket = ECVerifyPacket;
exports.ECVerifyResultPacket = ECVerifyResultPacket;
exports.ECSignPacket = ECSignPacket;
exports.ECSignResultPacket = ECSignResultPacket;
exports.MinePacket = MinePacket;
exports.MineResultPacket = MineResultPacket;
exports.ScryptPacket = ScryptPacket;
exports.ScryptResultPacket = ScryptResultPacket;
