// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

ByteReader = function(arrayBuffer, opt_offset, opt_length) {
  opt_offset = opt_offset || 0;
  opt_length = opt_length || (arrayBuffer.byteLength - opt_offset);
  this.view_ = new DataView(arrayBuffer, opt_offset, opt_length);
  this.pos_ = 0;
  this.seekStack_ = [];
  this.setByteOrder(ByteReader.BIG_ENDIAN);
};

// Static const and methods.

ByteReader.LITTLE_ENDIAN = 0;  // Intel, 0x1234 is [0x34, 0x12]
ByteReader.BIG_ENDIAN = 1;  // Motorola, 0x1234 is [0x12, 0x34]

ByteReader.SEEK_BEG = 0;  // Seek relative to the beginning of the buffer.
ByteReader.SEEK_CUR = 1;  // Seek relative to the current position.
ByteReader.SEEK_END = 2;  // Seek relative to the end of the buffer.

/**
 * Throw an error if (0 > pos >= end) or if (pos + size > end).
 *
 * Static utility function.
 */
ByteReader.validateRead = function(pos, size, end) {
  if (pos < 0 || pos >= end)
    throw new Error('Invalid read position');

  if (pos + size > end)
    throw new Error('Read past end of buffer');
};

/**
 * Read as a sequence of characters, returning them as a single string.
 *
 * This is a static utility function.  There is a member function with the
 * same name which side-effects the current read position.
 */
ByteReader.readString = function(dataView, pos, size, opt_end) {
  ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);

  var codes = [];

  for (var i = 0; i < size; ++i)
    codes.push(dataView.getUint8(pos + i));

  return String.fromCharCode.apply(null, codes);
};

/**
 * Read as a sequence of characters, returning them as a single string.
 *
 * This is a static utility function.  There is a member function with the
 * same name which side-effects the current read position.
 */
ByteReader.readNullTerminatedString = function(dataView, pos, size, opt_end) {
  ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);

  var codes = [];

  for (var i = 0; i < size; ++i) {
    var code = dataView.getUint8(pos + i);
    if (code == 0) break;
    codes.push(code);
  }

  return String.fromCharCode.apply(null, codes);
};

/**
 * Read as a sequence of UTF16 characters, returning them as a single string.
 *
 * This is a static utility function.  There is a member function with the
 * same name which side-effects the current read position.
 */
ByteReader.readNullTerminatedStringUTF16 = function(
    dataView, pos, bom, size, opt_end) {
  ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);

  var littleEndian = false;
  var start = 0;

  if (bom) {
    littleEndian = (dataView.getUint8(pos) == 0xFF);
    start = 2;
  }

  var codes = [];

  for (var i = start; i < size; i += 2) {
    var code = dataView.getUint16(pos + i, littleEndian);
    if (code == 0) break;
    codes.push(code);
  }

  return String.fromCharCode.apply(null, codes);
};

ByteReader.base64Alphabet_ =
    ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/').
    split('');

/**
 * Read as a sequence of bytes, returning them as a single base64 encoded
 * string.
 *
 * This is a static utility function.  There is a member function with the
 * same name which side-effects the current read position.
 */
ByteReader.readBase64 = function(dataView, pos, size, opt_end) {
  ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);

  var rv = [];
  var chars = [];
  var padding = 0;

  for (var i = 0; i < size; /* incremented inside */) {
    var bits = dataView.getUint8(pos + (i++)) << 16;

    if (i < size) {
      bits |= dataView.getUint8(pos + (i++)) << 8;

      if (i < size) {
        bits |= dataView.getUint8(pos + (i++));
      } else {
        padding = 1;
      }
    } else {
      padding = 2;
    }

    chars[3] = ByteReader.base64Alphabet_[bits & 63];
    chars[2] = ByteReader.base64Alphabet_[(bits >> 6) & 63];
    chars[1] = ByteReader.base64Alphabet_[(bits >> 12) & 63];
    chars[0] = ByteReader.base64Alphabet_[(bits >> 18) & 63];

    rv.push.apply(rv, chars);
  }

  if (padding > 0)
    rv[rv.length - 1] = '=';
  if (padding > 1)
    rv[rv.length - 2] = '=';

  return rv.join('');
};

/**
 * Read as an image encoded in a data url.
 *
 * This is a static utility function.  There is a member function with the
 * same name which side-effects the current read position.
 */
ByteReader.readImage = function(dataView, pos, size, opt_end) {
  opt_end = opt_end || dataView.byteLength;
  ByteReader.validateRead(pos, size, opt_end);

  // Two bytes is enough to identify the mime type.
  var prefixToMime = {
     '\x89P' : 'png',
     '\xFF\xD8' : 'jpeg',
     'BM' : 'bmp',
     'GI' : 'gif'
  };

  var prefix = ByteReader.readString(dataView, pos, 2, opt_end);
  var mime = prefixToMime[prefix] ||
      dataView.getUint16(pos, false).toString(16);  // For debugging.

  var b64 = ByteReader.readBase64(dataView, pos, size, opt_end);
  return 'data:image/' + mime + ';base64,' + b64;
};

// Instance methods.

/**
 * Return true if the requested number of bytes can be read from the buffer.
 */
ByteReader.prototype.canRead = function(size) {
   return this.pos_ + size <= this.view_.byteLength;
},

/**
 * Return true if the current position is past the end of the buffer.
 */
ByteReader.prototype.eof = function() {
  return this.pos_ >= this.view_.byteLength;
};

/**
 * Return true if the current position is before the beginning of the buffer.
 */
ByteReader.prototype.bof = function() {
  return this.pos_ < 0;
};

/**
 * Return true if the current position is outside the buffer.
 */
