// TransactionBuilder
// ==================
//
// Creates a bitcore Transaction object
//
//
// Synopsis
// --------
// ```
//     var tx = (new TransactionBuilder(opts))
//       .setUnspent(utxos)
//       .setOutputs(outs)
//       .sign(keys)
//       .build();
//
//
//     var builder = (new TransactionBuilder(opts))
//       .setUnspent(spent)
//       .setOutputs(outs);
//
//     // Uncomplete tx (no signed or partially signed)
//     var tx = builder.build();
//
//     ..later..
//
//     builder.sign(keys);
//     while ( builder.isFullySigned() ) {
//
//       ... get new keys ...
//
//       builder.sign(keys);
//     }
//
//     var tx = builder.build();
//     broadcast(tx.serialize());
//
//     //Serialize it and pass it around...
//     var string = JSON.stringify(builder.toObj()); 
//     // then...
//     var builder = TransactionBuilder.fromObj(JSON.parse(str); 
//     builder.sign(keys);
//     // Also
//     var builder2 = TransactionBuilder.fromObj(JSON.parse(str2); 
//     builder2.merge(builder); // Will merge signatures for p2sh mulsig txs.
//      
//
// ```
//
//  
//  


'use strict';

var imports = require('soop').imports();
var Address = imports.Address || require('./Address');
var Script = imports.Script || require('./Script');
var util = imports.util || require('../util');
var bignum = imports.bignum || require('bignum');
var buffertools = imports.buffertools || require('buffertools');
var networks = imports.networks || require('../networks');
var WalletKey = imports.WalletKey || require('./WalletKey');
var PrivateKey = imports.PrivateKey || require('./PrivateKey');
var Key = imports.Key || require('./Key');
var log = imports.log || require('../util/log');

var Transaction = imports.Transaction || require('./Transaction');
var FEE_PER_1000B_SAT = parseInt(0.0001 * util.COIN);

// Methods
// -------
//
// TransactionBuilder
// ------------------
// Creates a TransactionBuilder instance
// `opts`
//  ```
//      { 
//        remainderOut: null,
//        fee: 0.001,
//        lockTime: null,
//        spendUnconfirmed: false,
//        signhash: SIGHASH_ALL
//      }
//  ```    
//  Amounts are in BTC. instead of fee and amount; feeSat and amountSat can be given, 
//  repectively, to provide amounts in satoshis.
//  
//  If no remainderOut is given, and there are remainder coins, the
//  first IN out will be used to return the coins. remainderOut has the form:
//  ```
//      remainderOut = { address: 1xxxxx}
//  ```    
//  or
//  ```
//      remainderOut = { pubkeys: ['hex1','hex2',...} for multisig
//  ```    

function TransactionBuilder(opts) {
  opts                  = opts || {};
  this.lockTime         = opts.lockTime || 0;
  this.spendUnconfirmed = opts.spendUnconfirmed || false;

  if (opts.fee || opts.feeSat) {
    this.givenFeeSat = opts.fee ? opts.fee * util.COIN : opts.feeSat;
  }
  this.remainderOut = opts.remainderOut;
  this.signhash = opts.signhash || Transaction.SIGHASH_ALL;

  this.tx         = {};
  this.inputsSigned= 0;
  this.signaturesAdded= 0;

  return this;
}

/*
 * scriptForAddress
 *
 *  Returns a scriptPubKey for the given address type
 */

TransactionBuilder.scriptForAddress = function(addressString) {

  var livenet = networks.livenet;
  var testnet = networks.testnet;
  var address = new Address(addressString);

  var version = address.version();
  var script;
  if (version === livenet.addressVersion || version === testnet.addressVersion)
    script = Script.createPubKeyHashOut(address.payload());
  else if (version === livenet.P2SHVersion || version === testnet.P2SHVersion)
    script = Script.createP2SH(address.payload());
  else
    throw new Error('invalid output address');

  return script;
};


TransactionBuilder._scriptForPubkeys = function(out) {

  var l = out.pubkeys.length;
  var pubKeyBuf=[];

  for (var i=0; i<l; i++) {
    pubKeyBuf.push(new Buffer(out.pubkeys[i],'hex'));
  }

  return Script.createMultisig(out.nreq, pubKeyBuf);
};

