var imports            = require('soop').imports();
var config             = imports.config || require('./config');
var log                = imports.log || require('./util/log');
var Address            = imports.Address || require('./Address');
var Script             = imports.Script || require('./Script');
var ScriptInterpreter  = imports.ScriptInterpreter || require('./ScriptInterpreter');
var util               = imports.util || require('./util/util');
var bignum             = imports.bignum || require('bignum');
var Put                = imports.Put || require('bufferput');
var Parser             = imports.Parser || require('./util/BinaryParser');
var Step               = imports.Step || require('step');
var buffertools        = imports.buffertools || require('buffertools');
var error              = imports.error || require('./util/error');

var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]);

function TransactionIn(data) {
  if ("object" !== typeof data) {
    data = {};
  }
  if (data.o) {
    this.o = data.o;
  } else {
    if (data.oTxHash && typeof data.oIndex !== 'undefined' && data.oIndex >= 0) {
      var hash = new Buffer(data.oTxHash, 'hex');
      hash = buffertools.reverse(hash);
      var voutBuf = new Buffer(4);
      voutBuf.writeUInt32LE(data.oIndex, 0);
      this.o = Buffer.concat([hash, voutBuf]);
    }
  }
  this.s = Buffer.isBuffer(data.s) ? data.s :
           Buffer.isBuffer(data.script) ? data.script : util.EMPTY_BUFFER;
  this.q = data.q ? data.q : data.sequence;
}

TransactionIn.prototype.getScript = function getScript() {
  return new Script(this.s);
};

TransactionIn.prototype.isCoinBase = function isCoinBase() {
  return buffertools.compare(this.o, COINBASE_OP) === 0;
};

TransactionIn.prototype.serialize = function serialize() {
  var slen = util.varIntBuf(this.s.length);
  var qbuf = new Buffer(4);
  qbuf.writeUInt32LE(this.q, 0);

  var ret = Buffer.concat([this.o, slen, this.s, qbuf]);
  return ret;
};

TransactionIn.prototype.getOutpointHash = function getOutpointHash() {
  if ("undefined" !== typeof this.o.outHashCache) {
    return this.o.outHashCache;
  }

  return this.o.outHashCache = this.o.slice(0, 32);
};

TransactionIn.prototype.getOutpointIndex = function getOutpointIndex() {
  return (this.o[32]      ) +
         (this.o[33] <<  8) +
         (this.o[34] << 16) +
         (this.o[35] << 24);
};

TransactionIn.prototype.setOutpointIndex = function setOutpointIndex(n) {
  this.o[32] = n       & 0xff;
  this.o[33] = n >>  8 & 0xff;
  this.o[34] = n >> 16 & 0xff;
  this.o[35] = n >> 24 & 0xff;
};


function TransactionOut(data) {
  if ("object" !== typeof data) {
    data = {};
  }
  this.v = data.v ? data.v : data.value;
  this.s = data.s ? data.s : data.script;
};

TransactionOut.prototype.getValue = function getValue() {
  return new Parser(this.v).word64lu();
};

TransactionOut.prototype.getScript = function getScript() {
  return new Script(this.s);
};

TransactionOut.prototype.serialize = function serialize() {
  var slen = util.varIntBuf(this.s.length);
  return Buffer.concat([this.v, slen, this.s]);
};

function Transaction(data) {
  if ("object" !== typeof data) {
    data = {};
  }
  this.hash = data.hash || null;
  this.version = data.version;
  this.lock_time = data.lock_time;
  this.ins = Array.isArray(data.ins) ? data.ins.map(function (data) {
    var txin = new TransactionIn();
    txin.s = data.s;
    txin.q = data.q;
    txin.o = data.o;
    return txin;
  }) : [];
  this.outs = Array.isArray(data.outs) ? data.outs.map(function (data) {
    var txout = new TransactionOut();
    txout.v = data.v;
    txout.s = data.s;
    return txout;
  }) : [];
  if (data.buffer) this._buffer = data.buffer;
};
this.class = Transaction;
Transaction.In = TransactionIn;
Transaction.Out = TransactionOut;

