// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview The central node of a {@link goog.messaging.PortNetwork}. The
 * operator is responsible for providing the two-way communication channels (via
 * {@link MessageChannel}s) between each pair of nodes in the network that need
 * to communicate with one another. Each network should have one and only one
 * operator.
 *
 */

goog.provide('goog.messaging.PortOperator');

goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.log');
goog.require('goog.messaging.PortChannel');
goog.require('goog.messaging.PortNetwork'); // interface
goog.require('goog.object');



/**
 * The central node of a PortNetwork.
 *
 * @param {string} name The name of this node.
 * @constructor
 * @extends {goog.Disposable}
 * @implements {goog.messaging.PortNetwork}
 * @final
 */
goog.messaging.PortOperator = function(name) {
  goog.messaging.PortOperator.base(this, 'constructor');

  /**
   * The collection of channels for communicating with other contexts in the
   * network. These are the channels that are returned to the user, as opposed
   * to the channels used for internal network communication. This is lazily
   * populated as the user requests communication with other contexts, or other
   * contexts request communication with the operator.
   *
   * @type {!Object<!goog.messaging.PortChannel>}
   * @private
   */
  this.connections_ = {};

  /**
   * The collection of channels for internal network communication with other
   * contexts. This is not lazily populated, and always contains entries for
   * each member of the network.
   *
   * @type {!Object<!goog.messaging.MessageChannel>}
   * @private
   */
  this.switchboard_ = {};

  /**
   * The name of the operator context.
   *
   * @type {string}
   * @private
   */
  this.name_ = name;
};
goog.inherits(goog.messaging.PortOperator, goog.Disposable);


/**
 * The logger for PortOperator.
 * @type {goog.log.Logger}
 * @private
 */
goog.messaging.PortOperator.prototype.logger_ =
    goog.log.getLogger('goog.messaging.PortOperator');


/** @override */
goog.messaging.PortOperator.prototype.dial = function(name) {
  this.connectSelfToPort_(name);
  return this.connections_[name];
};


/**
 * Adds a caller to the network with the given name. This port should have no
 * services registered on it. It will be disposed along with the PortOperator.
 *
 * @param {string} name The name of the port to add.
 * @param {!goog.messaging.MessageChannel} port The port to add. Must be either
 *     a {@link goog.messaging.PortChannel} or a decorator wrapping a
 *     PortChannel; in particular, it must be able to send and receive
 *     {@link MessagePort}s.
 */
goog.messaging.PortOperator.prototype.addPort = function(name, port) {
  this.switchboard_[name] = port;
  port.registerService(goog.messaging.PortNetwork.REQUEST_CONNECTION_SERVICE,
                       goog.bind(this.requestConnection_, this, name));
};


/**
 * Connects two contexts by creating a {@link MessageChannel} and sending one
 * end to one context and the other end to the other. Called when we receive a
 * request from a caller to connect it to another context (including potentially
 * the operator).
 *
 * @param {string} sourceName The name of the context requesting the connection.
 * @param {!Object|string} message The name of the context to which
 *     the connection is requested.
 * @private
 */
goog.messaging.PortOperator.prototype.requestConnection_ = function(
    sourceName, message) {
  var requestedName = /** @type {string} */ (message);
  if (requestedName == this.name_) {
    this.connectSelfToPort_(sourceName);
    return;
  }

  var sourceChannel = this.switchboard_[sourceName];
  var requestedChannel = this.switchboard_[requestedName];

  goog.asserts.assert(goog.isDefAndNotNull(sourceChannel));
  if (!requestedChannel) {
    var err = 'Port "' + sourceName + '" requested a connection to port "' +
        requestedName + '", which doesn\'t exist';
    goog.log.warning(this.logger_, err);
    sourceChannel.send(goog.messaging.PortNetwork.GRANT_CONNECTION_SERVICE,
                       {'success': false, 'message': err});
    return;
  }

  var messageChannel = new MessageChannel();
  sourceChannel.send(goog.messaging.PortNetwork.GRANT_CONNECTION_SERVICE, {
    'success': true,
    'name': requestedName,
    'port': messageChannel.port1
  });
  requestedChannel.send(goog.messaging.PortNetwork.GRANT_CONNECTION_SERVICE, {
    'success': true,
    'name': sourceName,
    'port': messageChannel.port2
  });
};


/**
 * Connects together the operator and a caller by creating a
 * {@link MessageChannel} and sending one end to the remote context.
 *
 * @param {string} contextName The name of the context to which to connect the
 *     operator.
 * @private
 */
goog.messaging.PortOperator.prototype.connectSelfToPort_ = function(
    contextName) {
  if (contextName in this.connections_) {
    // We've already established a connection with this port.
    return;
  }

  var contextChannel = this.switchboard_[contextName];
  if (!contextChannel) {
    throw Error('Port "' + contextName + '" doesn\'t exist');
  }

  var messageChannel = new MessageChannel();
  contextChannel.send(goog.messaging.PortNetwork.GRANT_CONNECTION_SERVICE, {
    'success': true,
    'name': this.name_,
    'port': messageChannel.port1
  });
  messageChannel.port2.start();
  this.connections_[contextName] =
      new goog.messaging.PortChannel(messageChannel.port2);
};


/** @override */
goog.messaging.PortOperator.prototype.disposeInternal = function() {
  goog.object.forEach(this.switchboard_, goog.dispose);
  goog.object.forEach(this.connections_, goog.dispose);
  delete this.switchboard_;
  delete this.connections_;
  goog.messaging.PortOperator.base(this, 'disposeInternal');
};