TransactionBuilder._scriptForOut = function(out) {
  var ret;
  if (out.address)
    ret = this.scriptForAddress(out.address);
  else if (out.pubkeys || out.nreq || out.nreq > 1)
    ret = this._scriptForPubkeys(out);
  else
    throw new Error('unknown out type');

  return ret;
};


TransactionBuilder.infoForP2sh = function(opts, networkName) {
  var script = this._scriptForOut(opts);
  var hash   = util.sha256ripe160(script.getBuffer());

  var version = networkName === 'testnet' ?
    networks.testnet.P2SHVersion : networks.livenet.P2SHVersion;

  var addr = new Address(version, hash);
  var addrStr = addr.as('base58');
  return {
    script: script,
    scriptBufHex: script.getBuffer().toString('hex'),
    hash: hash,
    address: addrStr,
  };
};

// setUnspent
// ----------
//  Sets the `unspent` available for the transaction. Some (or all) 
//  of them to fullfil the transaction's outputs and fee.
//  The expected format is:
//  ```
//      [{
//         address: "mqSjTad2TKbPcKQ3Jq4kgCkKatyN44UMgZ",
//         txid: "2ac165fa7a3a2b535d106a0041c7568d03b531e58aeccdd3199d7289ab12cfc1",
//         scriptPubKey: "76a9146ce4e1163eb18939b1440c42844d5f0261c0338288ac",
//         vout: 1,
//         amount: 0.01,                
//        confirmations: 3
//         }, ...
//      ]
//  ```    
//   This is compatible con insight's utxo API. 
//   That amount is in BTCs (as returned in insight and bitcoind).
//   amountSat (instead of amount) can be given to provide amount in satochis.
TransactionBuilder.prototype.setUnspent = function(unspent) {
  this.utxos = unspent;
  return this;
};

TransactionBuilder.prototype._setInputMap = function() {
  var inputMap = [];

  var l = this.selectedUtxos.length;
  for (var i = 0; i < l; i++) {
    var utxo          = this.selectedUtxos[i];
    var scriptBuf     = new Buffer(utxo.scriptPubKey, 'hex');
    var scriptPubKey  = new Script(scriptBuf);
    var scriptType    = scriptPubKey.classify();

    if (scriptType === Script.TX_UNKNOWN)
      throw new Error('unkown output type at:' + i +
                      ' Type:' + scriptPubKey.getRawOutType());

    inputMap.push({
      address: utxo.address,
      scriptPubKey: scriptPubKey,
      scriptType: scriptType,
      i: i,
    });
  }
  this.inputMap = inputMap;
  return this;
};


// getSelectedUnspent
// ------------------
//
// Returns the selected unspent outputs, to be used in the transaction.

TransactionBuilder.prototype.getSelectedUnspent = function() {
  return this.selectedUtxos;
};

/* _selectUnspent
  * TODO(?): sort sel (at the end) and check is some inputs can be avoided.
  * If the initial utxos are sorted, this step would be necesary only if
  * utxos were selected from different minConfirmationSteps.
  */

TransactionBuilder.prototype._selectUnspent = function(neededAmountSat) {

  if (!this.utxos || !this.utxos.length)
    throw new Error('unspent not set');

  var minConfirmationSteps = [6, 1];
  if (this.spendUnconfirmed) minConfirmationSteps.push(0);

  var sel            = [],
    totalSat         = bignum(0),
    fulfill          = false,
    maxConfirmations = null,
    l                = this.utxos.length;

  do {
    var minConfirmations = minConfirmationSteps.shift();
    for (var i = 0; i < l; i++) {
      var u = this.utxos[i];
      var c = u.confirmations || 0;

      if (c < minConfirmations || (maxConfirmations && c >= maxConfirmations))
        continue;

      var sat = u.amountSat || util.parseValue(u.amount);
      totalSat = totalSat.add(sat);
      sel.push(u);
      if (totalSat.cmp(neededAmountSat) >= 0) {
        fulfill = true;
        break;
      }
    }
    maxConfirmations = minConfirmations;
  } while (!fulfill && minConfirmationSteps.length);

  if (!fulfill)
    throw new Error('no enough unspent to fulfill totalNeededAmount [SAT]:' +
                    neededAmountSat);

  this.selectedUtxos = sel;
  this._setInputMap();
  return this;
};