Transaction.prototype.isCoinBase = function () {
  return this.ins.length == 1 && this.ins[0].isCoinBase();
};

Transaction.prototype.isStandard = function isStandard() {
  var i;
  for (i = 0; i < this.ins.length; i++) {
    if (this.ins[i].getScript().getInType() == "Strange") {
      return false;
    }
  }
  for (i = 0; i < this.outs.length; i++) {
    if (this.outs[i].getScript().getOutType() == "Strange") {
      return false;
    }
  }
  return true;
};

Transaction.prototype.serialize = function serialize() {
  var bufs = [];

  var buf = new Buffer(4);
  buf.writeUInt32LE(this.version, 0);
  bufs.push(buf);

  bufs.push(util.varIntBuf(this.ins.length));
  this.ins.forEach(function (txin) {
    bufs.push(txin.serialize());
  });

  bufs.push(util.varIntBuf(this.outs.length));
  this.outs.forEach(function (txout) {
    bufs.push(txout.serialize());
  });

  var buf = new Buffer(4);
  buf.writeUInt32LE(this.lock_time, 0);
  bufs.push(buf);

  this._buffer = Buffer.concat(bufs);
  return this._buffer;
};

Transaction.prototype.getBuffer = function getBuffer() {
  if (this._buffer) return this._buffer;

  return this.serialize();
};

Transaction.prototype.calcHash = function calcHash() {
  this.hash =  util.twoSha256(this.getBuffer());
  return this.hash;
};

Transaction.prototype.checkHash = function checkHash() {
  if (!this.hash || !this.hash.length) return false;

  return buffertools.compare(this.calcHash(), this.hash) === 0;
};

Transaction.prototype.getHash = function getHash() {
  if (!this.hash || !this.hash.length) {
    this.hash = this.calcHash();
  }
  return this.hash;
};

// convert encoded list of inputs to easy-to-use JS list-of-lists
Transaction.prototype.inputs = function inputs() {
  var res = [];
  for (var i = 0; i < this.ins.length; i++) {
    var txin = this.ins[i];
    var outHash = txin.getOutpointHash();
    var outIndex = txin.getOutpointIndex();
    res.push([outHash, outIndex]);
  }

  return res;
}

/**
 * Load and cache transaction inputs.
 *
 * This function will try to load the inputs for a transaction.
 *
 * @param {BlockChain} blockChain A reference to the BlockChain object.
 * @param {TransactionMap|null} txStore Additional transactions to consider.
 * @param {Boolean} wait Whether to keep trying until the dependencies are
 * met (or a timeout occurs.)
 * @param {Function} callback Function to call on completion.
 */
Transaction.prototype.cacheInputs =
function cacheInputs(blockChain, txStore, wait, callback) {
  var self = this;

  var txCache = new TransactionInputsCache(this);
  txCache.buffer(blockChain, txStore, wait, callback);
};

