/**
 * An 8-6 bit state container for parsing 6-bit blocks in 8-bit memory.
 *  This implementation makes specific use of encode/decode char and is for
 *  this reason bound to an AIS iimplementation.  Appropriate care should
 *  be taken to convert between 6 and 8 bit characters in extended, 
 *  non-AIS implementations.
 *
 * @file     Sixbit.js
 * @date     2008-10-31 16:35 HST
 * @author   Paul Reuter
 * @version  1.2
 */


/**
 * A 6-bit state container for parsing AIS 6-bit messages.  The use of encode
 *  and decode char is what ties this implementation to AIS.
 *
 * @constructor
 * @param {string} data A 6-bit encoded data string.
 * @param {uint} end_state Number of bits to ignore at end of string.
 * @return {object} new sixbit state container.
 */
function Sixbit(data,end_state) { 
  this.data      = '' ; // a byte string
  this.data_ix   = 0;   // index of current data character
  this.remainder = 0;   // bit field remainder (actual bits)
  this.rem_len   = 0;   // number of bits (unprocessed) in remainder
  this.rem_end   = 0;   // number of bits at end of string to ignore
  this.masks     = [ 0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f ];
  
  this.initialize(data,end_state);
  return this;
}; // END: constructor Sixbit()


/**
 * Assign a new 6-bit data payload, resetting state.
 *
 * @public
 * @param {string} data 6-bit packed data.
 * @param {uint} end_state Number of bits to ignore at end of string.
 * @return {boolean} always true.
 */
Sixbit.prototype.set = function(data,end_state) {
  data = (!data) ? '' : data;
  this.initialize(data,end_state);
}; // END: function set(data)


/**
 * Appends extra 6-bit data to existing data.  Assumes bits remaining won't
 *  occur in first n-1 messages of an n-part message.
 *
 * @param {string} data 6-bit packed data.
 * @return {boolean} success or failure.
 */
Sixbit.prototype.append = function(other_six) {
  this.data = this.data + '' + other_six.data;
  this.rem_end = other_six.rem_end;

  return true;
}; // END: function append(data)


/**
 * Extracts the requested number of bits from the 6-bit packed data.  The
 *  extraction process is like a DFA (deterministic finite automata) that
 *  can't be accessed out of order.  Once the data has been set, you can
 *  only march forward through the data.  If you need to regrab an earlier
 *  portion of data, you'll have to reset().
 *
 * @public
 * @param {int} n_bits The number of bits to return, -1 for all remaining.
 * @return {int} The value of the bits at the current state pointer.
 */
Sixbit.prototype.getBits = function(n_bits) { 
  if( n_bits < 0 ) { 
    n_bits = this.bitsRemaining();
  }
  if( n_bits <= 32 ) { 
    return this.getBits_32(n_bits);
  }
  
  var str = '';
  while( n_bits > 0 ) {
    var ch = this.getBits_32(Math.min(n_bits,8));
    if( ch == null ) {
      return str;
    }
    str += String.fromCharCode(ch);
    n_bits -= 8;
  }
  return str;
}; // END: function getBits(n_bits)

    
/**
 * Extracts the requested number of bits from the 6-bit packed data.  The
 *  extraction process is like a DFA (deterministic finite automata) that
 *  can't be accessed out of order.  Once the data has been set, you can
 *  only march forward through the data.  If you need to regrab an earlier
 *  portion of data, you'll have to reset().
 *
 * @private
 * @param {int} n_bits The number of bits to return [0..32]
 * @return {int} The value of the bits at the current state pointer.
 */
Sixbit.prototype.getBits_32 = function(n_bits) { 
  var bits = 0;
  var bitten = false; // true when bits are extracted
    
  while( n_bits > 0 ) {
    // If anything left over from previous call to getBits
    if( this.rem_len > 0 ) {
      // Yes (leftover)
      // If we need another byte to return n_bits
      if( this.rem_len <= n_bits ) {
        // Yes (need byte)
        // slurp up and reset the remainder
        bits = (bits << 6) + this.remainder;
        n_bits -= this.rem_len;
        this.remainder = 0;
        this.rem_len = 0;
        bitten = true;
      } else {
        // No (no byte need)
        // scoot return bits over by requested amount
        bits = bits << n_bits;
        // move remainder bits out of high-bit position, then add them in.
        bits += (this.remainder >> (this.rem_len - n_bits));
        // recount remainder bits available and clear the ones we used.
        this.rem_len -= n_bits;
        this.remainder = this.remainder & this.masks[this.rem_len];
        // n_bits have been read into variable 'bits'.
        return bits;
      }
    } // end: if( rem_len > 0 ) AKA: if (have remainder)
    
    if( this.data_ix < this.data.length ) {
      this.remainder = this.decodeByte(this.data.charAt(this.data_ix));
      this.rem_len = 6;
      this.data_ix += 1;
    } else {
      // nothing left in data portion, return what we have.
      return (bitten) ? bits : null;
    }
  } // end: while( n_bits > 0 )

  return (bitten) ? bits : null;
}; // END: function getBits_32(n_bits)


