// RpcClient.js
// MIT/X11-like license.  See LICENSE.txt.
// Copyright 2013 BitPay, Inc.
require('classtool');

function ClassSpec(b) {
  var http = b.http || require('http');
  var https = b.https || require('https');
  var log = b.log || require('./util/log');

  function RpcClient(opts) {
    opts = opts || {};
    this.host = opts.host || '127.0.0.1';
    this.port = opts.port || 8332;
    this.user = opts.user || 'user';
    this.pass = opts.pass || 'pass';
    this.protocol = (opts.protocol == 'http') ? http : https;
    this.batchedCalls = null;
    this.disableAgent  = opts.disableAgent || false;
  }
    
  RpcClient.prototype.batch = function(batchCallback, resultCallback) {
    this.batchedCalls = [];
    batchCallback();
    rpc.call(this, this.batchedCalls, resultCallback);
    this.batchedCalls = null;
  }

  var callspec = {
    addMultiSigAddress: '',
    addNode: '',
    backupWallet: '',
    createMultiSig: '',
    createRawTransaction: '',
    decodeRawTransaction: '',
    dumpPrivKey: '',
    encryptWallet: '',
    getAccount: '',
    getAccountAddress: 'str',
    getAddedNodeInfo: '',
    getAddressesByAccount: '',
    getBalance: 'str int',
    getBestBlockHash: '',
    getBlock: '',
    getBlockCount: '',
    getBlockHash: 'int',
    getBlockNumber: '',
    getBlockTemplate: '',
    getConnectionCount: '',
    getDifficulty: '',
    getGenerate: '',
    getHashesPerSec: '',
    getInfo: '',
    getMemoryPool: '',
    getMiningInfo: '',
    getNewAddress: '',
    getPeerInfo: '',
    getRawMemPool: '',
    getRawTransaction: 'str int',
    getReceivedByAccount: 'str int',
    getReceivedByAddress: 'str int',
    getTransaction: '',
    getTxOut: 'str int bool',
    getTxOutSetInfo: '',
    getWork: '',
    help: '',
    importAddress: 'str str bool',
    importPrivKey: 'str str bool',
    keyPoolRefill: '',
    listAccounts: 'int',
    listAddressGroupings: '',
    listReceivedByAccount: 'int bool',
    listReceivedByAddress: 'int bool',
    listSinceBlock: 'str int',
    listTransactions: 'str int int',
    listUnspent: 'int int',
    listLockUnspent: 'bool',
    lockUnspent: '',
    move: 'str str float int str',
    sendFrom: 'str str float int str str',
    sendMany: 'str str int str',  //not sure this is will work
    sendRawTransaction: '',
    sendToAddress: 'str float str str',
    setAccount: '',
    setGenerate: 'bool int',
    setTxFee: 'float',
    signMessage: '',
    signRawTransaction: '',
    stop: '',
    submitBlock: '',
    validateAddress: '',
    verifyMessage: '',
    walletLock: '',
    walletPassPhrase: 'string int',
    walletPassphraseChange: '',
  };

  var slice = function(arr, start, end) {
    return Array.prototype.slice.call(arr, start, end);
  };

  function generateRPCMethods(constructor, apiCalls, rpc) {
    function createRPCMethod(methodName, argMap) {
      return function() {
        var limit = arguments.length - 1;
        if(this.batchedCalls) var limit = arguments.length;
        for (var i=0; i<limit; i++) {
          if(argMap[i]) arguments[i] = argMap[i](arguments[i]);
        };
        if(this.batchedCalls) {
          this.batchedCalls.push({jsonrpc: '2.0', method: methodName, params: slice(arguments)});
        } else {
          rpc.call(this, {method: methodName, params: slice(arguments, 0, arguments.length - 1)}, arguments[arguments.length - 1]);
        }
      };
    };

    var types = {
      str: function(arg) {return arg.toString();}, 
      int: function(arg) {return parseFloat(arg);},
      float: function(arg) {return parseFloat(arg);},
      bool: function(arg) {return (arg === true || arg == '1' || arg == 'true' || arg.toString().toLowerCase() == 'true');},
    };

    for(var k in apiCalls) {
      if (apiCalls.hasOwnProperty(k)) {
        var spec = apiCalls[k].split(' ');
        for (var i = 0; i < spec.length; i++) {
         if(types[spec[i]]) {
           spec[i] = types[spec[i]];
         } else {
           spec[i] = types.string;
         }
        }
        var methodName = k.toLowerCase();
        constructor.prototype[k] = createRPCMethod(methodName, spec);
        constructor.prototype[methodName] = constructor.prototype[k];
      }
    }
  }

  function rpc(request, callback) {
    var self = this;
    var request;
    request = JSON.stringify(request);
    var auth = Buffer(self.user + ':' + self.pass).toString('base64');

    var options = {
      host: self.host,
      path: '/',
      method: 'POST',
      port: self.port,
      agent: self.disableAgent ? false : undefined,                   
    };
    if(self.httpOptions) {
      for(var k in self.httpOptions) {
        options[k] = self.httpOptions[k];
      }
    }
    var err = null;
    var req = this.protocol.request(options, function(res) {

      var buf = '';
      res.on('data', function(data) {
        buf += data; 
      });
      res.on('end', function() {
        if(res.statusCode == 401) {
          callback(new Error('bitcoin JSON-RPC connection rejected: 401 unauthorized'));
          return;
        }
        if(res.statusCode == 403) {
          callback(new Error('bitcoin JSON-RPC connection rejected: 403 forbidden'));
          return;
        }
 
        if(err) {
          callback(err);
          return;
        }
        try {
          var parsedBuf = JSON.parse(buf);
        } catch(e) {
          log.err(e.stack);
          log.err(buf);
          log.err('HTTP Status code:' + res.statusCode);
          callback(e);
          return;
        }
        callback(parsedBuf.error, parsedBuf);
      });
    });
    req.on('error', function(e) {
      var err = new Error('Could not connect to bitcoin via RPC: '+e.message);
      log.err(err);
      callback(err);
    });
    
    req.setHeader('Content-Length', request.length);
    req.setHeader('Content-Type', 'application/json');
    req.setHeader('Authorization', 'Basic ' + auth);
    req.write(request);
    req.end();
  };

  generateRPCMethods(RpcClient, callspec, rpc);
  return RpcClient;
};
module.defineClass(ClassSpec);