TransactionBuilder.prototype._setInputs = function(txobj) {
  var ins = this.selectedUtxos;
  var l = ins.length;
  var valueInSat = bignum(0);

  txobj.ins=[];
  for (var i = 0; i < l; i++) {
    valueInSat = valueInSat.add(util.parseValue(ins[i].amount));

    var txin = {};
    txin.s = util.EMPTY_BUFFER;
    txin.q = 0xffffffff;

    var hash = new Buffer(ins[i].txid, 'hex');
    var hashReversed = buffertools.reverse(hash);

    var vout = parseInt(ins[i].vout);
    var voutBuf = new Buffer(4);
    voutBuf.writeUInt32LE(vout, 0);

    txin.o = Buffer.concat([hashReversed, voutBuf]);
    txobj.ins.push(txin);
  }
  this.valueInSat = valueInSat;
  return this;
};

TransactionBuilder.prototype._setFee = function(feeSat) {
  if ( typeof this.valueOutSat === 'undefined')
    throw new Error('valueOutSat undefined');


  var valueOutSat = this.valueOutSat.add(feeSat);

  if (this.valueInSat.cmp(valueOutSat) < 0) {
    var inv = this.valueInSat.toString();
    var ouv = valueOutSat.toString();
    throw new Error('transaction input amount is less than outputs: ' +
      inv + ' < ' + ouv + ' [SAT]');
  }
  this.feeSat = feeSat;
  return this;
};

TransactionBuilder.prototype._setRemainder = function(txobj, remainderIndex) {

  if ( typeof this.valueInSat === 'undefined' ||
      typeof this.valueOutSat === 'undefined')
    throw new Error('valueInSat / valueOutSat undefined');

  /* add remainder (without modifying outs[]) */
  var remainderSat = this.valueInSat.sub(this.valueOutSat).sub(this.feeSat);
  var l =txobj.outs.length;
  this.remainderSat = bignum(0);

  /*remove old remainder? */
  if (l > remainderIndex) {
    txobj.outs.pop();
  }

  if (remainderSat.cmp(0) > 0) {
    var remainderOut = this.remainderOut || this.selectedUtxos[0];
    var value = util.bigIntToValue(remainderSat);
    var script = TransactionBuilder._scriptForOut(remainderOut);
    var txout = {
      v: value,
      s: script.getBuffer(),
    };
    txobj.outs.push(txout);
    this.remainderSat = remainderSat;
  }

  return this;
};

TransactionBuilder.prototype._setFeeAndRemainder = function(txobj) {

  /* starting size estimation */
  var size = 500, maxSizeK, remainderIndex = txobj.outs.length;
  do {
    /* based on https://en.bitcoin.it/wiki/Transaction_fees */
    maxSizeK = parseInt(size / 1000) + 1;

    var feeSat = this.givenFeeSat ?
      this.givenFeeSat : maxSizeK * FEE_PER_1000B_SAT;

    var neededAmountSat = this.valueOutSat.add(feeSat);

    this._selectUnspent(neededAmountSat)
        ._setInputs(txobj)
        ._setFee(feeSat)
        ._setRemainder(txobj, remainderIndex);

        
    size = new Transaction(txobj).getSize();
  } while (size > (maxSizeK + 1) * 1000);
  return this;
};

// setOutputs
// ----------
// Sets the outputs for the transaction. Format is:
// ```
//      an array of [{
//        address: xx, 
//        amount:0.001
//       },...]
// ```      
//
// Note that only some of this outputs will be selected
// to create the transaction. The selected ones can be checked
// after calling `setOutputs`, with `.getSelectedUnspent`
//

TransactionBuilder.prototype.setOutputs = function(outs) {
  var valueOutSat = bignum(0);

  var txobj = {}; 
  txobj.version    = 1;
  txobj.lock_time  = this.lockTime || 0;
  txobj.ins  = [];
  txobj.outs = [];

  var l =outs.length;
  for (var i = 0; i < l; i++) {
    var amountSat = outs[i].amountSat || util.parseValue(outs[i].amount);
    var value = util.bigIntToValue(amountSat);
    var script = TransactionBuilder._scriptForOut(outs[i]);
    var txout = {
      v: value,
      s: script.getBuffer(),
    };
    txobj.outs.push(txout);

    var sat = outs[i].amountSat || util.parseValue(outs[i].amount);
    valueOutSat = valueOutSat.add(sat);
  }

  this.valueOutSat = valueOutSat;

  this._setFeeAndRemainder(txobj);

  this.tx = new Transaction(txobj);
  return this;
};

