/**
 * @fileoverview BinaryEncode defines methods for encoding Javascript values
 * into arrays of bytes compatible with the Protocol Buffer wire format.
 *
 * @author aappleby@google.com (Austin Appleby)
 */

goog.module('jspb.binary.encoder');
goog.module.declareLegacyNamespace();

const BinaryConstants = goog.require('jspb.BinaryConstants');
const asserts = goog.require('goog.asserts');
const utils = goog.require('jspb.utils');

// The maximum number of bytes to push onto `buffer_` at a time, limited to
// prevent stack overflow errors.
const MAX_PUSH = 8192;
/**
 * BinaryEncoder implements encoders for all the wire types specified in
 * https://developers.google.com/protocol-buffers/docs/encoding.
 */
class BinaryEncoder {
  constructor() {
    /** @private {!Array<number>} */
    this.buffer_ = [];
  }

  /**
   * @return {number}
   */
  length() {
    return this.buffer_.length;
  }

  /**
   * @return {!Array<number>}
   */
  end() {
    const buffer = this.buffer_;
    this.buffer_ = [];
    return buffer;
  }

  /**
   * Encodes a 64-bit integer in 32:32 split representation into its wire-format
   * varint representation and stores it in the buffer.
   * @param {number} lowBits The low 32 bits of the int.
   * @param {number} highBits The high 32 bits of the int.
   */
  writeSplitVarint64(lowBits, highBits) {
    asserts.assert(lowBits == Math.floor(lowBits));
    asserts.assert(highBits == Math.floor(highBits));
    asserts.assert((lowBits >= 0) && (lowBits < BinaryConstants.TWO_TO_32));
    asserts.assert((highBits >= 0) && (highBits < BinaryConstants.TWO_TO_32));

    // Break the binary representation into chunks of 7 bits, set the 8th bit
    // in each chunk if it's not the final chunk, and append to the result.
    while (highBits > 0 || lowBits > 127) {
      this.buffer_.push((lowBits & 0x7f) | 0x80);
      lowBits = ((lowBits >>> 7) | (highBits << 25)) >>> 0;
      highBits = highBits >>> 7;
    }
    this.buffer_.push(lowBits);
  }

  /**
   * Encodes a 64-bit integer in 32:32 split representation into its wire-format
   * fixed representation and stores it in the buffer.
   * @param {number} lowBits The low 32 bits of the int.
   * @param {number} highBits The high 32 bits of the int.
   */
  writeSplitFixed64(lowBits, highBits) {
    asserts.assert(lowBits == Math.floor(lowBits));
    asserts.assert(highBits == Math.floor(highBits));
    asserts.assert((lowBits >= 0) && (lowBits < BinaryConstants.TWO_TO_32));
    asserts.assert((highBits >= 0) && (highBits < BinaryConstants.TWO_TO_32));
    this.writeUint32(lowBits);
    this.writeUint32(highBits);
  }

  /**
   * Encodes a 64-bit integer in 32:32 split representation into its wire-format
   * a zigzag varint representation and stores it in the buffer.
   * @param {number} lowBits The low 32 bits of the int.
   * @param {number} highBits The high 32 bits of the int.
   */
  writeSplitZigzagVarint64(lowBits, highBits) {
    utils.toZigzag64(lowBits, highBits, (lo, hi) => {
      this.writeSplitVarint64(lo >>> 0, hi >>> 0);
    });
  }

