/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var Int64 = require('node-int64');
var InputBufferUnderrunError = require('./transport').InputBufferUnderrunError;
var Thrift = require('./thrift');
var Type = Thrift.Type;
var util = require("util");

var Int64Util = require('./int64_util');
var json_parse = require('./json_parse');

var InputBufferUnderrunError = require('./input_buffer_underrun_error');

module.exports = TJSONProtocol;

/**
 * Initializes a Thrift JSON protocol instance.
 * @constructor
 * @param {Thrift.Transport} trans - The transport to serialize to/from.
 * @classdesc Apache Thrift Protocols perform serialization which enables cross
 * language RPC. The Protocol type is the JavaScript browser implementation
 * of the Apache Thrift TJSONProtocol.
 * @example
 *     var protocol  = new Thrift.Protocol(transport);
 */
function TJSONProtocol(trans) {
  this.tstack = [];
  this.tpos = [];
  this.trans = trans;
};

/**
 * Thrift IDL type Id to string mapping.
 * @readonly
 * @see {@link Thrift.Type}
 */
TJSONProtocol.Type = {};
TJSONProtocol.Type[Type.BOOL] = '"tf"';
TJSONProtocol.Type[Type.BYTE] = '"i8"';
TJSONProtocol.Type[Type.I16] = '"i16"';
TJSONProtocol.Type[Type.I32] = '"i32"';
TJSONProtocol.Type[Type.I64] = '"i64"';
TJSONProtocol.Type[Type.DOUBLE] = '"dbl"';
TJSONProtocol.Type[Type.STRUCT] = '"rec"';
TJSONProtocol.Type[Type.STRING] = '"str"';
TJSONProtocol.Type[Type.MAP] = '"map"';
TJSONProtocol.Type[Type.LIST] = '"lst"';
TJSONProtocol.Type[Type.SET] = '"set"';

/**
 * Thrift IDL type string to Id mapping.
 * @readonly
 * @see {@link Thrift.Type}
 */
TJSONProtocol.RType = {};
TJSONProtocol.RType.tf = Type.BOOL;
TJSONProtocol.RType.i8 = Type.BYTE;
TJSONProtocol.RType.i16 = Type.I16;
TJSONProtocol.RType.i32 = Type.I32;
TJSONProtocol.RType.i64 = Type.I64;
TJSONProtocol.RType.dbl = Type.DOUBLE;
TJSONProtocol.RType.rec = Type.STRUCT;
TJSONProtocol.RType.str = Type.STRING;
TJSONProtocol.RType.map = Type.MAP;
TJSONProtocol.RType.lst = Type.LIST;
TJSONProtocol.RType.set = Type.SET;

/**
 * The TJSONProtocol version number.
 * @readonly
 * @const {number} Version
 * @memberof Thrift.Protocol
 */
TJSONProtocol.Version = 1;

TJSONProtocol.prototype.flush = function() {
  this.writeToTransportIfStackIsFlushable();
  return this.trans.flush();
};

TJSONProtocol.prototype.writeToTransportIfStackIsFlushable = function() {
  if (this.tstack.length === 1) {
    this.trans.write(this.tstack.pop());
  }
};

/**
 * Serializes the beginning of a Thrift RPC message.
 * @param {string} name - The service method to call.
 * @param {Thrift.MessageType} messageType - The type of method call.
 * @param {number} seqid - The sequence number of this call (always 0 in Apache Thrift).
 */
TJSONProtocol.prototype.writeMessageBegin = function(name, messageType, seqid) {
  this.tstack.push([TJSONProtocol.Version, '"' + name + '"', messageType, seqid]);
};

/**
 * Serializes the end of a Thrift RPC message.
 */
TJSONProtocol.prototype.writeMessageEnd = function() {
  var obj = this.tstack.pop();

  this.wobj = this.tstack.pop();
  this.wobj.push(obj);

  this.wbuf = '[' + this.wobj.join(',') + ']';

  // we assume there is nothing more to come so we write
  this.trans.write(this.wbuf);
};

/**
 * Serializes the beginning of a struct.
 * @param {string} name - The name of the struct.
 */
TJSONProtocol.prototype.writeStructBegin = function(name) {
  this.tpos.push(this.tstack.length);
  this.tstack.push({});
};

/**
 * Serializes the end of a struct.
 */
TJSONProtocol.prototype.writeStructEnd = function() {
  var p = this.tpos.pop();
  var struct = this.tstack[p];
  var str = '{';
  var first = true;
  for (var key in struct) {
    if (first) {
      first = false;
    } else {
      str += ',';
    }

    str += key + ':' + struct[key];
  }

  str += '}';
  this.tstack[p] = str;

  this.writeToTransportIfStackIsFlushable();
};

