// Copyright 2008 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 Protocol Buffer 2 Serializer which serializes messages
 *  into PB-Lite ("JsPbLite") format.
 *
 * PB-Lite format is an array where each index corresponds to the associated tag
 * number. For example, a message like so:
 *
 * message Foo {
 *   optional int bar = 1;
 *   optional int baz = 2;
 *   optional int bop = 4;
 * }
 *
 * would be represented as such:
 *
 * [, (bar data), (baz data), (nothing), (bop data)]
 *
 * Note that since the array index is used to represent the tag number, sparsely
 * populated messages with tag numbers that are not continuous (and/or are very
 * large) will have many (empty) spots and thus, are inefficient.
 *
 *
 */

goog.provide('goog.proto2.PbLiteSerializer');

goog.require('goog.asserts');
goog.require('goog.proto2.FieldDescriptor');
goog.require('goog.proto2.LazyDeserializer');
goog.require('goog.proto2.Serializer');



/**
 * PB-Lite serializer.
 *
 * @constructor
 * @extends {goog.proto2.LazyDeserializer}
 */
goog.proto2.PbLiteSerializer = function() {};
goog.inherits(goog.proto2.PbLiteSerializer, goog.proto2.LazyDeserializer);


/**
 * If true, fields will be serialized with 0-indexed tags (i.e., the proto
 * field with tag id 1 will have index 0 in the array).
 * @type {boolean}
 * @private
 */
goog.proto2.PbLiteSerializer.prototype.zeroIndexing_ = false;


/**
 * By default, the proto tag with id 1 will have index 1 in the serialized
 * array.
 *
 * If the serializer is set to use zero-indexing, the tag with id 1 will have
 * index 0.
 *
 * @param {boolean} zeroIndexing Whether this serializer should deal with
 *     0-indexed protos.
 */
goog.proto2.PbLiteSerializer.prototype.setZeroIndexed = function(zeroIndexing) {
  this.zeroIndexing_ = zeroIndexing;
};


/**
 * Serializes a message to a PB-Lite object.
 *
 * @param {goog.proto2.Message} message The message to be serialized.
 * @return {!Array<?>} The serialized form of the message.
 * @override
 */
goog.proto2.PbLiteSerializer.prototype.serialize = function(message) {
  var descriptor = message.getDescriptor();
  var fields = descriptor.getFields();

  var serialized = [];
  var zeroIndexing = this.zeroIndexing_;

  // Add the known fields.
  for (var i = 0; i < fields.length; i++) {
    var field = fields[i];

    if (!message.has(field)) {
      continue;
    }

    var tag = field.getTag();
    var index = zeroIndexing ? tag - 1 : tag;

    if (field.isRepeated()) {
      serialized[index] = [];

      for (var j = 0; j < message.countOf(field); j++) {
        serialized[index][j] =
            this.getSerializedValue(field, message.get(field, j));
      }
    } else {
      serialized[index] = this.getSerializedValue(field, message.get(field));
    }
  }

  // Add any unknown fields.
  message.forEachUnknown(function(tag, value) {
    var index = zeroIndexing ? tag - 1 : tag;
    serialized[index] = value;
  });

  return serialized;
};


/** @override */
goog.proto2.PbLiteSerializer.prototype.deserializeField =
    function(message, field, value) {

  if (value == null) {
    // Since value double-equals null, it may be either null or undefined.
    // Ensure we return the same one, since they have different meanings.
    // TODO(user): If the field is repeated, this method should probably
    // return [] instead of null.
    return value;
  }

  if (field.isRepeated()) {
    var data = [];

    goog.asserts.assert(goog.isArray(value), 'Value must be array: %s', value);

    for (var i = 0; i < value.length; i++) {
      data[i] = this.getDeserializedValue(field, value[i]);
    }

    return data;
  } else {
    return this.getDeserializedValue(field, value);
  }
};


/** @override */
goog.proto2.PbLiteSerializer.prototype.getSerializedValue =
    function(field, value) {
  if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.BOOL) {
    // Booleans are serialized in numeric form.
    return value ? 1 : 0;
  }

  return goog.proto2.Serializer.prototype.getSerializedValue.apply(this,
                                                                   arguments);
};


/** @override */
goog.proto2.PbLiteSerializer.prototype.getDeserializedValue =
    function(field, value) {

  if (field.getFieldType() == goog.proto2.FieldDescriptor.FieldType.BOOL) {
    goog.asserts.assert(goog.isNumber(value) || goog.isBoolean(value),
        'Value is expected to be a number or boolean');
    return !!value;
  }

  return goog.proto2.Serializer.prototype.getDeserializedValue.apply(this,
                                                                     arguments);
};


/** @override */
goog.proto2.PbLiteSerializer.prototype.deserialize =
    function(descriptor, data) {
  var toConvert = data;
  if (this.zeroIndexing_) {
    // Make the data align with tag-IDs (1-indexed) by shifting everything
    // up one.
    toConvert = [];
    for (var key in data) {
      toConvert[parseInt(key, 10) + 1] = data[key];
    }
  }
  return goog.proto2.PbLiteSerializer.base(
      this, 'deserialize', descriptor, toConvert);
};