ByteReader.prototype.beof = function() {
  return this.pos_ >= this.view_.byteLength || this.pos_ < 0;
};

/**
 * Set the expected byte ordering for future reads.
 */
ByteReader.prototype.setByteOrder = function(order) {
  this.littleEndian_ = order == ByteReader.LITTLE_ENDIAN;
};

/**
 * Throw an error if the reader is at an invalid position, or if a read a read
 * of |size| would put it in one.
 *
 * You may optionally pass opt_end to override what is considered to be the
 * end of the buffer.
 */
ByteReader.prototype.validateRead = function(size, opt_end) {
  if (typeof opt_end == 'undefined')
    opt_end = this.view_.byteLength;

  ByteReader.validateRead(this.view_, this.pos_, size, opt_end);
};

ByteReader.prototype.readScalar = function(width, opt_signed, opt_end) {
  var method = opt_signed ? 'getInt' : 'getUint';

  switch (width) {
    case 1:
      method += '8';
      break;

    case 2:
      method += '16';
      break;

    case 4:
      method += '32';
      break;

    case 8:
      method += '64';
      break;

    default:
      throw new Error('Invalid width: ' + width);
      break;
  }

  this.validateRead(width, opt_end);
  var rv = this.view_[method](this.pos_, this.littleEndian_);
  this.pos_ += width;
  return rv;
}

/**
 * Read as a sequence of characters, returning them as a single string.
 *
 * Adjusts the current position on success.  Throws an exception if the
 * read would go past the end of the buffer.
 */
ByteReader.prototype.readString = function(size, opt_end) {
  var rv = ByteReader.readString(this.view_, this.pos_, size, opt_end);
  this.pos_ += size;
  return rv;
};


/**
 * Read as a sequence of characters, returning them as a single string.
 *
 * Adjusts the current position on success.  Throws an exception if the
 * read would go past the end of the buffer.
 */
ByteReader.prototype.readNullTerminatedString = function(size, opt_end) {
  var rv = ByteReader.readNullTerminatedString(this.view_,
                                               this.pos_,
                                               size,
                                               opt_end);
  this.pos_ += rv.length;

  if (rv.length < size) {
    // If we've stopped reading because we found '0' but didn't hit size limit
    // then we should skip additional '0' character
    this.pos_++;
  }

  return rv;
};


/**
 * Read as a sequence of UTF16 characters, returning them as a single string.
 *
 * Adjusts the current position on success.  Throws an exception if the
 * read would go past the end of the buffer.
 */
ByteReader.prototype.readNullTerminatedStringUTF16 =
    function(bom, size, opt_end) {
  var rv = ByteReader.readNullTerminatedStringUTF16(
      this.view_, this.pos_, bom, size, opt_end);

  if (bom) {
    // If the BOM word was present advance the position.
    this.pos_ += 2;
  }

  this.pos_ += rv.length;

  if (rv.length < size) {
    // If we've stopped reading because we found '0' but didn't hit size limit
    // then we should skip additional '0' character
    this.pos_ += 2;
  }

  return rv;
};


/**
 * Read as an array of numbers.
 *
 * Adjusts the current position on success.  Throws an exception if the
 * read would go past the end of the buffer.
 */
ByteReader.prototype.readSlice = function(size, opt_end,
                                          opt_arrayConstructor) {
  this.validateRead(width, opt_end);

  var arrayConstructor = opt_arrayConstructor || Uint8Array;
  var slice = new arrayConstructor(
      this.view_.buffer, this.view_.byteOffset + this.pos, size);
  this.pos_ += size;

  return slice;
};

/**
 * Read as a sequence of bytes, returning them as a single base64 encoded
 * string.
 *
 * Adjusts the current position on success.  Throws an exception if the
 * read would go past the end of the buffer.
 */
ByteReader.prototype.readBase64 = function(size, opt_end) {
  var rv = ByteReader.readBase64(this.view_, this.pos_, size, opt_end);
  this.pos_ += size;
  return rv;
};

/**
 * Read an image returning it as a data url.
 *
 * Adjusts the current position on success.  Throws an exception if the
 * read would go past the end of the buffer.
 */
ByteReader.prototype.readImage = function(size, opt_end) {
  var rv = ByteReader.readImage(this.view_, this.pos_, size, opt_end);
  this.pos_ += size;
  return rv;
};

/**
 * Seek to a give position relative to opt_seekStart.
 */
ByteReader.prototype.seek = function(pos, opt_seekStart, opt_end) {
  opt_end = opt_end || this.view_.byteLength;

  var newPos;
  if (opt_seekStart == ByteReader.SEEK_CUR) {
    newPos = this.pos_ + pos;
  } else if (opt_seekStart == ByteReader.SEEK_END) {
    newPos = opt_end + pos;
  } else {
    newPos = pos;
  }

  if (newPos < 0 || newPos > this.view_.byteLength)
    throw new Error('Seek outside of buffer: ' + (newPos - opt_end));

  this.pos_ = newPos;
};

/**
 * Seek to a given position relative to opt_seekStart, saving the current
 * position.
 *
 * Recover the current position with a call to seekPop.
 */
ByteReader.prototype.pushSeek = function(pos, opt_seekStart) {
  var oldPos = this.pos_;
  this.seek(pos, opt_seekStart);
  // Alter the seekStack_ after the call to seek(), in case it throws.
  this.seekStack_.push(oldPos);
};

/**
 * Undo a previous seekPush.
 */
ByteReader.prototype.popSeek = function() {
  this.seek(this.seekStack_.pop());
};

/**
 * Return the current read position.
 */
ByteReader.prototype.tell = function() {
  return this.pos_;
};