TransactionBuilder._mapKeys = function(keys) {
  /* prepare keys */
  var walletKeyMap = {};
  var l = keys.length;
  var wk;
  for (var i = 0; i < l; i++) {
    var k = keys[i];

    if (typeof k === 'string') {
      var pk = new PrivateKey(k);
      wk = new WalletKey({ network: pk.network() });
      wk.fromObj({ priv: k });
    }
    else if (k instanceof WalletKey) {
      wk = k;
    }
    else {
      throw new Error('argument must be an array of strings (WIF format) or WalletKey objects');
    }
    walletKeyMap[wk.storeObj().addr] = wk;
  }
  return walletKeyMap;
};

TransactionBuilder._signHashAndVerify = function(wk, txSigHash) {
  var triesLeft = 10, sigRaw;

  do {
    sigRaw = wk.privKey.signSync(txSigHash);
  } while (wk.privKey.verifySignatureSync(txSigHash, sigRaw) === false &&
           triesLeft--);

  if (triesLeft<0)
    throw new Error('could not sign input: verification failed');

  return sigRaw;
};

TransactionBuilder.prototype._checkTx = function() {
  if (! this.tx || !this.tx.ins.length || !this.tx.outs.length)
    throw new Error('tx is not defined');
};


TransactionBuilder.prototype._multiFindKey = function(walletKeyMap,pubKeyHash) {
  var wk;
  [ networks.livenet, networks.testnet].forEach(function(n) {
    [ n.addressVersion, n.P2SHVersion].forEach(function(v) {
      var a = new Address(v,pubKeyHash);
      if (!wk && walletKeyMap[a]) {
        wk = walletKeyMap[a];
      }
    });
  });

  return wk;
};

TransactionBuilder.prototype._findWalletKey = function(walletKeyMap, input) {
  var wk;

  if (input.address) {
    wk        = walletKeyMap[input.address];
  }
  else if (input.pubKeyHash) {
    wk             = this._multiFindKey(walletKeyMap, input.pubKeyHash);
  }
  else if (input.pubKeyBuf) {
    var pubKeyHash = util.sha256ripe160(input.pubKeyBuf);
    wk             = this._multiFindKey(walletKeyMap, pubKeyHash);
  } else {
    throw new Error('no infomation at input to find keys');
  }
  return wk;
};

TransactionBuilder.prototype._signPubKey = function(walletKeyMap, input, txSigHash) {
  if (this.tx.ins[input.i].s.length > 0) return {};

  var wk        = this._findWalletKey(walletKeyMap, input);
  if (!wk) return;

  var sigRaw    = TransactionBuilder._signHashAndVerify(wk, txSigHash);
  var sigType   = new Buffer(1);
  sigType[0]    = this.signhash;
  var sig       = Buffer.concat([sigRaw, sigType]);

  var scriptSig = new Script();
  scriptSig.chunks.push(sig);
  scriptSig.updateBuffer();
  return {inputFullySigned: true, signaturesAdded: 1, script: scriptSig.getBuffer()};
};

TransactionBuilder.prototype._signPubKeyHash = function(walletKeyMap, input, txSigHash) {

  if (this.tx.ins[input.i].s.length > 0) return {};

  var wk        = this._findWalletKey(walletKeyMap, input);
  if (!wk) return;

  var sigRaw    = TransactionBuilder._signHashAndVerify(wk, txSigHash);
  var sigType   = new Buffer(1);
  sigType[0]    = this.signhash;
  var sig       = Buffer.concat([sigRaw, sigType]);

  var scriptSig = new Script();
  scriptSig.chunks.push(sig);
  scriptSig.chunks.push(wk.privKey.public);
  scriptSig.updateBuffer();
  return {inputFullySigned: true, signaturesAdded: 1, script: scriptSig.getBuffer()};
};

/* FOR TESTING
var _dumpChunks = function (scriptSig, label) {
  console.log('## DUMP: ' + label + ' ##');
  for(var i=0; i<scriptSig.chunks.length; i++) {
    console.log('\tCHUNK ', i, Buffer.isBuffer(scriptSig.chunks[i])
                ?scriptSig.chunks[i].toString('hex'):scriptSig.chunks[i] ); 
  }
};
*/