/**
 * Serializes the beginning of a struct field.
 * @param {string} name - The name of the field.
 * @param {Thrift.Protocol.Type} fieldType - The data type of the field.
 * @param {number} fieldId - The field's unique identifier.
 */
TJSONProtocol.prototype.writeFieldBegin = function(name, fieldType, fieldId) {
  this.tpos.push(this.tstack.length);
  this.tstack.push({ 'fieldId': '"' +
    fieldId + '"', 'fieldType': TJSONProtocol.Type[fieldType]
  });
};

/**
 * Serializes the end of a field.
 */
TJSONProtocol.prototype.writeFieldEnd = function() {
  var value = this.tstack.pop();
  var fieldInfo = this.tstack.pop();

  if (':' + value === ":[object Object]") {
    this.tstack[this.tstack.length - 1][fieldInfo.fieldId] = '{' +
      fieldInfo.fieldType + ':' + JSON.stringify(value) + '}';
  } else {
    this.tstack[this.tstack.length - 1][fieldInfo.fieldId] = '{' +
      fieldInfo.fieldType + ':' + value + '}';
  }
  this.tpos.pop();

  this.writeToTransportIfStackIsFlushable();
};

/**
 * Serializes the end of the set of fields for a struct.
 */
TJSONProtocol.prototype.writeFieldStop = function() {
};

/**
 * Serializes the beginning of a map collection.
 * @param {Thrift.Type} keyType - The data type of the key.
 * @param {Thrift.Type} valType - The data type of the value.
 * @param {number} [size] - The number of elements in the map (ignored).
 */
TJSONProtocol.prototype.writeMapBegin = function(keyType, valType, size) {
  //size is invalid, we'll set it on end.
  this.tpos.push(this.tstack.length);
  this.tstack.push([TJSONProtocol.Type[keyType], TJSONProtocol.Type[valType], 0]);
};

/**
 * Serializes the end of a map.
 */
TJSONProtocol.prototype.writeMapEnd = function() {
  var p = this.tpos.pop();

  if (p == this.tstack.length) {
    return;
  }

  if ((this.tstack.length - p - 1) % 2 !== 0) {
    this.tstack.push('');
  }

  var size = (this.tstack.length - p - 1) / 2;

  this.tstack[p][this.tstack[p].length - 1] = size;

  var map = '}';
  var first = true;
  while (this.tstack.length > p + 1) {
    var v = this.tstack.pop();
    var k = this.tstack.pop();
    if (first) {
      first = false;
    } else {
      map = ',' + map;
    }

    if (! isNaN(k)) { k = '"' + k + '"'; } //json "keys" need to be strings
    map = k + ':' + v + map;
  }
  map = '{' + map;

  this.tstack[p].push(map);
  this.tstack[p] = '[' + this.tstack[p].join(',') + ']';

  this.writeToTransportIfStackIsFlushable();
};

/**
 * Serializes the beginning of a list collection.
 * @param {Thrift.Type} elemType - The data type of the elements.
 * @param {number} size - The number of elements in the list.
 */
TJSONProtocol.prototype.writeListBegin = function(elemType, size) {
  this.tpos.push(this.tstack.length);
  this.tstack.push([TJSONProtocol.Type[elemType], size]);
};

/**
 * Serializes the end of a list.
 */
TJSONProtocol.prototype.writeListEnd = function() {
  var p = this.tpos.pop();

  while (this.tstack.length > p + 1) {
    var tmpVal = this.tstack[p + 1];
    this.tstack.splice(p + 1, 1);
    this.tstack[p].push(tmpVal);
  }

  this.tstack[p] = '[' + this.tstack[p].join(',') + ']';

  this.writeToTransportIfStackIsFlushable();
};

/**
 * Serializes the beginning of a set collection.
 * @param {Thrift.Type} elemType - The data type of the elements.
 * @param {number} size - The number of elements in the list.
 */
TJSONProtocol.prototype.writeSetBegin = function(elemType, size) {
    this.tpos.push(this.tstack.length);
    this.tstack.push([TJSONProtocol.Type[elemType], size]);
};

/**
 * Serializes the end of a set.
 */
TJSONProtocol.prototype.writeSetEnd = function() {
  var p = this.tpos.pop();

  while (this.tstack.length > p + 1) {
    var tmpVal = this.tstack[p + 1];
    this.tstack.splice(p + 1, 1);
    this.tstack[p].push(tmpVal);
  }

  this.tstack[p] = '[' + this.tstack[p].join(',') + ']';

  this.writeToTransportIfStackIsFlushable();
};

