/*
 *
 * Copyright 2014, Google Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

var _ = require('underscore');

var capitalize = require('underscore.string/capitalize');
var decapitalize = require('underscore.string/decapitalize');

var client = require('./client.js');

var common = require('./common.js');

var EventEmitter = require('events').EventEmitter;

var stream = require('stream');

var Readable = stream.Readable;
var Writable = stream.Writable;
var Duplex = stream.Duplex;
var util = require('util');


function forwardEvent(fromEmitter, toEmitter, event) {
  fromEmitter.on(event, function forward() {
    _.partial(toEmitter.emit, event).apply(toEmitter, arguments);
  });
}

util.inherits(ClientReadableObjectStream, Readable);

/**
 * Class for representing a gRPC server streaming call as a Node stream on the
 * client side. Extends from stream.Readable.
 * @constructor
 * @param {stream} stream Underlying binary Duplex stream for the call
 */
function ClientReadableObjectStream(stream) {
  var options = {objectMode: true};
  Readable.call(this, options);
  this._stream = stream;
  var self = this;
  forwardEvent(stream, this, 'status');
  forwardEvent(stream, this, 'metadata');
  this._stream.on('data', function forwardData(chunk) {
    if (!self.push(chunk)) {
      self._stream.pause();
    }
  });
  this._stream.pause();
}

/**
 * _read implementation for both types of streams that allow reading.
 * @this {ClientReadableObjectStream}
 * @param {number} size Ignored
 */
function _read(size) {
  this._stream.resume();
}

/**
 * See docs for _read
 */
ClientReadableObjectStream.prototype._read = _read;

util.inherits(ClientWritableObjectStream, Writable);

/**
 * Class for representing a gRPC client streaming call as a Node stream on the
 * client side. Extends from stream.Writable.
 * @constructor
 * @param {stream} stream Underlying binary Duplex stream for the call
 */
function ClientWritableObjectStream(stream) {
  var options = {objectMode: true};
  Writable.call(this, options);
  this._stream = stream;
  forwardEvent(stream, this, 'status');
  forwardEvent(stream, this, 'metadata');
  this.on('finish', function() {
    this._stream.end();
  });
}

/**
 * _write implementation for both types of streams that allow writing
 * @this {ClientWritableObjectStream}
 * @param {*} chunk The value to write to the stream
 * @param {string} encoding Ignored
 * @param {function(Error)} callback Callback to call when finished writing
 */
function _write(chunk, encoding, callback) {
  this._stream.write(chunk, encoding, callback);
}

/**
 * See docs for _write
 */
ClientWritableObjectStream.prototype._write = _write;

/**
 * Cancel the underlying call
 */
function cancel() {
  this._stream.cancel();
}

ClientReadableObjectStream.prototype.cancel = cancel;
ClientWritableObjectStream.prototype.cancel = cancel;

/**
 * Get a function that can make unary requests to the specified method.
 * @param {string} method The name of the method to request
 * @param {function(*):Buffer} serialize The serialization function for inputs
 * @param {function(Buffer)} deserialize The deserialization function for
 *     outputs
 * @return {Function} makeUnaryRequest
 */
function makeUnaryRequestFunction(method, serialize, deserialize) {
  /**
   * Make a unary request with this method on the given channel with the given
   * argument, callback, etc.
   * @this {SurfaceClient} Client object. Must have a channel member.
   * @param {*} argument The argument to the call. Should be serializable with
   *     serialize
   * @param {function(?Error, value=)} callback The callback to for when the
   *     response is received
   * @param {array=} metadata Array of metadata key/value pairs to add to the
   *     call
   * @param {(number|Date)=} deadline The deadline for processing this request.
   *     Defaults to infinite future
   * @return {EventEmitter} An event emitter for stream related events
   */
  function makeUnaryRequest(argument, callback, metadata, deadline) {
    var stream = client.makeRequest(this.channel, method, serialize,
                                    deserialize, metadata, deadline);
    var emitter = new EventEmitter();
    emitter.cancel = function cancel() {
      stream.cancel();
    };
    forwardEvent(stream, emitter, 'status');
    forwardEvent(stream, emitter, 'metadata');
    stream.write(argument);
    stream.end();
    stream.on('data', function forwardData(chunk) {
      try {
        callback(null, chunk);
      } catch (e) {
        callback(e);
      }
    });
    stream.on('status', function forwardStatus(status) {
      if (status.code !== client.status.OK) {
        callback(status);
      }
    });
    return emitter;
  }
  return makeUnaryRequest;
}

/**
 * Get a function that can make client stream requests to the specified method.
 * @param {string} method The name of the method to request
 * @param {function(*):Buffer} serialize The serialization function for inputs
 * @param {function(Buffer)} deserialize The deserialization function for
 *     outputs
 * @return {Function} makeClientStreamRequest
 */
