Meteor._MethodInvocation = function (isSimulation, userId,

// true if we're running not the actual method, but a stub (that is,

keep keep replace replace keep keep keep

Meteor._MethodInvocation = function (options) {

/******************************************************************************/
/* LivedataSession                                                            */
/******************************************************************************/

Meteor._LivedataSession = function (server) {
  var self = this;
  self.id = Meteor.uuid();

  self.server = server;

  self.initialized = false;
  self.socket = null;
  self.last_connect_time = 0;
  self.last_detach_time = +(new Date);

  self.in_queue = [];
  self.blocked = false;
  self.worker_running = false;

  self.out_queue = [];

  // id of invocation => {result or error, when}
  self.result_cache = {};

  // Sub objects for active subscriptions
  self.named_subs = {};
  self.universal_subs = [];

  self.next_sub_priority = 0;

  // map from collection name -> id -> key -> subscription id -> true
  self.provides_key = {};

  // if set, ignore flush requests on any subsubcription on this
  // session. when set this back to false, don't forget to call flush
  // manually. this is sometimes needed because subscriptions
  // frequently call flush
  self.dontFlush = false;

  self.userId = null;
};

_.extend(Meteor._LivedataSession.prototype, {
  // Connect a new socket to this session, displacing (and closing)
  // any socket that was previously connected
  connect: function (socket) {
    var self = this;
    if (self.socket) {
      self.socket.close();
      self.detach(self.socket);
    }

    self.socket = socket;
    self.last_connect_time = +(new Date);
    _.each(self.out_queue, function (msg) {
      self.socket.send(JSON.stringify(msg));
    });
    self.out_queue = [];

    // On initial connect, spin up all the universal publishers.
    if (!self.initialized) {
      self.initialized = true;
      Fiber(function () {
        _.each(self.server.universal_publish_handlers, function (handler) {
          self._startSubscription(handler, self.next_sub_priority--);
        });
      }).run();
    }
  },

  // If 'socket' is the socket currently connected to this session,
  // detach it (the session will then have no socket -- it will
  // continue running and queue up its messages.) If 'socket' isn't
  // the currently connected socket, just clean up the pointer that
  // may have led us to believe otherwise.
  detach: function (socket) {
    var self = this;
    if (socket === self.socket) {
      self.socket = null;
      self.last_detach_time = +(new Date);
    }
    if (socket.meteor_session === self)
      socket.meteor_session = null;
  },

  // Should be called periodically to prune the method invocation
  // replay cache.
  cleanup: function () {
    var self = this;
    // Only prune if we're connected, and we've been connected for at
    // least five minutes. That seems like enough time for the client
    // to finish its reconnection. Then, keep five minutes of
    // history. That seems like enough time for the client to receive
    // our responses, or else for us to notice that the connection is
    // gone.
    var now = +(new Date);
    if (!(self.socket && (now - self.last_connect_time) > 5 * 60 * 1000))
      return; // not connected, or not connected long enough

    var kill = [];
    _.each(self.result_cache, function (info, id) {
      if (now - info.when > 5 * 60 * 1000)
        kill.push(id);
    });
    _.each(kill, function (id) {
      delete self.result_cache[id];
    });
  },

  // Destroy this session. Stop all processing and tear everything
  // down. If a socket was attached, close it.
  destroy: function () {
    var self = this;
    if (self.socket) {
      self.socket.close();
      self.detach(self.socket);
    }
    self._stopAllSubscriptions();
    self.in_queue = self.out_queue = [];
  },

  // Send a message (queueing it if no socket is connected right now.)
  // It should be a JSON object (it will be stringified.)
  send: function (msg) {
    var self = this;
    if (self.socket)
      self.socket.send(JSON.stringify(msg));
    else
      self.out_queue.push(msg);
  },

  // Send a connection error.
  sendError: function (reason, offending_message) {
    var self = this;
    var msg = {msg: 'error', reason: reason};
    if (offending_message)
      msg.offending_message = offending_message;
    self.send(msg);
  },

  // Process 'msg' as an incoming message. (But as a guard against
  // race conditions during reconnection, ignore the message if
  // 'socket' is not the currently connected socket.)
  //
  // We run the messages from the client one at a time, in the order
  // given by the client. The message handler is passed an idempotent
  // function 'unblock' which it may call to allow other messages to
  // begin running in parallel in another fiber (for example, a method
  // that wants to yield.) Otherwise, it is automatically unblocked
  // when it returns.
  //
  // Actually, we don't have to 'totally order' the messages in this
  // way, but it's the easiest thing that's correct. (unsub needs to
  // be ordered against sub, methods need to be ordered against each
  // other.)
  processMessage: function (msg_in, socket) {
    var self = this;
    if (socket !== self.socket)
      return;

    self.in_queue.push(msg_in);
    if (self.worker_running)
      return;
    self.worker_running = true;

    var processNext = function () {
      var msg = self.in_queue.shift();
      if (!msg) {
        self.worker_running = false;
        return;
      }

      Fiber(function () {
        var blocked = true;

        var unblock = function () {
          if (!blocked)
            return; // idempotent
          blocked = false;
          processNext();
        };

        if (msg.msg in self.protocol_handlers)
          self.protocol_handlers[msg.msg].call(self, msg, unblock);
        else
          self.sendError('Bad request', msg);
        unblock(); // in case the handler didn't already do it
      }).run();
    };

    processNext();
  },

  protocol_handlers: {
    sub: function (msg) {
      var self = this;

      // reject malformed messages
      if (typeof (msg.id) !== "string" ||
          typeof (msg.name) !== "string" ||
          (('params' in msg) && !(msg.params instanceof Array))) {
        self.sendError("Malformed subscription", msg);
        return;
      }

      if (!self.server.publish_handlers[msg.name]) {
        self.send({
          msg: 'nosub', id: msg.id,
          error: {error: 404, reason: "Subscription not found"}});
        return;
      }

      if (msg.id in self.named_subs)
        // subs are idempotent, or rather, they are ignored if a sub
        // with that id already exists. this is important during
        // reconnect.
        return;

      var handler = self.server.publish_handlers[msg.name];
      self._startSubscription(handler, self.next_sub_priority--,
                              msg.id, msg.params);
    },

    unsub: function (msg) {
      var self = this;

      self._stopSubscription(msg.id);
      self.send({msg: 'nosub', id: msg.id});
    },

    method: function (msg, unblock) {
      var self = this;

      // reject malformed messages
      // XXX should also reject messages with unknown attributes?
      if (typeof (msg.id) !== "string" ||
          typeof (msg.method) !== "string" ||
          (('params' in msg) && !(msg.params instanceof Array))) {
        self.sendError("Malformed method invocation", msg);
        return;
      }

      // set up to mark the method as satisfied once all observers
      // (and subscriptions) have reacted to any writes that were
      // done.
      var fence = new Meteor._WriteFence;
      fence.onAllCommitted(function () {
        // Retire the fence so that future writes are allowed.
        // This means that callbacks like timers are free to use
        // the fence, and if they fire before it's armed (for
        // example, because the method waits for them) their
        // writes will be included in the fence.
        fence.retire();
        self.send({
          msg: 'data', methods: [msg.id]});
      });

      // check for a replayed method (this is important during
      // reconnect)
      if (msg.id in self.result_cache) {
        // found -- just resend whatever we sent last time
        var payload = _.clone(self.result_cache[msg.id]);
        delete payload.when;
        self.send(
          _.extend({msg: 'result', id: msg.id}, payload));
        fence.arm();
        return;
      }

      // find the handler
      var handler = self.server.method_handlers[msg.method];
      if (!handler) {
        self.send({
          msg: 'result', id: msg.id,
          error: {error: 404, reason: "Method not found"}});
        fence.arm();
        return;
      }

      var setUserId = function(userId) {
        self._setUserId(userId);
      };

      var invocation = new Meteor._MethodInvocation(
        false /* isSimulation */, self.userId, setUserId, unblock);
      try {
        var ret =
          Meteor._CurrentWriteFence.withValue(fence, function () {
            return Meteor._CurrentInvocation.withValue(invocation, function () {
              return handler.apply(invocation, msg.params || []);
            });
          });
      } catch (e) {
        var exception = e;
      }

      fence.arm(); // we're done adding writes to the fence
      unblock(); // unblock, if the method hasn't done it already

      // "blind" exceptions other than those that were deliberately
      // thrown to signal errors to the client
      if (exception && !(exception instanceof Meteor.Error)) {
        // tests can set the 'expected' flag on an exception so it
        // won't go to the server log
        if (!exception.expected)
          Meteor._debug("Exception while invoking method '" +
                        msg.method + "'", exception.stack);
        exception = new Meteor.Error(500, "Internal server error");
      }

      // send response and add to cache
      var payload =
        exception ? {error: exception} : (ret !== undefined ?
                                          {result: ret} : {});
      self.result_cache[msg.id] = _.extend({when: +(new Date)}, payload);
      self.send(_.extend({msg: 'result', id: msg.id}, payload));
    }
  },

  // Sets the current user id in all appropriate contexts and reruns
  // all subscriptions
  _setUserId: function(userId) {
    var self = this;
    self.userId = userId;
    this._rerunAllSubscriptions();
  },

  _startSubscription: function (handler, priority, sub_id, params) {
    var self = this;

    var sub = new Meteor._LivedataSubscription(self, sub_id, priority);
    if (sub_id)
      self.named_subs[sub_id] = sub;
    else
      self.universal_subs.push(sub);

    // Store a function to re-run the handler in case we want to rerun
    // subscriptions, for example when the current user id changes
    sub._runHandler = function() {
      try {
        var res = handler.apply(sub, params || []);
      } catch (e) {
        Meteor._debug("Internal exception while starting subscription", sub_id,
                      e.stack);
        return;
      }

      // if Meteor._RemoteCollectionDriver is available (defined in
      // mongo-livedata), automatically wire up handlers that return a
      // Cursor.  otherwise, the handler is completely responsible for
      // delivering its own data messages and registering stop
      // functions.
      //
      // XXX generalize
      if (Meteor._RemoteCollectionDriver && (res instanceof Meteor._Mongo.Cursor))
        sub._publishCursor(res);
    };

    sub._runHandler();
  },

  // tear down specified subscription
  _stopSubscription: function (sub_id) {
    var self = this;

    if (sub_id && self.named_subs[sub_id]) {
      self.named_subs[sub_id].stop();
      delete self.named_subs[sub_id];
    }
  },

  // tear down all subscriptions
  _stopAllSubscriptions: function () {
    var self = this;

    _.each(self.named_subs, function (sub, id) {
      sub.stop();
    });
    self.named_subs = {};

    _.each(self.universal_subs, function (sub) {
      sub.stop();
    });
    self.universal_subs = [];
  },

  // Rerun all subscriptions without sending intermediate state down
  // the wire
  _rerunAllSubscriptions: function () {
    var self = this;

    var rerunSub = function(sub) {
      sub._teardown();
      sub._userId = self.userId;
      sub._runHandler();
    };
    var flushSub = function(sub) {
      sub.flush();
    };

    self.dontFlush = true;
    _.each(self.named_subs, rerunSub);
    _.each(self.universal_subs, rerunSub);

    self.dontFlush = false;
    _.each(self.named_subs, flushSub);
    _.each(self.universal_subs, flushSub);
  },

  // RETURN the current value for a particular key, as given by the
  // current contents of each subscription's snapshot.
  _effectiveValueForKey: function (collection_name, id, key) {
    var self = this;

    // Find all subs that publish a value for this key
    var provided_by = Meteor._get(self.provides_key, collection_name, id, key);
    provided_by = _.values(provided_by || {});

    if (provided_by.length === 0)
      return undefined; // no value for key

    // Which one is highest priority?
    var authority = _.max(provided_by, function (sub) {
      return sub.priority;
    });

    return authority.snapshot[collection_name][id][key];
  }
});

/******************************************************************************/
/* LivedataSubscription                                                       */
/******************************************************************************/

// ctor for a sub handle: the input to each publish function
Meteor._LivedataSubscription = function (session, sub_id, priority) {
  // LivedataSession
  this.session = session;

  // my subscription ID (generated by client, null for universal subs).
  this.sub_id = sub_id;

  // number (possibly negative.) when two subs return conflicting
  // values for the same key, the client will see the value from the
  // sub with the higher priority.
  this.priority = priority;

  // data queued up to be sent by the next flush()
  // map from collection name -> id -> key -> value
  // to indicate unset, value === undefined
  this.pending_data = {};
  this.pending_complete = false;

  // the current data for this subscription (as has been flush()ed to
  // the client.)
  // map from collection name -> id -> key -> value
  this.snapshot = {};
  this.sent_complete = false;

  // has stop() been called?
  this.stopped = false;

  // stop callbacks to g/c this sub.  called w/ zero arguments.
  this.stop_callbacks = [];

  this._userId = session.userId;
};

_.extend(Meteor._LivedataSubscription.prototype, {
  stop: function () {
    var self = this;

    if (self.stopped)
      return;

    self._teardown();
    self.flush();
    self.stopped = true;
  },

  onStop: function (callback) {
    this.stop_callbacks.push(callback);
  },

  set: function (collection_name, id, attributes) {
    var self = this;
    var obj = Meteor._ensure(self.pending_data, collection_name, id);
    _.each(attributes, function (value, key) {
      if (key !== '_id')
        obj[key] = value;
    });
  },

  unset: function (collection_name, id, keys) {
    var self = this;
    var obj = Meteor._ensure(self.pending_data, collection_name, id);
    _.each(keys, function (key) {
      if (key !== '_id')
        obj[key] = undefined; // do not delete - need to mark as 'to be unset'
    });
  },

  complete: function () {
    var self = this;

    // universal subs (sub_id is null) can't signal completion.  it's
    // not an error, since the same handler (eg publishQuery) might be
    // used to implement both named and universal subs.

    if (self.sub_id)
      self.pending_complete = true;
  },

  flush: function () {
    var self = this;

    if (self.session.dontFlush)
      return;

    if (self.stopped)
      return;

    for (var name in self.pending_data)
      for (var id in self.pending_data[name]) {
        // construct outbound DDP data message
        var msg = {msg: 'data', collection: name, id: id};

        // snapshot holds this subscription's values for each key
        var snapshot = Meteor._ensure(self.snapshot, name, id);

        for (var key in self.pending_data[name][id]) {
          // value: set by this run of this publish handler.
          var value = self.pending_data[name][id][key];

          // old_value: set by previous run of this publish handler.
          var old_value = snapshot[key];

          if (value !== old_value) {
            // First, find the effective value that the client currently
            // has, thanks to the highest priority subscription.
            var old_effective_value = self.session._effectiveValueForKey(name, id, key);

            // Update our snapshot based on the written value. Update
            // our session's index too.
            if (value === undefined) {
              delete snapshot[key];
              Meteor._delete(self.session.provides_key, name, id, key, self.sub_id);
            } else {
              snapshot[key] = value;
              var provides = Meteor._ensure(self.session.provides_key,
                                            name, id, key);
              provides[self.sub_id] = self;
            }

            // Now compute new effective value, taking into account our new value.
            var new_effective_value = self.session._effectiveValueForKey(name, id, key);

            // If the effective values differ, this sub is responsible
            // for sending the new data down to the client.
            if (old_effective_value !== new_effective_value) {
              if (new_effective_value === undefined) {
                if (!('unset' in msg))
                  msg.unset = [];
                msg.unset.push(key);
              } else {
                if (!('set' in msg))
                  msg.set = {};
                msg.set[key] = new_effective_value;
              }
            }
          }
        }

        // Send an update for one object.
        if ('set' in msg || 'unset' in msg)
          self.session.send(msg);
      }

    if (self.pending_complete && !self.sent_complete) {
      self.session.send({msg: 'data', subs: [self.sub_id]});
      self.sent_complete = true;
    }

    self.pending_data = {};
    self.pending_complete = false;
  },

  userId: function() {
    return this._userId;
  },

  _teardown: function() {
    var self = this;
    // tell listeners, so they can clean up
    for (var i = 0; i < self.stop_callbacks.length; i++)
      (self.stop_callbacks[i])();

    // remove our data from the client (possibly unshadowing data from
    // lower priority subscriptions)
    self.pending_data = {};
    self.pending_complete = false;
    for (var name in self.snapshot) {
      self.pending_data[name] = {};
      for (var id in self.snapshot[name]) {
        self.pending_data[name][id] = {};
        for (var key in self.snapshot[name][id])
          self.pending_data[name][id][key] = undefined;
      }
    }
  },

  _publishCursor: function (cursor, name) {
    var self = this;
    var collection = name || cursor.collection_name;

    var observe_handle = cursor.observe({
      added: function (obj) {
        self.set(collection, obj._id, obj);
        self.flush();
      },
      changed: function (obj, old_idx, old_obj) {
        var set = {};
        _.each(obj, function (v, k) {
          if (!_.isEqual(v, old_obj[k]))
            set[k] = v;
        });
        self.set(collection, obj._id, set);
        var dead_keys = _.difference(_.keys(old_obj), _.keys(obj));
        self.unset(collection, obj._id, dead_keys);
        self.flush();
      },
      removed: function (old_obj, old_idx) {
        self.unset(collection, old_obj._id, _.keys(old_obj));
        self.flush();
      }
    });

    // observe only returns after the initial added callbacks have
    // run.  mark subscription as completed.
    self.complete();
    self.flush();

    // register stop callback (expects lambda w/ no args).
    self.onStop(_.bind(observe_handle.stop, observe_handle));
  }
});

/******************************************************************************/
/* LivedataServer                                                             */
/******************************************************************************/

Meteor._LivedataServer = function () {
  var self = this;

  self.publish_handlers = {};
  self.universal_publish_handlers = [];

  self.method_handlers = {};

  self.on_autopublish = []; // array of func if AP disabled, null if enabled
  self.warned_about_autopublish = false;

  self.sessions = {}; // map from id to session

  self.stream_server = new Meteor._StreamServer;

  self.stream_server.register(function (socket) {
    socket.meteor_session = null;

    var sendError = function (reason, offending_message) {
      var msg = {msg: 'error', reason: reason};
      if (offending_message)
        msg.offending_message = offending_message;
      socket.send(JSON.stringify(msg));
    };

    socket.on('data', function (raw_msg) {
      try {
        try {
          var msg = JSON.parse(raw_msg);
        } catch (err) {
          sendError('Parse error');
          return;
        }
        if (typeof msg !== 'object' || !msg.msg) {
          sendError('Bad request', msg);
          return;
        }

        if (msg.msg === 'connect') {
          if (socket.meteor_session) {
            sendError("Already connected", msg);
            return;
          }

          // XXX session resumption does not work yet!
          // https://app.asana.com/0/159908330244/577350817064
          // disabled here:
          /*
          if (msg.session)
            var old_session = self.sessions[msg.session];
          if (old_session) {
            // Resuming a session
            socket.meteor_session = old_session;
          }
          else */ {
            // Creating a new session
            socket.meteor_session = new Meteor._LivedataSession(self);
            self.sessions[socket.meteor_session.id] = socket.meteor_session;
          }

          socket.send(JSON.stringify({msg: 'connected',
                                      session: socket.meteor_session.id}));
          // will kick off previous connection, if any
          socket.meteor_session.connect(socket);
          return;
        }

        if (!socket.meteor_session) {
          sendError('Must connect first', msg);
          return;
        }
        socket.meteor_session.processMessage(msg, socket);
      } catch (e) {
        // XXX print stack nicely
        Meteor._debug("Internal exception while processing message", msg,
                      e.stack);
      }
    });

    socket.on('close', function () {
      if (socket.meteor_session)
        socket.meteor_session.detach(socket);
    });
  });

  // Every minute, clean up sessions that have been abandoned for 15
  // minutes. Also run result cache cleanup.
  // XXX at scale, we'll want to have a separate timer for each
  // session, and stagger them
  Meteor.setInterval(function () {
    var now = +(new Date);
    _.each(self.sessions, function (s) {
      s.cleanup();
      if (!s.socket && (now - s.last_detach_time) > 15 * 60 * 1000)
        s.destroy();
    });
  }, 1 * 60 * 1000);
};

_.extend(Meteor._LivedataServer.prototype, {
  /**
   * Register a publish handler function.
   *
   * @param name {String} identifier for query
   * @param handler {Function} publish handler
   * @param options {Object}
   *
   * Server will call handler function on each new subscription,
   * either when receiving DDP sub message for a named subscription, or on
   * DDP connect for a universal subscription.
   *
   * If name is null, this will be a subscription that is
   * automatically established and permanently on for all connected
   * client, instead of a subscription that can be turned on and off
   * with subscribe().
   *
   * options to contain:
   *  - (mostly internal) is_auto: true if generated automatically
   *    from an autopublish hook. this is for cosmetic purposes only
   *    (it lets us determine whether to print a warning suggesting
   *    that you turn off autopublish.)
   */
  publish: function (name, handler, options) {
    var self = this;

    options = options || {};

    if (name && name in self.publish_handlers) {
      Meteor._debug("Ignoring duplicate publish named '" + name + "'");
      return;
    }

    if (!self.on_autopublish && !options.is_auto) {
      // They have autopublish on, yet they're trying to manually
      // picking stuff to publish. They probably should turn off
      // autopublish. (This check isn't perfect -- if you create a
      // publish before you turn on autopublish, it won't catch
      // it. But this will definitely handle the simple case where
      // you've added the autopublish package to your app, and are
      // calling publish from your app code.)
      if (!self.warned_about_autopublish) {
        self.warned_about_autopublish = true;
        Meteor._debug(
"** You've set up some data subscriptions with Meteor.publish(), but\n" +
"** you still have autopublish turned on. Because autopublish is still\n" +
"** on, your Meteor.publish() calls won't have much effect. All data\n" +
"** will still be sent to all clients.\n" +
"**\n" +
"** Turn off autopublish by removing the autopublish package:\n" +
"**\n" +
"**   $ meteor remove autopublish\n" +
"**\n" +
"** .. and make sure you have Meteor.publish() and Meteor.subscribe() calls\n" +
"** for each collection that you want clients to see.\n");
      }
    }

    if (name)
      self.publish_handlers[name] = handler;
    else
      self.universal_publish_handlers.push(handler);
  },

  methods: function (methods) {
    var self = this;
    _.each(methods, function (func, name) {
      if (self.method_handlers[name])
        throw new Error("A method named '" + name + "' is already defined");
      self.method_handlers[name] = func;
    });
  },

  call: function (name /*, arguments */) {
    // if it's a function, the last argument is the result callback,
    // not a parameter to the remote method.
    var args = Array.prototype.slice.call(arguments, 1);
    if (args.length && typeof args[args.length - 1] === "function")
      var callback = args.pop();
    return this.apply(name, args, callback);
  },

  // @param options {Optional Object}
  // @param callback {Optional Function}
  apply: function (name, args, options, callback) {
    var self = this;

    // We were passed 3 arguments. They may be either (name, args, options)
    // or (name, args, callback)
    if (!callback && typeof options === 'function') {
      callback = options;
      options = {};
    }
    options = options || {};

    if (callback)
      // It's not really necessary to do this, since we immediately
      // run the callback in this fiber before returning, but we do it
      // anyway for regularity.
      callback = Meteor.bindEnvironment(callback, function (e) {
        // XXX improve error message (and how we report it)
        Meteor._debug("Exception while delivering result of invoking '" +
                      name + "'", e.stack);
      });

    // Run the handler
    var handler = self.method_handlers[name];
    if (!handler)
      var exception = new Meteor.Error(404, "Method not found");
    else {
      // If this is a method call from within another method, get the
      // user state from the outer method, otherwise don't allow
      // setUserId to be called
      var userId = null;
      var setUserId = function() {
        throw new Error("Can't call setUserId on a server initiated method call");
      };
      var currentInvocation = Meteor._CurrentInvocation.get();
      if (currentInvocation) {
        userId = currentInvocation.userId();
        setUserId = function(userId) {
          currentInvocation.setUserId(userId);
        };
      }

      var invocation = new Meteor._MethodInvocation(
        false /* isSimulation */, userId, setUserId);
      try {
        var ret = Meteor._CurrentInvocation.withValue(invocation, function () {
          return handler.apply(invocation, args);
        });
      } catch (e) {
        var exception = e;
      }
    }

    // Return the result in whichever way the caller asked for it
    if (callback) {
      callback(exception, ret);
      return;
    }
    if (exception)
      throw exception;
    return ret;
  },

  // A much more elegant way to do this would be: let any autopublish
  // provider (eg, mongo-livedata) declare a weak package dependency
  // on the autopublish package, then have that package simply set a
  // flag that eg the Collection constructor checks, and autopublishes
  // if necessary.
  autopublish: function () {
    var self = this;
    _.each(self.on_autopublish || [], function (f) { f(); });
    self.on_autopublish = null;
  },

  onAutopublish: function (f) {
    var self = this;
    if (self.on_autopublish)
      self.on_autopublish.push(f);
    else
      f();
  }
});


#!/usr/bin/env python2.7

import sys
import json
import time
import argparse
import thread

from ws4py.client.threadedclient import WebSocketClient
from cmd import Cmd

def log(msg):
    """A shortcut to write to the standard error file descriptor"""
    sys.stderr.write('{}\n'.format(msg))

def parse_command(params):
    """Parses a command with a first string param and a second
    json-encoded param"""
    name, args = (params + ' ').split(' ', 1)
    return name, args and json.loads(args) or []

class DDPClient(WebSocketClient):
    """simple wrapper around Websockets for DDP connections"""
    def __init__(self, url, print_raw):
        WebSocketClient.__init__(self, url)
        self.print_raw = print_raw

        # We keep track of methods and subs that have been sent from the
        # client so that we only return to the prompt or quit the app
        # once we get back all the results from the server.
        #
        # `id`
        #
        #   The operation id, informed by the client and returned by the
        #   server to make sure both are talking about the same thing.
        #
        # `result_acked`
        #
        #   Flag to make sure we were answered.
        #
        # `data_acked`
        #
        #   Flag to make sure we received the correct data from the
        #   message we were waiting for.
        self.pending = {}

    def send(self, msg_dict):
        """Send a message through the websocket client and wait for the
        answer if the message being sent contains an id attribute.

        Also prints to the standard error fd.

        (NOTE: DDP does not require waiting for an answer but this is
        a simple proof-of-concept client)"""
        message = json.dumps(msg_dict)
        if self.print_raw:
            log('[RAW] >> {}'.format(message))
        super(DDPClient, self).send(message)

        # We don't need to wait for certain messages, just for the ones
        # with ids.
        if 'id' in msg_dict:
            self.block_until_return(msg_dict['msg'], msg_dict['id'])

    def block_until_return(self, msg_type, msg_id):
        """Wait until the msg_id that was sent to the server is answered"""
        self.pending = {'id': msg_id}

        while self.pending.get('id') is not None:
            if msg_type == 'method':
                # Methods must validate both data and result flag
                we_are_good = all((
                    self.pending.get('result_acked'),
                    self.pending.get('data_acked')))
            else:
                # Subs just need to validate data flag
                we_are_good = self.pending.get('data_acked')

            if we_are_good:
                return
            time.sleep(0)

    def opened(self):
        """Set the connecte flag to true and send the connect message to
        the server."""
        self.send({"msg": "connect"})

    def received_message(self, data):
        """Parse an incoming message and print it. Also update
        self.pending appropriately"""
        if self.print_raw:
            log('[RAW] << {}'.format(data))

        msg = json.loads(str(data))

        if msg.get('msg') == 'error':
            log("* ERROR {}".format(msg['reason']))
            # Reset all pending state
            self.pending = {}

        elif msg.get('msg') == 'connected':
            log("* CONNECTED")

        elif msg.get('msg') == 'result':
            if msg['id'] == self.pending.get('id'):
                if msg.get('result'):
                    log("* METHOD RESULT {}".format(msg['result']))
                elif msg.get('error'):
                    log("* ERROR {}".format(msg['error']['reason']))
                self.pending.update({'result_acked': True})

        elif msg.get('msg') == 'data':
            if msg.get('collection'):
                if msg.get('set'):
                    for key, value in msg['set'].items():
                        log("* SET {} {} {} {}".format(
                                msg['collection'], msg['id'], key, value))
                if msg.get('unset'):
                    for key in msg['unset']:
                        log("* UNSET {} {} {}".format(
                                msg['collection'], msg['id'], key))

            if msg.get('methods'):
                if self.pending.get('id') in msg['methods']:
                    log("* UPDATED")
                    self.pending.update({'data_acked': True})

            if msg.get('subs'):
                if self.pending.get('id') in msg['subs']:
                    log("* READY")
                    self.pending.update({'data_acked': True})

        elif msg.get('msg') == 'nosub':
            log("* NO SUCH SUB")
            self.pending.update({'data_acked': True})

    def closed(self, code, reason=None):
        """Called when the connection is closed"""
        log('* CONNECTION CLOSED {}'.format(code, reason))

        # Send a KeyboardInterrupt error to the main thread. For some
        # reason Cmd doesn't immediately respect this so the client only
        # dies once you press Enter.
        thread.interrupt_main()

class App(Cmd):
    """Main input loop."""

    def __init__(self, ddp_endpoint, print_raw):
        Cmd.__init__(self)

        # Should we print the raw websocket messages in addition to
        # parsing them?
        self.print_raw = print_raw

        # This is the websocket client that will actually talk with
        # meteor
        self.ddpclient = DDPClient(
            'ws://' + ddp_endpoint + '/websocket',
            self.print_raw)
        self.ddpclient.connect()

        # Showing a fancy prompt string if we're interactive
        if sys.stdin.isatty():
            self.prompt = ddp_endpoint + '> '
        else:
            self.prompt = ''

        # Initializing the message id counter that will be incremented
        # by the `next_id() method
        self.unique_id = 0

    def do_call(self, params):
        """The `call` command"""
        try:
            method_name, params = parse_command(params)
        except ValueError:
            log('Error parsing parameter list - try `help call`')
            return
        self.ddpclient.send({
            "msg": "method",
            "method": method_name,
            "params": params,
            "id": self.next_id(),
        })

    def do_sub(self, params):
        """The `sub` command"""
        try:
            sub_name, params = parse_command(params)
        except ValueError:
            log('Error parsing parameter list - try `help sub`')
            return
        self.ddpclient.send({
            "msg": "sub",
            "name": sub_name,
            "params": params,
            "id": self.next_id(),
        })

    def do_EOF(self, line):
        """The `EOF` "command"

        It's here to support `cat file | python ddpclient.py`
        """
        return True

    def do_help(self, line):
        """The `help` command"""

        msgs = {
            'call': (
                'call <method name> <json array of parameters>\n'
                '  Calls a remote method\n'
                '  Example: call vote ["foo.meteor.com"]'),
            'sub': (
                'sub <subscription name> [<json array of parameters>]\n'
                '  Subscribes to a remote dataset\n'
                '  Examples: `sub allApps` or `sub myApp '
                '["foo.meteor.com"]`'),
        }

        line = line.strip()
        if line and line in msgs:
            return log('\n' + msgs[line])

        for msg in msgs.values():
            log('\n' + msg)

    def emptyline(self):
        """Disable the default Cmd empty line behavior"""
        pass

    def next_id(self):
        """Calculates the next id for messages that will be sent to the
        server"""
        self.unique_id = self.unique_id + 1
        return str(self.unique_id)

def main():
    """Parse the command line arguments and create a new App instance"""
    parser = argparse.ArgumentParser(
        description='A command-line tool for communicating with a DDP server.')
    parser.add_argument(
        'ddp_endpoint', metavar='ddp_endpoint',
        help='DDP websocket endpoint to connect ' +
        'to, e.g. madewith.meteor.com')
    parser.add_argument(
        '--print-raw', dest='print_raw', action="store_true",
        help='print raw websocket data in addition to parsed results')
    args = parser.parse_args()

    app = App(args.ddp_endpoint, args.print_raw)
    app.cmdloop()

if __name__ == '__main__':
    main()


// A write fence collects a group of writes, and provides a callback
// when all of the writes are fully committed and propagated (all
// observers have been notified of the write and acknowledged it.)
Meteor._WriteFence = function () {
  var self = this;

  self.armed = false;
  self.fired = false;
  self.retired = false;
  self.outstanding_writes = 0;
  self.completion_callbacks = [];
};

// The current write fence. When there is a current write fence, code
// that writes to databases should register their writes with it using
// beginWrite().
Meteor._CurrentWriteFence = new Meteor.EnvironmentVariable;

_.extend(Meteor._WriteFence.prototype, {
  // Start tracking a write, and return an object to represent it. The
  // object has a single method, committed(). This method should be
  // called when the write is fully committed and propagated. You can
  // continue to add writes to the WriteFence up until it is triggered
  // (calls its callbacks because all writes have committed.)
  beginWrite: function () {
    var self = this;

    if (self.retired)
      return { committed: function () {} };

    if (self.fired)
      throw new Error("fence has already activated -- too late to add writes");

    self.outstanding_writes++;
    var committed = false;
    return {
      committed: function () {
        if (committed)
          throw new Error("committed called twice on the same write");
        committed = true;
        self.outstanding_writes--;
        self._maybeFire();
      }
    };
  },

  // Arm the fence. Once the fence is armed, and there are no more
  // uncommitted writes, it will activate.
  arm: function () {
    var self = this;
    self.armed = true;
    self._maybeFire();
  },

  // Register a function to be called when the fence fires.
  onAllCommitted: function (func) {
    var self = this;
    if (self.fired)
      throw new Error("fence has already activated -- too late to " +
                      "add a callback");
    self.completion_callbacks.push(func);
  },

  // Convenience function. Arms the fence, then blocks until it fires.
  armAndWait: function () {
    var self = this;
    var future = new Future;
    self.onAllCommitted(function () {
      future['return']();
    });
    self.arm();
    future.wait();
  },

  _maybeFire: function () {
    var self = this;
    if (self.fired)
      throw new Error("write fence already activated?");
    if (self.armed && !self.outstanding_writes) {
      self.fired = true;
      _.each(self.completion_callbacks, function (f) {f(self);});
      self.completion_callbacks = [];
    }
  },

  // Deactivate this fence so that adding more writes has no effect.
  // The fence must have already fired.
  retire: function () {
    var self = this;
    if (! self.fired)
      throw new Error("Can't retire a fence that hasn't fired.");
    self.retired = true;
  }
});


keep keep keep keep keep replace keep keep

// XXX namespacing

Meteor._MethodInvocation = function (isSimulation, userId,
                                     globallySetUserId, unblock) {
  var self = this;

  // true if we're running not the actual method, but a stub (that is,
  // if we're on a client (which may be a browser, or in the future a
  // server connecting to another server) and presently running a
  // simulation of a server-side method for latency compensation
  // purposes). not currently true except in a client such as a browser,
  // since there's usually no point in running stubs unless you have a
  // zero-latency connection to the user.
  this.isSimulation = isSimulation;

  // XXX Backwards compatibility only. Remove this before 1.0.
  this.is_simulation = isSimulation;

  // call this function to allow other method invocations (from the
  // same client) to continue running without waiting for this one to
  // complete.
  this.unblock = unblock || function () {};

  // current user id
  this._userId = userId;

  // sets current user id in all appropriate server contexts and
  // reruns subscriptions
  this._setUserId = globallySetUserId || function () {};
};

_.extend(Meteor._MethodInvocation.prototype, {
  userId: function() {
    return this._userId;
  },

  setUserId: function(userId) {
    this._userId = userId;
    this._setUserId(userId);
  }
});

Meteor._CurrentInvocation = new Meteor.EnvironmentVariable;

Meteor.Error = function (error, reason, details) {
  var self = this;

  // Currently, a numeric code, likely similar to a HTTP code (eg,
  // 404, 500). That is likely to change though.
  self.error = error;

  // Optional: A short human-readable summary of the error. Not
  // intended to be shown to end users, just developers. ("Not Found",
  // "Internal Server Error")
  self.reason = reason;

  // Optional: Additional information about the error, say for
  // debugging. It might be a (textual) stack trace if the server is
  // willing to provide one. The corresponding thing in HTTP would be
  // the body of a 404 or 500 response. (The difference is that we
  // never expect this to be shown to end users, only developers, so
  // it doesn't need to be pretty.)
  self.details = details;
};

Meteor.Error.prototype = new Error;


// Source: http://code.google.com/p/google-code-prettify

// XXX this sucks. I would much rather do this processing at
// bundle-time on the server, not in the client. (though I'd like to
// support both..)

Package.describe({
  summary: "Syntax highlighting of code, from Google"
});

// XXX this code dumps symbols into the global namespace (directly
// onto 'window'.) we need to fix that.
Package.on_use(function (api) {
  api.add_files([
    'prettify.js',
    'prettify.css',
    'styles/sunburst.css'], "client");
});


if (!Accounts.facebook) {
  Accounts.facebook = {};
}

Accounts.facebook.config = function(options) {
  Accounts.facebook._options = options;
};


Package.describe({
  summary: "Provides JSON.stringify and JSON.parse for older browsers",
  internal: true
});

// We need to figure out how to serve this file only to browsers that
// don't have JSON.stringify (eg, IE7 and earlier -- or is that IE8?)

Package.on_use(function (api, where) {
  where = where || ['client', 'server'];

  api.add_files('json2.js', where);
});


// call this function to allow other method invocations (from the

// same client) to continue running without waiting for this one to

if (Meteor.isServer) {
  // XXX namespacing
  var Future = __meteor_bootstrap__.require('fibers/future');
}

// list of subscription tokens outstanding during a
// captureDependencies run. only set when we're doing a run. The fact
// that this is a singleton means we can't do recursive
// Meteor.subscriptions(). But what would that even mean?
// XXX namespacing
Meteor._capture_subs = null;

// @param url {String|Object} URL to Meteor app or sockjs endpoint (deprecated),
//     or an object as a test hook (see code)
// Options:
//   reloadOnUpdate: should we try to reload when the server says
//                      there's new code available?
//   reloadWithOutstanding: is it OK to reload if there are outstanding methods?
Meteor._LivedataConnection = function (url, options) {
  var self = this;
  options = _.extend({
    reloadOnUpdate: false,
    reloadWithOutstanding: false
  }, options);

  // as a test hook, allow passing a stream instead of a url.
  if (typeof url === "object") {
    self.stream = url;
    // if we have two test streams, auto reload stuff will break because
    // the url is used as a key for the migration data.
    self.url = "/debug";
  } else {
    self.url = url;
  }

  self.last_session_id = null;
  self.stores = {}; // name -> object with methods
  self.method_handlers = {}; // name -> func
  self.next_method_id = 1;

  // --- Three classes of outstanding methods ---

  // 1. either already sent, or waiting to be sent with no special
  // consideration once we reconnect
  self.outstanding_methods = []; // each item has keys: msg, callback

  // 2. the sole outstanding method that needs to be waited on, or null
  // same keys as outstanding_methods (notably wait is implicitly true
  // but not set)
  self.outstanding_wait_method = null; // same keys as outstanding_methods
  // stores response from `outstanding_wait_method` while we wait for
  // previous method calls to complete, as received in _livedata_result
  self.outstanding_wait_method_response = null;

  // 3. methods blocked on outstanding_wait_method being completed.
  self.blocked_methods = []; // each item has keys: msg, callback, wait

  // if set, called when we reconnect, queuing method calls _before_
  // the existing outstanding ones
  self.onReconnect = null;
  // waiting for data from method
  self.unsatisfied_methods = {}; // map from method_id -> true
  // sub was ready, is no longer (due to reconnect)
  self.unready_subscriptions = {}; // map from sub._id -> true
  // messages from the server that have not been applied
  self.pending_data = []; // array of pending data messages
  // name -> updates for (yet to be created) collection
  self.queued = {};
  // if we're blocking a migration, the retry func
  self._retryMigrate = null;

  // metadata for subscriptions
  self.subs = new LocalCollection;
  // keyed by subs._id. value is unset or an array. if set, sub is not
  // yet ready.
  self.sub_ready_callbacks = {};

  // just for testing
  self.quiesce_callbacks = [];

  // Block auto-reload while we're waiting for method responses.
  if (!options.reloadWithOutstanding) {
    Meteor._reload.onMigrate(function (retry) {
      if (!self._readyToMigrate()) {
        if (self._retryMigrate)
          throw new Error("Two migrations in progress?");
        self._retryMigrate = retry;
        return false;
      } else {
        return [true];
      }
    });
  }

  // Setup stream (if not overriden above)
  self.stream = self.stream || new Meteor._Stream(self.url);

  self.stream.on('message', function (raw_msg) {
    try {
      var msg = JSON.parse(raw_msg);
    } catch (err) {
      Meteor._debug("discarding message with invalid JSON", raw_msg);
      return;
    }
    if (typeof msg !== 'object' || !msg.msg) {
      Meteor._debug("discarding invalid livedata message", msg);
      return;
    }

    if (msg.msg === 'connected')
      self._livedata_connected(msg);
    else if (msg.msg === 'data')
      self._livedata_data(msg);
    else if (msg.msg === 'nosub')
      self._livedata_nosub(msg);
    else if (msg.msg === 'result')
      self._livedata_result(msg);
    else if (msg.msg === 'error')
      self._livedata_error(msg);
    else
      Meteor._debug("discarding unknown livedata message type", msg);
  });

  self.stream.on('reset', function () {
    // Send a connect message at the beginning of the stream.
    // NOTE: reset is called even on the first connection, so this is
    // the only place we send this message.
    var msg = {msg: 'connect'};
    if (self.last_session_id)
      msg.session = self.last_session_id;
    self.stream.send(JSON.stringify(msg));

    // Now, to minimize setup latency, go ahead and blast out all of
    // our pending methods ands subscriptions before we've even taken
    // the necessary RTT to know if we successfully reconnected. (1)
    // They're supposed to be idempotent; (2) even if we did
    // reconnect, we're not sure what messages might have gotten lost
    // (in either direction) since we were disconnected (TCP being
    // sloppy about that.)

    // XXX we may have an issue where we lose 'data' messages sent
    // immediately before disconnection.. do we need to add app-level
    // acking of data messages?

    // If an `onReconnect` handler is set, call it first. Go through
    // some hoops to ensure that methods that are called from within
    // `onReconnect` get executed _before_ ones that were originally
    // outstanding (since `onReconnect` is used to re-establish auth
    // certificates)
    if (self.onReconnect)
      self._callOnReconnectAndSendAppropriateOutstandingMethods();
    else
      self._sendOutstandingMethods();

    // add new subscriptions at the end. this way they take effect after
    // the handlers and we don't see flicker.
    self.subs.find().forEach(function (sub) {
      self.stream.send(JSON.stringify(
        {msg: 'sub', id: sub._id, name: sub.name, params: sub.args}));
    });
  });

  if (options.reloadOnUpdate) {
    self.stream.on('update_available', function () {
      // Start trying to migrate to a new version. Until all packages
      // signal that they're ready for a migration, the app will
      // continue running normally.
      Meteor._reload.reload();
    });
  }

  // we never terminate the observe(), since there is no way to
  // destroy a LivedataConnection.. but this shouldn't matter, since we're
  // the only one that holds a reference to the self.subs collection
  self.subs_token = self.subs.find({}).observe({
    added: function (sub) {
      self.stream.send(JSON.stringify({
        msg: 'sub', id: sub._id, name: sub.name, params: sub.args}));
    },
    changed: function (sub) {
      if (sub.count <= 0) {
        // minimongo not re-entrant.
        _.defer(function () { self.subs.remove({_id: sub._id}); });
      }
    },
    removed: function (obj) {
      self.stream.send(JSON.stringify({msg: 'unsub', id: obj._id}));
    }
  });
};

_.extend(Meteor._LivedataConnection.prototype, {
  // 'name' is the name of the data on the wire that should go in the
  // store. 'store' should be an object with methods beginUpdate,
  // update, endUpdate, reset. see Collection for an example.
  registerStore: function (name, store) {
    var self = this;

    if (name in self.stores)
      return false;
    self.stores[name] = store;

    var queued = self.queued[name] || [];
    store.beginUpdate(queued.length);
    _.each(queued, function (msg) {
      store.update(msg);
    });
    store.endUpdate();
    delete self.queued[name];

    return true;
  },

  subscribe: function (name /* .. [arguments] .. callback */) {
    var self = this;
    var id;

    var args = Array.prototype.slice.call(arguments, 1);
    if (args.length && typeof args[args.length - 1] === "function")
      var callback = args.pop();

    var existing = self.subs.find({name: name, args: args}, {reactive: false}).fetch();

    if (existing && existing[0]) {
      // already subbed, inc count.
      id = existing[0]._id;
      self.subs.update({_id: id}, {$inc: {count: 1}});

      if (callback) {
        if (self.sub_ready_callbacks[id])
          self.sub_ready_callbacks[id].push(callback);
        else
          callback(); // XXX maybe _.defer?
      }
    } else {
      // new sub, add object.
      // generate our own id so we can know it w/ a find afterwards.
      id = LocalCollection.uuid();
      self.subs.insert({_id: id, name: name, args: args, count: 1});

      self.sub_ready_callbacks[id] = [];

      if (callback)
        self.sub_ready_callbacks[id].push(callback);
    }

    // return an object with a stop method.
    var token = {stop: function () {
      if (!id) return; // must have an id (local from above).
      // just update the database. observe takes care of the rest.
      self.subs.update({_id: id}, {$inc: {count: -1}});
    }};

    if (Meteor._capture_subs)
      Meteor._capture_subs.push(token);

    return token;
  },

  methods: function (methods) {
    var self = this;
    _.each(methods, function (func, name) {
      if (self.method_handlers[name])
        throw new Error("A method named '" + name + "' is already defined");
      self.method_handlers[name] = func;
    });
  },

  call: function (name /* .. [arguments] .. callback */) {
    // if it's a function, the last argument is the result callback,
    // not a parameter to the remote method.
    var args = Array.prototype.slice.call(arguments, 1);
    if (args.length && typeof args[args.length - 1] === "function")
      var callback = args.pop();
    return this.apply(name, args, callback);
  },

  // @param options {Optional Object}
  //   wait: Boolean - Should we block subsequent method calls on this
  //                   method's result having been received?
  //                   (does not affect methods called from within this method)
  // @param callback {Optional Function}
  apply: function (name, args, options, callback) {
    var self = this;

    // We were passed 3 arguments. They may be either (name, args, options)
    // or (name, args, callback)
    if (!callback && typeof options === 'function') {
      callback = options;
      options = {};
    }
    options = options || {};

    if (callback) {
      // XXX would it be better form to do the binding in stream.on,
      // or caller, instead of here?
      callback = Meteor.bindEnvironment(callback, function (e) {
        // XXX improve error message (and how we report it)
        Meteor._debug("Exception while delivering result of invoking '" +
                      name + "'", e.stack);
      });
    }

    if (Meteor.isClient) {
      // If on a client, run the stub, if we have one. The stub is
      // supposed to make some temporary writes to the database to
      // give the user a smooth experience until the actual result of
      // executing the method comes back from the server (whereupon
      // the temporary writes to the database will be reversed during
      // the beginUpdate/endUpdate process.)
      //
      // Normally, we ignore the return value of the stub (even if it
      // is an exception), in favor of the real return value from the
      // server. The exception is if the *caller* is a stub. In that
      // case, we're not going to do a RPC, so we use the return value
      // of the stub as our return value.
      var stub = self.method_handlers[name];
      if (stub) {
        var setUserId = function(userId) {
          self.setUserId(userId);
        };
        var invocation = new Meteor._MethodInvocation(
          true /* isSimulation */, self.userId(), setUserId);
        try {
          var ret = Meteor._CurrentInvocation.withValue(invocation,function () {
            return stub.apply(invocation, args);
          });
        }
        catch (e) {
          var exception = e;
        }
      }

      // If we're in a simulation, stop and return the result we have,
      // rather than going on to do an RPC. If there was no stub,
      // we'll end up returning undefined.
      var enclosing = Meteor._CurrentInvocation.get();
      var isSimulation = enclosing && enclosing.isSimulation;
      if (isSimulation) {
        if (callback) {
          callback(exception, ret);
          return;
        }
        if (exception)
          throw exception;
        return ret;
      }

      // If an exception occurred in a stub, and we're ignoring it
      // because we're doing an RPC and want to use what the server
      // returns instead, log it so the developer knows.
      //
      // Tests can set the 'expected' flag on an exception so it won't
      // go to log.
      if (exception && !exception.expected)
        Meteor._debug("Exception while simulating the effect of invoking '" +
                      name + "'", exception.stack);
    }

    // At this point we're definitely doing an RPC, and we're going to
    // return the value of the RPC to the caller.

    // If the caller didn't give a callback, decide what to do.
    if (!callback) {
      if (Meteor.isClient)
        // On the client, we don't have fibers, so we can't block. The
        // only thing we can do is to return undefined and discard the
        // result of the RPC.
        callback = function () {};
      else {
        // On the server, make the function synchronous.
        var future = new Future;
        callback = function (err, result) {
          future['return']([err, result]);
        };
      }
    }

    // Send the RPC. Note that on the client, it is important that the
    // stub have finished before we send the RPC (or at least we need
    // to guaranteed that the snapshot is not restored until the stub
    // has stopped doing writes.)
    var msg = {
      msg: 'method',
      method: name,
      params: args,
      id: '' + (self.next_method_id++)
    };

    if (self.outstanding_wait_method) {
      self.blocked_methods.push({
        msg: msg,
        callback: callback,
        wait: options.wait
      });
    } else {
      var method_object = {
        msg: msg,
        callback: callback
      };

      if (options.wait)
        self.outstanding_wait_method = method_object;
      else
        self.outstanding_methods.push(method_object);

      self.stream.send(JSON.stringify(msg));
    }

    // Even if we are waiting on other method calls mark this method
    // as unsatisfied so that the user never ends up seeing
    // intermediate versions of the server's datastream
    self.unsatisfied_methods[msg.id] = true;

    // If we're using the default callback on the server,
    // synchronously return the result from the remote host.
    if (future) {
      var outcome = future.wait();
      if (outcome[0])
        throw outcome[0];
      return outcome[1];
    }
  },

  status: function (/*passthrough args*/) {
    var self = this;
    return self.stream.status.apply(self.stream, arguments);
  },

  reconnect: function (/*passthrough args*/) {
    var self = this;
    return self.stream.reconnect.apply(self.stream, arguments);
  },

  ///
  /// Reactive user system
  /// XXX Can/should this be generalized pattern?
  ///
  userId: function () {
    var self = this;
    var context = Meteor.deps && Meteor.deps.Context.current;
    if (context && !(context.id in self._userIdListeners)) {
      self._userIdListeners[context.id] = context;
      context.onInvalidate(function () {
        delete self._userIdListeners[context.id];
      });
    }
    return self._userId;
  },

  setUserId: function (userId) {
    var self = this;
    self._userId = userId;
    _.each(self._userIdListeners, function (context) {
      context.invalidate();
    });
  },

  _userId: null,
  _userIdListeners: {}, // context.id -> context

  // PRIVATE: called when we are up-to-date with the server. intended
  // for use only in tests. currently, you are very limited in what
  // you may do inside your callback -- in particular, don't do
  // anything that could result in another call to onQuiesce, or
  // results are undefined.
  onQuiesce: function (f) {
    var self = this;

    f = Meteor.bindEnvironment(f, function (e) {
      Meteor._debug("Exception in quiesce callback", e.stack);
    });

    for (var method_id in self.unsatisfied_methods) {
      // we are not quiesced -- wait until we are
      self.quiesce_callbacks.push(f);
      return;
    }

    f();
  },

  _livedata_connected: function (msg) {
    var self = this;

    if (typeof (msg.session) === "string") {
      var reconnected = (self.last_session_id === msg.session);
      self.last_session_id = msg.session;
    }

    if (reconnected)
      // successful reconnection -- pick up where we left off.
      return;

    // Server doesn't have our data any more. Re-sync a new session.

    // Put a reset message into the pending data queue and discard any
    // previous messages (they are unimportant now).
    self.pending_data = ["reset"];
    self.queued = {};

    // Mark all currently ready subscriptions as 'unready'.
    var all_subs = self.subs.find({}).fetch();
    self.unready_subscriptions = {};
    _.each(all_subs, function (sub) {
      if (!self.sub_ready_callbacks[sub._id])
        self.unready_subscriptions[sub._id] = true;
    });

    // Do not clear the database here. That happens once all the subs
    // are re-ready and we process pending_data.
  },

  _livedata_data: function (msg) {
    var self = this;

    // Add the data message to the queue
    self.pending_data.push(msg);

    // Process satisfied methods and subscriptions.
    // NOTE: does not fire callbacks here, that happens when
    // the data message is processed for real. This is just for
    // quiescing.
    _.each(msg.methods || [], function (method_id) {
      delete self.unsatisfied_methods[method_id];
    });
    _.each(msg.subs || [], function (sub_id) {
      delete self.unready_subscriptions[sub_id];
    });

    // If there are still method invocations in flight, stop
    for (var method_id in self.unsatisfied_methods)
      return;
    // If there are still uncomplete subscriptions, stop
    for (var sub_id in self.unready_subscriptions)
      return;

    // We have quiesced. Blow away local changes and replace
    // with authoritative changes from server.

    var messagesPerStore = {};
    _.each(self.pending_data, function (msg) {
      if (msg.collection && msg.id && self.stores[msg.collection]) {
        if (_.has(messagesPerStore, msg.collection))
          ++messagesPerStore[msg.collection];
        else
          messagesPerStore[msg.collection] = 1;
      }
    });

    _.each(self.stores, function (s, name) {
      s.beginUpdate(_.has(messagesPerStore, name) ? messagesPerStore[name] : 0);
    });

    _.each(self.pending_data, function (msg) {
      // Reset message from reconnect. Blow away everything.
      //
      // XXX instead of reset message, we could have a flag, and pass
      // that to beginUpdate. This would be more efficient since we don't
      // have to restore a snapshot if we're just going to blow away the
      // db.
      if (msg === "reset") {
        _.each(self.stores, function (s) { s.reset(); });
        return;
      }

      if (msg.collection && msg.id) {
        var store = self.stores[msg.collection];

        if (!store) {
          // Nobody's listening for this data. Queue it up until
          // someone wants it.
          // XXX memory use will grow without bound if you forget to
          // create a collection.. going to have to do something about
          // that.
          if (!(msg.collection in self.queued))
            self.queued[msg.collection] = [];
          self.queued[msg.collection].push(msg);
          return;
        }

        store.update(msg);
      }

      if (msg.subs) {
        _.each(msg.subs, function (id) {
          var arr = self.sub_ready_callbacks[id];
          if (arr) _.each(arr, function (c) { c(); });
          delete self.sub_ready_callbacks[id];
        });
      }
    });

    _.each(self.stores, function (s) { s.endUpdate(); });

    _.each(self.quiesce_callbacks, function (cb) { cb(); });
    self.quiesce_callbacks = [];

    self.pending_data = [];
  },

  _livedata_nosub: function (msg) {
    var self = this;
    // Meteor._debug("NOSUB", msg);
  },

  _livedata_result: function (msg) {
    // id, result or error. error has error (code), reason, details

    var self = this;
    // find the outstanding request
    // should be O(1) in nearly all realistic use cases
    var m;
    if (self.outstanding_wait_method &&
        self.outstanding_wait_method.msg.id === msg.id) {
      m = self.outstanding_wait_method;
      self.outstanding_wait_method_response = msg;
    } else {
      for (var i = 0; i < self.outstanding_methods.length; i++) {
        m = self.outstanding_methods[i];
        if (m.msg.id === msg.id)
          break;
      }

      // remove
      self.outstanding_methods.splice(i, 1);
    }

    if (!m) {
      Meteor._debug("Can't match method response to original method call", msg);
      return;
    }

    if (self.outstanding_wait_method) {
      // Wait until we have completed all outstanding methods.
      if (self.outstanding_methods.length === 0 &&
         self.outstanding_wait_method_response) {

        // Start by saving the outstanding wait method details, since
        // we're going to reshift the blocked ones and try to send
        // them *before* calling the method callback. It is necessary
        // to call method callbacks last since they might themselves
        // call other methods
        var savedOutstandingWaitMethod = self.outstanding_wait_method;
        var savedOutstandingWaitMethodResponse = self.outstanding_wait_method_response;
        self.outstanding_wait_method_response = null;
        self.outstanding_wait_method = null;

        // Find first blocked method with wait: true
        var i;
        for (i = 0; i < self.blocked_methods.length; i++)
          if (self.blocked_methods[i].wait)
            break;

        // Move as many blocked methods as we can into
        // outstanding_methods and outstanding_wait_method if needed
        self.outstanding_methods = _.first(self.blocked_methods, i);
        if (i !== self.blocked_methods.length) {
          self.outstanding_wait_method = self.blocked_methods[i];
          self.blocked_methods = _.rest(self.blocked_methods, i+1);
        } else {
          self.blocked_methods = [];
        }

        // Send any new outstanding methods after we reshift the
        // blocked methods.  Intentionally do this before calling the
        // method response because they might call additional methods
        // that shouldn't be sent twice.
        self._sendOutstandingMethods();

        // Fire necessary outstanding method callbacks, making sure we
        // only fire the outstanding wait method after all other outstanding
        // methods' callbacks were fired
        if (m === savedOutstandingWaitMethod) {
          self._deliverMethodResponse(savedOutstandingWaitMethod,
                       savedOutstandingWaitMethodResponse /*(=== msg)*/);
        } else {
          self._deliverMethodResponse(m, msg);
          self._deliverMethodResponse(savedOutstandingWaitMethod,
                       savedOutstandingWaitMethodResponse /*(!== msg)*/);
        }
      } else {
        if (m !== self.outstanding_wait_method)
          self._deliverMethodResponse(m, msg);
      }
    } else {
      self._deliverMethodResponse(m, msg);
    }

    // if we were blocking a migration, see if it's now possible to
    // continue
    if (self._retryMigrate && self._readyToMigrate()) {
      self._retryMigrate();
      self._retryMigrate = null;
    }
  },

  // @param method {Object} as in `outstanding_methods`
  // @param response {Object{id, result | error}}
  _deliverMethodResponse: function(method, response) {
    // callback will have already been bindEnvironment'd by apply(),
    // so no need to catch exceptions
    if ('error' in response) {
      method.callback(new Meteor.Error(
        response.error.error, response.error.reason,
        response.error.details));
    } else {
      // msg.result may be undefined if the method didn't return a
      // value
      method.callback(undefined, response.result);
    }
  },

  _sendOutstandingMethods: function() {
    var self = this;
    _.each(self.outstanding_methods, function (m) {
      self.stream.send(JSON.stringify(m.msg));
    });
    if (self.outstanding_wait_method)
      self.stream.send(JSON.stringify(self.outstanding_wait_method.msg));
  },

  _livedata_error: function (msg) {
    Meteor._debug("Received error from server: ", msg.reason);
    if (msg.offending_message)
      Meteor._debug("For: ", msg.offending_message);
  },

  _callOnReconnectAndSendAppropriateOutstandingMethods: function() {
    var self = this;
    var old_outstanding_methods = self.outstanding_methods;
    var old_outstanding_wait_method = self.outstanding_wait_method;
    var old_blocked_methods = self.blocked_methods;
    self.outstanding_methods = [];
    self.outstanding_wait_method = null;
    self.blocked_methods = [];

    self.onReconnect();

    if (self.outstanding_wait_method) {
      // self.onReconnect() caused us to wait on a method. Add all old
      // methods to blocked_methods, and we don't need to send any
      // additional methods
      self.blocked_methods = self.blocked_methods.concat(
        old_outstanding_methods);

      if (old_outstanding_wait_method) {
        self.blocked_methods.push(_.extend(
          old_outstanding_wait_method, {wait: true}));
      }

      self.blocked_methods = self.blocked_methods.concat(
        old_blocked_methods);
    } else {
      // self.onReconnect() did not cause us to wait on a method. Add
      // as many methods as we can to outstanding_methods and send
      // them
      _.each(old_outstanding_methods, function(method) {
        self.outstanding_methods.push(method);
        self.stream.send(JSON.stringify(method.msg));
      });

      self.outstanding_wait_method = old_outstanding_wait_method;
      if (self.outstanding_wait_method)
        self.stream.send(JSON.stringify(self.outstanding_wait_method.msg));

      self.blocked_methods = old_blocked_methods;
    }
  },

  _readyToMigrate: function() {
    var self = this;
    return self.outstanding_methods.length === 0 &&
      !self.outstanding_wait_method &&
      self.blocked_methods.length === 0;
  }
});

_.extend(Meteor, {
  // @param url {String} URL to Meteor app, or to sockjs endpoint (deprecated),
  //     e.g.:
  //     "subdomain.meteor.com",
  //     "http://subdomain.meteor.com",
  //     "/",
  //     "http://subdomain.meteor.com/sockjs" (deprecated),
  //     "/sockjs" (deprecated)
  connect: function (url, _reloadOnUpdate) {
    var ret = new Meteor._LivedataConnection(
      url, {reloadOnUpdate: _reloadOnUpdate});
    Meteor._LivedataConnection._allConnections.push(ret); // hack. see below.
    return ret;
  },

  autosubscribe: function (sub_func) {
    var local_subs = [];
    var context = new Meteor.deps.Context();

    context.onInvalidate(function () {
      // recurse.
      Meteor.autosubscribe(sub_func);
      // unsub after re-subbing, to avoid bouncing.
      _.each(local_subs, function (x) { x.stop(); });
    });

    context.run(function () {
      if (Meteor._capture_subs)
        throw new Error("Meteor.autosubscribe may not be called recursively");

      Meteor._capture_subs = [];
      try {
        sub_func();
      } finally {
        local_subs = Meteor._capture_subs;
        Meteor._capture_subs = null;
      }
    });
  }
});

// Hack for `spiderable` package: a way to see if the page is done
// loading all the data it needs.
Meteor._LivedataConnection._allConnections = [];
Meteor._LivedataConnection._allSubscriptionsReady = function () {
  return _.all(Meteor._LivedataConnection._allConnections, function (conn) {
    for (var k in conn.sub_ready_callbacks)
      return false;
    return true;
  });
};


// Client-side JavaScript, bundled and sent to client.

// Define Minimongo collections to match server/publish.js.
Lists = new Meteor.Collection("lists");
Todos = new Meteor.Collection("todos");

// ID of currently selected list
Session.set('list_id', null);

// Name of currently selected tag for filtering
Session.set('tag_filter', null);

// When adding tag to a todo, ID of the todo
Session.set('editing_addtag', null);

// When editing a list name, ID of the list
Session.set('editing_listname', null);

// When editing todo text, ID of the todo
Session.set('editing_itemname', null);

// Subscribe to 'lists' collection on startup.
// Select a list once data has arrived.
Meteor.subscribe('lists', function () {
  if (!Session.get('list_id')) {
    var list = Lists.findOne({}, {sort: {name: 1}});
    if (list)
      Router.setList(list._id);
  }
});

// Always be subscribed to the todos for the selected list.
Meteor.autosubscribe(function () {
  var list_id = Session.get('list_id');
  if (list_id)
    Meteor.subscribe('todos', list_id);
});

////////// Helpers for in-place editing //////////

// Returns an event map that handles the "escape" and "return" keys and
// "blur" events on a text input (given by selector) and interprets them
// as "ok" or "cancel".
var okCancelEvents = function (selector, callbacks) {
  var ok = callbacks.ok || function () {};
  var cancel = callbacks.cancel || function () {};

  var events = {};
  events['keyup '+selector+', keydown '+selector+', focusout '+selector] =
    function (evt) {
      if (evt.type === "keydown" && evt.which === 27) {
        // escape = cancel
        cancel.call(this, evt);

      } else if (evt.type === "keyup" && evt.which === 13 ||
                 evt.type === "focusout") {
        // blur/return/enter = ok/submit if non-empty
        var value = String(evt.target.value || "");
        if (value)
          ok.call(this, value, evt);
        else
          cancel.call(this, evt);
      }
    };
  return events;
};

var activateInput = function (input) {
  input.focus();
  input.select();
};

////////// Lists //////////

Template.lists.lists = function () {
  return Lists.find({}, {sort: {name: 1}});
};

Template.lists.events({
  'mousedown .list': function (evt) { // select list
    Router.setList(this._id);
  },
  'click .list': function (evt) {
    // prevent clicks on <a> from refreshing the page.
    evt.preventDefault();
  },
  'dblclick .list': function (evt, tmpl) { // start editing list name
    Session.set('editing_listname', this._id);
    Meteor.flush(); // force DOM redraw, so we can focus the edit field
    activateInput(tmpl.find("#list-name-input"));
  }
});

// Attach events to keydown, keyup, and blur on "New list" input box.
Template.lists.events(okCancelEvents(
  '#new-list',
  {
    ok: function (text, evt) {
      var id = Lists.insert({name: text});
      Router.setList(id);
      evt.target.value = "";
    }
  }));

Template.lists.events(okCancelEvents(
  '#list-name-input',
  {
    ok: function (value) {
      Lists.update(this._id, {$set: {name: value}});
      Session.set('editing_listname', null);
    },
    cancel: function () {
      Session.set('editing_listname', null);
    }
  }));

Template.lists.selected = function () {
  return Session.equals('list_id', this._id) ? 'selected' : '';
};

Template.lists.name_class = function () {
  return this.name ? '' : 'empty';
};

Template.lists.editing = function () {
  return Session.equals('editing_listname', this._id);
};

////////// Todos //////////

Template.todos.any_list_selected = function () {
  return !Session.equals('list_id', null);
};

Template.todos.events(okCancelEvents(
  '#new-todo',
  {
    ok: function (text, evt) {
      var tag = Session.get('tag_filter');
      Todos.insert({
        text: text,
        list_id: Session.get('list_id'),
        done: false,
        timestamp: (new Date()).getTime(),
        tags: tag ? [tag] : []
      });
      evt.target.value = '';
    }
  }));

Template.todos.todos = function () {
  // Determine which todos to display in main pane,
  // selected based on list_id and tag_filter.

  var list_id = Session.get('list_id');
  if (!list_id)
    return {};

  var sel = {list_id: list_id};
  var tag_filter = Session.get('tag_filter');
  if (tag_filter)
    sel.tags = tag_filter;

  return Todos.find(sel, {sort: {timestamp: 1}});
};

Template.todo_item.tag_objs = function () {
  var todo_id = this._id;
  return _.map(this.tags || [], function (tag) {
    return {todo_id: todo_id, tag: tag};
  });
};

Template.todo_item.done_class = function () {
  return this.done ? 'done' : '';
};

Template.todo_item.done_checkbox = function () {
  return this.done ? 'checked="checked"' : '';
};

Template.todo_item.editing = function () {
  return Session.equals('editing_itemname', this._id);
};

Template.todo_item.adding_tag = function () {
  return Session.equals('editing_addtag', this._id);
};

Template.todo_item.events({
  'click .check': function () {
    Todos.update(this._id, {$set: {done: !this.done}});
  },

  'click .destroy': function () {
    Todos.remove(this._id);
  },

  'click .addtag': function (evt, tmpl) {
    Session.set('editing_addtag', this._id);
    Meteor.flush(); // update DOM before focus
    activateInput(tmpl.find("#edittag-input"));
  },

  'dblclick .display .todo-text': function (evt, tmpl) {
    Session.set('editing_itemname', this._id);
    Meteor.flush(); // update DOM before focus
    activateInput(tmpl.find("#todo-input"));
  },

  'click .remove': function (evt) {
    var tag = this.tag;
    var id = this.todo_id;

    evt.target.parentNode.style.opacity = 0;
    // wait for CSS animation to finish
    Meteor.setTimeout(function () {
      Todos.update(id, {$pull: {tags: tag}});
    }, 300);
  },

  'click .make-public': function () {
    Todos.update(this._id, {$set: {privateTo: null}});
  },

  'click .make-private': function () {
    Todos.update(this._id, {$set: {
      privateTo: Meteor.user()._id
    }});
  }
});

Template.todo_item.events(okCancelEvents(
  '#todo-input',
  {
    ok: function (value) {
      Todos.update(this._id, {$set: {text: value}});
      Session.set('editing_itemname', null);
    },
    cancel: function () {
      Session.set('editing_itemname', null);
    }
  }));

Template.todo_item.events(okCancelEvents(
  '#edittag-input',
  {
    ok: function (value) {
      Todos.update(this._id, {$addToSet: {tags: value}});
      Session.set('editing_addtag', null);
    },
    cancel: function () {
      Session.set('editing_addtag', null);
    }
  }));

////////// Tag Filter //////////

// Pick out the unique tags from all todos in current list.
Template.tag_filter.tags = function () {
  var tag_infos = [];
  var total_count = 0;

  Todos.find({list_id: Session.get('list_id')}).forEach(function (todo) {
    _.each(todo.tags, function (tag) {
      var tag_info = _.find(tag_infos, function (x) { return x.tag === tag; });
      if (! tag_info)
        tag_infos.push({tag: tag, count: 1});
      else
        tag_info.count++;
    });
    total_count++;
  });

  tag_infos = _.sortBy(tag_infos, function (x) { return x.tag; });
  tag_infos.unshift({tag: null, count: total_count});

  return tag_infos;
};

Template.tag_filter.tag_text = function () {
  return this.tag || "All items";
};

Template.tag_filter.selected = function () {
  return Session.equals('tag_filter', this.tag) ? 'selected' : '';
};

Template.tag_filter.events({
  'mousedown .tag': function () {
    if (Session.equals('tag_filter', this.tag))
      Session.set('tag_filter', null);
    else
      Session.set('tag_filter', this.tag);
  }
});

////////// Tracking selected list in URL //////////

var TodosRouter = Backbone.Router.extend({
  routes: {
    ":list_id": "main"
  },
  main: function (list_id) {
    Session.set("list_id", list_id);
    Session.set("tag_filter", null);
  },
  setList: function (list_id) {
    this.navigate(list_id, true);
  }
});

Router = new TodosRouter;

Meteor.startup(function () {
  Backbone.history.start({pushState: true});
});


Spark._patch = function(tgtParent, srcParent, tgtBefore, tgtAfter, preservations,
                        results) {

  var copyFunc = function(t, s) {
    LiveRange.transplantTag(Spark._TAG, t, s);
  };

  var patcher = new Spark._Patcher(
    tgtParent, srcParent, tgtBefore, tgtAfter);

  var visitNodes = function(parent, before, after, func) {
    for(var n = before ? before.nextSibling : parent.firstChild;
        n && n !== after;
        n = n.nextSibling) {
      if (func(n) !== false && n.firstChild)
        visitNodes(n, null, null, func);
    }
  };

  // results arg is optional; it is mutated if provided; returned either way
  results = (results || {});
  // array of LiveRanges that were successfully preserved from
  // the region preservations
  var regionPreservations = (results.regionPreservations =
                             results.regionPreservations || []);

  var lastTgtMatch = null;

  visitNodes(srcParent, null, null, function(src) {
    // XXX inefficient to scan for match for every node!
    // We could at least skip non-element nodes, except for "range matches"
    // used for constant chunks, which may begin on a non-element.
    // But really this shouldn't be a linear search.
    var pres = _.find(preservations, function (p) {
      // find preserved region starting at `src`, if any
      return p.type === 'region' && p.newRange.firstNode() === src;
    }) || _.find(preservations, function (p) {
      // else, find preservation of `src`
      return p.type === 'node' && p.to === src;
    });

    if (pres) {
      var tgt = (pres.type === 'region' ? pres.fromStart : pres.from);
      if (! lastTgtMatch ||
          DomUtils.compareElementIndex(lastTgtMatch, tgt) < 0) {
        if (pres.type === 'region') {
          // preserved region for constant landmark
          if (patcher.match(pres.fromStart, pres.newRange.firstNode(),
                            copyFunc, true)) {
            patcher.skipToSiblings(pres.fromEnd, pres.newRange.lastNode());
            // without knowing or caring what DOM nodes are in pres.newRange,
            // transplant the range data to pres.fromStart and pres.fromEnd
            // (including references to enclosing ranges).
            LiveRange.transplantRange(
              pres.fromStart, pres.fromEnd, pres.newRange);
            regionPreservations.push(pres.newRange);
          }
        } else if (pres.type === 'node') {
          if (patcher.match(tgt, src, copyFunc)) {
            // match succeeded
            lastTgtMatch = tgt;
            if (tgt.firstChild || src.firstChild) {
              // Don't patch contents of TEXTAREA tag,
              // which are only the initial contents but
              // may affect the tag's .value in IE.
              if (tgt.nodeName !== "TEXTAREA") {
                // recurse!
                Spark._patch(tgt, src, null, null, preservations);
              }
            }
            return false; // tell visitNodes not to recurse
          }
        }
      }
    }
    return true;
  });

  patcher.finish();

  return results;
};

// A Patcher manages the controlled replacement of a region of the DOM.
// The target region is changed in place to match the source region.
//
// The target region consists of the children of tgtParent, extending from
// the child after tgtBefore to the child before tgtAfter.  A null
// or absent tgtBefore or tgtAfter represents the beginning or end
// of tgtParent's children.  The source region consists of all children
// of srcParent, which may be a DocumentFragment.
//
// To use a new Patcher, call `match` zero or more times followed by
// `finish`.
//
// A match is a correspondence between an old node in the target region
// and a new node in the source region that will replace it.  Based on
// this correspondence, the target node is preserved and the attributes
// and children of the source node are copied over it.  The `match`
// method declares such a correspondence.  A Patcher that makes no matches,
// for example, just removes the target nodes and inserts the source nodes
// in their place.
//
// Constructor:
Spark._Patcher = function(tgtParent, srcParent, tgtBefore, tgtAfter) {
  this.tgtParent = tgtParent;
  this.srcParent = srcParent;

  this.tgtBefore = tgtBefore;
  this.tgtAfter = tgtAfter;

  this.lastKeptTgtNode = null;
  this.lastKeptSrcNode = null;
};

// Advances the patching process up to tgtNode in the target tree,
// and srcNode in the source tree.  tgtNode will be preserved, with
// the attributes of srcNode copied over it, in essence identifying
// the two nodes with each other.  The same treatment is given to
// any parents of the nodes that are newly implicated as corresponding.
// In the process of traversing from the last matched nodes to these
// ones, all nodes "in between" in the target document, at any level,
// are removed, and all nodes "in between" in the source document
// are copied over to their appropriate positions.
//
// For example, if match() is called only once, and then finish()
// is called, the effect is to preserve tgtNode, its children,
// and its ancestors (parent chain), while swapping out all its
// siblings and the siblings of its ancestors, so that the target
// tree is mutated to look like the source tree did.
//
// The caller is responsible for ensuring the precondition that
// subsequent tgtNodes and subsequent srcNodes are strictly "in order."
// The ordering referred to here is a partial order in which A comes
// before B if their tags would be disjoint in HTML, i.e. the end of
// A comes before the beginning of B.  Put another way, there is some
// ancestor of A and some ancestor of B that have the same parent,
// are different, and are in order.
//
// There are other requirements for two nodes to be "matched,"
// but match() can detect them and exit gracefully returning false.
// For example, the tag-names must be the same, and the tag-names
// of their parents.  More subtly, it may be impossible to match
// the parents of tgtNode or srcNode because they have been
// previously matched.  If we are to match a series of P tags
// that are each inside one DIV, for example, is it the same DIV
// or not?  If the source and target disagree, we will have to
// reparent one of the Ps.  Users should not be moving identified
// nodes, but we want to still be correct (fall back on replacement)
// if they do.
//
// If false is returned, the match was impossible, but patching
// can continue and will still be otherwise correct.  The next call
// to match() must still obey the order constraint, as the patcher
// internally only moves forwards and patches as it goes.
//
// copyCallback is called on every new matched (tgt, src) pair
// right after copying attributes.  It's a good time to transplant
// liveranges and patch children.
Spark._Patcher.prototype.match = function(
  tgtNode, srcNode, copyCallback, onlyAdvance) {

  // last nodes "kept" (matched/identified with each other)
  var lastKeptTgt = this.lastKeptTgtNode;
  var lastKeptSrc = this.lastKeptSrcNode;
  // nodes to match and keep, this time around
  var tgt = tgtNode;
  var src = srcNode;

  if ((! tgt) != (! src)) {
    return false; // truthinesses don't match
  }

  var starting = ! lastKeptTgt;
  var finishing = ! tgt;

  if (! starting) {
    // move lastKeptTgt/lastKeptSrc forward and out,
    // until they are siblings of tgt/src or of an ancestor of tgt/src,
    // replacing as we go.  If tgt/src is falsy, we make it to the
    // top level.
    while (lastKeptTgt.parentNode !== this.tgtParent &&
           ! (tgt && DomUtils.elementContains(lastKeptTgt.parentNode, tgt))) {
      // Last-kept nodes are inside parents that are not
      // parents of the newly matched nodes.  Must finish
      // replacing their contents and back out.
      this._replaceNodes(lastKeptTgt, null, lastKeptSrc, null);
      lastKeptTgt = lastKeptTgt.parentNode;
      lastKeptSrc = lastKeptSrc.parentNode;
    }

    // update instance vars; there's no going back inside these nodes
    this.lastKeptTgtNode = lastKeptTgt;
    this.lastKeptSrcNode = lastKeptSrc;

    // Make sure same number of levels of "moving up" are
    // appropriate for src as well, i.e. we aren't trying
    // to match <c> in (<c/>, <c/>)
    // after matching , or vice versa.  In other words,
    // if tag names and depths match, but identities of parents
    // are inconsistent relative to previous matches, we catch it
    // here.  In the example, lastKeptTgt would be the  node
    // on the left, which is not sibling of <c/> or of an ancestor
    // of <c/> on the right.  If the example were reversed,
    // lastKeptTgt would be the first  node, which is an
    // ancestor of <c/> on the left rather than a sibling of an
    // ancestor.
    if (! finishing &&
        (DomUtils.elementContains(lastKeptSrc, src) ||
         ! (lastKeptSrc.parentNode === this.srcParent ||
            DomUtils.elementContains(lastKeptSrc.parentNode, src)))) {
      return false;
    }
  }

  if (finishing) {
    this._replaceNodes(lastKeptTgt, null, lastKeptSrc, null,
                       this.tgtParent, this.srcParent);
  } else {
    // Compare tag names and depths to make sure we can match nodes...
    if (! onlyAdvance) {
      if (tgt.nodeName !== src.nodeName)
        return false;
    }

    // Look at tags of parents until we hit parent of last-kept,
    // which we know is ok.
    for(var a=tgt.parentNode, b=src.parentNode;
        a !== (starting ? this.tgtParent : lastKeptTgt.parentNode);
        a = a.parentNode, b = b.parentNode) {
      if (b === (starting ? this.srcParent : lastKeptSrc.parentNode))
        return false; // src is shallower, b hit top first
      if (a.nodeName !== b.nodeName)
        return false; // tag names don't match
    }
    if (b !== (starting ? this.srcParent : lastKeptSrc.parentNode)) {
      return false; // src is deeper, b didn't hit top when a did
    }

    var firstIter = true;
    // move tgt and src backwards and out, replacing as we go
    while (true) {
      if (! (firstIter && onlyAdvance)) {
        if (tgt.nodeType === 1) /* ELEMENT */
          Spark._Patcher._copyAttributes(tgt, src);
        if (copyCallback)
          copyCallback(tgt, src);
      }

      firstIter = false;

      if ((starting ? this.tgtParent : lastKeptTgt.parentNode)
          === tgt.parentNode) {
        // we've worked our way up to the same level as the last-kept nodes
        this._replaceNodes(lastKeptTgt, tgt, lastKeptSrc, src);
        break;
      } else {
        this._replaceNodes(null, tgt, null, src);
        // move up to keep (match) parents as well
        tgt = tgt.parentNode;
        src = src.parentNode;
      }
    }
  }

  this.lastKeptTgtNode = tgtNode;
  this.lastKeptSrcNode = srcNode;

  return true;
};

// After a match, skip ahead to later siblings of the last kept nodes,
// without performing any replacements.
Spark._Patcher.prototype.skipToSiblings = function(tgt, src) {
  var lastTgt = this.lastKeptTgtNode;
  var lastSrc = this.lastKeptSrcNode;

  if (! (lastTgt && lastTgt.parentNode === tgt.parentNode))
    return false;

  if (! (lastSrc && lastSrc.parentNode === src.parentNode))
    return false;

  this.lastKeptTgtNode = tgt;
  this.lastKeptSrcNode = src;

  return true;
};

// Completes patching assuming no more matches.
//
// Patchers are single-use, so no more methods can be called
// on the Patcher.
Spark._Patcher.prototype.finish = function() {
  return this.match(null, null);
};

// Replaces the siblings between tgtBefore and tgtAfter (exclusive on both
// sides) with the siblings between srcBefore and srcAfter (exclusive on both
// sides).  Falsy values indicate start or end of siblings as appropriate.
//
// Precondition: tgtBefore and tgtAfter have same parent; either may be falsy,
// but not both, unless optTgtParent is provided.  Same with srcBefore/srcAfter.
Spark._Patcher.prototype._replaceNodes = function(
  tgtBefore, tgtAfter, srcBefore, srcAfter, optTgtParent, optSrcParent)
{
  var tgtParent = optTgtParent || (tgtBefore || tgtAfter).parentNode;
  var srcParent = optSrcParent || (srcBefore || srcAfter).parentNode;

  // deal with case where top level is a range
  if (tgtParent === this.tgtParent) {
    tgtBefore = tgtBefore || this.tgtBefore;
    tgtAfter = tgtAfter || this.tgtAfter;
  }
  if (srcParent === this.srcParent) {
    srcBefore = srcBefore || this.srcBefore;
    srcAfter = srcAfter || this.srcAfter;
  }

  // remove old children
  var n;
  while ((n = tgtBefore ? tgtBefore.nextSibling : tgtParent.firstChild)
         && n !== tgtAfter) {
    tgtParent.removeChild(n);
  }

  // add new children
  var m;
  while ((m = srcBefore ? srcBefore.nextSibling : srcParent.firstChild)
         && m !== srcAfter) {
    tgtParent.insertBefore(m, tgtAfter || null);
  }
};

// Copy HTML attributes of node `src` onto node `tgt`.
//
// The effect we are trying to achieve is best expresed in terms of
// HTML.  Whatever HTML generated `tgt`, we want to mutate the DOM element
// so that it is as if it were the HTML that generated `src`.
// We want to preserve JavaScript properties in general (tgt.foo),
// while syncing the HTML attributes (tgt.getAttribute("foo")).
//
// This is complicated by form controls and the fact that old IE
// can't keep the difference straight between properties and attributes.
Spark._Patcher._copyAttributes = function(tgt, src) {
  var srcAttrs = src.attributes;
  var tgtAttrs = tgt.attributes;

  // Determine whether tgt has focus; works in all browsers
  // as of FF3, Safari4
  var target_focused = (tgt === document.activeElement);

  // Is this a control with a user-mutated "value" property?
  var has_user_value = (
    (tgt.nodeName === "INPUT" &&
     (tgt.type === "text")) ||
      tgt.nodeName === "TEXTAREA");

  ///// Clear current attributes

  if (tgt.style.cssText)
    tgt.style.cssText = '';

  var isRadio = false;
  if (tgt.nodeName === "INPUT") {
    // Record for later whether this is a radio button.
    isRadio = (tgt.type === 'radio');
    // Clearing the attributes of a checkbox won't necessarily
    // uncheck it, eg in FF12, so we uncheck explicitly
    // (if necessary; we don't want to generate spurious
    // propertychange events in old IE).
    if (tgt.checked === true && src.checked === false) {
      tgt.checked = false;
    }
  }

  for(var i=tgtAttrs.length-1; i>=0; i--) {
    var attr = tgtAttrs[i];
    // In old IE, attributes that are possible on a node
    // but not actually present will show up in this loop
    // with specified=false.  All other browsers support
    // 'specified' (because it's part of the spec) and
    // set it to true.
    if (! attr.specified)
      continue;
    var name = attr.name;
    // Filter out attributes that are indexable by number
    // but not by name.  This kills the weird "propdescname"
    // attribute in IE 8.
    if (! tgtAttrs[name])
      continue;
    // Some properties don't mutate well, and we simply
    // don't try to patch them.  For example, you can't
    // change a control's type in IE.
    if (name === "id" || name === "type")
      continue;
    // Removing a radio button's "name" property and restoring
    // it is harmless in most browsers but breaks in IE 7.
    // It seems unlikely enough that a radio button will
    // sometimes have a group and sometimes not.
    if (isRadio && name === "name")
      continue;
    // Never delete the "value" attribute.  It's more effective
    // to simply overwrite it in the next phase.
    if (name === "value")
      continue;
    // Removing 'src' (e.g. in an iframe) can only be bad.
    if (name === "src")
      continue;

    // We want to patch any HTML attributes that were specified in the
    // source, but preserve DOM properties set programmatically.
    // Old IE makes this difficult by exposing properties as attributes.
    // Expando properties will even appear in innerHTML, though not if the
    // value is an object rather than a primitive.
    //
    // We use a heuristic to determine if we are looking at a programmatic
    // property (an expando) rather than a DOM attribute.
    //
    // Losing jQuery's expando (whose value is a number) is very bad,
    // because it points to event handlers that only jQuery can detach,
    // and only if the expando is in place.
    var possibleExpando = tgt[name];
    if (possibleExpando &&
        (typeof possibleExpando === "object" ||
         /^jQuery/.test(name)))
      continue; // for object properties that surface attributes only in IE
    tgt.removeAttributeNode(attr);
  }

  ///// Copy over src's attributes

  if (tgt.mergeAttributes) {
    // IE code path:
    //
    // Only IE (all versions) has mergeAttributes.
    // It's probably a good bit faster in old IE than
    // iterating over all the attributes, and the treatment
    // of form controls is sufficiently different in IE from
    // other browsers that we keep the special cases separate.

    tgt.mergeAttributes(src);

    if (typeof tgt.checked !== "undefined" && src.checked)
      tgt.checked = src.checked;

    if (src.name)
      tgt.name = src.name;

  } else {
    // Non-IE code path:

    for(var i=0, L=srcAttrs.length; i<L; i++) {
      var srcA = srcAttrs.item(i);
      if (srcA.specified) {
        var name = srcA.name.toLowerCase();
        var value = String(srcA.value);
        if (name === "type") {
        // can't change type of INPUT in IE; don't support it
        } else if (name === "checked") {
          tgt.checked = tgt.defaultChecked = (value && value !== "false");
          tgt.setAttribute("checked", "checked");
        } else if (name === "style") {
          tgt.style.cssText = src.style.cssText;
        } else if (name === "class") {
          tgt.className = src.className;
        } else if (name === "value") {
          // don't set attribute, just overwrite property
          // (in next phase)
        } else if (name === "src") {
          // only set if different.  protects iframes
          if (src.src !== tgt.src)
            tgt.src = src.src;
        } else {
          tgt.setAttribute(name, value);
        }
      }
    }
  }

  // Copy the control's value, only if tgt doesn't have focus.
  if (has_user_value) {
    if (! target_focused)
      tgt.value = src.value;
  }

};