/** Serializes a boolean */
TJSONProtocol.prototype.writeBool = function(bool) {
  this.tstack.push(bool ? 1 : 0);
};

/** Serializes a number */
TJSONProtocol.prototype.writeByte = function(byte) {
  this.tstack.push(byte);
};

/** Serializes a number */
TJSONProtocol.prototype.writeI16 = function(i16) {
  this.tstack.push(i16);
};

/** Serializes a number */
TJSONProtocol.prototype.writeI32 = function(i32) {
  this.tstack.push(i32);
};

/** Serializes a number */
TJSONProtocol.prototype.writeI64 = function(i64) {
  if (i64 instanceof Int64) {
    this.tstack.push(Int64Util.toDecimalString(i64));
  } else {
    this.tstack.push(i64);
  }
};

/** Serializes a number */
TJSONProtocol.prototype.writeDouble = function(dub) {
  this.tstack.push(dub);
};

/** Serializes a string */
TJSONProtocol.prototype.writeString = function(arg) {
  // We do not encode uri components for wire transfer:
  if (arg === null) {
      this.tstack.push(null);
  } else {
      if (typeof arg === 'string') {
        var str = arg;
      } else if (arg instanceof Buffer) {
        var str = arg.toString('utf8');
      } else {
        throw new Error('writeString called without a string/Buffer argument: ' + arg);
      }

      // concat may be slower than building a byte buffer
      var escapedString = '';
      for (var i = 0; i < str.length; i++) {
          var ch = str.charAt(i);      // a single double quote: "
          if (ch === '\"') {
              escapedString += '\\\"'; // write out as: \"
          } else if (ch === '\\') {    // a single backslash: \
              escapedString += '\\\\'; // write out as: \\
          /* Currently escaped forward slashes break TJSONProtocol.
           * As it stands, we can simply pass forward slashes into
           * our strings across the wire without being escaped.
           * I think this is the protocol's bug, not thrift.js
           * } else if(ch === '/') {   // a single forward slash: /
           *  escapedString += '\\/';  // write out as \/
           * }
           */
          } else if (ch === '\b') {    // a single backspace: invisible
              escapedString += '\\b';  // write out as: \b"
          } else if (ch === '\f') {    // a single formfeed: invisible
              escapedString += '\\f';  // write out as: \f"
          } else if (ch === '\n') {    // a single newline: invisible
              escapedString += '\\n';  // write out as: \n"
          } else if (ch === '\r') {    // a single return: invisible
              escapedString += '\\r';  // write out as: \r"
          } else if (ch === '\t') {    // a single tab: invisible
              escapedString += '\\t';  // write out as: \t"
          } else {
              escapedString += ch;     // Else it need not be escaped
          }
      }
      this.tstack.push('"' + escapedString + '"');
  }
};

/** Serializes a string */
TJSONProtocol.prototype.writeBinary = function(arg) {
  if (typeof arg === 'string') {
    var buf = new Buffer(arg, 'binary');
  } else if (arg instanceof Buffer ||
             Object.prototype.toString.call(arg) == '[object Uint8Array]')  {
    var buf = arg;
  } else {
    throw new Error('writeBinary called without a string/Buffer argument: ' + arg);
  }
  this.tstack.push('"' + buf.toString('base64') + '"');
};

/**
 * @class
 * @name AnonReadMessageBeginReturn
 * @property {string} fname - The name of the service method.
 * @property {Thrift.MessageType} mtype - The type of message call.
 * @property {number} rseqid - The sequence number of the message (0 in Thrift RPC).
 */
/**
 * Deserializes the beginning of a message.
 * @returns {AnonReadMessageBeginReturn}
 */
