/* ProtocolBuffer client 0.1.0*/

/**
 * pomelo-protobuf
 * @author <zhang0935@gmail.com>
 */

/**
 * Protocol buffer root
 * In browser, it will be window.protbuf
 */
(function (exports, global){
  var Protobuf = exports;

  Protobuf.init = function(opts){
    //On the serverside, use serverProtos to encode messages send to client
    Protobuf.encoder.init(opts.encoderProtos);

    //On the serverside, user clientProtos to decode messages receive from clients
    Protobuf.decoder.init(opts.decoderProtos);
  };

  Protobuf.encode = function(key, msg){
    return Protobuf.encoder.encode(key, msg);
  };

  Protobuf.decode = function(key, msg){
    return Protobuf.decoder.decode(key, msg);
  };

  // exports to support for components
  module.exports = Protobuf;
  if(typeof(window) != "undefined") {
    window.protobuf = Protobuf;
  }
  
})(typeof(window) == "undefined" ? module.exports : (this.protobuf = {}), this);

/**
 * constants
 */
(function (exports, global){
  var constants = exports.constants = {};

  constants.TYPES = {
    uInt32 : 0,
    sInt32 : 0,
    int32 : 0,
    double : 1,
    string : 2,
    message : 2,
    float : 5
  };

})('undefined' !== typeof protobuf ? protobuf : module.exports, this);

/**
 * util module
 */
(function (exports, global){

  var Util = exports.util = {};

  Util.isSimpleType = function(type){
    return ( type === 'uInt32' ||
             type === 'sInt32' ||
             type === 'int32'  ||
             type === 'uInt64' ||
             type === 'sInt64' ||
             type === 'float'  ||
             type === 'double' );
  };

})('undefined' !== typeof protobuf ? protobuf : module.exports, this);

/**
 * codec module
 */
(function (exports, global){

  var Codec = exports.codec = {};

  var buffer = new ArrayBuffer(8);
  var float32Array = new Float32Array(buffer);
  var float64Array = new Float64Array(buffer);
  var uInt8Array = new Uint8Array(buffer);

  Codec.encodeUInt32 = function(n){
    var n = parseInt(n);
    if(isNaN(n) || n < 0){
      return null;
    }

    var result = [];
    do{
      var tmp = n % 128;
      var next = Math.floor(n/128);

      if(next !== 0){
        tmp = tmp + 128;
      }
      result.push(tmp);
      n = next;
    }while(n !== 0);

    return result;
  };

  Codec.encodeSInt32 = function(n){
    var n = parseInt(n);
    if(isNaN(n)){
      return null;
    }
    n = n<0?(Math.abs(n)*2-1):n*2;

    return Codec.encodeUInt32(n);
  };

  Codec.decodeUInt32 = function(bytes){
    var n = 0;

    for(var i = 0; i < bytes.length; i++){
      var m = parseInt(bytes[i]);
      n = n + ((m & 0x7f) * Math.pow(2,(7*i)));
      if(m < 128){
        return n;
      }
    }

    return n;
  };

  Codec.decodeSInt32 = function(bytes){
    var n = this.decodeUInt32(bytes);
    var flag = ((n%2) === 1)?-1:1;

    n = ((n%2 + n)/2)*flag;

    return n;
  };

  Codec.encodeFloat = function(float){
    float32Array[0] = float;
    return uInt8Array;
  };

  Codec.decodeFloat = function(bytes, offset){
    if(!bytes || bytes.length < (offset + 4)){
      return null;
    }

    for(var i = 0; i < 4; i++){
      uInt8Array[i] = bytes[offset + i];
    }

    return float32Array[0];
  };

  Codec.encodeDouble = function(double){
    float64Array[0] = double;
    return uInt8Array.subarray(0, 8);
  };

  Codec.decodeDouble = function(bytes, offset){
    if(!bytes || bytes.length < (offset + 8)){
      return null;
    }

    for(var i = 0; i < 8; i++){
      uInt8Array[i] = bytes[offset + i];
    }

    return float64Array[0];
  };

  Codec.encodeStr = function(bytes, offset, str){
    for(var i = 0; i < str.length; i++){
      var code = str.charCodeAt(i);
      var codes = encode2UTF8(code);

      for(var j = 0; j < codes.length; j++){
        bytes[offset] = codes[j];
        offset++;
      }
    }

    return offset;
  };

  /**
   * Decode string from utf8 bytes
   */
  Codec.decodeStr = function(bytes, offset, length){
    var array = [];
    var end = offset + length;

    while(offset < end){
      var code = 0;

      if(bytes[offset] < 128){
        code = bytes[offset];

        offset += 1;
      }else if(bytes[offset] < 224){
        code = ((bytes[offset] & 0x3f)<<6) + (bytes[offset+1] & 0x3f);
        offset += 2;
      }else{
        code = ((bytes[offset] & 0x0f)<<12) + ((bytes[offset+1] & 0x3f)<<6) + (bytes[offset+2] & 0x3f);
        offset += 3;
      }

      array.push(code);

    }

    var str = '';
    for(var i = 0; i < array.length;){
      str += String.fromCharCode.apply(null, array.slice(i, i + 10000));
      i += 10000;
    }

    return str;
  };

  /**
   * Return the byte length of the str use utf8
   */
  Codec.byteLength = function(str){
    if(typeof(str) !== 'string'){
      return -1;
    }

    var length = 0;

    for(var i = 0; i < str.length; i++){
      var code = str.charCodeAt(i);
      length += codeLength(code);
    }

    return length;
  };

  /**
   * Encode a unicode16 char code to utf8 bytes
   */
  function encode2UTF8(charCode){
    if(charCode <= 0x7f){
      return [charCode];
    }else if(charCode <= 0x7ff){
      return [0xc0|(charCode>>6), 0x80|(charCode & 0x3f)];
    }else{
      return [0xe0|(charCode>>12), 0x80|((charCode & 0xfc0)>>6), 0x80|(charCode & 0x3f)];
    }
  }

  function codeLength(code){
    if(code <= 0x7f){
      return 1;
    }else if(code <= 0x7ff){
      return 2;
    }else{
      return 3;
    }
  }
})('undefined' !== typeof protobuf ? protobuf : module.exports, this);