Transaction.prototype.verify = function verify(txCache, blockChain, callback) {
  var self = this;

  var txIndex = txCache.txIndex;

  var outpoints = [];

  var valueIn = bignum(0);
  var valueOut = bignum(0);

  function getTxOut(txin, n) {
    var outHash = txin.getOutpointHash();
    var outIndex = txin.getOutpointIndex();
    var outHashBase64 = outHash.toString('base64');
    var fromTxOuts = txIndex[outHashBase64];

    if (!fromTxOuts) {
      throw new MissingSourceError(
        "Source tx " + util.formatHash(outHash) +
          " for inputs " + n  + " not found",
        // We store the hash of the missing tx in the error
        // so that the txStore can watch out for it.
        outHash.toString('base64')
      );
    }

    var txout = fromTxOuts[outIndex];

    if (!txout) {
      throw new Error("Source output index "+outIndex+
                      " for input "+n+" out of bounds");
    }

    return txout;
  };

  Step(
    function verifyInputs() {
      var group = this.group();

      if (self.isCoinBase()) {
        throw new Error("Coinbase tx are invalid unless part of a block");
      }

      self.ins.forEach(function (txin, n) {
        var txout = getTxOut(txin, n);

        // TODO: Verify coinbase maturity

        valueIn = valueIn.add(util.valueToBigInt(txout.v));

        outpoints.push(txin.o);

        self.verifyInput(n, txout.getScript(), group());
      });
    },

    function verifyInputsResults(err, results) {
      if (err) throw err;

      for (var i = 0, l = results.length; i < l; i++) {
        if (!results[i]) {
          var txout = getTxOut(self.ins[i]);
          log.debug('Script evaluated to false');
          log.debug('|- scriptSig', ""+self.ins[i].getScript());
          log.debug('`- scriptPubKey', ""+txout.getScript());
          throw new VerificationError('Script for input '+i+' evaluated to false');
        }
      }

      this();
    },

    function queryConflicts(err) {
      if (err) throw err;

      // Make sure there are no other transactions spending the same outs
      blockChain.countConflictingTransactions(outpoints, this);
    },
    function checkConflicts(err, count) {
      if (err) throw err;

      self.outs.forEach(function (txout) {
        valueOut = valueOut.add(util.valueToBigInt(txout.v));
      });

      if (valueIn.cmp(valueOut) < 0) {
        var outValue = util.formatValue(valueOut);
        var inValue = util.formatValue(valueIn);
        throw new Error("Tx output value (BTC "+outValue+") "+
                        "exceeds input value (BTC "+inValue+")");
      }

      var fees = valueIn.sub(valueOut);

      if (count) {
        // Spent output detected, retrieve transaction that spends it
        blockChain.getConflictingTransactions(outpoints, function (err, results) {
          if (results.length) {
            if (buffertools.compare(results[0].getHash(), self.getHash()) === 0) {
              log.warn("Detected tx re-add (recoverable db corruption): "
                          + util.formatHashAlt(results[0].getHash()));
              // TODO: Needs to return an error for the memory pool case?
              callback(null, fees);
            } else {
              callback(new Error("At least one referenced output has"
                                 + " already been spent in tx "
                                 + util.formatHashAlt(results[0].getHash())));
            }
          } else {
            callback(new Error("Outputs of this transaction are spent, but "+
                               "the transaction(s) that spend them are not "+
                               "available. This probably means you need to "+
                               "reset your database."));
          }
        });
        return;
      }

      // Success
      this(null, fees);
    },
    callback
  );
};

Transaction.prototype.verifyInput = function verifyInput(n, scriptPubKey, callback) {
  return ScriptInterpreter.verify(this.ins[n].getScript(),
                                  scriptPubKey,
                                  this, n, 0,
                                  callback);
};

/**
 * Returns an object containing all pubkey hashes affected by this transaction.
 *
 * The return object contains the base64-encoded pubKeyHash values as keys
 * and the original pubKeyHash buffers as values.
 */
Transaction.prototype.getAffectedKeys = function getAffectedKeys(txCache) {
  // TODO: Function won't consider results cached if there are no affected
  //       accounts.
  if (!(this.affects && this.affects.length)) {
    this.affects = [];

    // Index any pubkeys affected by the outputs of this transaction
    for (var i = 0, l = this.outs.length; i < l; i++) {
      try {
        var txout = this.outs[i];
        var script = txout.getScript();

        var outPubKey = script.simpleOutPubKeyHash();
        if (outPubKey) {
          this.affects.push(outPubKey);
        }
      } catch (err) {
        // It's not our job to validate, so we just ignore any errors and issue
        // a very low level log message.
        log.debug("Unable to determine affected pubkeys: " +
                     (err.stack ? err.stack : ""+err));
      }
    };

    // Index any pubkeys affected by the inputs of this transaction
    var txIndex = txCache.txIndex;
    for (var i = 0, l = this.ins.length; i < l; i++) {
      try {
        var txin = this.ins[i];

        if (txin.isCoinBase()) continue;

        // In the case of coinbase or IP transactions, the txin doesn't
        // actually contain the pubkey, so we look at the referenced txout
        // instead.
        var outHash = txin.getOutpointHash();
        var outIndex = txin.getOutpointIndex();
        var outHashBase64 = outHash.toString('base64');
        var fromTxOuts = txIndex[outHashBase64];

        if (!fromTxOuts) {
          throw new Error("Input not found!");
        }

        var txout = fromTxOuts[outIndex];
        var script = txout.getScript();

        var outPubKey = script.simpleOutPubKeyHash();
        if (outPubKey) {
          this.affects.push(outPubKey);
        }
      } catch (err) {
        // It's not our job to validate, so we just ignore any errors and issue
        // a very low level log message.
        log.debug("Unable to determine affected pubkeys: " +
                     (err.stack ? err.stack : ""+err));
      }
    }
  }

  var affectedKeys = {};

  this.affects.forEach(function (pubKeyHash) {
    affectedKeys[pubKeyHash.toString('base64')] = pubKeyHash;
  });

  return affectedKeys;
};

