text: /*
   * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
   *  
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"), 
   * to deal in the Software without restriction, including without limitation 
   * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
   * and/or sell copies of the Software, and to permit persons to whom the 
   * Software is furnished to do so, subject to the following conditions:
   *  
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *  
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
   * DEALINGS IN THE SOFTWARE.
   * 
   */
  
  
  /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4,
  maxerr: 50, browser: true */
  /*global $, define, brackets, WebSocket, ArrayBuffer, Uint32Array */
  
  define(function (require, exports, module) {
      "use strict";
      
      /**
       * Connection attempts to make before failing
       * @type {number}
       */
      var CONNECTION_ATTEMPTS = 10;
  
      /**
       * Milliseconds to wait before a particular connection attempt is considered failed.
       * NOTE: It's okay for the connection timeout to be long because the
       * expected behavior of WebSockets is to send a "close" event as soon
       * as they realize they can't connect. So, we should rarely hit the
       * connection timeout even if we try to connect to a port that isn't open.
       * @type {number}
       */
      var CONNECTION_TIMEOUT  = 10000; // 10 seconds
  
      /**
       * Milliseconds to wait before retrying connecting
       * @type {number}
       */
      var RETRY_DELAY         = 500;   // 1/2 second
  
      /**
       * Maximum value of the command ID counter
       * @type  {number}
       */
      var MAX_COUNTER_VALUE = 4294967295; // 2^32 - 1
      
      /**
       * @private
       * Helper function to auto-reject a deferred after a given amount of time.
       * If the deferred is resolved/rejected manually, then the timeout is
       * automatically cleared.
       */
      function setDeferredTimeout(deferred, delay) {
          var timer = setTimeout(function () {
              deferred.reject("timeout");
          }, delay);
          deferred.always(function () { clearTimeout(timer); });
      }
      
      /**
       * @private
       * Helper function to attempt a single connection to the node server
       */
      function attemptSingleConnect() {
          var deferred = $.Deferred();
          var port = null;
          var ws = null;
          setDeferredTimeout(deferred, CONNECTION_TIMEOUT);
          
          brackets.app.getNodeState(function (err, nodePort) {
              if (!err && nodePort && deferred.state() !== "rejected") {
                  port = nodePort;
                  ws = new WebSocket("ws://localhost:" + port);
                  
                  // Expect ArrayBuffer objects from Node when receiving binary
                  // data instead of DOM Blobs, which are the default.
                  ws.binaryType = "arraybuffer";
                  
                  // If the server port isn't open, we get a close event
                  // at some point in the future (and will not get an onopen 
                  // event)
                  ws.onclose = function () {
                      deferred.reject("WebSocket closed");
                  };
  
                  ws.onopen = function () {
                      // If we successfully opened, remove the old onclose 
                      // handler (which was present to detect failure to 
                      // connect at all).
                      ws.onclose = null;
                      deferred.resolveWith(null, [ws, port]);
                  };
              } else {
                  deferred.reject("brackets.app.getNodeState error: " + err);
              }
          });
          
          return deferred.promise();
      }
      
      /**
       * Provides an interface for interacting with the node server.
       * @constructor
       */
      function NodeConnection() {
          this.domains = {};
          this._registeredModules = [];
          this._pendingInterfaceRefreshDeferreds = [];
          this._pendingCommandDeferreds = [];
      }
      
      /**
       * @type {Object}
       * Exposes the domains registered with the server. This object will
       * have a property for each registered domain. Each of those properties
       * will be an object containing properties for all the commands in that
       * domain. So, myConnection.base.enableDebugger would point to the function
       * to call to enable the debugger.
       *
       * This object is automatically replaced every time the API changes (based
       * on the base:newDomains event from the server). Therefore, code that
       * uses this object should not keep their own pointer to the domain property.
       */
      NodeConnection.prototype.domains = null;
      
      /**
       * @private
       * @type {Array.<string>}
       * List of module pathnames that should be re-registered if there is
       * a disconnection/connection (i.e. if the server died).
       */
      NodeConnection.prototype._registeredModules = null;
  
      /**
       * @private
       * @type {WebSocket}
       * The connection to the server
       */
      NodeConnection.prototype._ws = null;
      
      /**
       * @private
       * @type {?number}
       * The port the WebSocket is currently connected to
       */
      NodeConnection.prototype._port = null;
      
      /**
       * @private
       * @type {number}
       * Unique ID for commands
       */
      NodeConnection.prototype._commandCount = 1;
      
      /**
       * @private
       * @type {boolean}
       * Whether to attempt reconnection if connection fails
       */
      NodeConnection.prototype._autoReconnect = false;
      
      /**
       * @private
       * @type {Array.<jQuery.Deferred>}
       * List of deferred objects that should be resolved pending
       * a successful refresh of the API
       */
      NodeConnection.prototype._pendingInterfaceRefreshDeferreds = null;
      
      /**
       * @private
       * @type {Array.<jQuery.Deferred>}
       * Array (indexed on command ID) of deferred objects that should be
       * resolved/rejected with the response of commands.
       */
      NodeConnection.prototype._pendingCommandDeferreds = null;
      
      /**
       * @private
       * @return {number} The next command ID to use. Always representable as an
       * unsigned 32-bit integer.
       */
      NodeConnection.prototype._getNextCommandID = function () {
          var nextID;
          
          if (this._commandCount > MAX_COUNTER_VALUE) {
              nextID = this._commandCount = 0;
          } else {
              nextID = this._commandCount++;
          }
          
          return nextID;
      };
      
      /**
       * @private
       * Helper function to do cleanup work when a connection fails
       */
      NodeConnection.prototype._cleanup = function () {
          // clear out the domains, since we may get different ones
          // on the next connection
          this.domains = {};
          
          // shut down the old connection if there is one
          if (this._ws && this._ws.readyState !== WebSocket.CLOSED) {
              try {
                  this._ws.close();
              } catch (e) { }
          }
          var failedDeferreds = this._pendingInterfaceRefreshDeferreds
              .concat(this._pendingCommandDeferreds);
          failedDeferreds.forEach(function (d) {
              d.reject("cleanup");
          });
          this._pendingInterfaceRefreshDeferreds = [];
          this._pendingCommandDeferreds = [];
          
          this._ws = null;
          this._port = null;
      };
      
      /**
       * Connect to the node server. After connecting, the NodeConnection
       * object will trigger a "close" event when the underlying socket
       * is closed. If the connection is set to autoReconnect, then the
       * event will also include a jQuery promise for the connection.
       * 
       * @param {boolean} autoReconnect Whether to automatically try to
       *    reconnect to the server if the connection succeeds and then
       *    later disconnects. Note if this connection fails initially, the
       *    autoReconnect flag is set to false. Future calls to connect()
       *    can reset it to true
       * @return {jQuery.Promise} Promise that resolves/rejects when the
       *    connection succeeds/fails
       */
      NodeConnection.prototype.connect = function (autoReconnect) {
          var self = this;
          self._autoReconnect = autoReconnect;
          var deferred = $.Deferred();
          var attemptCount = 0;
          var attemptTimestamp = null;
          
          // Called after a successful connection to do final setup steps
          function registerHandlersAndDomains(ws, port) {
              // Called if we succeed at the final setup
              function success() {
                  self._ws.onclose = function () {
                      if (self._autoReconnect) {
                          var $promise = self.connect(true);
                          $(self).triggerHandler("close", [$promise]);
                      } else {
                          self._cleanup();
                          $(self).triggerHandler("close");
                      }
                  };
                  deferred.resolve();
              }
              // Called if we fail at the final setup
              function fail(err) {
                  self._cleanup();
                  deferred.reject(err);
              }
              
              self._ws = ws;
              self._port = port;
              self._ws.onmessage = self._receive.bind(self);
              
              // refresh the current domains, then re-register any
              // "autoregister" modules
              self._refreshInterface().then(
                  function () {
                      if (self._registeredModules.length > 0) {
                          self.loadDomains(self._registeredModules, false).then(
                              success,
                              fail
                          );
                      } else {
                          success();
                      }
                  },
                  fail
              );
          }
          
          // Repeatedly tries to connect until we succeed or until we've
          // failed CONNECTION_ATTEMPT times. After each attempt, waits
          // at least RETRY_DELAY before trying again.
          function doConnect() {
              attemptCount++;
              attemptTimestamp = new Date();
              attemptSingleConnect().then(
                  registerHandlersAndDomains, // succeded
                  function () { // failed this attempt, possibly try again
                      if (attemptCount < CONNECTION_ATTEMPTS) { //try again
                          // Calculate how long we should wait before trying again
                          var now = new Date();
                          var delay = Math.max(
                              RETRY_DELAY - (now - attemptTimestamp),
                              1
                          );
                          setTimeout(doConnect, delay);
                      } else { // too many attempts, give up
                          deferred.reject("Max connection attempts reached");
                      }
                  }
              );
          }
          
          // Start the connection process
          self._cleanup();
          doConnect();
  
          return deferred.promise();
      };
  
      /**
       * Determines whether the NodeConnection is currently connected
       * @return {boolean} Whether the NodeConnection is connected.
       */
      NodeConnection.prototype.connected = function () {
          return !!(this._ws && this._ws.readyState === WebSocket.OPEN);
      };
  
      /**
       * Explicitly disconnects from the server. Note that even if
       * autoReconnect was set to true at connection time, the connection
       * will not reconnect after this call. Reconnection can be manually done
       * by calling connect() again.
       */
      NodeConnection.prototype.disconnect = function () {
          this._autoReconnect = false;
          this._cleanup();
      };
  
      /**
       * Load domains into the server by path
       * @param {Array.<string>} List of absolute paths to load
       * @param {boolean} autoReload Whether to auto-reload the domains if the server
       *    fails and restarts. Note that the reload is initiated by the
       *    client, so it will only happen after the client reconnects.
       * @return {jQuery.Promise} Promise that resolves after the load has
       *    succeeded and the new API is availale at NodeConnection.domains,
       *    or that rejects on failure. 
       */
      NodeConnection.prototype.loadDomains = function (paths, autoReload) {
          var deferred = $.Deferred();
          setDeferredTimeout(deferred, CONNECTION_TIMEOUT);
          var pathArray = paths;
          if (!Array.isArray(paths)) {
              pathArray = [paths];
          }
          
          if (autoReload) {
              Array.prototype.push.apply(this._registeredModules, pathArray);
          }
  
          if (this.domains.base && this.domains.base.loadDomainModulesFromPaths) {
              this.domains.base.loadDomainModulesFromPaths(pathArray).then(
                  function (success) { // command call succeeded
                      if (!success) {
                          // response from commmand call was "false" so we know
                          // the actual load failed.
                          deferred.reject("loadDomainModulesFromPaths failed");
                      }
                      // if the load succeeded, we wait for the API refresh to
                      // resolve the deferred.
                  },
                  function (reason) { // command call failed
                      deferred.reject("Unable to load one of the modules: " + pathArray + (reason ? ", reason: " + reason : ""));
                  }
              );
  
              this._pendingInterfaceRefreshDeferreds.push(deferred);
          } else {
              deferred.reject("this.domains.base is undefined");
          }
          
          return deferred.promise();
      };
      
      /**
       * @private
       * Sends a message over the WebSocket. Automatically JSON.stringifys
       * the message if necessary.
       * @param {Object|string} m Object to send. Must be JSON.stringify-able.
       */
      NodeConnection.prototype._send = function (m) {
          if (this.connected()) {
  
              // Convert the message to a string
              var messageString = null;
              if (typeof m === "string") {
                  messageString = m;
              } else {
                  try {
                      messageString = JSON.stringify(m);
                  } catch (stringifyError) {
                      console.error("[NodeConnection] Unable to stringify message in order to send: " + stringifyError.message);
                  }
              }
              
              // If we succeded in making a string, try to send it
              if (messageString) {
                  try {
                      this._ws.send(messageString);
                  } catch (sendError) {
                      console.error("[NodeConnection] Error sending message: " + sendError.message);
                  }
              }
          } else {
              console.error("[NodeConnection] Not connected to node, unable to send.");
          }
      };
  
      /**
       * @private
       * Handler for receiving events on the WebSocket. Parses the message
       * and dispatches it appropriately.
       * @param {WebSocket.Message} message Message object from WebSocket
       */
      NodeConnection.prototype._receive = function (message) {
          var responseDeferred = null;
          var data = message.data;
          var m;
          
          if (message.data instanceof ArrayBuffer) {
              // The first four bytes encode the command ID as an unsigned 32-bit integer
              if (data.byteLength < 4) {
                  console.error("[NodeConnection] received malformed binary message");
                  return;
              }
              
              var header = data.slice(0, 4),
                  body = data.slice(4),
                  headerView = new Uint32Array(header),
                  id = headerView[0];
              
              // Unpack the binary message into a commandResponse
              m = {
                  type: "commandResponse",
                  message: {
                      id: id,
                      response: body
                  }
              };
          } else {
              try {
                  m = JSON.parse(data);
              } catch (e) {
                  console.error("[NodeConnection] received malformed message", message, e.message);
                  return;
              }
          }
          
          switch (m.type) {
          case "event":
              var $this = $(this);
  
              if (m.message.domain === "base" && m.message.event === "newDomains") {
                  this._refreshInterface();
              }
              
              // Event type for backwards compatibility for original design: "domain.event"
              $this.triggerHandler(m.message.domain + "." + m.message.event,
                                     m.message.parameters);
  
              // Event type "domain:event"
              $this.triggerHandler(m.message.domain + ":" + m.message.event,
                                     m.message.parameters);
              break;
          case "commandResponse":
              responseDeferred = this._pendingCommandDeferreds[m.message.id];
              if (responseDeferred) {
                  responseDeferred.resolveWith(this, [m.message.response]);
                  delete this._pendingCommandDeferreds[m.message.id];
              }
              break;
          case "commandError":
              responseDeferred = this._pendingCommandDeferreds[m.message.id];
              if (responseDeferred) {
                  responseDeferred.rejectWith(
                      this,
                      [m.message.message, m.message.stack]
                  );
                  delete this._pendingCommandDeferreds[m.message.id];
              }
              break;
          case "error":
              console.error("[NodeConnection] received error: " +
                              m.message.message);
              break;
          default:
              console.error("[NodeConnection] unknown event type: " + m.type);
          }
      };
      
      /**
       * @private
       * Helper function for refreshing the interface in the "domain" property.
       * Automatically called when the connection receives a base:newDomains
       * event from the server, and also called at connection time.
       */
      NodeConnection.prototype._refreshInterface = function () {
          var deferred = $.Deferred();
          var self = this;
          
          var pendingDeferreds = this._pendingInterfaceRefreshDeferreds;
          this._pendingInterfaceRefreshDeferreds = [];
          deferred.then(
              function () {
                  pendingDeferreds.forEach(function (d) { d.resolve(); });
              },
              function (err) {
                  pendingDeferreds.forEach(function (d) { d.reject(err); });
              }
          );
          
          function refreshInterfaceCallback(spec) {
              function makeCommandFunction(domainName, commandSpec) {
                  return function () {
                      var deferred = $.Deferred();
                      var parameters = Array.prototype.slice.call(arguments, 0);
                      var id = self._getNextCommandID();
                      self._pendingCommandDeferreds[id] = deferred;
                      self._send({id: id,
                                 domain: domainName,
                                 command: commandSpec.name,
                                 parameters: parameters
                                 });
                      return deferred;
                  };
              }
              
              // TODO: Don't replace the domain object every time. Instead, merge.
              self.domains = {};
              self.domainEvents = {};
              spec.forEach(function (domainSpec) {
                  self.domains[domainSpec.domain] = {};
                  domainSpec.commands.forEach(function (commandSpec) {
                      self.domains[domainSpec.domain][commandSpec.name] =
                          makeCommandFunction(domainSpec.domain, commandSpec);
                  });
                  self.domainEvents[domainSpec.domain] = {};
                  domainSpec.events.forEach(function (eventSpec) {
                      var parameters = eventSpec.parameters;
                      self.domainEvents[domainSpec.domain][eventSpec.name] = parameters;
                  });
              });
              deferred.resolve();
          }
          
          if (this.connected()) {
              $.getJSON("http://localhost:" + this._port + "/api")
                  .done(refreshInterfaceCallback)
                  .fail(function (err) { deferred.reject(err); });
          } else {
              deferred.reject("Attempted to call _refreshInterface when not connected.");
          }
          
          return deferred.promise();
      };
      
      /**
       * @private
       * Get the default timeout value
       * @return {number} Timeout value in milliseconds
       */
      NodeConnection._getConnectionTimeout = function () {
          return CONNECTION_TIMEOUT;
      };
      
      module.exports = NodeConnection;
      
  });
  