/**
 * encoder module
 */
(function (exports, global){

  var protobuf = exports;
  var MsgEncoder = exports.encoder = {};

  var codec = protobuf.codec;
  var constant = protobuf.constants;
  var util = protobuf.util;

  MsgEncoder.init = function(protos){
    this.protos = protos || {};
  };

  MsgEncoder.encode = function(route, msg){
    //Get protos from protos map use the route as key
    var protos = this.protos[route];

    //Check msg
    if(!checkMsg(msg, protos)){
      return null;
    }

    //Set the length of the buffer 2 times bigger to prevent overflow
    var length = codec.byteLength(JSON.stringify(msg));

    //Init buffer and offset
    var buffer = new ArrayBuffer(length);
    var uInt8Array = new Uint8Array(buffer);
    var offset = 0;

    if(!!protos){
      offset = encodeMsg(uInt8Array, offset, protos, msg);
      if(offset > 0){
        return uInt8Array.subarray(0, offset);
      }
    }

    return null;
  };

  /**
   * Check if the msg follow the defination in the protos
   */
  function checkMsg(msg, protos){
    if(!protos){
      return false;
    }

    for(var name in protos){
      var proto = protos[name];

      //All required element must exist
      switch(proto.option){
        case 'required' :
          if(typeof(msg[name]) === 'undefined'){
            console.warn('no property exist for required! name: %j, proto: %j, msg: %j', name, proto, msg);
            return false;
          }
        case 'optional' :
          if(typeof(msg[name]) !== 'undefined'){
            var message = protos.__messages[proto.type] || MsgEncoder.protos['message ' + proto.type];
            if(!!message && !checkMsg(msg[name], message)){
              console.warn('inner proto error! name: %j, proto: %j, msg: %j', name, proto, msg);
              return false;
            }
          }
        break;
        case 'repeated' :
          //Check nest message in repeated elements
          var message = protos.__messages[proto.type] || MsgEncoder.protos['message ' + proto.type];
          if(!!msg[name] && !!message){
            for(var i = 0; i < msg[name].length; i++){
              if(!checkMsg(msg[name][i], message)){
                return false;
              }
            }
          }
        break;
      }
    }

    return true;
  }

  function encodeMsg(buffer, offset, protos, msg){
    for(var name in msg){
      if(!!protos[name]){
        var proto = protos[name];

        switch(proto.option){
          case 'required' :
          case 'optional' :
            offset = writeBytes(buffer, offset, encodeTag(proto.type, proto.tag));
            offset = encodeProp(msg[name], proto.type, offset, buffer, protos);
          break;
          case 'repeated' :
            if(msg[name].length > 0){
              offset = encodeArray(msg[name], proto, offset, buffer, protos);
            }
          break;
        }
      }
    }

    return offset;
  }

  function encodeProp(value, type, offset, buffer, protos){
    switch(type){
      case 'uInt32':
        offset = writeBytes(buffer, offset, codec.encodeUInt32(value));
      break;
      case 'int32' :
      case 'sInt32':
        offset = writeBytes(buffer, offset, codec.encodeSInt32(value));
      break;
      case 'float':
        writeBytes(buffer, offset, codec.encodeFloat(value));
        offset += 4;
      break;
      case 'double':
        writeBytes(buffer, offset, codec.encodeDouble(value));
        offset += 8;
      break;
      case 'string':
        var length = codec.byteLength(value);

        //Encode length
        offset = writeBytes(buffer, offset, codec.encodeUInt32(length));
        //write string
        codec.encodeStr(buffer, offset, value);
        offset += length;
      break;
      default :
        var message = protos.__messages[type] || MsgEncoder.protos['message ' + type];
        if(!!message){
          //Use a tmp buffer to build an internal msg
          var tmpBuffer = new ArrayBuffer(codec.byteLength(JSON.stringify(value))*2);
          var length = 0;

          length = encodeMsg(tmpBuffer, length, message, value);
          //Encode length
          offset = writeBytes(buffer, offset, codec.encodeUInt32(length));
          //contact the object
          for(var i = 0; i < length; i++){
            buffer[offset] = tmpBuffer[i];
            offset++;
          }
        }
      break;
    }

    return offset;
  }

  /**
   * Encode reapeated properties, simple msg and object are decode differented
   */
  function encodeArray(array, proto, offset, buffer, protos){
    var i = 0;

    if(util.isSimpleType(proto.type)){
      offset = writeBytes(buffer, offset, encodeTag(proto.type, proto.tag));
      offset = writeBytes(buffer, offset, codec.encodeUInt32(array.length));
      for(i = 0; i < array.length; i++){
        offset = encodeProp(array[i], proto.type, offset, buffer);
      }
    }else{
      for(i = 0; i < array.length; i++){
        offset = writeBytes(buffer, offset, encodeTag(proto.type, proto.tag));
        offset = encodeProp(array[i], proto.type, offset, buffer, protos);
      }
    }

    return offset;
  }

  function writeBytes(buffer, offset, bytes){
    for(var i = 0; i < bytes.length; i++, offset++){
      buffer[offset] = bytes[i];
    }

    return offset;
  }

  function encodeTag(type, tag){
    var value = constant.TYPES[type]||2;

    return codec.encodeUInt32((tag<<3)|value);
  }
})('undefined' !== typeof protobuf ? protobuf : module.exports, this);