var OP_CODESEPARATOR = 171;

var SIGHASH_ALL = 1;
var SIGHASH_NONE = 2;
var SIGHASH_SINGLE = 3;
var SIGHASH_ANYONECANPAY = 80;

Transaction.SIGHASH_ALL=SIGHASH_ALL;
Transaction.SIGHASH_NONE=SIGHASH_NONE;
Transaction.SIGHASH_SINGLE=SIGHASH_SINGLE;
Transaction.SIGHASH_ANYONECANPAY=SIGHASH_ANYONECANPAY;

Transaction.prototype.hashForSignature =
function hashForSignature(script, inIndex, hashType) {
  if (+inIndex !== inIndex ||
      inIndex < 0 || inIndex >= this.ins.length) {
    throw new Error("Input index '"+inIndex+"' invalid or out of bounds "+
                    "("+this.ins.length+" inputs)");
  }

  // Clone transaction
  var txTmp = new Transaction();
  this.ins.forEach(function (txin, i) {
    txTmp.ins.push(new TransactionIn(txin));
  });
  this.outs.forEach(function (txout) {
    txTmp.outs.push(new TransactionOut(txout));
  });
  txTmp.version = this.version;
  txTmp.lock_time = this.lock_time;

  // In case concatenating two scripts ends up with two codeseparators,
  // or an extra one at the end, this prevents all those possible
  // incompatibilities.
  script.findAndDelete(OP_CODESEPARATOR);

  // Get mode portion of hashtype
  var hashTypeMode = hashType & 0x1f;

  // Generate modified transaction data for hash
  var bytes = (new Put());
  bytes.word32le(this.version);

  // Serialize inputs
  if (hashType & SIGHASH_ANYONECANPAY) {
    // Blank out all inputs except current one, not recommended for open
    // transactions.
    bytes.varint(1);
    bytes.put(this.ins[inIndex].o);
    bytes.varint(script.buffer.length);
    bytes.put(script.buffer);
    bytes.word32le(this.ins[inIndex].q);
  } else {
    bytes.varint(this.ins.length);
    for (var i = 0, l = this.ins.length; i < l; i++) {
      var txin = this.ins[i];
      bytes.put(this.ins[i].o);

      // Current input's script gets set to the script to be signed, all others
      // get blanked.
      if (inIndex === i) {
        bytes.varint(script.buffer.length);
        bytes.put(script.buffer);
      } else {
        bytes.varint(0);
      }

      if (hashTypeMode === SIGHASH_NONE && inIndex !== i) {
        bytes.word32le(0);
      } else {
        bytes.word32le(this.ins[i].q);
      }
    }
  }

  // Serialize outputs
  if (hashTypeMode === SIGHASH_NONE) {
    bytes.varint(0);
  } else {
    var outsLen;
    if (hashTypeMode === SIGHASH_SINGLE) {
      // TODO: Untested
      if (inIndex >= txTmp.outs.length) {
        throw new Error("Transaction.hashForSignature(): SIGHASH_SINGLE " +
                        "no corresponding txout found - out of bounds");
      }
      outsLen = inIndex + 1;
    } else {
      outsLen = this.outs.length;
    }

    // TODO: If hashTypeMode !== SIGHASH_SINGLE, we could memcpy this whole
    //       section from the original transaction as is.
    bytes.varint(outsLen);
    for (var i = 0; i < outsLen; i++) {
      if (hashTypeMode === SIGHASH_SINGLE && i !== inIndex) {
        // Zero all outs except the one we want to keep
        bytes.put(util.INT64_MAX);
        bytes.varint(0);
      } else {
        bytes.put(this.outs[i].v);
        bytes.varint(this.outs[i].s.length);
        bytes.put(this.outs[i].s);
      }
    }
  }

  bytes.word32le(this.lock_time);

  var buffer = bytes.buffer();

  // Append hashType
  buffer = Buffer.concat([buffer, new Buffer([parseInt(hashType), 0, 0, 0])]);

  return util.twoSha256(buffer);
};

