text: // (c) 2012 Patrick Williams, BitTorrent Inc.
  // Btapp may be freely distributed under the MIT license.
  // For all details and documentation:
  // http://pwmckenna.github.com/btapp
  
  // Torrent Client (base functionality for Falcon/Local Torrent Clients)
  // -------------
  
  // TorrentClient provides a very thin wrapper around the web api
  // It should facilitate finding the correct port and connecting if
  // necessary to the user computer through falcon...by default uses localhost
  
  // TorrentClient is responsible for the mapping between functions
  // passed to as arguments and the string that proxies them to the client
  
  // TorrentClient is responsible for creating functions that wrap around
  // specific urls that can can be dangled off of models so that when we call
  // stop on a torrent file, the torrent specific url is accessed without any
  // effort on the part of the client
  
  // Keep in mind that because jsonp won't time out naturally, we impose our own
  // timeouts...this can lead to some less than desirable code :(
  
  (function() {
      "use strict";
      function assert(b, err) { if(!b) { throw err; } }
  
      //validate dependencies
      assert(typeof JSON !== 'undefined', 'JSON is a hard dependency');
      assert(typeof _ !== 'undefined', 'underscore/lodash is a hard dependency');
      assert(typeof PluginManager !== 'undefined', 'plugin.btapp.js is a hard dependency');
      assert(typeof Pairing !== 'undefined', 'pairing.btapp.js is a hard dependency');
      assert(typeof jQuery !== 'undefined', 'jQuery is a hard dependency');
      assert(typeof jQuery.jStorage !== 'undefined', 'jQuery.jStorage is a hard dependency');
  
  
      //we will sadly need to fiddle with some globals for falcon one offs.
      var root = this;
  
      this.TorrentClient = Backbone.Model.extend({
          initialize: function(attributes) {
              this.btappCallbacks = {};
          },
          // We can't send function pointers to the torrent client server, so we'll send
          // the name of the callback, and the server can call this by sending an event with
          // the name and args back to us. We're responsible for making the call to the function
          // when we detect this. This is the same way that jquery handles ajax callbacks.
          storeCallbackFunction: function(cb) {
              cb = cb || function() {};
              var str = 'bt_';
              for(var i = 0; i < 20 || (str in this.btappCallbacks); i++) { str += Math.floor(Math.random() * 10); }
              this.btappCallbacks[str] = cb;
              return str;
          },
          // We expect function signatures that come from the client to have a specific syntax
          isRPCFunctionSignature: function(f) {
              assert(typeof f === 'string', 'do not check function signature of non-strings');
              return f.match(/\[native function\](\([^\)]*\))+/) ||
                      f.match(/\[nf\](\([^\)]*\))+/);
          },
          isJSFunctionSignature: function(f) {
              assert(typeof f === 'string', 'do not check function signature of non-strings');
              return f.match(/\[nf\]bt_/);
          },
          getStoredFunction: function(f) {
              assert(TorrentClient.prototype.isJSFunctionSignature(f), 'only store functions that match the pattern "[nf]bt_*"');
              var key = f.substring(4);
              assert(key in this.btappCallbacks, 'trying to get a function with a key that is not recognized');
              return this.btappCallbacks[key];
          },
          // Seeing as we're interfacing with a strongly typed language c/c++ we need to
          // ensure that our types are at least close enough to coherse into the desired types
          // takes something along the lines of "[native function](string,unknown)(string)".
          validateArguments: function(functionValue, variables) {
              assert(typeof functionValue === 'string', 'expected functionValue to be a string');
              assert(typeof variables === 'object', 'expected variables to be an object');
              var signatures = functionValue.match(/\(.*?\)/g);
              return _.any(signatures, function(signature) {
                  signature = signature.match(/\w+/g) || []; //["string","unknown"]
                  return signature.length === variables.length && _.all(signature, function(type,index) {
                      if(typeof variables[index] === 'undefined') {
                          throw 'client functions do not support undefined arguments';
                      } else if(typeof variables[index] === 'null') {
                          return true;
                      }
  
                      switch(type) {
                          //Most of these types that the client sends up match the typeof values of the javascript
                          //types themselves so we can do a direct comparison
                          case 'number':
                          case 'string':
                          case 'boolean':
                              return typeof variables[index] === type;
                          //In the case of unknown, we have no choice but to trust the argument as
                          //the client hasn't specified what type it should be
                          case 'unknown':
                              return true;
                          case 'array':
                              return typeof variables[index] === 'object';
                          case 'dispatch':
                              return typeof variables[index] === 'object' || typeof variables[index] === 'function';
                          default:
                              //has the client provided a type that we weren't expecting?
                              throw 'there is an invalid type in the function signature exposed by the client';
                      }
                  });
              });
          },
          convertCallbackFunctionArgs: function(args) {
              _.each(args, function(value, key) {
                  // We are responsible for converting functions to variable names...
                  // this will be called later via a event with a callback and arguments variables
                  if(typeof value === 'function') {
                     args[key] = this.storeCallbackFunction(value);
                  } else if(typeof value === 'object' && value) {
                      this.convertCallbackFunctionArgs(value);
                  }
              }, this);
          },
          // Functions are simply urls that we make ajax request to. The cb is called with the
          // result of that ajax request.
          createFunction: function(session, path, signatures) {
              assert(session, 'cannot create a function without a session id');
              var func = _.bind(function() {
                  var args = [];
  
                  // Lets do a bit of validation of the arguments that we're passing into the client
                  // unfortunately arguments isn't a completely authetic javascript array, so we'll have
                  // to "splice" by hand. All this just to validate the correct types! sheesh...
                  var i;
                  for(i = 0; i < arguments.length; i++) {
                      args.push(arguments[i]);
                  }
                  // This is as close to a static class function as you can get in javascript i guess
                  // we should be able to use verifySignaturesArguments to determine if the client will
                  // consider the arguments that we're passing to be valid
                  if(!TorrentClient.prototype.validateArguments.call(this, signatures, args)) {
                      throw 'arguments do not match any of the function signatures exposed by the client';
                  }
  
                  this.convertCallbackFunctionArgs(args);
                  var ret = new jQuery.Deferred();
                  var success = _.bind(function(data) {
                      //lets strip down to the relevent path data
                      _.each(path, function(segment) {
                          var decoded = decodeURIComponent(segment);
                          if(typeof data !== 'undefined') {
                              data = data[decoded];
                          }
                      });
                      if(typeof data === 'undefined') {
                          ret.reject('return value parsing error ' + JSON.stringify(data));
                      } else if(typeof data === 'string' && this.isJSFunctionSignature(data)) {
                          var func = this.getStoredFunction(data);
                          assert(func, 'the client is returning a function name that does not exist');
                          ret.resolve(func);
                      } else {
                          ret.resolve(data);
                      }
                  }, this);
                  var error = function(data) {
                      ret.reject(data);
                  };
                  this.query({
                      type: 'function', 
                      path: JSON.stringify(path),
                      args: JSON.stringify(args),
                      session: session
                  }).done(success).fail(error);
                  this.trigger('queries', path);
                  return ret;
              }, this);
              func.valueOf = function() { return signatures; };
              return func;
          },
          query: function(args) {
              var abort = false;
              var ret = new jQuery.Deferred();
              assert(args.type === 'update' || args.type === 'state' || args.type === 'function' || args.type === 'disconnect', 'the query type must be either "update", "state", or "function"');
  
              args.hostname = window.location.hostname || window.location.pathname;
              var success_callback = _.bind(function(data) {
                  if (data === 'invalid request') {
                      setTimeout(_.bind(this.reset, this), 1000);
                      throw 'pairing occured with a torrent client that does not support the btapp api';
                  } else if(typeof data !== 'object' || 'error' in data) {
                      ret.reject();
                      this.trigger('client:error', data);
                  } else {
                      ret.resolve(data);
                  }
              }, this);
              this.send_query(args)
                  .done(function() {
                      if(!abort) {
                          success_callback.apply(this, arguments);
                      }
                  }).fail(function() {
                      if(!abort) {
                          ret.reject.apply(this, arguments);
                      }
                  });
              ret.abort = function() {
                  abort = true;
              };
              return ret;
          }
      });
  
      // Falcon Torrent Client
      // -------------
  
      // Falcon torrent client connections are a bit more involved than a client on the local machine
      // We have additional javascript dependencies that are substantial enough that we don't load them
      // unless you open a falcon connection. In addition we have some handshaking with the falcon proxy
      // that we need to wait for.
      this.FalconTorrentClient = TorrentClient.extend({
          initialize: function(attributes) {
              TorrentClient.prototype.initialize.call(this, attributes);
              
              assert(('username' in attributes && 'password' in attributes) || 'remote_data' in attributes,
                  'attempting to connect to client through falcon without providing the necessary credentials');
  
              this.username = attributes.username;
              this.password = attributes.password;
              if ('remote_data' in attributes) {
                  this.remote_data = attributes.remote_data;
              }
              if ('login_success' in attributes) {
                  this.login_success = attributes.login_success;
              }
              if ('login_error' in attributes) {
                  this.login_error = attributes.login_error;
              }
              if ('login_progress' in attributes) {
                  this.login_progress = attributes.login_progress;
              }
  
              // We only have to load all those expensive js dependencies once...
              // We can just skip straight to the good stuff (signing in) if we've
              // done this previously...the last dependency we load sets the
              // window.falcon variable, so we can just check for that
              if(typeof falcon !== 'undefined') {
                  _.defer(_.bind(this.reset, this));
                  return;
              }
  
              // If we choose to use falcon we need this specific global config variable defined
              root.config = {
                  srp_root:'https://remote.utorrent.com'
              };
  
              var jsload = 'https://remote.utorrent.com/static/js/jsloadv2.js?v=0.57';
              jQuery.getScript(jsload, _.bind(function(data, textStatus) {
                  function create_tags(list) {
                      var tags = [];
                      var deps = [];
                      for (var i = 0; i < list.length - 1; i++) {
                          var current = list[i];
                          tags.push( { name: current } );
                          deps.push( current );
                      }
                      tags.push( { name: list[list.length-1],
                      requires: deps } );
                      return tags;
                  }
                  var dependencies = [
                      'falcon/deps/SHA-1.js',
                      'falcon/deps/jsbn.js',
                      'falcon/deps/jsbn2.js',
                      'falcon/deps/sjcl.js',
                      'falcon/falcon.js',
                      'falcon/falcon.encryption.js',
                      'falcon/falcon.api.js',
                      'falcon/falcon.session.js'
                  ];
                  var tags = create_tags(dependencies);
                  (new JSLoad(tags, "https://remote.utorrent.com/static/js/")).load(['falcon/falcon.session.js'], _.bind(function() {
                      if (this.remote_data) {
                          this.session = new falcon.session( { client_data: this.remote_data } );
                          this.falcon = this.session.api;
                          this.trigger('client:connected');
                      } else {
                          this.delayed_reset();
                      }
                  }, this));
              }, this));
          },
          connect: function() {
              assert(!this.falcon, 'trying to connect with falcon already set');
              // set up some connection variables
              var opts = {
                  success: _.bind(function(session) {
                      if (this.login_success) { this.login_success(session); }
                      this.falcon = this.session;
                      this.trigger('client:connected', session);
                  }, this),
                  error: _.bind(function(xhr, status, data) {
                      if (this.login_error) { this.login_error(xhr, status, data); }
                      this.trigger('client:error_connecting', data);
                      this.trigger('client:error', data);
                  }, this)
              };
              this.session = new falcon.session();
              this.session.negotiate(this.username, this.password, { success: opts.success, error: opts.error, progress: this.login_progress } );
          },
          disconnect: function() {
              //TODO...how do we disconnect from a remote connection?
          },
          // This is the Btapp object's gateway to the actual client requests. These requests look slightly
          // different than those headed to a local client because they are encrypted.
          send_query: function(args) {
              var ret = new jQuery.Deferred();
              assert(this.falcon, 'cannot send a query to the client without falcon properly connected');
  
              this.falcon.request(
                  '/gui/',
                  {'btapp':'backbone.btapp.js'},
                  args,
                  function(data) {
                      assert('build' in data, 'expected build information in the falcon response');
                      assert('result' in data, 'expected result information in the falcon response');
                      ret.resolve(data.result);
                  },
                  _.bind(function() {
                      ret.reject();
                      this.delayed_reset();
                  }, this),
                  {}
              );
              return ret;
          },
          delayed_reset: function() {
              this.reset_timeout = setTimeout(_.bind(function() { 
                  this.reset();
                  this.reset_timeout = null;
              }, this), 1000 );
          },
          reset: function() {
              this.falcon = null;
              this.connect();
          }
      });
  
      // Local Torrent Client
      // -------------
  
      // For clients on the local machine very little setup is neeeded. We have a known port that
      // the client listens on, so we can just make requests to that. We can also immediately
      // consider ourselves "connected", which indicates that we're connected to the machine
      // (for falcon clients we may not ever reach the client even if it is logged into falcon)
      this.LocalTorrentClient = TorrentClient.extend({
          defaults: {
              product: 'Torque'
          },
          initialize: function(attributes) {
              TorrentClient.prototype.initialize.call(this, attributes);
              this.btapp = attributes.btapp;
              this.initialize_objects(attributes);
              this.reset_timeout = null;
          },
          disconnect: function() {
              if(this.pairing) {
                  this.pairing.disconnect();
              }
              if(this.plugin_manager) {
                  this.plugin_manager.disconnect();
              }
              if(this.reset_timeout) {
                  clearTimeout(this.reset_timeout);
              }
          },
          initialize_objects: function(attributes) {
              this.initialize_plugin(attributes);
              this.initialize_pairing(attributes);
          },
          // We have a seperate object that is responsible for managing the browser
          // plugin and using that plugin to ensure that the client is downloaded and
          // running on the local machine.
          initialize_plugin: function(attributes) {
              if(this.get('plugin') === false) {
                  return;
              }
              this.plugin_manager = new PluginManager(attributes);
              new PluginManagerView({'model': this.plugin_manager});
              this.plugin_manager.on('all', this.trigger, this);
          },
          // We have a seperate object responsible for determining exactly which
          // port the torrent client is bound to.
          initialize_pairing: function(attributes) {
              assert(this.get('plugin') === false || typeof this.plugin_manager !== 'undefined', 'you must initialize the plugin manager before the pairing object');
              //all right...ready to roll.
              this.pairing = new Pairing(_.extend(attributes, {'plugin_manager': this.plugin_manager}));
              if(this.pairing.get('pairing_type') !== 'native') {
                  new PairingView({'model': this.pairing});
              }
              this.pairing.on('all', this.trigger, this);
  
              assert(this.has('product'), 'client does not know what product to connect to');
              var product = this.get('product');
              this.pairing.on('pairing:found', function(options) {
                  if(options && options.name.toLowerCase() === product.toLowerCase()) {
                      var key = jQuery.jStorage.get(product + '_pairing_key');
                      if(key) {
                          // Let whoever triggered the pairing:found event know that they don't need
                          // to continue scanning, nor do they need to handle aquiring a pairing key
                          options.abort = true;
                          options.authorize = false;
                          this.connect_to_authenticated_port(options.port, key);
                      } else {
                          // We've found the port we want to work with, but we don't have a pairing key.
                          // We'll set authorize to true so that a pairing dialog is presented 
                          // to the user
                          options.abort = true;
                          options.authorize = true;
                      }
                  } else {
                      options.abort = false;
                      options.authorize = false;
                  }
              }, this);
              this.pairing.on('pairing:authorized', _.bind(function(options) {
                  // Holy smokes! We found a port, and the client that's listening likes our pairing key.
                  // Store the key off so that we don't have to bother the user again.
                  jQuery.jStorage.set(product + '_pairing_key', options.key);
                  this.connect_to_authenticated_port(options.port, options.key);
              }, this));
  
              this.pairing.on('pairing:stop', this.delayed_reset, this);
              if(this.get('plugin') === false) {
                  this.delayed_reset();
              } else {
                  this.plugin_manager.on('plugin:client_running', this.reset, this);
              }
          },
          ajax: function(url, cb, err) {
              if(this.get('plugin') === false) {
                  jQuery.ajax({
                      url: url,
                      success: cb,
                      error: err,
                      dataType: 'jsonp'
                  });
              } else {
                  this.plugin_manager.get_plugin().ajax(url, _.bind(function(response) {
                      var obj;
                      if(response.allowed && response.success && response.data !== 'invalid request') {
                          try {
                              obj = JSON.parse(response.data);
                          } catch(e) {
                              var msg = 'parsererror';
                              err(msg);
                              this.trigger('client:error', msg);
                              return;
                          }
                          cb(obj);
                      } else {
                          this.trigger('client:error', response);
                          err(response);
                      }
                  }, this));
              }
          },
          // Before we actual start making requests against a client, we need to make sure
          // we have a valid pairing key. This might be redundant if we just got one from the
          // client, but its very very necessary if we've stored off a pairing key that's invalid
          // for some reason.
          connect_to_authenticated_port: function(port, key) {
              // Called if the pairing key is good to go. Tell whoever is listening that we're
              // ready to roll.
              var cb = _.bind(function() {
                  this.url = 'http://127.0.0.1:' + port + '/btapp/?pairing=' + key;
                  this.trigger('client:connected');
              }, this);
              // Handle the case of an invalid pairing key. Flush it out and start over.
              var err = _.bind(function() {
                  jQuery.jStorage.deleteKey(this.get('product') + '_pairing_key');
                  this.delayed_reset();
              }, this);
          
              var url = 'http://127.0.0.1:' + port + '/btapp/?pairing=' + key;
              this.ajax(url, cb, err);
          },
          delayed_reset: function() {
              this.reset_timeout = setTimeout(_.bind(function() { 
                  this.reset();
                  this.reset_timeout = null;
              }, this), 1000 );
          },
          reset: function() {
              // Reset is called upon initialization (or when we load pairing.btapp.js)
              // and whenever the btapp object errors out trying to communicate with the
              // torrent client. In both cases we probably need to scan through the ports
              // again as the torrent client won't necessarily be able to connect to the
              // same port when it is relaunched.
              this.pairing.connect();
          },
          send_query: function(args) {
              var ret = new jQuery.Deferred();
              this.trigger('client:query', this.url, args);
              var url = this.url;
              _.each(args, function(value, key) {
                  url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(value);
              });
              this.ajax(url, 
                  function(data) {
                      ret.resolve(data);
                  },
                  function() {
                      ret.reject();
                  }
              );
              return ret;
          }
      }); 
  }).call(this);