TJSONProtocol.prototype.readMessageBegin = function() {
  this.rstack = [];
  this.rpos = [];

  //Borrow the inbound transport buffer and ensure data is present/consistent
  var transBuf = this.trans.borrow();
  if (transBuf.readIndex >= transBuf.writeIndex) {
    throw new InputBufferUnderrunError();
  }
  var cursor = transBuf.readIndex;

  if (transBuf.buf[cursor] !== 0x5B) { //[
    throw new Error("Malformed JSON input, no opening bracket");
  }

  //Parse a single message (there may be several in the buffer)
  //  TODO: Handle characters using multiple code units
  cursor++;
  var openBracketCount = 1;
  var inString = false;
  for (; cursor < transBuf.writeIndex; cursor++) {
    var chr = transBuf.buf[cursor];
    //we use hexa charcode here because data[i] returns an int and not a char
    if (inString) {
      if (chr === 0x22) { //"
        inString = false;
      } else if (chr === 0x5C) { //\
        //escaped character, skip
        cursor += 1;
      }
    } else {
      if (chr === 0x5B) { //[
        openBracketCount += 1;
      } else if (chr === 0x5D) { //]
        openBracketCount -= 1;
        if (openBracketCount === 0) {
          //end of json message detected
          break;
        }
      } else if (chr === 0x22) { //"
        inString = true;
      }
    }
  }

  if (openBracketCount !== 0) {
    // Missing closing bracket. Can be buffer underrun.
    throw new InputBufferUnderrunError();
  }

  //Reconstitute the JSON object and conume the necessary bytes
  this.robj = json_parse(transBuf.buf.slice(transBuf.readIndex, cursor+1).toString());
  this.trans.consume(cursor + 1 - transBuf.readIndex);

  //Verify the protocol version
  var version = this.robj.shift();
  if (version != TJSONProtocol.Version) {
    throw new Error('Wrong thrift protocol version: ' + version);
  }

  //Objectify the thrift message {name/type/sequence-number} for return
  // and then save the JSON object in rstack
  var r = {};
  r.fname = this.robj.shift();
  r.mtype = this.robj.shift();
  r.rseqid = this.robj.shift();
  this.rstack.push(this.robj.shift());
  return r;
};

/** Deserializes the end of a message. */
TJSONProtocol.prototype.readMessageEnd = function() {
};

/**
 * Deserializes the beginning of a struct.
 * @param {string} [name] - The name of the struct (ignored)
 * @returns {object} - An object with an empty string fname property
 */
TJSONProtocol.prototype.readStructBegin = function() {
  var r = {};
  r.fname = '';

  //incase this is an array of structs
  if (this.rstack[this.rstack.length - 1] instanceof Array) {
    this.rstack.push(this.rstack[this.rstack.length - 1].shift());
  }

  return r;
};

/** Deserializes the end of a struct. */
TJSONProtocol.prototype.readStructEnd = function() {
  this.rstack.pop();
};

/**
 * @class
 * @name AnonReadFieldBeginReturn
 * @property {string} fname - The name of the field (always '').
 * @property {Thrift.Type} ftype - The data type of the field.
 * @property {number} fid - The unique identifier of the field.
 */
/**
 * Deserializes the beginning of a field.
 * @returns {AnonReadFieldBeginReturn}
 */
TJSONProtocol.prototype.readFieldBegin = function() {
  var r = {};

  var fid = -1;
  var ftype = Type.STOP;

  //get a fieldId
  for (var f in (this.rstack[this.rstack.length - 1])) {
    if (f === null) {
      continue;
    }

    fid = parseInt(f, 10);
    this.rpos.push(this.rstack.length);

    var field = this.rstack[this.rstack.length - 1][fid];

    //remove so we don't see it again
    delete this.rstack[this.rstack.length - 1][fid];

    this.rstack.push(field);

    break;
  }

  if (fid != -1) {
    //should only be 1 of these but this is the only
    //way to match a key
    for (var i in (this.rstack[this.rstack.length - 1])) {
      if (TJSONProtocol.RType[i] === null) {
        continue;
      }

      ftype = TJSONProtocol.RType[i];
      this.rstack[this.rstack.length - 1] = this.rstack[this.rstack.length - 1][i];
    }
  }

  r.fname = '';
  r.ftype = ftype;
  r.fid = fid;

  return r;
};

/** Deserializes the end of a field. */
TJSONProtocol.prototype.readFieldEnd = function() {
  var pos = this.rpos.pop();

  //get back to the right place in the stack
  while (this.rstack.length > pos) {
    this.rstack.pop();
  }
};

/**
 * @class
 * @name AnonReadMapBeginReturn
 * @property {Thrift.Type} ktype - The data type of the key.
 * @property {Thrift.Type} vtype - The data type of the value.
 * @property {number} size - The number of elements in the map.
 */
/**
 * Deserializes the beginning of a map.
 * @returns {AnonReadMapBeginReturn}
 */
TJSONProtocol.prototype.readMapBegin = function() {
  var map = this.rstack.pop();
  var first = map.shift();
  if (first instanceof Array) {
    this.rstack.push(map);
    map = first;
    first = map.shift();
  }

  var r = {};
  r.ktype = TJSONProtocol.RType[first];
  r.vtype = TJSONProtocol.RType[map.shift()];
  r.size = map.shift();


  this.rpos.push(this.rstack.length);
  this.rstack.push(map.shift());

  return r;
};

/** Deserializes the end of a map. */
TJSONProtocol.prototype.readMapEnd = function() {
  this.readFieldEnd();
};

