/*!
 * memwallet.js - in-memory wallet object for bcoin
 * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('assert');
const Network = require('../../lib/protocol/network');
const MTX = require('../../lib/primitives/mtx');
const HD = require('../../lib/hd/hd');
const Bloom = require('../../lib/utils/bloom');
const KeyRing = require('../../lib/primitives/keyring');
const Outpoint = require('../../lib/primitives/outpoint');
const Coin = require('../../lib/primitives/coin');

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

  this.network = Network.primary;
  this.master = null;
  this.key = null;
  this.witness = false;
  this.account = 0;
  this.receiveDepth = 1;
  this.changeDepth = 1;
  this.receive = null;
  this.change = null;
  this.map = new Set();
  this.coins = new Map();
  this.spent = new Map();
  this.paths = new Map();
  this.balance = 0;
  this.txs = 0;
  this.filter = Bloom.fromRate(1000000, 0.001, -1);

  if (options)
    this.fromOptions(options);

  this.init();
}

MemWallet.prototype.fromOptions = function fromOptions(options) {
  if (options.network != null) {
    assert(options.network);
    this.network = Network.get(options.network);
  }

  if (options.master != null) {
    assert(options.master);
    this.master = HD.PrivateKey.fromOptions(options.master, this.network);
  }

  if (options.key != null) {
    assert(HD.isPrivate(options.key));
    this.key = options.key;
  }

  if (options.witness != null) {
    assert(typeof options.witness === 'boolean');
    this.witness = options.witness;
  }

  if (options.account != null) {
    assert(typeof options.account === 'number');
    this.account = options.account;
  }

  if (options.receiveDepth != null) {
    assert(typeof options.receiveDepth === 'number');
    this.receiveDepth = options.receiveDepth;
  }

  if (options.changeDepth != null) {
    assert(typeof options.changeDepth === 'number');
    this.changeDepth = options.changeDepth;
  }

  return this;
};

MemWallet.prototype.init = function init() {
  let i;

  if (!this.master)
    this.master = HD.PrivateKey.generate();

  if (!this.key)
    this.key = this.master.deriveBIP44(this.account);

  i = this.receiveDepth;
  while (i--)
    this.createReceive();

  i = this.changeDepth;
  while (i--)
    this.createChange();
};

MemWallet.prototype.createReceive = function createReceive() {
  const index = this.receiveDepth++;
  const key = this.deriveReceive(index);
  const hash = key.getHash('hex');
  this.filter.add(hash, 'hex');
  this.paths.set(hash, new Path(hash, 0, index));
  this.receive = key;
  return key;
};

MemWallet.prototype.createChange = function createChange() {
  const index = this.changeDepth++;
  const key = this.deriveChange(index);
  const hash = key.getHash('hex');
  this.filter.add(hash, 'hex');
  this.paths.set(hash, new Path(hash, 1, index));
  this.change = key;
  return key;
};

MemWallet.prototype.deriveReceive = function deriveReceive(index) {
  return this.deriveKey(0, index);
};

MemWallet.prototype.deriveChange = function deriveChange(index) {
  return this.deriveKey(1, index);
};

MemWallet.prototype.derivePath = function derivePath(path) {
  return this.deriveKey(path.branch, path.index);
};

MemWallet.prototype.deriveKey = function deriveKey(branch, index) {
  let key = this.master.deriveBIP44(this.account);
  key = key.derive(branch).derive(index);
  const ring = new KeyRing({
    network: this.network,
    privateKey: key.privateKey,
    witness: this.witness
  });
  ring.witness = this.witness;
  return ring;
};

MemWallet.prototype.getKey = function getKey(hash) {
  const path = this.paths.get(hash);

  if (!path)
    return null;

  return this.derivePath(path);
};

MemWallet.prototype.getPath = function getPath(hash) {
  return this.paths.get(hash);
};

MemWallet.prototype.getCoin = function getCoin(key) {
  return this.coins.get(key);
};

MemWallet.prototype.getUndo = function getUndo(key) {
  return this.spent.get(key);
};

MemWallet.prototype.addCoin = function addCoin(coin) {
  const op = Outpoint(coin.hash, coin.index);
  const key = op.toKey();

  this.filter.add(op.toRaw());

  this.spent.delete(key);

  this.coins.set(key, coin);
  this.balance += coin.value;
};

MemWallet.prototype.removeCoin = function removeCoin(key) {
  const coin = this.coins.get(key);

  if (!coin)
    return;

  this.spent.set(key, coin);
  this.balance -= coin.value;

  this.coins.delete(key);
};

MemWallet.prototype.getAddress = function getAddress() {
  return this.receive.getAddress();
};

MemWallet.prototype.getReceive = function getReceive() {
  return this.receive.getAddress();
};

MemWallet.prototype.getChange = function getChange() {
  return this.change.getAddress();
};

MemWallet.prototype.getCoins = function getCoins() {
  const coins = [];

  for (const coin of this.coins.values())
    coins.push(coin);

  return coins;
};

MemWallet.prototype.syncKey = function syncKey(path) {
  switch (path.branch) {
    case 0:
      if (path.index === this.receiveDepth - 1)
        this.createReceive();
      break;
    case 1:
      if (path.index === this.changeDepth - 1)
        this.createChange();
      break;
    default:
      assert(false);
      break;
  }
};

MemWallet.prototype.addBlock = function addBlock(entry, txs) {
  for (let i = 0; i < txs.length; i++) {
    const tx = txs[i];
    this.addTX(tx, entry.height);
  }
};

MemWallet.prototype.removeBlock = function removeBlock(entry, txs) {
  for (let i = txs.length - 1; i >= 0; i--) {
    const tx = txs[i];
    this.removeTX(tx, entry.height);
  }
};

MemWallet.prototype.addTX = function addTX(tx, height) {
  const hash = tx.hash('hex');
  let result = false;

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

  if (this.map.has(hash))
    return true;

  for (let i = 0; i < tx.inputs.length; i++) {
    const input = tx.inputs[i];
    const op = input.prevout.toKey();
    const coin = this.getCoin(op);

    if (!coin)
      continue;

    result = true;

    this.removeCoin(op);
  }

  for (let i = 0; i < tx.outputs.length; i++) {
    const output = tx.outputs[i];
    const addr = output.getHash('hex');

    if (!addr)
      continue;

    const path = this.getPath(addr);

    if (!path)
      continue;

    result = true;

    const coin = Coin.fromTX(tx, i, height);

    this.addCoin(coin);
    this.syncKey(path);
  }

  if (result) {
    this.txs++;
    this.map.add(hash);
  }

  return result;
};

MemWallet.prototype.removeTX = function removeTX(tx, height) {
  const hash = tx.hash('hex');
  let result = false;

  if (!this.map.has(hash))
    return false;

  for (let i = 0; i < tx.outputs.length; i++) {
    const op = Outpoint(hash, i).toKey();
    const coin = this.getCoin(op);

    if (!coin)
      continue;

    result = true;

    this.removeCoin(op);
  }

  for (let i = 0; i < tx.inputs.length; i++) {
    const input = tx.inputs[i];
    const op = input.prevout.toKey();
    const coin = this.getUndo(op);

    if (!coin)
      continue;

    result = true;

    this.addCoin(coin);
  }

  if (result)
    this.txs--;

  this.map.delete(hash);

  return result;
};

MemWallet.prototype.deriveInputs = function deriveInputs(mtx) {
  const keys = [];

  for (let i = 0; i < mtx.inputs.length; i++) {
    const input = mtx.inputs[i];
    const coin = mtx.view.getOutputFor(input);

    if (!coin)
      continue;

    const addr = coin.getHash('hex');

    if (!addr)
      continue;

    const path = this.getPath(addr);

    if (!path)
      continue;

    const key = this.derivePath(path);

    keys.push(key);
  }

  return keys;
};

MemWallet.prototype.fund = function fund(mtx, options) {
  const coins = this.getCoins();

  if (!options)
    options = {};

  return mtx.fund(coins, {
    selection: options.selection || 'age',
    round: options.round,
    depth: options.depth,
    hardFee: options.hardFee,
    subtractFee: options.subtractFee,
    changeAddress: this.getChange(),
    height: -1,
    rate: options.rate,
    maxFee: options.maxFee
  });
};

MemWallet.prototype.template = function template(mtx) {
  const keys = this.deriveInputs(mtx);
  mtx.template(keys);
};

MemWallet.prototype.sign = function sign(mtx) {
  const keys = this.deriveInputs(mtx);
  mtx.template(keys);
  mtx.sign(keys);
};

MemWallet.prototype.create = async function create(options) {
  const mtx = new MTX(options);

  await this.fund(mtx, options);

  assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.');

  mtx.sortMembers();

  if (options.locktime != null)
    mtx.setLocktime(options.locktime);

  this.sign(mtx);

  if (!mtx.isSigned())
    throw new Error('Cannot sign tx.');

  return mtx;
};

MemWallet.prototype.send = async function send(options) {
  const mtx = await this.create(options);
  this.addTX(mtx.toTX());
  return mtx;
};

function Path(hash, branch, index) {
  this.hash = hash;
  this.branch = branch;
  this.index = index;
}

module.exports = MemWallet;