TransactionBuilder.prototype._chunkSignedWithKey = function(scriptSig, txSigHash, publicKey) {
  var ret;
  var k = new Key();
  k.public =publicKey;

  for(var i=1; i<= scriptSig.countSignatures(); i++) {
    var chunk = scriptSig.chunks[i];
    var sigRaw = new Buffer(chunk.slice(0,chunk.length-1));
    if (k.verifySignatureSync(txSigHash, sigRaw) ) {
      ret=chunk;  
    }
  }
  return ret;
};


TransactionBuilder.prototype._getSignatureOrder = function(sigPrio, sigRaw, txSigHash, pubkeys) {
  var l=pubkeys.length;
  for(var j=0; j<l; j++) {
    var k = new Key();
    k.public = new Buffer(pubkeys[j],'hex');
    if (k.verifySignatureSync(txSigHash, sigRaw))
      break;
  }
  return j;
};

TransactionBuilder.prototype._getNewSignatureOrder = function(sigPrio, scriptSig, txSigHash, pubkeys) {
  var iPrio;
  for(var i=1; i<= scriptSig.countSignatures(); i++) {
    var chunk = scriptSig.chunks[i];
    var sigRaw = new Buffer(chunk.slice(0,chunk.length-1));
    iPrio = this._getSignatureOrder(sigPrio, sigRaw, txSigHash, pubkeys);
    if (sigPrio <= iPrio) break;
  }
  return (sigPrio === iPrio? -1: i-1);
};

TransactionBuilder.prototype._chunkIsEmpty = function(chunk) {
  return chunk === 0 ||  // when serializing and back, EMPTY_BUFFER becomes 0
    buffertools.compare(chunk, util.EMPTY_BUFFER) === 0;
};

TransactionBuilder.prototype._initMultiSig = function(script) {
  var wasUpdated = false;
  if (script.chunks[0] !== 0) {
    script.prependOp0();
    wasUpdated = true;
  }
  return wasUpdated;
};

TransactionBuilder.prototype._updateMultiSig = function(sigPrio, wk, scriptSig, txSigHash, pubkeys) {
  var wasUpdated = this._initMultiSig(scriptSig);

  if (this._chunkSignedWithKey(scriptSig, txSigHash, wk.privKey.public))
    return null;

  // Create signature
  var sigRaw  = TransactionBuilder._signHashAndVerify(wk, txSigHash);
  var sigType = new Buffer(1);
  sigType[0]  = this.signhash;
  var sig     = Buffer.concat([sigRaw, sigType]);

  // Add signature
  var order = this._getNewSignatureOrder(sigPrio,scriptSig,txSigHash,pubkeys);
  scriptSig.chunks.splice(order+1,0,sig);
  scriptSig.updateBuffer();
  wasUpdated=true;

  return wasUpdated ? scriptSig : null;
};


TransactionBuilder.prototype._signMultiSig = function(walletKeyMap, input, txSigHash) {
  var pubkeys = input.scriptPubKey.capture(),
    nreq    = input.scriptPubKey.chunks[0] - 80, //see OP_2-OP_16
    l = pubkeys.length,
    originalScriptBuf = this.tx.ins[input.i].s;

  var scriptSig = new Script (originalScriptBuf);
  var signaturesAdded = 0;

  for(var j=0; j<l && scriptSig.countSignatures() < nreq ; j++) {
    var wk = this._findWalletKey(walletKeyMap, {pubKeyBuf: pubkeys[j]});
    if (!wk) continue;

    var newScriptSig = this._updateMultiSig(j, wk, scriptSig, txSigHash, pubkeys);
    if (newScriptSig) {
      scriptSig = newScriptSig;
      signaturesAdded++;
    }
  }

  var ret = {
    inputFullySigned:  scriptSig.countSignatures() === nreq,
    signaturesAdded: signaturesAdded,
    script: scriptSig.getBuffer(),
  };
  return ret;
};
 