  /**
   * Encodes a 32-bit unsigned integer into its wire-format varint
   * representation and stores it in the buffer.
   * @param {number} value The integer to convert.
   */
  writeUnsignedVarint32(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= 0) && (value < BinaryConstants.TWO_TO_32));

    while (value > 127) {
      this.buffer_.push((value & 0x7f) | 0x80);
      value = value >>> 7;
    }

    this.buffer_.push(value);
  }

  /**
   * Encodes a 32-bit signed integer into its wire-format varint representation
   * and stores it in the buffer.
   * @param {number} value The integer to convert.
   */
  writeSignedVarint32(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_31) &&
        (value < BinaryConstants.TWO_TO_31));

    // Use the unsigned version if the value is not negative.
    if (value >= 0) {
      this.writeUnsignedVarint32(value);
      return;
    }

    // Write nine bytes with a _signed_ right shift so we preserve the sign bit.
    for (let i = 0; i < 9; i++) {
      this.buffer_.push((value & 0x7f) | 0x80);
      value = value >> 7;
    }

    // The above loop writes out 63 bits, so the last byte is always the sign
    // bit which is always set for negative numbers.
    this.buffer_.push(1);
  }

  /**
   * Encodes a 64-bit unsigned integer into its wire-format varint
   * representation and stores it in the buffer. Integers that are not
   * representable in 64 bits will be truncated.
   * @param {number} value The integer to convert.
   */
  writeUnsignedVarint64(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= 0) && (value < BinaryConstants.TWO_TO_64));
    utils.splitInt64(value);
    this.writeSplitVarint64(utils.getSplit64Low(), utils.getSplit64High());
  }

  /**
   * Encodes a 64-bit signed integer into its wire-format varint representation
   * and stores it in the buffer. Integers that are not representable in 64 bits
   * will be truncated.
   * @param {number} value The integer to convert.
   */
  writeSignedVarint64(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_63) &&
        (value < BinaryConstants.TWO_TO_63));
    utils.splitInt64(value);
    this.writeSplitVarint64(utils.getSplit64Low(), utils.getSplit64High());
  }

  /**
   * Encodes a JavaScript integer into its wire-format, zigzag-encoded varint
   * representation and stores it in the buffer.
   * @param {number} value The integer to convert.
   */
  writeZigzagVarint32(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_31) &&
        (value < BinaryConstants.TWO_TO_31));
    this.writeUnsignedVarint32(utils.toZigzag32(value));
  }

  /**
   * Encodes a JavaScript integer into its wire-format, zigzag-encoded varint
   * representation and stores it in the buffer. Integers not representable in
   * 64 bits will be truncated.
   * @param {number} value The integer to convert.
   */
  writeZigzagVarint64(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_63) &&
        (value < BinaryConstants.TWO_TO_63));
    utils.splitZigzag64(value);
    this.writeSplitVarint64(utils.getSplit64Low(), utils.getSplit64High());
  }

  /**
   * Encodes a JavaScript decimal string into its wire-format, zigzag-encoded
   * varint representation and stores it in the buffer. Integers not
   * representable in 64 bits will be truncated.
   * @param {string} value The integer to convert.
   */
  writeZigzagVarint64String(value) {
    utils.splitDecimalString(value);
    utils.toZigzag64(
        utils.getSplit64Low(), utils.getSplit64High(), (lo, hi) => {
          this.writeSplitVarint64(lo >>> 0, hi >>> 0);
        });
  }

  /**
   * Writes an 8-bit unsigned integer to the buffer. Numbers outside the range
   * [0,2^8) will be truncated.
   * @param {number} value The value to write.
   */
  writeUint8(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= 0) && (value < 256));
    this.buffer_.push((value >>> 0) & 0xFF);
  }

  /**
   * Writes a 16-bit unsigned integer to the buffer. Numbers outside the
   * range [0,2^16) will be truncated.
   * @param {number} value The value to write.
   */
  writeUint16(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= 0) && (value < 65536));
    this.buffer_.push((value >>> 0) & 0xFF);
    this.buffer_.push((value >>> 8) & 0xFF);
  }

  /**
   * Writes a 32-bit unsigned integer to the buffer. Numbers outside the
   * range [0,2^32) will be truncated.
   * @param {number} value The value to write.
   */
  writeUint32(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= 0) && (value < BinaryConstants.TWO_TO_32));
    this.buffer_.push((value >>> 0) & 0xFF);
    this.buffer_.push((value >>> 8) & 0xFF);
    this.buffer_.push((value >>> 16) & 0xFF);
    this.buffer_.push((value >>> 24) & 0xFF);
  }

  /**
   * Writes a 64-bit unsigned integer to the buffer. Numbers outside the
   * range [0,2^64) will be truncated.
   * @param {number} value The value to write.
   */
  writeUint64(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= 0) && (value < BinaryConstants.TWO_TO_64));
    utils.splitUint64(value);
    this.writeUint32(utils.getSplit64Low());
    this.writeUint32(utils.getSplit64High());
  }

  /**
   * Writes an 8-bit integer to the buffer. Numbers outside the range
   * [-2^7,2^7) will be truncated.
   * @param {number} value The value to write.
   */
  writeInt8(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= -128) && (value < 128));
    this.buffer_.push((value >>> 0) & 0xFF);
  }

  /**
   * Writes a 16-bit integer to the buffer. Numbers outside the range
   * [-2^15,2^15) will be truncated.
   * @param {number} value The value to write.
   */
  writeInt16(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert((value >= -32768) && (value < 32768));
    this.buffer_.push((value >>> 0) & 0xFF);
    this.buffer_.push((value >>> 8) & 0xFF);
  }

  /**
   * Writes a 32-bit integer to the buffer. Numbers outside the range
   * [-2^31,2^31) will be truncated.
   * @param {number} value The value to write.
   */
  writeInt32(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_31) &&
        (value < BinaryConstants.TWO_TO_31));
    this.buffer_.push((value >>> 0) & 0xFF);
    this.buffer_.push((value >>> 8) & 0xFF);
    this.buffer_.push((value >>> 16) & 0xFF);
    this.buffer_.push((value >>> 24) & 0xFF);
  }

  /**
   * Writes a 64-bit integer to the buffer. Numbers outside the range
   * [-2^63,2^63) will be truncated.
   * @param {number} value The value to write.
   */
  writeInt64(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_63) &&
        (value < BinaryConstants.TWO_TO_63));
    utils.splitInt64(value);
    this.writeSplitFixed64(utils.getSplit64Low(), utils.getSplit64High());
  }

  /**
   * Writes a single-precision floating point value to the buffer. Numbers
   * requiring more than 32 bits of precision will be truncated.
   * @param {number|string} value The value to write, accepts
   *     'Infinity'/'-Infinity'/'NaN' for JSPB wire format compatibility.
   */
  writeFloat(value) {
    asserts.assert(
        // Explicitly using == to accept strings
        (value == Infinity || value == -Infinity || isNaN(value) ||
         (typeof value === 'number' &&
          (value >= -BinaryConstants.FLOAT32_MAX) &&
          (value <= BinaryConstants.FLOAT32_MAX))));
    utils.splitFloat32(value);
    this.writeUint32(utils.getSplit64Low());
  }

  /**
   * Writes a double-precision floating point value to the buffer. As this is
   * the native format used by JavaScript, no precision will be lost.
   * @param {number|string} value The value to write, accepts
   *     'Infinity'/'-Infinity'/'NaN' for JSPB wire format compatibility.
   */
  writeDouble(value) {
    asserts.assert(
        typeof value === 'number' || value === 'Infinity' ||
        value === '-Infinity' || value === 'NaN');
    utils.splitFloat64(value);
    this.writeUint32(utils.getSplit64Low());
    this.writeUint32(utils.getSplit64High());
  }

  /**
   * Writes a boolean value to the buffer as a varint. We allow numbers as input
   * because the JSPB code generator uses 0/1 instead of true/false to save
   * space in the string representation of the proto.
   * @param {boolean|number} value The value to write.
   */
  writeBool(value) {
    asserts.assert(typeof value === 'boolean' || typeof value === 'number');
    this.buffer_.push(value ? 1 : 0);
  }

  /**
   * Writes an enum value to the buffer as a varint.
   * @param {number} value The value to write.
   */
  writeEnum(value) {
    asserts.assert(value == Math.floor(value));
    asserts.assert(
        (value >= -BinaryConstants.TWO_TO_31) &&
        (value < BinaryConstants.TWO_TO_31));
    this.writeSignedVarint32(value);
  }

  /**
   * Writes a byte array to our buffer.
   * @param {!Uint8Array} bytes The array of bytes to write.
   */
  writeBytes(bytes) {
    // avoid a stackoverflow on large arrays.
    while (bytes.length > MAX_PUSH) {
      Array.prototype.push.apply(this.buffer_, bytes.subarray(0, MAX_PUSH));
      bytes = bytes.subarray(MAX_PUSH);
    }
    Array.prototype.push.apply(this.buffer_, bytes);
  }
}

exports = {BinaryEncoder};