/**
 * Returns an object with the same field names as jgarzik's getblock patch.
 */
Transaction.prototype.getStandardizedObject = function getStandardizedObject() {
  var tx = {
    hash: util.formatHashFull(this.getHash()),
    version: this.version,
    lock_time: this.lock_time
  };

  var totalSize = 8; // version + lock_time
  totalSize += util.getVarIntSize(this.ins.length); // tx_in count
  var ins = this.ins.map(function (txin) {
    var txinObj = {
      prev_out: {
        hash: buffertools.reverse(new Buffer(txin.getOutpointHash())).toString('hex'),
        n: txin.getOutpointIndex()
      }
    };
    if (txin.isCoinBase()) {
      txinObj.coinbase = txin.s.toString('hex');
    } else {
      txinObj.scriptSig = new Script(txin.s).getStringContent(false, 0);
    }
    totalSize += 36 + util.getVarIntSize(txin.s.length) +
      txin.s.length + 4; // outpoint + script_len + script + sequence
    return txinObj;
  });

  totalSize += util.getVarIntSize(this.outs.length);
  var outs = this.outs.map(function (txout) {
    totalSize += util.getVarIntSize(txout.s.length) +
      txout.s.length + 8; // script_len + script + value
    return {
      value: util.formatValue(txout.v),
      scriptPubKey: new Script(txout.s).getStringContent(false, 0)
    };
  });

  tx.size = totalSize;

  tx["in"] = ins;
  tx["out"] = outs;

  return tx;
};

// Add some Mongoose compatibility functions to the plain object
Transaction.prototype.toObject = function toObject() {
  return this;
};

Transaction.prototype.fromObj = function fromObj(obj) {
  var txobj = {};
  txobj.version = obj.version || 1;
  txobj.lock_time = obj.lock_time || 0;
  txobj.ins = [];
  txobj.outs = [];

  obj.inputs.forEach(function(inputobj) {
    var txin = new TransactionIn();
    txin.s = util.EMPTY_BUFFER;
    txin.q = 0xffffffff;

    var hash = new Buffer(inputobj.txid, 'hex');
    hash = buffertools.reverse(hash);
    var vout = parseInt(inputobj.vout);
    var voutBuf = new Buffer(4);
    voutBuf.writeUInt32LE(vout, 0);

    txin.o = Buffer.concat([hash, voutBuf]);

    txobj.ins.push(txin);
  });

  var keys = Object.keys(obj.outputs);
  keys.forEach(function(addrStr) {
    var addr = new Address(addrStr);
    var script = Script.createPubKeyHashOut(addr.payload());

    var valueNum = bignum(obj.outputs[addrStr]);
    var value = util.bigIntToValue(valueNum);

    var txout = new TransactionOut();
    txout.v = value;
    txout.s = script.getBuffer();

    txobj.outs.push(txout);
  });

  this.lock_time = txobj.lock_time;
  this.version = txobj.version;
  this.ins = txobj.ins;
  this.outs = txobj.outs;
}

Transaction.prototype.parse = function (parser) {
  if (Buffer.isBuffer(parser)) {
    this._buffer = parser;
    parser = new Parser(parser);
  }

  var i, sLen, startPos = parser.pos;

  this.version = parser.word32le();
  
  var txinCount = parser.varInt();

  this.ins = [];
  for (j = 0; j < txinCount; j++) {
    var txin = new TransactionIn();
    txin.o = parser.buffer(36);               // outpoint
    sLen = parser.varInt();                   // script_len
    txin.s = parser.buffer(sLen);             // script
    txin.q = parser.word32le();               // sequence
    this.ins.push(txin);
  }

  var txoutCount = parser.varInt();

  this.outs = [];
  for (j = 0; j < txoutCount; j++) {
    var txout = new TransactionOut();
    txout.v = parser.buffer(8);               // value
    sLen = parser.varInt();                   // script_len
    txout.s = parser.buffer(sLen);            // script
    this.outs.push(txout);
  }

  this.lock_time = parser.word32le();
  this.calcHash();
};

