// Copyright 2013 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 Implementation of a WebChannel transport using WebChannelBase.
 *
 * When WebChannelBase is used as the underlying transport, the capabilities
 * of the WebChannel are limited to what's supported by the implementation.
 * Particularly, multiplexing is not possible, and only strings are
 * supported as message types.
 *
 */

goog.provide('goog.labs.net.webChannel.WebChannelBaseTransport');

goog.require('goog.asserts');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.WebChannelBase');
goog.require('goog.log');
goog.require('goog.net.WebChannel');
goog.require('goog.net.WebChannelTransport');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.string.path');



/**
 * Implementation of {@link goog.net.WebChannelTransport} with
 * {@link goog.labs.net.webChannel.WebChannelBase} as the underlying channel
 * implementation.
 *
 * @constructor
 * @struct
 * @implements {goog.net.WebChannelTransport}
 * @final
 */
goog.labs.net.webChannel.WebChannelBaseTransport = function() {
  if (!goog.labs.net.webChannel.ChannelRequest.supportsXhrStreaming()) {
    throw new Error('Environmental error: no available transport.');
  }
};


goog.scope(function() {
var WebChannelBaseTransport = goog.labs.net.webChannel.WebChannelBaseTransport;
var WebChannelBase = goog.labs.net.webChannel.WebChannelBase;


/**
 * @override
 */
WebChannelBaseTransport.prototype.createWebChannel = function(
    url, opt_options) {
  return new WebChannelBaseTransport.Channel(url, opt_options);
};



/**
 * Implementation of the {@link goog.net.WebChannel} interface.
 *
 * @param {string} url The URL path for the new WebChannel instance.
 * @param {!goog.net.WebChannel.Options=} opt_options Configuration for the
 *     new WebChannel instance.
 *
 * @constructor
 * @implements {goog.net.WebChannel}
 * @extends {goog.events.EventTarget}
 * @final
 */
WebChannelBaseTransport.Channel = function(url, opt_options) {
  WebChannelBaseTransport.Channel.base(this, 'constructor');

  /**
   * @private {!WebChannelBase} The underlying channel object.
   */
  this.channel_ = new WebChannelBase(
      opt_options, goog.net.WebChannelTransport.CLIENT_VERSION);

  /**
   * @private {string} The URL of the target server end-point.
   */
  this.url_ = url;

  /**
   * The test URL of the target server end-point. This value defaults to
   * this.url_ + '/test'.
   *
   * @private {string}
   */
  this.testUrl_ = (opt_options && opt_options.testUrl) ?
      opt_options.testUrl :
      goog.string.path.join(this.url_, 'test');

  /**
   * @private {goog.log.Logger} The logger for this class.
   */
  this.logger_ =
      goog.log.getLogger('goog.labs.net.webChannel.WebChannelBaseTransport');

  /**
   * @private {Object<string, string>} Extra URL parameters
   * to be added to each HTTP request.
   */
  this.messageUrlParams_ =
      (opt_options && opt_options.messageUrlParams) || null;

  var messageHeaders = (opt_options && opt_options.messageHeaders) || null;

  // default is false
  if (opt_options && opt_options.clientProtocolHeaderRequired) {
    if (messageHeaders) {
      goog.object.set(
          messageHeaders, goog.net.WebChannel.X_CLIENT_PROTOCOL,
          goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL);
    } else {
      messageHeaders = goog.object.create(
          goog.net.WebChannel.X_CLIENT_PROTOCOL,
          goog.net.WebChannel.X_CLIENT_PROTOCOL_WEB_CHANNEL);
    }
  }

  this.channel_.setExtraHeaders(messageHeaders);

  var initHeaders = (opt_options && opt_options.initMessageHeaders) || null;

  if (opt_options && opt_options.messageContentType) {
    if (initHeaders) {
      goog.object.set(
          initHeaders, goog.net.WebChannel.X_WEBCHANNEL_CONTENT_TYPE,
          opt_options.messageContentType);
    } else {
      initHeaders = goog.object.create(
          goog.net.WebChannel.X_WEBCHANNEL_CONTENT_TYPE,
          opt_options.messageContentType);
    }
  }

  if (opt_options && opt_options.clientProfile) {
    if (initHeaders) {
      goog.object.set(
          initHeaders, goog.net.WebChannel.X_WEBCHANNEL_CLIENT_PROFILE,
          opt_options.clientProfile);
    } else {
      initHeaders = goog.object.create(
          goog.net.WebChannel.X_WEBCHANNEL_CLIENT_PROFILE,
          opt_options.clientProfile);
    }
  }

  this.channel_.setInitHeaders(initHeaders);

  var httpHeadersOverwriteParam =
      opt_options && opt_options.httpHeadersOverwriteParam;
  if (httpHeadersOverwriteParam &&
      !goog.string.isEmptyOrWhitespace(httpHeadersOverwriteParam)) {
    this.channel_.setHttpHeadersOverwriteParam(httpHeadersOverwriteParam);
  }

  /**
   * @private {boolean} Whether to enable CORS.
   */
  this.supportsCrossDomainXhr_ =
      (opt_options && opt_options.supportsCrossDomainXhr) || false;

  /**
   * @private {boolean} Whether to send raw Json and bypass v8 wire format.
   */
  this.sendRawJson_ = (opt_options && opt_options.sendRawJson) || false;

  // Note that httpSessionIdParam will be ignored if the same parameter name
  // has already been specified with messageUrlParams
  var httpSessionIdParam = opt_options && opt_options.httpSessionIdParam;
  if (httpSessionIdParam &&
      !goog.string.isEmptyOrWhitespace(httpSessionIdParam)) {
    this.channel_.setHttpSessionIdParam(httpSessionIdParam);
    if (goog.object.containsKey(this.messageUrlParams_, httpSessionIdParam)) {
      goog.object.remove(this.messageUrlParams_, httpSessionIdParam);
      goog.log.warning(this.logger_,
          'Ignore httpSessionIdParam also specified with messageUrlParams: '
          + httpSessionIdParam);
    }
  }

  /**
   * The channel handler.
   *
   * @private {!WebChannelBaseTransport.Channel.Handler_}
   */
  this.channelHandler_ = new WebChannelBaseTransport.Channel.Handler_(this);
};
goog.inherits(WebChannelBaseTransport.Channel, goog.events.EventTarget);


/**
 * @override
 * @suppress {checkTypes}
 */
WebChannelBaseTransport.Channel.prototype.addEventListener = function(
    type, handler, /** boolean= */ opt_capture, opt_handlerScope) {
  WebChannelBaseTransport.Channel.base(
      this, 'addEventListener', type, handler, opt_capture, opt_handlerScope);
};


/**
 * @override
 * @suppress {checkTypes}
 */
WebChannelBaseTransport.Channel.prototype.removeEventListener = function(
    type, handler, /** boolean= */ opt_capture, opt_handlerScope) {
  WebChannelBaseTransport.Channel.base(
      this, 'removeEventListener', type, handler, opt_capture,
      opt_handlerScope);
};


/**
 * Test path is always set to "/url/test".
 *
 * @override
 */
WebChannelBaseTransport.Channel.prototype.open = function() {
  this.channel_.setHandler(this.channelHandler_);
  if (this.supportsCrossDomainXhr_) {
    this.channel_.setSupportsCrossDomainXhrs(true);
  }
  this.channel_.connect(
      this.testUrl_, this.url_, (this.messageUrlParams_ || undefined));
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.prototype.close = function() {
  this.channel_.disconnect();
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.prototype.halfClose = function() {
  // to be implemented
  throw new Error('Not implemented');
};


/**
 * The WebChannelBase only supports object types.
 *
 * @param {!goog.net.WebChannel.MessageData} message The message to send.
 *
 * @override
 */
WebChannelBaseTransport.Channel.prototype.send = function(message) {
  goog.asserts.assert(
      goog.isObject(message) || goog.isString(message),
      'only object type or raw string is supported');

  if (goog.isString(message)) {
    var rawJson = {};
    rawJson['__data__'] = message;
    this.channel_.sendMap(rawJson);
  } else if (this.sendRawJson_) {
    var rawJson = {};
    rawJson['__data__'] = goog.json.serialize(message);
    this.channel_.sendMap(rawJson);
  } else {
    this.channel_.sendMap(message);
  }
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.prototype.disposeInternal = function() {
  this.channel_.setHandler(null);
  delete this.channelHandler_;
  this.channel_.disconnect();
  delete this.channel_;

  WebChannelBaseTransport.Channel.base(this, 'disposeInternal');
};



/**
 * The message event.
 *
 * @param {!Array<?>|!Object} array The data array from the underlying channel.
 * @constructor
 * @extends {goog.net.WebChannel.MessageEvent}
 * @final
 */
WebChannelBaseTransport.Channel.MessageEvent = function(array) {
  WebChannelBaseTransport.Channel.MessageEvent.base(this, 'constructor');

  // single-metadata only
  var metadata = array['__sm__'];
  if (metadata) {
    this.metadataKey = goog.object.getAnyKey(metadata);
    if (this.metadataKey) {
      this.data = goog.object.get(metadata, this.metadataKey);
    } else {
      this.data = metadata;  // empty
    }
  } else {
    this.data = array;
  }
};
goog.inherits(
    WebChannelBaseTransport.Channel.MessageEvent,
    goog.net.WebChannel.MessageEvent);



/**
 * The error event.
 *
 * @param {WebChannelBase.Error} error The error code.
 * @constructor
 * @extends {goog.net.WebChannel.ErrorEvent}
 * @final
 */
WebChannelBaseTransport.Channel.ErrorEvent = function(error) {
  WebChannelBaseTransport.Channel.ErrorEvent.base(this, 'constructor');

  /**
   * High-level status code.
   */
  this.status = goog.net.WebChannel.ErrorStatus.NETWORK_ERROR;

  /**
   * @const {WebChannelBase.Error} Internal error code, for debugging use only.
   */
  this.errorCode = error;
};
goog.inherits(
    WebChannelBaseTransport.Channel.ErrorEvent, goog.net.WebChannel.ErrorEvent);



/**
 * Implementation of {@link WebChannelBase.Handler} interface.
 *
 * @param {!WebChannelBaseTransport.Channel} channel The enclosing WebChannel.
 *
 * @constructor
 * @extends {WebChannelBase.Handler}
 * @private
 */
WebChannelBaseTransport.Channel.Handler_ = function(channel) {
  WebChannelBaseTransport.Channel.Handler_.base(this, 'constructor');

  /**
   * @type {!WebChannelBaseTransport.Channel}
   * @private
   */
  this.channel_ = channel;
};
goog.inherits(WebChannelBaseTransport.Channel.Handler_, WebChannelBase.Handler);


/**
 * @override
 */
WebChannelBaseTransport.Channel.Handler_.prototype.channelOpened = function(
    channel) {
  goog.log.info(
      this.channel_.logger_, 'WebChannel opened on ' + this.channel_.url_);
  this.channel_.dispatchEvent(goog.net.WebChannel.EventType.OPEN);
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.Handler_.prototype.channelHandleArray =
    function(channel, array) {
  goog.asserts.assert(array, 'array expected to be defined');
  this.channel_.dispatchEvent(
      new WebChannelBaseTransport.Channel.MessageEvent(array));
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.Handler_.prototype.channelError = function(
    channel, error) {
  goog.log.info(
      this.channel_.logger_, 'WebChannel aborted on ' + this.channel_.url_ +
          ' due to channel error: ' + error);
  this.channel_.dispatchEvent(
      new WebChannelBaseTransport.Channel.ErrorEvent(error));
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.Handler_.prototype.channelClosed = function(
    channel, opt_pendingMaps, opt_undeliveredMaps) {
  goog.log.info(
      this.channel_.logger_, 'WebChannel closed on ' + this.channel_.url_);
  this.channel_.dispatchEvent(goog.net.WebChannel.EventType.CLOSE);
};


/**
 * @override
 */
WebChannelBaseTransport.Channel.prototype.getRuntimeProperties = function() {
  return new WebChannelBaseTransport.ChannelProperties(this.channel_);
};



/**
 * Implementation of the {@link goog.net.WebChannel.RuntimeProperties}.
 *
 * @param {!WebChannelBase} channel The underlying channel object.
 *
 * @constructor
 * @implements {goog.net.WebChannel.RuntimeProperties}
 * @final
 */
WebChannelBaseTransport.ChannelProperties = function(channel) {
  /**
   * The underlying channel object.
   *
   * @private {!WebChannelBase}
   */
  this.channel_ = channel;

};


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.getConcurrentRequestLimit =
    function() {
  return this.channel_.getForwardChannelRequestPool().getMaxSize();
};


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.isSpdyEnabled = function() {
  return this.getConcurrentRequestLimit() > 1;
};


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.getPendingRequestCount =
    function() {
  return this.channel_.getForwardChannelRequestPool().getRequestCount();
};


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.getHttpSessionId =
    function() {
  return this.channel_.getHttpSessionId();
};


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.commit =
    goog.abstractMethod;


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.getNonAckedMessageCount =
    goog.abstractMethod;


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.notifyNonAckedMessageCount =
    goog.abstractMethod;


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.onCommit =
    goog.abstractMethod;


/**
 * @override
 */
WebChannelBaseTransport.ChannelProperties.prototype.ackCommit =
    goog.abstractMethod;


/** @override */
WebChannelBaseTransport.ChannelProperties.prototype.getLastStatusCode =
    function() {
  return this.channel_.getLastStatusCode();
};
});  // goog.scope