var fnToSign = {};
TransactionBuilder.prototype._scriptIsAppended = function(script, scriptToAddBuf) {
  var len = script.chunks.length;

  if (script.chunks[len-1] === undefined)
    return false;
  if (typeof script.chunks[len-1] === 'number')
    return false;
  if (buffertools.compare(script.chunks[len-1] , scriptToAddBuf) !==0 )
    return false;

  return true;
};

TransactionBuilder.prototype._addScript = function(scriptBuf, scriptToAddBuf) {
  var s = new Script(scriptBuf);

  if (!this._scriptIsAppended(s, scriptToAddBuf)) {
    s.chunks.push(scriptToAddBuf);
    s.updateBuffer();
  }
  return s.getBuffer();
};
 
TransactionBuilder.prototype._getInputForP2sh = function(script, index) {
  var scriptType = script.classify();
  /* pubKeyHash is needed for TX_PUBKEYHASH and TX_PUBKEY to retrieve the keys. */
  var pubKeyHash;
  switch(scriptType) {
    case Script.TX_PUBKEYHASH:
      pubKeyHash = script.captureOne();
      break;
    case Script.TX_PUBKEY:
      var chunk  = script.captureOne();
      pubKeyHash = util.sha256ripe160(chunk);
  }

  return {
    i: index,
    pubKeyHash: pubKeyHash,
    scriptPubKey: script,
    scriptType: scriptType,
    isP2sh: true,
  };
};

TransactionBuilder.prototype._p2shInput = function(input) {
  if (!this.hashToScriptMap)
    throw new Error('hashToScriptMap not set');

  var scriptHex = this.hashToScriptMap[input.address];
  if (!scriptHex) return;

  var scriptBuf     = new Buffer(scriptHex,'hex');
  var script        = new Script(scriptBuf);
  var scriptType    = script.classify();

  if (!fnToSign[scriptType] || scriptType === Script.TX_SCRIPTHASH)
    throw new Error('dont know how to sign p2sh script type:'+ script.getRawOutType());

  return {
    input: this._getInputForP2sh(script, input.i),
    txSigHash: this.tx.hashForSignature( script, input.i, this.signhash),
    scriptType: script.classify(),
    scriptBuf: scriptBuf,
  };
};

TransactionBuilder.prototype._signScriptHash = function(walletKeyMap, input, txSigHash) {

  var p2sh  = this._p2shInput(input);

  var ret   = fnToSign[p2sh.scriptType].call(this, walletKeyMap, p2sh.input, p2sh.txSigHash);
  if (ret && ret.script && ret.signaturesAdded) {
    ret.script = this._addScript(ret.script, p2sh.scriptBuf);
  }
  return ret;
};

fnToSign[Script.TX_PUBKEYHASH] = TransactionBuilder.prototype._signPubKeyHash;
fnToSign[Script.TX_PUBKEY]     = TransactionBuilder.prototype._signPubKey;
fnToSign[Script.TX_MULTISIG]   = TransactionBuilder.prototype._signMultiSig;
fnToSign[Script.TX_SCRIPTHASH] = TransactionBuilder.prototype._signScriptHash;

// sign
// ----
// Signs a transaction. 
// `keys`: an array of strings representing private keys to sign the 
// transaction in WIF private key format OR bitcore's `WalletKey` objects
//
// If multiple keys are given, each will be tested against the transaction's 
// scriptPubKeys. Only the valid private keys will be used to sign.
// This method is fully compatible with *multisig* transactions.
//
// `.isFullySigned` can be queried to check is the transactions have all the needed
// signatures.
//
//
TransactionBuilder.prototype.sign = function(keys) {
  this._checkTx();
  var tx  = this.tx,
      ins = tx.ins,
      l   = ins.length,
      walletKeyMap = TransactionBuilder._mapKeys(keys);

  for (var i = 0; i < l; i++) {
    var input = this.inputMap[i];

    var txSigHash = this.tx.hashForSignature(
      input.scriptPubKey, i, this.signhash);

    var ret = fnToSign[input.scriptType].call(this, walletKeyMap, input, txSigHash);
    if (ret && ret.script) {
      tx.ins[i].s = ret.script;
      if (ret.inputFullySigned) this.inputsSigned++;
      if (ret.signaturesAdded) this.signaturesAdded +=ret.signaturesAdded;
    }
  }
  return this;
};