var TransactionInputsCache = exports.TransactionInputsCache =
function TransactionInputsCache(tx)
{
  var txList = [];
  var txList64 = [];
  var reqOuts = {};

  // Get list of transactions required for verification
  tx.ins.forEach(function (txin) {
    if (txin.isCoinBase()) return;

    var hash = txin.o.slice(0, 32);
    var hash64 = hash.toString('base64');
    if (txList64.indexOf(hash64) == -1) {
      txList.push(hash);
      txList64.push(hash64);
    }
    if (!reqOuts[hash64]) {
      reqOuts[hash64] = [];
    }
    reqOuts[hash64][txin.getOutpointIndex()] = true;
  });

  this.tx = tx;
  this.txList = txList;
  this.txList64 = txList64;
  this.txIndex = {};
  this.requiredOuts = reqOuts;
  this.callbacks = [];
};

TransactionInputsCache.prototype.buffer = function buffer(blockChain, txStore, wait, callback)
{
  var self = this;

  var complete = false;

  if ("function" === typeof callback) {
    self.callbacks.push(callback);
  }

  var missingTx = {};
  self.txList64.forEach(function (hash64) {
    missingTx[hash64] = true;
  });

  // A utility function to create the index object from the txs result lists
  function indexTxs(err, txs) {
    if (err) throw err;

    // Index memory transactions
    txs.forEach(function (tx) {
      var hash64 = tx.getHash().toString('base64');
      var obj = {};
      Object.keys(self.requiredOuts[hash64]).forEach(function (o) {
        obj[+o] = tx.outs[+o];
      });
      self.txIndex[hash64] = obj;
      delete missingTx[hash64];
    });

    this(null);
  };

  Step(
    // First find and index memory transactions (if a txStore was provided)
    function findMemTx() {
      if (txStore) {
        txStore.find(self.txList64, this);
      } else {
        this(null, []);
      }
    },
    indexTxs,
    // Second find and index persistent transactions
    function findBlockChainTx(err) {
      if (err) throw err;

      // TODO: Major speedup should be possible if we load only the outs and not
      //       whole transactions.
      var callback = this;
      blockChain.getOutputsByHashes(self.txList, function (err, result) {
        callback(err, result);
      });
    },
    indexTxs,
    function saveTxCache(err) {
      if (err) throw err;

      var missingTxDbg = '';
      if (Object.keys(missingTx).length) {
        missingTxDbg = Object.keys(missingTx).map(function (hash64) {
          return util.formatHash(new Buffer(hash64, 'base64'));
        }).join(',');
      }

      if (wait && Object.keys(missingTx).length) {
        // TODO: This might no longer be needed now that saveTransactions uses
        //       the safe=true option.
        setTimeout(function () {
          var missingHashes = Object.keys(missingTx);
          if (missingHashes.length) {
            self.callback(new Error('Missing inputs (timeout while searching): '
                                    + missingTxDbg));
          } else if (!complete) {
            self.callback(new Error('Callback failed to trigger'));
          }
        }, 10000);
      } else {
        complete = true;
        this(null, self);
      }
    },
    self.callback.bind(self)
  );
};


TransactionInputsCache.prototype.callback = function callback(err)
{
  var args = Array.prototype.slice.apply(arguments);

  // Empty the callback array first (because downstream functions could add new
  // callbacks or otherwise interfere if were not in a consistent state.)
  var cbs = this.callbacks;
  this.callbacks = [];

  try {
    cbs.forEach(function (cb) {
      cb.apply(null, args);
    });
  } catch (err) {
    log.err("Callback error after connecting tx inputs: "+
                 (err.stack ? err.stack : err.toString()));
  }
};

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