/* !
 *  Copyright © 2008 Fair Oaks Labs, Inc.
 *  All rights reserved.
 */

// Utility object:  Encode / Decode C-style binary primitives to / from octet arrays
function JSPack() {
  // Module-level (private) variables
  var el;
  var bBE = false;
  var m = this;

  // Raw byte arrays
  m._DeArray = function (a, p, l) {
    return [a.slice(p, p + l)];
  };

  m._EnArray = function (a, p, l, v) {
    for (var i = 0; i < l; a[p + i] = v[i] ? v[i] : 0, i++) { }
  };

  // ASCII characters
  m._DeChar = function (a, p) {
    return String.fromCharCode(a[p]);
  };

  m._EnChar = function (a, p, v) {
    a[p] = v.charCodeAt(0);
  };

  // Little-endian (un)signed N-byte integers
  m._DeInt = function (a, p) {
    var lsb = bBE ? (el.len - 1) : 0;
    var nsb = bBE ? -1 : 1;
    var stop = lsb + nsb * el.len;
    var rv, i, f;
    for (rv = 0, i = lsb, f = 1; i != stop; rv += (a[p + i] * f), i += nsb, f *= 256) { }
    if (el.bSigned && (rv & Math.pow(2, el.len * 8 - 1))) { rv -= Math.pow(2, el.len * 8); }
    return rv;
  };

  m._EnInt = function (a, p, v) {
    var lsb = bBE ? (el.len - 1) : 0;
    var nsb = bBE ? -1 : 1;
    var stop = lsb + nsb * el.len;
    var i;
    v = (v < el.min) ? el.min : (v > el.max) ? el.max : v;
    for (i = lsb; i != stop; a[p + i] = v & 0xff, i += nsb, v >>= 8) { }
  };

  // ASCII character strings
  m._DeString = function (a, p, l) {
    for (var rv = new Array(l), i = 0; i < l; rv[i] = String.fromCharCode(a[p + i]), i++) { }
    return rv.join('');
  };
  m._EnString = function (a, p, l, v) {

    for (var t, i = 0; i < l; a[p + i] = (t = v.charCodeAt(i)) ? t : 0, i++) { }
  };

  // Little-endian N-bit IEEE 754 floating point
  m._De754 = function (a, p) {
    var s, e, m, i, d, nBits, mLen, eLen, eBias, eMax;
    mLen = el.mLen, eLen = el.len * 8 - el.mLen - 1, eMax = (1 << eLen) - 1, eBias = eMax >> 1;

    i = bBE ? 0 : (el.len - 1); d = bBE ? 1 : -1; s = a[p + i]; i += d; nBits = -7;
    for (e = s & ((1 << (-nBits)) - 1), s >>= (-nBits), nBits += eLen;
     nBits > 0; e = e * 256 + a[p + i], i += d, nBits -= 8) { }
    for (m = e & ((1 << (-nBits)) - 1), e >>= (-nBits), nBits += mLen; nBits > 0;
      m = m * 256 + a[p + i], i += d, nBits -= 8) { }

    switch (e) {

      case 0:
        // Zero, or denormalized number
        e = 1 - eBias;
        break;
      case eMax:
        // NaN, or +/-Infinity
        return m ? NaN : ((s ? -1 : 1) * Infinity);
      default:
        // Normalized number
        m = m + Math.pow(2, mLen);
        e = e - eBias;
        break;
    }
    return (s ? -1 : 1) * m * Math.pow(2, e - mLen);
  };
  m._En754 = function (a, p, v) {

    var s, e, m, i, d, c, mLen, eLen, eBias, eMax;
    mLen = el.mLen, eLen = el.len * 8 - el.mLen - 1, eMax = (1 << eLen) - 1, eBias = eMax >> 1;

    s = v < 0 ? 1 : 0;
    v = Math.abs(v);
    if (isNaN(v) ||  (v  ==  Infinity)) {

      m = isNaN(v) ? 1 : 0;
      e = eMax;
    }
    else {
      e = Math.floor(Math.log(v) / Math.LN2);     // Calculate log2 of the value
      if (v * (c = Math.pow(2, -e)) < 1) { e--; c *= 2; }   // Math.log() isn't 100% reliable

      // Round by adding 1 / 2 the significand's LSD
      if (e + eBias >= 1) {
        // Normalized:  mLen significand digits
        v += el.rt / c;
      }
      else {
        // Denormalized:  <= mLen significand digits
        v += el.rt * Math.pow(2, 1 - eBias);
      }
      if (v * c >= 2) {
        // Rounding can increment the exponent
        e++; c /= 2;
      }

      if (e + eBias >= eMax) {
        // Overflow
        m = 0;
        e = eMax;
      }
      else if (e + eBias >= 1) {

        // Normalized - term order matters, as Math.pow(2, 52 - e) and v * Math.pow(2, 52) can overflow
        m = (v * c - 1) * Math.pow(2, mLen);
        e = e + eBias;
      }
      else {

        // Denormalized - also catches the '0' case, somewhat by chance
        m = v * Math.pow(2, eBias - 1) * Math.pow(2, mLen);
        e = 0;
      }
    }

    for (i = bBE ? (el.len - 1) : 0, d = bBE ? -1 : 1; mLen >= 8; a[p + i] = m & 0xff, i += d, m /= 256, mLen -= 8) { }
    for (e = (e << mLen) | m, eLen += mLen; eLen > 0; a[p + i] = e & 0xff, i += d, e /= 256, eLen -= 8) { }
    a[p + i - d] |= s * 128;
  };

  // Convert int64 to array with 3 elements: [lowBits, highBits, unsignedFlag]
  // '>>>' trick to convert signed 32bit int to unsigned int (because << always results in a signed 32bit int)
  m._DeInt64 = function (a, p) {
    var start = bBE ? 0 : 7;
    var nsb = bBE ? 1 : -1;
    var stop = start + nsb * 8;
    var rv = [0, 0, !el.bSigned];
    var i, f, rvi;
    for (i = start, rvi = 1, f = 0;
      i != stop;
      rv[rvi] = (((rv[rvi] << 8) >>> 0) + a[p + i]), i += nsb, f++, rvi = (f < 4 ? 1 : 0)) {}
    return rv;
  };

  m._EnInt64 = function (a, p, v) {
    var start = bBE ? 0 : 7;
    var nsb = bBE ? 1 : -1;
    var stop = start + nsb * 8;
    var i, f, rvi, s;
    for (i = start, rvi = 1, f = 0, s = 24;
      i != stop;
      a[p + i] = v[rvi] >> s & 0xff, i += nsb, f++, rvi = (f < 4 ? 1 : 0), s = 24 - (8 * (f % 4))) { }
  };

  // Class data
  m._sPattern = '(\\d + )?([AxcbBhHsfdiIlLqQ])';
  m._lenLut = {'A': 1, 'x': 1, 'c': 1, 'b': 1, 'B': 1, 'h': 2, 'H': 2, 's': 1,
    'f': 4, 'd': 8, 'i': 4, 'I': 4, 'l': 4, 'L': 4, 'q': 8, 'Q': 8
  };
  m._elLut  = {'A': {en: m._EnArray, de: m._DeArray},
        's': {en: m._EnString, de: m._DeString},
        'c': {en: m._EnChar, de: m._DeChar},
        'b': {en: m._EnInt, de: m._DeInt, len: 1, bSigned: true, min: -Math.pow(2, 7), max: Math.pow(2, 7) - 1},
        'B': {en: m._EnInt, de: m._DeInt, len: 1, bSigned: false, min: 0, max: Math.pow(2, 8) - 1},
        'h': {en: m._EnInt, de: m._DeInt, len: 2, bSigned: true, min: -Math.pow(2, 15), max: Math.pow(2, 15) - 1},
        'H': {en: m._EnInt, de: m._DeInt, len: 2, bSigned: false, min: 0, max: Math.pow(2, 16) - 1},
        'i': {en: m._EnInt, de: m._DeInt, len: 4, bSigned: true, min: -Math.pow(2, 31), max: Math.pow(2, 31) - 1},
        'I': {en: m._EnInt, de: m._DeInt, len: 4, bSigned: false, min: 0, max: Math.pow(2, 32) - 1},
        'l': {en: m._EnInt, de: m._DeInt, len: 4, bSigned: true, min: -Math.pow(2, 31), max: Math.pow(2, 31) - 1},
        'L': {en: m._EnInt, de: m._DeInt, len: 4, bSigned: false, min: 0, max: Math.pow(2, 32) - 1},
        'f': {en: m._En754, de: m._De754, len: 4, mLen: 23, rt: Math.pow(2, -24) - Math.pow(2, -77)},
        'd': {en: m._En754, de: m._De754, len: 8, mLen: 52, rt: 0},
        'q': {en: m._EnInt64, de: m._DeInt64, bSigned: true},
        'Q': {en: m._EnInt64, de: m._DeInt64, bSigned: false}};

  // Unpack a series of n elements of size s from array a at offset p with fxn
  m._UnpackSeries = function (n, s, a, p) {
    for (var fxn = el.de, rv = [], i = 0; i < n; rv.push(fxn(a, p + i * s)), i++) { }
    return rv;
  };

  // Pack a series of n elements of size s from array v at offset i to array a at offset p with fxn
  m._PackSeries = function (n, s, a, p, v, i) {
    for (var fxn = el.en, o = 0; o < n; fxn(a, p + o * s, v[i + o]), o++) { }
  };

  // Unpack the octet array a, beginning at offset p, according to the fmt string
  m.Unpack = function (fmt, a, p) {
    // Set the private bBE flag based on the format string - assume big-endianness
    bBE = (fmt.charAt(0) != '<');

    p = p ? p : 0;
    var re = new RegExp(this._sPattern, 'g');
    var m, n, s;
    var rv = [];
    while (m = re.exec(fmt)) {

      n = ((m[1]  == undefined) || (m[1] == '')) ? 1 : parseInt(m[1]);
      s = this._lenLut[m[2]];
      if ((p + n * s) > a.length) {

        return undefined;
      }
      switch (m[2]) {

        case 'A': case 's':
          rv.push(this._elLut[m[2]].de(a, p, n));
          break;
        case 'c': case 'b': case 'B': case 'h': case 'H':
        case 'i': case 'I': case 'l': case 'L': case 'f': case 'd': case 'q': case 'Q':
          el = this._elLut[m[2]];
          rv.push(this._UnpackSeries(n, s, a, p));
          break;
      }
      p += n * s;
    }
    return Array.prototype.concat.apply([], rv);
  };

  // Pack the supplied values into the octet array a, beginning at offset p, according to the fmt string
  m.PackTo = function (fmt, a, p, values) {
    // Set the private bBE flag based on the format string - assume big-endianness
    bBE = (fmt.charAt(0) != '<');

    var re = new RegExp(this._sPattern, 'g');
    var m, n, s, j;
    var i = 0;
    while (m = re.exec(fmt)) {
      n = ((m[1] == undefined) || (m[1] == '')) ? 1 : parseInt(m[1]);
      s = this._lenLut[m[2]];
      if ((p + n * s) > a.length) {
        return false;
      }
      switch (m[2]) {
        case 'A': case 's':
          if ((i + 1) > values.length) { return false; }
          this._elLut[m[2]].en(a, p, n, values[i]);
          i += 1;
          break;
        case 'c': case 'b': case 'B': case 'h': case 'H':
        case 'i': case 'I': case 'l': case 'L': case 'f': case 'd': case 'q': case 'Q':
          el = this._elLut[m[2]];
          if ((i + n) > values.length) { return false; }
          this._PackSeries(n, s, a, p, values, i);
          i += n;
          break;
        case 'x':
          for (j = 0; j < n; j++) { a[p + j] = 0; }
          break;
      }
      p += n * s;
    }
    return a;
  };

  // Pack the supplied values into a new octet array, according to the fmt string
  m.Pack = function (fmt, values) {
    return this.PackTo(fmt, new Array(this.CalcLength(fmt)), 0, values);
  };

  // Determine the number of bytes represented by the format string
  m.CalcLength = function (fmt) {
    var re = new RegExp(this._sPattern, 'g');
    var sum = 0;
    var m;
    while (m = re.exec(fmt)) {
      sum += (((m[1]  ==  undefined) ||  (m[1]  ==  '')) ? 1 : parseInt(m[1])) * this._lenLut[m[2]];
    }
    return sum;
  };
}

exports.jspack = new JSPack();