/**
 * Returns a string of embedded ascii characters.
 *
 * @param {uint} n_bits The number of bits to return.
 * @return {string} An unencoded ascii string.
 */
Sixbit.prototype.getBitsAscii = function(n_bits) { 
  if( n_bits%6 != 0 ) { 
    return false;
  }
  var str = '';
  for(var i=0,n=n_bits/6; i<n; i++) { 
    str += this.decodeEmbeddedAscii( this.getBits(6) );
  }
  return str;
}; // END: function getBitsAscii


/**
 * Performs an ASCII to Binary conversion on the 6-bit packed character as
 * a bloated ASCII character.
 *
 * @public
 * @param {char} ch An AIS-supported ASCII character.
 * @return {int} A 6-bit number [0..63] (-1 for failure).
 */
Sixbit.prototype.decodeByte = function(ch) { 
  ch = ch.charCodeAt(0);
  if( ch < 0x30 || ch > 0x77 || (ch > 0x57 && ch < 0x60) ) { 
    return -1;
  }
  if( ch<0x60 ) { 
    return (ch-0x30)&0x3f;
  } else { 
    return (ch-0x38)&0x3f;
  }
}; // END: function decodeByte(ch)


/**
 * Performs a Binary to ASCII conversion on a 6-bit value, to transform it
 *  to the AIS ASCII representation. (perform bit stuffing)
 *
 * @public
 * @param {int} A 6-bit number [0..63].
 * @return {char} ch An AIS-supported ASCII character (zero for failure).
 */
Sixbit.prototype.encodeByte = function(bin) { 
  if( bin > 0x3f ) { 
    return 0;
  }
  return (bin < 0x27) ? (bin + 0x30) : (bin + 0x38);
}; // END: function encodeByte(bin);


/**
 * Decode embedded ASCII from within an AIS message.  This is different from
 *  the usual decodeByte because here, the character represent actual data
 *  AIS may also embed ASCII within its messages.  For these embedded ASCII
 *  characters, we simply add an offset where applicable.
 *
 * @param {char} val An unpacked AIS encoded character.
 * @return {char} A printable character which had been previously encoded.
 */
Sixbit.prototype.decodeEmbeddedAscii = function(val) { 
  val = val & 0x3f;
  val = (val < 0x20) ? (val+0x40) : val;
  return (val==0x40) ? '' : String.fromCharCode(val);
}; // END: function decodeEmbeddedAscii(ch)


/**
 * Resets the parser back to the beginning of the data.  Does not clear
 *  the data.  Allows you to parse the data from the beginning, again.
 *
 * @return {boolean} true always.
 */
Sixbit.prototype.reset = function() { 
  this.data_ix   = 0;
  this.remainder = 0;
  this.rem_len   = 0;
}; // END: function reset()


/**
 * Calculate the number of bits remaining in the string.
 *
 * @public
 * @return {int} Number of bits left unprocessed in the string.
 */
Sixbit.prototype.bitsRemaining = function() { 
  // this.remainder stores some unprocessed bits
  // this.rem_len stores the number of these remaining unprocessed bits
  // this.data stores 8-bit characters, each representing only 6 bits
  // this.data_ix points to first unused character in data string
  // this.rem_end is the number of bits to ignore
  return ( this.rem_len + (this.data.length - this.data_ix)*6 - this.rem_end );
}; // END: function bitsRemaining()


/**
 * Copy another sixbit object to this one.
 *
 * @public
 * @param {Sixbit} sixbit A live Sixbit object.
 * @return {boolean} true always.
 */
Sixbit.prototype.copy = function(sixbit) { 
  this.data      = sixbit.data;
  this.data_ix   = sixbit.data_ix;
  this.remainder = sixbit.remainder;
  this.rem_len   = sixbit.rem_len;
  this.rem_end   = sixbit.rem_end;
  return true;
}; // END: function coyp(sixbit)


/**
 * Initializes the sixbit object.
 *
 * @private
 * @return {boolean} true always.
 */
Sixbit.prototype.initialize = function(data,end_state) {
  this.data      = data || '';     // zero-terminated byte string
  this.data_ix   = 0;              // index of current data character
  this.remainder = 0;              // bit field remainder (actual bits)
  this.rem_len   = 0;              // num bits (unprocessed) in remainder
  this.rem_end   = end_state || 0; // num bits to ignore at end

  return true;
}; // END: function initialize()