/**
 * decoder module
 */
(function (exports, global){
  var protobuf = exports;
  var MsgDecoder = exports.decoder = {};

  var codec = protobuf.codec;
  var util = protobuf.util;

  var buffer;
  var offset = 0;

  MsgDecoder.init = function(protos){
    this.protos = protos || {};
  };

  MsgDecoder.setProtos = function(protos){
    if(!!protos){
      this.protos = protos;
    }
  };

  MsgDecoder.decode = function(route, buf){
    var protos = this.protos[route];

    buffer = buf;
    offset = 0;

    if(!!protos){
      return decodeMsg({}, protos, buffer.length);
    }

    return null;
  };

  function decodeMsg(msg, protos, length){
    while(offset<length){
      var head = getHead();
      var type = head.type;
      var tag = head.tag;
      var name = protos.__tags[tag];

      switch(protos[name].option){
        case 'optional' :
        case 'required' :
          msg[name] = decodeProp(protos[name].type, protos);
        break;
        case 'repeated' :
          if(!msg[name]){
            msg[name] = [];
          }
          decodeArray(msg[name], protos[name].type, protos);
        break;
      }
    }

    return msg;
  }

  /**
   * Test if the given msg is finished
   */
  function isFinish(msg, protos){
    return (!protos.__tags[peekHead().tag]);
  }
  /**
   * Get property head from protobuf
   */
  function getHead(){
    var tag = codec.decodeUInt32(getBytes());

    return {
      type : tag&0x7,
      tag : tag>>3
    };
  }

  /**
   * Get tag head without move the offset
   */
  function peekHead(){
    var tag = codec.decodeUInt32(peekBytes());

    return {
      type : tag&0x7,
      tag : tag>>3
    };
  }

  function decodeProp(type, protos){
    switch(type){
      case 'uInt32':
        return codec.decodeUInt32(getBytes());
      case 'int32' :
      case 'sInt32' :
        return codec.decodeSInt32(getBytes());
      case 'float' :
        var float = codec.decodeFloat(buffer, offset);
        offset += 4;
        return float;
      case 'double' :
        var double = codec.decodeDouble(buffer, offset);
        offset += 8;
        return double;
      case 'string' :
        var length = codec.decodeUInt32(getBytes());

        var str =  codec.decodeStr(buffer, offset, length);
        offset += length;

        return str;
      default :
        var message = protos && (protos.__messages[type] || MsgDecoder.protos['message ' + type]);
        if(!!message){
          var length = codec.decodeUInt32(getBytes());
          var msg = {};
          decodeMsg(msg, message, offset+length);
          return msg;
        }
      break;
    }
  }

  function decodeArray(array, type, protos){
    if(util.isSimpleType(type)){
      var length = codec.decodeUInt32(getBytes());

      for(var i = 0; i < length; i++){
        array.push(decodeProp(type));
      }
    }else{
      array.push(decodeProp(type, protos));
    }
  }

  function getBytes(flag){
    var bytes = [];
    var pos = offset;
    flag = flag || false;

    var b;

    do{
      b = buffer[pos];
      bytes.push(b);
      pos++;
    }while(b >= 128);

    if(!flag){
      offset = pos;
    }
    return bytes;
  }

  function peekBytes(){
    return getBytes(true);
  }

})('undefined' !== typeof protobuf ? protobuf : module.exports, this);