// setHashToScriptMap
// ------------------
// Needed for setup Address to Script maps
// for p2sh transactions. See `.infoForP2sh`
// for generate the input for this call.
//
TransactionBuilder.prototype.setHashToScriptMap = function(hashToScriptMap) {
  this.hashToScriptMap= hashToScriptMap;
  return this;
};


// isFullySigned
// -------------
// Checks if the transaction have all the necesary signatures.
// Also, `.signaturesAdded` and `.inputsSigned` can be queried
// for more information about the transaction signature status.
//
TransactionBuilder.prototype.isFullySigned = function() {
  return this.inputsSigned === this.tx.ins.length;
};

TransactionBuilder.prototype.build = function() {
  this._checkTx();
  return this.tx;
};

// toObj
// -----
// Returns a plain Javascript object that contains
// the full status of the TransactionBuilder instance,
// suitable for serialization, storage and transmition.
// See `.fromObj`
//
TransactionBuilder.prototype.toObj = function() {
  var data = { 
    valueInSat       : this.valueInSat.toString(),
    valueOutSat      : this.valueOutSat.toString(),
    feeSat           : this.feeSat.toString(),
    remainderSat     : this.remainderSat.toString(),

    hashToScriptMap  : this.hashToScriptMap,
    selectedUtxos    : this.selectedUtxos,

    inputsSigned     : this.inputsSigned,
    signaturesAdded  : this.signaturesAdded,

    signhash         : this.signhash,
    spendUnconfirmed : this.spendUnconfirmed,
  };
  if (this.tx) {
    data.tx  =this.tx.serialize().toString('hex');
  }
  return data;
};

// fromObj
// -------
// Returns a TransactionBuilder instance given
// a plain Javascript object created previously 
// with `.toObj`. See `.toObj`.

TransactionBuilder.fromObj = function(data) {
  var b = new TransactionBuilder();
  b.valueInSat       = data.valueInSat.toString();
  b.valueOutSat      = data.valueOutSat.toString();
  b.feeSat           = data.feeSat.toString();
  b.remainderSat     = data.remainderSat.toString();

  b.hashToScriptMap  = data.hashToScriptMap;
  b.selectedUtxos    = data.selectedUtxos;

  b.inputsSigned     = data.inputsSigned;
  b.signaturesAdded  = data.signaturesAdded;

  b.signhash         = data.signhash;
  b.spendUnconfirmed = data.spendUnconfirmed;

  b._setInputMap();

  if (data.tx) {
    // Tx may have signatures, that are not on txobj
    var t = new Transaction();
    t.parse(new Buffer(data.tx,'hex'));
    b.tx = t;
  }
  return b;
};


TransactionBuilder.prototype._checkMergeability = function(b) {
  var self=this;

  // Builder should have the same params
  ['valueInSat', 'valueOutSat', 'feeSat', 'remainderSat', 'signhash', 'spendUnconfirmed']
      .forEach(function (k) {

    if (self[k].toString() !== b[k].toString()) {
      throw new Error('mismatch at TransactionBuilder match: ' 
        + k + ': ' + self[k] + ' vs. ' + b[k]);
    }
  });

  if (self.hashToScriptMap) {
    var err = 0;
    if(! b.hashToScriptMap) err=1;
    Object.keys(self.hashToScriptMap).forEach(function(k) {
      if (!b.hashToScriptMap[k]) err=1;
      if (self.hashToScriptMap[k] !== b.hashToScriptMap[k]) err=1;
    });
    if (err)
      throw new Error('mismatch at TransactionBuilder hashToScriptMap');
  }


  var err = 0, i=0;;
  self.selectedUtxos.forEach(function(u) {
    if (!err) {
      var v=b.selectedUtxos[i++];
      if (!v) err=1;
      // confirmations could differ
      ['address', 'hash', 'scriptPubKey', 'vout', 'amount'].forEach(function(k) {
        if (u[k] !== v[k])
          err=k;
      });
    }
  });
  if (err)
    throw new Error('mismatch at TransactionBuilder selectedUtxos #' + i-1+ ' Key:' + err);


  err = 0; i=0;;
  self.inputMap.forEach(function(u) {
    if (!err) {
      var v=b.inputMap[i++];
      if (!v) err=1;
      // confirmations could differ
      ['address', 'scriptType', 'scriptPubKey', 'i'].forEach(function(k) {
        if (u[k].toString() !== v[k].toString())
          err=k;
      });
    }
  });
  if (err)
    throw new Error('mismatch at TransactionBuilder inputMap #' + i-1 + ' Key:' + err);

};