function makeClientStreamRequestFunction(method, serialize, deserialize) {
  /**
   * Make a client stream request with this method on the given channel with the
   * given callback, etc.
   * @this {SurfaceClient} Client object. Must have a channel member.
   * @param {function(?Error, value=)} callback The callback to for when the
   *     response is received
   * @param {array=} metadata Array of metadata key/value pairs to add to the
   *     call
   * @param {(number|Date)=} deadline The deadline for processing this request.
   *     Defaults to infinite future
   * @return {EventEmitter} An event emitter for stream related events
   */
  function makeClientStreamRequest(callback, metadata, deadline) {
    var stream = client.makeRequest(this.channel, method, serialize,
                                    deserialize, metadata, deadline);
    var obj_stream = new ClientWritableObjectStream(stream);
    stream.on('data', function forwardData(chunk) {
      try {
        callback(null, chunk);
      } catch (e) {
        callback(e);
      }
    });
    stream.on('status', function forwardStatus(status) {
      if (status.code !== client.status.OK) {
        callback(status);
      }
    });
    return obj_stream;
  }
  return makeClientStreamRequest;
}

/**
 * Get a function that can make server stream requests to the specified method.
 * @param {string} method The name of the method to request
 * @param {function(*):Buffer} serialize The serialization function for inputs
 * @param {function(Buffer)} deserialize The deserialization function for
 *     outputs
 * @return {Function} makeServerStreamRequest
 */
function makeServerStreamRequestFunction(method, serialize, deserialize) {
  /**
   * Make a server stream request with this method on the given channel with the
   * given argument, etc.
   * @this {SurfaceClient} Client object. Must have a channel member.
   * @param {*} argument The argument to the call. Should be serializable with
   *     serialize
   * @param {array=} metadata Array of metadata key/value pairs to add to the
   *     call
   * @param {(number|Date)=} deadline The deadline for processing this request.
   *     Defaults to infinite future
   * @return {EventEmitter} An event emitter for stream related events
   */
  function makeServerStreamRequest(argument, metadata, deadline) {
    var stream = client.makeRequest(this.channel, method, serialize,
                                    deserialize, metadata, deadline);
    var obj_stream = new ClientReadableObjectStream(stream);
    stream.write(argument);
    stream.end();
    return obj_stream;
  }
  return makeServerStreamRequest;
}

/**
 * Get a function that can make bidirectional stream requests to the specified
 * method.
 * @param {string} method The name of the method to request
 * @param {function(*):Buffer} serialize The serialization function for inputs
 * @param {function(Buffer)} deserialize The deserialization function for
 *     outputs
 * @return {Function} makeBidiStreamRequest
 */
function makeBidiStreamRequestFunction(method, serialize, deserialize) {
  /**
   * Make a bidirectional stream request with this method on the given channel.
   * @this {SurfaceClient} Client object. Must have a channel member.
   * @param {array=} metadata Array of metadata key/value pairs to add to the
   *     call
   * @param {(number|Date)=} deadline The deadline for processing this request.
   *     Defaults to infinite future
   * @return {EventEmitter} An event emitter for stream related events
   */
  function makeBidiStreamRequest(metadata, deadline) {
    return client.makeRequest(this.channel, method, serialize,
                              deserialize, metadata, deadline);
  }
  return makeBidiStreamRequest;
}

/**
 * Map with short names for each of the requester maker functions. Used in
 * makeClientConstructor
 */
var requester_makers = {
  unary: makeUnaryRequestFunction,
  server_stream: makeServerStreamRequestFunction,
  client_stream: makeClientStreamRequestFunction,
  bidi: makeBidiStreamRequestFunction
}

/**
 * Creates a constructor for clients for the given service
 * @param {ProtoBuf.Reflect.Service} service The service to generate a client
 *     for
 * @return {function(string, Object)} New client constructor
 */
function makeClientConstructor(service) {
  var prefix = '/' + common.fullyQualifiedName(service) + '/';
  /**
   * Create a client with the given methods
   * @constructor
   * @param {string} address The address of the server to connect to
   * @param {Object} options Options to pass to the underlying channel
   */
  function SurfaceClient(address, options) {
    this.channel = new client.Channel(address, options);
  }

  _.each(service.children, function(method) {
    var method_type;
    if (method.requestStream) {
      if (method.responseStream) {
        method_type = 'bidi';
      } else {
        method_type = 'client_stream';
      }
    } else {
      if (method.responseStream) {
        method_type = 'server_stream';
      } else {
        method_type = 'unary';
      }
    }
    SurfaceClient.prototype[decapitalize(method.name)] =
        requester_makers[method_type](
            prefix + capitalize(method.name),
            common.serializeCls(method.resolvedRequestType.build()),
            common.deserializeCls(method.resolvedResponseType.build()));
  });

  SurfaceClient.service = service;

  return SurfaceClient;
}

exports.makeClientConstructor = makeClientConstructor;

/**
 * See docs for client.status
 */
exports.status = client.status;
/**
 * See docs for client.callError
 */
exports.callError = client.callError;