/**
 * @class
 * @name AnonReadColBeginReturn
 * @property {Thrift.Type} etype - The data type of the element.
 * @property {number} size - The number of elements in the collection.
 */
/**
 * Deserializes the beginning of a list.
 * @returns {AnonReadColBeginReturn}
 */
TJSONProtocol.prototype.readListBegin = function() {
  var list = this.rstack[this.rstack.length - 1];

  var r = {};
  r.etype = TJSONProtocol.RType[list.shift()];
  r.size = list.shift();

  this.rpos.push(this.rstack.length);
  this.rstack.push(list.shift());

  return r;
};

/** Deserializes the end of a list. */
TJSONProtocol.prototype.readListEnd = function() {
  var pos = this.rpos.pop() - 2;
  var st = this.rstack;
  st.pop();
  if (st instanceof Array && st.length > pos && st[pos].length > 0) {
    st.push(st[pos].shift());
  }
};

/**
 * Deserializes the beginning of a set.
 * @returns {AnonReadColBeginReturn}
 */
TJSONProtocol.prototype.readSetBegin = function() {
  return this.readListBegin();
};

/** Deserializes the end of a set. */
TJSONProtocol.prototype.readSetEnd = function() {
  return this.readListEnd();
};

TJSONProtocol.prototype.readBool = function() {
  return this.readValue() == '1';
};

TJSONProtocol.prototype.readByte = function() {
  return this.readI32();
};

TJSONProtocol.prototype.readI16 = function() {
  return this.readI32();
};

TJSONProtocol.prototype.readI32 = function(f) {
  return +this.readValue();
}

/** Returns the next value found in the protocol buffer */
TJSONProtocol.prototype.readValue = function(f) {
  if (f === undefined) {
    f = this.rstack[this.rstack.length - 1];
  }

  var r = {};

  if (f instanceof Array) {
    if (f.length === 0) {
      r.value = undefined;
    } else {
      r.value = f.shift();
    }
  } else if (!(f instanceof Int64) && f instanceof Object) {
    for (var i in f) {
      if (i === null) {
        continue;
      }
      this.rstack.push(f[i]);
      delete f[i];

      r.value = i;
      break;
    }
  } else {
    r.value = f;
    this.rstack.pop();
  }

  return r.value;
};

TJSONProtocol.prototype.readI64 = function() {
  var n = this.readValue()
  if (typeof n === 'string') {
    // Assuming no one is sending in 1.11111e+33 format
    return Int64Util.fromDecimalString(n);
  } else {
    return new Int64(n);
  }
};

TJSONProtocol.prototype.readDouble = function() {
  return this.readI32();
};

TJSONProtocol.prototype.readBinary = function() {
  return new Buffer(this.readValue(), 'base64');
};

TJSONProtocol.prototype.readString = function() {
  return this.readValue();
};

/**
 * Returns the underlying transport.
 * @readonly
 * @returns {Thrift.Transport} The underlying transport.
 */
TJSONProtocol.prototype.getTransport = function() {
  return this.trans;
};

/**
 * Method to arbitrarily skip over data
 */
TJSONProtocol.prototype.skip = function(type) {
    switch (type) {
    case Type.STOP:
      return;
    case Type.BOOL:
      this.readBool();
      break;
    case Type.BYTE:
      this.readByte();
      break;
    case Type.I16:
      this.readI16();
      break;
    case Type.I32:
      this.readI32();
      break;
    case Type.I64:
      this.readI64();
      break;
    case Type.DOUBLE:
      this.readDouble();
      break;
    case Type.STRING:
      this.readString();
      break;
    case Type.STRUCT:
      this.readStructBegin();
      while (true) {
        var r = this.readFieldBegin();
        if (r.ftype === Type.STOP) {
          break;
        }
        this.skip(r.ftype);
        this.readFieldEnd();
      }
      this.readStructEnd();
      break;
    case Type.MAP:
      var mapBegin = this.readMapBegin();
      for (var i = 0; i < mapBegin.size; ++i) {
        this.skip(mapBegin.ktype);
        this.skip(mapBegin.vtype);
      }
      this.readMapEnd();
      break;
    case Type.SET:
      var setBegin = this.readSetBegin();
      for (var i2 = 0; i2 < setBegin.size; ++i2) {
        this.skip(setBegin.etype);
      }
      this.readSetEnd();
      break;
    case Type.LIST:
      var listBegin = this.readListBegin();
      for (var i3 = 0; i3 < listBegin.size; ++i3) {
        this.skip(listBegin.etype);
      }
      this.readListEnd();
      break;
    default:
      throw new  Error("Invalid type: " + type);
  }
};