// TODO this could be on Script class
TransactionBuilder.prototype._mergeInputSigP2sh = function(input,s0,s1) {
  var p2sh  = this._p2shInput(input);
  var redeemScript = new Script(p2sh.scriptBuf);
  var pubkeys = redeemScript.capture();

  // Look for differences
  var s0keys = {};
  var l = pubkeys.length;
  for (var j=0; j<l; j++) {
    if ( this._chunkSignedWithKey(s0, p2sh.txSigHash, pubkeys[j]))
      s0keys[pubkeys[j].toString('hex')] = 1;
  }

  var diff = [];
  for (var j=0; j<l; j++) {
    var chunk = this._chunkSignedWithKey(s1, p2sh.txSigHash, pubkeys[j]);
    var pubHex = pubkeys[j].toString('hex');
    if (chunk && !s0keys[pubHex]) {
      diff.push({
        prio: j,
        chunk: chunk,
        pubHex: pubHex,
      });
    }
  }

  // Add signatures
  for(var j in diff) {
    var newSig = diff[j];
    var order = this._getNewSignatureOrder(newSig.prio,s0,p2sh.txSigHash,pubkeys);
    s0.chunks.splice(order+1,0,newSig.chunk);
    this.signaturesAdded++;
  }
  s0.updateBuffer();
  return s0.getBuffer();
};

// TODO this could be on Script class
TransactionBuilder.prototype._mergeInputSig = function(index, s0buf, s1buf) {
  if (buffertools.compare(s0buf,s1buf) === 0)
    return s0buf;

  var s0 = new Script(s0buf);
  var s1 = new Script(s1buf);
  var l0 = s0.chunks.length;
  var l1 = s1.chunks.length;
  var s0map = {};

  if (l0 && l1 && ((l0<2 && l1>2) || (l1<2 && l0>2 )))
    throw new Error('TX sig types mismatch in merge');

  if ((!l0 && !l1) || ( l0 && !l1) || (!l0 &&  l1)) 
    return s1buf;

  // Get the pubkeys
  var input = this.inputMap[index];
  var type  = input.scriptPubKey.classify();

  //p2pubkey or p2pubkeyhash
  if (type === Script.TX_PUBKEYHASH || type === Script.TX_PUBKEY) {
    log.debug('Merging two signed inputs type:' +
      input.scriptPubKey.getRawOutType() + '. Signatures differs. Using the first version.');
    return s0buf;
  }
  else if (type!== Script.TX_SCRIPTHASH) {
    // No support for normal multisig or strange txs.
    throw new Error('Script type:'+input.scriptPubKey.getRawOutType()+'not supported at #merge');
  }
  return this._mergeInputSigP2sh(input,s0, s1);
};

// TODO this could be on Transaction class
TransactionBuilder.prototype._mergeTx = function(tx) {
    var v0 = this.tx;
    var v1 = tx;

    var l = v0.ins.length;
    if (l !== v1.ins.length) 
      throw new Error('TX in length mismatch in merge');

    this.inputsSigned =0;
    for(var i=0; i<l; i++) {
      var i0 =  v0.ins[i];
      var i1 =  v1.ins[i];

      if (i0.q !==  i1.q)
        throw new Error('TX sequence ins mismatch in merge. Input:',i);

      if (buffertools.compare(i0.o,i1.o) !== 0)
        throw new Error('TX .o in mismatch in merge. Input:',i);

      i0.s=this._mergeInputSig(i, i0.s,i1.s);

      if (v0.isInputComplete(i)) this.inputsSigned++;
    }
};

// merge
// -----
// Merge to TransactionBuilder objects, merging inputs signatures.
// This function supports multisig p2sh inputs.

TransactionBuilder.prototype.merge = function(b) {
  this._checkMergeability(b);

  // Does this tX have any signature already?
  if (this.tx || b.tx) {
    if (this.tx.getNormalizedHash().toString('hex') 
        !== b.tx.getNormalizedHash().toString('hex')) 
      throw new Error('mismatch at TransactionBuilder NTXID');

    this._mergeTx(b.tx);   
  }
};

module.exports = require('soop')(TransactionBuilder);