/**
 * Created by delian on 3/12/14.
 * This module provides encoding and decoding of the AMF0 and AMF3 format
 */
const Logger = require('./node_core_logger');

const amf3dRules = {
  0x00: amf3decUndefined,
  0x01: amf3decNull,
  0x02: amf3decFalse,
  0x03: amf3decTrue,
  0x04: amf3decInteger,
  0x05: amf3decDouble,
  0x06: amf3decString,
  0x07: amf3decXmlDoc,
  0x08: amf3decDate,
  0x09: amf3decArray,
  0x0A: amf3decObject,
  0x0B: amf3decXml,
  0x0C: amf3decByteArray //,
  //    0x0D: amf3decVecInt,
  //    0x0E: amf3decVecUInt,
  //    0x0F: amf3decVecDouble,
  //    0x10: amf3decVecObject,
  //    0x11: amf3decDict // No dictionary support for the moment!
};

const amf3eRules = {
  'string': amf3encString,
  'integer': amf3encInteger,
  'double': amf3encDouble,
  'xml': amf3encXmlDoc,
  'object': amf3encObject,
  'array': amf3encArray,
  'sarray': amf3encArray,
  'binary': amf3encByteArray,
  'true': amf3encTrue,
  'false': amf3encFalse,
  'undefined': amf3encUndefined,
  'null': amf3encNull
};

const amf0dRules = {
  0x00: amf0decNumber,
  0x01: amf0decBool,
  0x02: amf0decString,
  0x03: amf0decObject,
  //    0x04: amf0decMovie, // Reserved
  0x05: amf0decNull,
  0x06: amf0decUndefined,
  0x07: amf0decRef,
  0x08: amf0decArray,
  // 0x09: amf0decObjEnd, // Should never happen normally
  0x0A: amf0decSArray,
  0x0B: amf0decDate,
  0x0C: amf0decLongString,
  //    0x0D: amf0decUnsupported, // Has been never originally implemented by Adobe!
  //    0x0E: amf0decRecSet, // Has been never originally implemented by Adobe!
  0x0F: amf0decXmlDoc,
  0x10: amf0decTypedObj,
  0x11: amf0decSwitchAmf3
};

const amf0eRules = {
  'string': amf0encString,
  'integer': amf0encNumber,
  'double': amf0encNumber,
  'xml': amf0encXmlDoc,
  'object': amf0encObject,
  'array': amf0encArray,
  'sarray': amf0encSArray,
  'binary': amf0encString,
  'true': amf0encBool,
  'false': amf0encBool,
  'undefined': amf0encUndefined,
  'null': amf0encNull
};

function amfType(o) {
  let jsType = typeof o;

  if (o === null) return 'null';
  if (jsType == 'undefined') return 'undefined';
  if (jsType == 'number') {
    if (parseInt(o) == o) return 'integer';
    return 'double';
  }
  if (jsType == 'boolean') return o ? 'true' : 'false';
  if (jsType == 'string') return 'string';
  if (jsType == 'object') {
    if (o instanceof Array) {
      if (o.sarray) return 'sarray';
      return 'array';
    }
    return 'object';
  }
  throw new Error('Unsupported type!')
}

// AMF3 implementation

/**
 * AMF3 Decode undefined value
 * @returns {{len: number, value: undefined}}
 */
function amf3decUndefined() {
  return { len: 1, value: undefined }
}

/**
 * AMF3 Encode undefined value
 * @returns {Buffer}
 */
function amf3encUndefined() {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x00);
  return buf;
}

/**
 * AMF3 Decode null
 * @returns {{len: number, value: null}}
 */
function amf3decNull() {
  return { len: 1, value: null }
}

/**
 * AMF3 Encode null
 * @returns {Buffer}
 */
function amf3encNull() {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x01);
  return buf;
}

/**
 * AMF3 Decode false
 * @returns {{len: number, value: boolean}}
 */
function amf3decFalse() {
  return { len: 1, value: false }
}

/**
 * AMF3 Encode false
 * @returns {Buffer}
 */
function amf3encFalse() {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x02);
  return buf;
}

/**
 * AMF3 Decode true
 * @returns {{len: number, value: boolean}}
 */
function amf3decTrue() {
  return { len: 1, value: true }
}

/**
 * AMF3 Encode true
 * @returns {Buffer}
 */
function amf3encTrue() {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x03);
  return buf;
}

/**
 * Generic decode of AMF3 UInt29 values
 * @param buf
 * @returns {{len: number, value: number}}
 */
function amf3decUI29(buf) {
  let val = 0;
  let len = 1;
  let b;

  do {
    b = buf.readUInt8(len++);
    val = (val << 7) + (b & 0x7F);
  } while (len < 5 || b > 0x7F);

  if (len == 5) val = val | b; // Preserve the major bit of the last byte

  return { len: len, value: val }
}

/**
 * Generic encode of AMF3 UInt29 value
 * @param num
 * @returns {Buffer}
 */
function amf3encUI29(num) {
  let len = 0;
  if (num < 0x80) len = 1;
  if (num < 0x4000) len = 2;
  if (num < 0x200000) len = 3;
  if (num >= 0x200000) len = 4;
  let buf = Buffer.alloc(len);
  switch (len) {
    case 1:
      buf.writeUInt8(num, 0);
      break;
    case 2:
      buf.writeUInt8(num & 0x7F, 0);
      buf.writeUInt8((num >> 7) | 0x80, 1);
      break;
    case 3:
      buf.writeUInt8(num & 0x7F, 0);
      buf.writeUInt8((num >> 7) & 0x7F, 1);
      buf.writeUInt8((num >> 14) | 0x80, 2);
      break;
    case 4:
      buf.writeUInt8(num & 0xFF, 0);
      buf.writeUInt8((num >> 8) & 0x7F, 1);
      buf.writeUInt8((num >> 15) | 0x7F, 2);
      buf.writeUInt8((num >> 22) | 0x7F, 3);
      break;
  }
  return buf;
}

/**
 * AMF3 Decode an integer
 * @param buf
 * @returns {{len: number, value: number}}
 */
function amf3decInteger(buf) { // Invert the integer
  let resp = amf3decUI29(buf);
  if (resp.value > 0x0FFFFFFF) resp.value = (resp.value & 0x0FFFFFFF) - 0x10000000;
  return resp;
}

/**
 * AMF3 Encode an integer
 * @param num
 * @returns {Buffer}
 */
function amf3encInteger(num) {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x4, 0);
  return Buffer.concat([buf, amf3encUI29(num & 0x3FFFFFFF)]); // This AND will auto convert the sign bit!
}

/**
 * AMF3 Decode String
 * @param buf
 * @returns {{len: *, value: (*|String)}}
 */
function amf3decString(buf) {
  let sLen = amf3decUI29(buf);
  let s = sLen & 1;
  sLen = sLen >> 1; // The real length without the lowest bit
  if (s) return { len: sLen.value + 5, value: buf.slice(5, sLen.value + 5).toString('utf8') };
  throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: Implement references!
}

/**
 * AMF3 Encode String
 * @param str
 * @returns {Buffer}
 */
function amf3encString(str) {
  let sLen = amf3encUI29(str.length << 1);
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x6, 0);
  return Buffer.concat([buf, sLen, Buffer.from(str, 'utf8')]);
}

/**
 * AMF3 Decode XMLDoc
 * @param buf
 * @returns {{len: *, value: (*|String)}}
 */
function amf3decXmlDoc(buf) {
  let sLen = amf3decUI29(buf);
  let s = sLen & 1;
  sLen = sLen >> 1; // The real length without the lowest bit
  if (s) return { len: sLen.value + 5, value: buf.slice(5, sLen.value + 5).toString('utf8') };
  throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: Implement references!
}

/**
 * AMF3 Encode XMLDoc
 * @param str
 * @returns {Buffer}
 */
function amf3encXmlDoc(str) {
  let sLen = amf3encUI29(str.length << 1);
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x7, 0);
  return Buffer.concat([buf, sLen, Buffer.from(str, 'utf8')]);
}

/**
 * AMF3 Decode Generic XML
 * @param buf
 * @returns {{len: *, value: (*|String)}}
 */
function amf3decXml(buf) {
  let sLen = amf3decUI29(buf);
  let s = sLen & 1;
  sLen = sLen >> 1; // The real length without the lowest bit
  if (s) return { len: sLen.value + 5, value: buf.slice(5, sLen.value + 5).toString('utf8') };
  throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: Implement references!
}

/**
 * AMF3 Encode Generic XML
 * @param str
 * @returns {Buffer}
 */
function amf3encXml(str) {
  let sLen = amf3encUI29(str.length << 1);
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x0B, 0);
  return Buffer.concat([buf, sLen, Buffer.from(str, 'utf8')]);
}

/**
 * AMF3 Decide Byte Array
 * @param buf
 * @returns {{len: *, value: (Array|string|*|Buffer|Blob)}}
 */
function amf3decByteArray(buf) {
  let sLen = amf3decUI29(buf);
  let s = sLen & 1; // TODO: Check if we follow the same rule!
  sLen = sLen >> 1; // The real length without the lowest bit
  if (s) return { len: sLen.value + 5, value: buf.slice(5, sLen.value + 5) };
  throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: Implement references!
}

/**
 * AMF3 Encode Byte Array
 * @param str
 * @returns {Buffer}
 */
function amf3encByteArray(str) {
  let sLen = amf3encUI29(str.length << 1);
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x0C, 0);
  return Buffer.concat([buf, sLen, (typeof str == 'string') ? Buffer.from(str, 'binary') : str]);
}

/**
 * AMF3 Decode Double
 * @param buf
 * @returns {{len: number, value: (*|Number)}}
 */
function amf3decDouble(buf) {
  return { len: 9, value: buf.readDoubleBE(1) }
}

/**
 * AMF3 Encode Double
 * @param num
 * @returns {Buffer}
 */
function amf3encDouble(num) {
  let buf = Buffer.alloc(9);
  buf.writeUInt8(0x05, 0);
  buf.writeDoubleBE(num, 1);
  return buf;
}

/**
 * AMF3 Decode Date
 * @param buf
 * @returns {{len: *, value: (*|Number)}}
 */
function amf3decDate(buf) {  // The UI29 should be 1
  let uTz = amf3decUI29(buf);
  let ts = buf.readDoubleBE(uTz.len);
  return { len: uTz.len + 8, value: ts }
}

/**
 * AMF3 Encode Date
 * @param ts
 * @returns {Buffer}
 */
function amf3encDate(ts) {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x8, 0);
  let tsBuf = Buffer.alloc(8);
  tsBuf.writeDoubleBE(ts, 0);
  return Buffer.concat([buf, amf3encUI29(1), tsBuf]); // We always do 1
}

/**
 * AMF3 Decode Array
 * @param buf
 * @returns {{len: *, value: *}}
 */
function amf3decArray(buf) {
  let count = amf3decUI29(buf.slice(1));
  let obj = amf3decObject(buf.slice(count.len));
  if (count.value % 2 == 1) throw new Error("This is a reference to another array, which currently we don't support!");
  return { len: count.len + obj.len, value: obj.value }
}

/**
 * AMF3 Encode Array
 */
function amf3encArray() {
  throw new Error('Encoding arrays is not supported yet!'); // TODO: Implement encoding of arrays
}

/**
 * AMF3 Decode Object
 * @param buf
 */
function amf3decObject(buf) {
  let obj = {};
  let pos = 0;
  return obj;
}

/**
 * AMF3 Encode Object
 * @param o
 */
function amf3encObject(o) {

}

// AMF0 Implementation

/**
 * AMF0 Decode Number
 * @param buf
 * @returns {{len: number, value: (*|Number)}}
 */
function amf0decNumber(buf) {
  return { len: 9, value: buf.readDoubleBE(1) }
}

/**
 * AMF0 Encode Number
 * @param num
 * @returns {Buffer}
 */
function amf0encNumber(num) {
  let buf = Buffer.alloc(9);
  buf.writeUInt8(0x00, 0);
  buf.writeDoubleBE(num, 1);
  return buf;
}

/**
 * AMF0 Decode Boolean
 * @param buf
 * @returns {{len: number, value: boolean}}
 */
function amf0decBool(buf) {
  return { len: 2, value: (buf.readUInt8(1) != 0) }
}

/**
 * AMF0 Encode Boolean
 * @param num
 * @returns {Buffer}
 */
function amf0encBool(num) {
  let buf = Buffer.alloc(2);
  buf.writeUInt8(0x01, 0);
  buf.writeUInt8((num ? 1 : 0), 1);
  return buf;
}

/**
 * AMF0 Decode Null
 * @returns {{len: number, value: null}}
 */
function amf0decNull() {
  return { len: 1, value: null }
}

/**
 * AMF0 Encode Null
 * @returns {Buffer}
 */
function amf0encNull() {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x05, 0);
  return buf;
}

/**
 * AMF0 Decode Undefined
 * @returns {{len: number, value: undefined}}
 */
function amf0decUndefined() {
  return { len: 1, value: undefined }
}

/**
 * AMF0 Encode Undefined
 * @returns {Buffer}
 */
function amf0encUndefined() {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x06, 0);
  return buf;
}

/**
 * AMF0 Decode Date
 * @param buf
 * @returns {{len: number, value: (*|Number)}}
 */
function amf0decDate(buf) {
  //    let s16 = buf.readInt16BE(1);
  let ts = buf.readDoubleBE(3);
  return { len: 11, value: ts }
}

/**
 * AMF0 Encode Date
 * @param ts
 * @returns {Buffer}
 */
function amf0encDate(ts) {
  let buf = Buffer.alloc(11);
  buf.writeUInt8(0x0B, 0);
  buf.writeInt16BE(0, 1);
  buf.writeDoubleBE(ts, 3);
  return buf;
}

/**
 * AMF0 Decode Object
 * @param buf
 * @returns {{len: number, value: {}}}
 */
function amf0decObject(buf) { // TODO: Implement references!
  let obj = {};
  let iBuf = buf.slice(1);
  let len = 1;
  //    Logger.debug('ODec',iBuf.readUInt8(0));
  while (iBuf.readUInt8(0) != 0x09) {
    // Logger.debug('Field', iBuf.readUInt8(0), iBuf);
    let prop = amf0decUString(iBuf);
    // Logger.debug('Got field for property', prop);
    len += prop.len;
    if (iBuf.length < prop.len) {
      break;
    }
    if (iBuf.slice(prop.len).readUInt8(0) == 0x09) {
      len++;
      // Logger.debug('Found the end property');
      break;
    } // END Object as value, we shall leave
    if (prop.value == '') break;
    let val = amf0DecodeOne(iBuf.slice(prop.len));
    // Logger.debug('Got field for value', val);
    obj[prop.value] = val.value;
    len += val.len;
    iBuf = iBuf.slice(prop.len + val.len);
  }
  return { len: len, value: obj }
}

/**
 * AMF0 Encode Object
 */
function amf0encObject(o) {
  if (typeof o !== 'object') return;

  let data = Buffer.alloc(1);
  data.writeUInt8(0x03, 0); // Type object
  let k;
  for (k in o) {
    data = Buffer.concat([data, amf0encUString(k), amf0EncodeOne(o[k])]);
  }
  let termCode = Buffer.alloc(1);
  termCode.writeUInt8(0x09, 0);
  return Buffer.concat([data, amf0encUString(''), termCode]);
}

/**
 * AMF0 Decode Reference
 * @param buf
 * @returns {{len: number, value: string}}
 */
function amf0decRef(buf) {
  let index = buf.readUInt16BE(1);
  return { len: 3, value: 'ref' + index }
}

/**
 * AMF0 Encode Reference
 * @param index
 * @returns {Buffer}
 */
function amf0encRef(index) {
  let buf = Buffer.alloc(3);
  buf.writeUInt8(0x07, 0);
  buf.writeUInt16BE(index, 1);
  return buf;
}

/**
 * AMF0 Decode String
 * @param buf
 * @returns {{len: *, value: (*|string|String)}}
 */
function amf0decString(buf) {
  let sLen = buf.readUInt16BE(1);
  return { len: 3 + sLen, value: buf.toString('utf8', 3, 3 + sLen) }
}

/**
 * AMF0 Decode Untyped (without the type byte) String
 * @param buf
 * @returns {{len: *, value: (*|string|String)}}
 */
function amf0decUString(buf) {
  let sLen = buf.readUInt16BE(0);
  return { len: 2 + sLen, value: buf.toString('utf8', 2, 2 + sLen) }
}

/**
 * Do AMD0 Encode of Untyped String
 * @param s
 * @returns {Buffer}
 */
function amf0encUString(str) {
  let data = Buffer.from(str, 'utf8');
  let sLen = Buffer.alloc(2);
  sLen.writeUInt16BE(data.length, 0);
  return Buffer.concat([sLen, data]);
}

/**
 * AMF0 Encode String
 * @param str
 * @returns {Buffer}
 */
function amf0encString(str) {
  let buf = Buffer.alloc(3);
  buf.writeUInt8(0x02, 0);
  buf.writeUInt16BE(str.length, 1);
  return Buffer.concat([buf, Buffer.from(str, 'utf8')]);
}

/**
 * AMF0 Decode Long String
 * @param buf
 * @returns {{len: *, value: (*|string|String)}}
 */
function amf0decLongString(buf) {
  let sLen = buf.readUInt32BE(1);
  return { len: 5 + sLen, value: buf.toString('utf8', 5, 5 + sLen) }
}

/**
 * AMF0 Encode Long String
 * @param str
 * @returns {Buffer}
 */
function amf0encLongString(str) {
  let buf = Buffer.alloc(5);
  buf.writeUInt8(0x0C, 0);
  buf.writeUInt32BE(str.length, 1);
  return Buffer.concat([buf, Buffer.from(str, 'utf8')]);
}

/**
 * AMF0 Decode Array
 * @param buf
 * @returns {{len: *, value: ({}|*)}}
 */
function amf0decArray(buf) {
  //    let count = buf.readUInt32BE(1);
  let obj = amf0decObject(buf.slice(4));
  return { len: 5 + obj.len, value: obj.value }
}

/**
 * AMF0 Encode Array
 */
function amf0encArray(a) {
  let l = 0;
  if (a instanceof Array) l = a.length; else l = Object.keys(a).length;
  Logger.debug('Array encode', l, a);
  let buf = Buffer.alloc(5);
  buf.writeUInt8(8, 0);
  buf.writeUInt32BE(l, 1);
  let data = amf0encObject(a);
  return Buffer.concat([buf, data.slice(1)]);
}

/**
 * AMF0 Encode Binary Array into binary Object
 * @param aData
 * @returns {Buffer}
 */
function amf0cnletray2Object(aData) {
  let buf = Buffer.alloc(1);
  buf.writeUInt8(0x3, 0); // Object id
  return Buffer.concat([buf, aData.slice(5)]);
}

/**
 * AMF0 Encode Binary Object into binary Array
 * @param oData
 * @returns {Buffer}
 */
function amf0cnvObject2Array(oData) {
  let buf = Buffer.alloc(5);
  let o = amf0decObject(oData);
  let l = Object.keys(o).length;
  buf.writeUInt32BE(l, 1);
  return Buffer.concat([buf, oData.slice(1)]);
}

/**
 * AMF0 Decode XMLDoc
 * @param buf
 * @returns {{len: *, value: (*|string|String)}}
 */
function amf0decXmlDoc(buf) {
  let sLen = buf.readUInt16BE(1);
  return { len: 3 + sLen, value: buf.toString('utf8', 3, 3 + sLen) }
}

/**
 * AMF0 Encode XMLDoc
 * @param str
 * @returns {Buffer}
 */
function amf0encXmlDoc(str) { // Essentially it is the same as string
  let buf = Buffer.alloc(3);
  buf.writeUInt8(0x0F, 0);
  buf.writeUInt16BE(str.length, 1);
  return Buffer.concat([buf, Buffer.from(str, 'utf8')]);
}

/**
 * AMF0 Decode Strict Array
 * @param buf
 * @returns {{len: number, value: Array}}
 */
function amf0decSArray(buf) {
  let a = [];
  let len = 5;
  let ret;
  for (let count = buf.readUInt32BE(1); count; count--) {
    ret = amf0DecodeOne(buf.slice(len));
    a.push(ret.value);
    len += ret.len;
  }
  return { len: len, value: amf0markSArray(a) }
}

/**
 * AMF0 Encode Strict Array
 * @param a Array
 */
function amf0encSArray(a) {
  Logger.debug('Do strict array!');
  let buf = Buffer.alloc(5);
  buf.writeUInt8(0x0A, 0);
  buf.writeUInt32BE(a.length, 1);
  let i;
  for (i = 0; i < a.length; i++) {
    buf = Buffer.concat([buf, amf0EncodeOne(a[i])]);
  }
  return buf;
}

function amf0markSArray(a) {
  Object.defineProperty(a, 'sarray', { value: true });
  return a;
}

/**
 * AMF0 Decode Typed Object
 * @param buf
 * @returns {{len: number, value: ({}|*)}}
 */
function amf0decTypedObj(buf) {
  let className = amf0decString(buf);
  let obj = amf0decObject(buf.slice(className.len - 1));
  obj.value.__className__ = className.value;
  return { len: className.len + obj.len - 1, value: obj.value }
}

/**
 * AMF0 Decode Switch AMF3 Object
 * @param buf
 * @returns {{len: number, value: ({}|*)}}
 */
function amf0decSwitchAmf3(buf) {
  let r = amf3DecodeOne(buf.slice(1));
  return r;
}

/**
 * AMF0 Encode Typed Object
 */
function amf0encTypedObj() {
  throw new Error("Error: SArray encoding is not yet implemented!"); // TODO: Error
}

/**
 * Decode one value from the Buffer according to the applied rules
 * @param rules
 * @param buffer
 * @returns {*}
 */
function amfXDecodeOne(rules, buffer) {
  if (!rules[buffer.readUInt8(0)]) {
    Logger.error('Unknown field', buffer.readUInt8(0));
    return null;
  }
  return rules[buffer.readUInt8(0)](buffer);
}

/**
 * Decode one AMF0 value
 * @param buffer
 * @returns {*}
 */
function amf0DecodeOne(buffer) {
  return amfXDecodeOne(amf0dRules, buffer);
}

/**
 * Decode one AMF3 value
 * @param buffer
 * @returns {*}
 */
function amf3DecodeOne(buffer) {
  return amfXDecodeOne(amf3dRules, buffer);
}

/**
 * Decode a whole buffer of AMF values according to rules and return in array
 * @param rules
 * @param buffer
 * @returns {Array}
 */
function amfXDecode(rules, buffer) {
  // We shall receive clean buffer and will respond with an array of values
  let resp = [];
  let res;
  for (let i = 0; i < buffer.length;) {
    res = amfXDecodeOne(rules, buffer.slice(i));
    i += res.len;
    resp.push(res.value); // Add the response
  }
  return resp;
}

/**
 * Decode a buffer of AMF3 values
 * @param buffer
 * @returns {Array}
 */
function amf3Decode(buffer) {
  return amfXDecode(amf3dRules, buffer);
}

/**
 * Decode a buffer of AMF0 values
 * @param buffer
 * @returns {Array}
 */
function amf0Decode(buffer) {
  return amfXDecode(amf0dRules, buffer);
}

/**
 * Encode one AMF value according to rules
 * @param rules
 * @param o
 * @returns {*}
 */
function amfXEncodeOne(rules, o) {
  //    Logger.debug('amfXEncodeOne type',o,amfType(o),rules[amfType(o)]);
  let f = rules[amfType(o)];
  if (f) return f(o);
  throw new Error('Unsupported type for encoding!');
}

/**
 * Encode one AMF0 value
 * @param o
 * @returns {*}
 */
function amf0EncodeOne(o) {
  return amfXEncodeOne(amf0eRules, o);
}

/**
 * Encode one AMF3 value
 * @param o
 * @returns {*}
 */
function amf3EncodeOne(o) {
  return amfXEncodeOne(amf3eRules, o);
}

/**
 * Encode an array of values into a buffer
 * @param a
 * @returns {Buffer}
 */
function amf3Encode(a) {
  let buf = Buffer.alloc(0);
  a.forEach(function (o) {
    buf = Buffer.concat([buf, amf3EncodeOne(o)]);
  });
  return buf;
}

/**
 * Encode an array of values into a buffer
 * @param a
 * @returns {Buffer}
 */
function amf0Encode(a) {
  let buf = Buffer.alloc(0);
  a.forEach(function (o) {
    buf = Buffer.concat([buf, amf0EncodeOne(o)]);
  });
  return buf;
}


const rtmpCmdCode = {
  "_result": ["transId", "cmdObj", "info"],
  "_error": ["transId", "cmdObj", "info", "streamId"], // Info / Streamid are optional
  "onStatus": ["transId", "cmdObj", "info"],
  "releaseStream": ["transId", "cmdObj", "streamName"],
  "getStreamLength": ["transId", "cmdObj", "streamId"],
  "getMovLen": ["transId", "cmdObj", "streamId"],
  "FCPublish": ["transId", "cmdObj", "streamName"],
  "FCUnpublish": ["transId", "cmdObj", "streamName"],
  "FCSubscribe": ["transId", "cmdObj", "streamName"],
  "onFCPublish": ["transId", "cmdObj", "info"],
  "connect": ["transId", "cmdObj", "args"],
  "call": ["transId", "cmdObj", "args"],
  "createStream": ["transId", "cmdObj"],
  "close": ["transId", "cmdObj"],
  "play": ["transId", "cmdObj", "streamName", "start", "duration", "reset"],
  "play2": ["transId", "cmdObj", "params"],
  "deleteStream": ["transId", "cmdObj", "streamId"],
  "closeStream": ["transId", "cmdObj"],
  "receiveAudio": ["transId", "cmdObj", "bool"],
  "receiveVideo": ["transId", "cmdObj", "bool"],
  "publish": ["transId", "cmdObj", "streamName", "type"],
  "seek": ["transId", "cmdObj", "ms"],
  "pause": ["transId", "cmdObj", "pause", "ms"]
};

const rtmpDataCode = {
  "@setDataFrame": ["method", "dataObj"],
  "onFI":["info"],
  "onMetaData": ["dataObj"],
  "|RtmpSampleAccess": ["bool1", "bool2"],
};


/**
 * Decode a data!
 * @param dbuf
 * @returns {{cmd: (*|string|String|*), value: *}}
 */
function decodeAmf0Data(dbuf) {
  let buffer = dbuf;
  let resp = {};

  let cmd = amf0DecodeOne(buffer);
  if (cmd) {
    resp.cmd = cmd.value;
    buffer = buffer.slice(cmd.len);

    if (rtmpDataCode[cmd.value]) {
      rtmpDataCode[cmd.value].forEach(function (n) {
        if (buffer.length > 0) {
          let r = amf0DecodeOne(buffer);
          if (r) {
            buffer = buffer.slice(r.len);
            resp[n] = r.value;
          }
        }
      });
    } else {
      Logger.error('Unknown command', resp);
    }
  }

  return resp
}

/**
 * Decode a command!
 * @param dbuf
 * @returns {{cmd: (*|string|String|*), value: *}}
 */
function decodeAMF0Cmd(dbuf) {
  let buffer = dbuf;
  let resp = {};

  let cmd = amf0DecodeOne(buffer);
  resp.cmd = cmd.value;
  buffer = buffer.slice(cmd.len);

  if (rtmpCmdCode[cmd.value]) {
    rtmpCmdCode[cmd.value].forEach(function (n) {
      if (buffer.length > 0) {
        let r = amf0DecodeOne(buffer);
        buffer = buffer.slice(r.len);
        resp[n] = r.value;
      }
    });
  } else {
    Logger.error('Unknown command', resp);
  }
  return resp
}

/**
 * Encode AMF0 Command
 * @param opt
 * @returns {*}
 */
function encodeAMF0Cmd(opt) {
  let data = amf0EncodeOne(opt.cmd);

  if (rtmpCmdCode[opt.cmd]) {
    rtmpCmdCode[opt.cmd].forEach(function (n) {
      if (opt.hasOwnProperty(n))
        data = Buffer.concat([data, amf0EncodeOne(opt[n])]);
    });
  } else {
    Logger.error('Unknown command', opt);
  }
  // Logger.debug('Encoded as',data.toString('hex'));
  return data
}

function encodeAMF0Data(opt) {
  let data = amf0EncodeOne(opt.cmd);

  if (rtmpDataCode[opt.cmd]) {
    rtmpDataCode[opt.cmd].forEach(function (n) {
      if (opt.hasOwnProperty(n))
        data = Buffer.concat([data, amf0EncodeOne(opt[n])]);
    });
  } else {
    Logger.error('Unknown data', opt);
  }
  // Logger.debug('Encoded as',data.toString('hex'));
  return data
}

/**
 *
 * @param dbuf
 * @returns {{}}
 */
function decodeAMF3Cmd(dbuf) {
  let buffer = dbuf;
  let resp = {};

  let cmd = amf3DecodeOne(buffer);
  resp.cmd = cmd.value;
  buffer = buffer.slice(cmd.len);

  if (rtmpCmdCode[cmd.value]) {
    rtmpCmdCode[cmd.value].forEach(function (n) {
      if (buffer.length > 0) {
        let r = amf3DecodeOne(buffer);
        buffer = buffer.slice(r.len);
        resp[n] = r.value;
      }
    });
  } else {
    Logger.error('Unknown command', resp);
  }
  return resp
}

/**
 * Encode AMF3 Command
 * @param opt
 * @returns {*}
 */
function encodeAMF3Cmd(opt) {
  let data = amf0EncodeOne(opt.cmd);

  if (rtmpCmdCode[opt.cmd]) {
    rtmpCmdCode[opt.cmd].forEach(function (n) {
      if (opt.hasOwnProperty(n))
        data = Buffer.concat([data, amf3EncodeOne(opt[n])]);
    });
  } else {
    Logger.error('Unknown command', opt);
  }
  return data
}

module.exports = {
  decodeAmf3Cmd: decodeAMF3Cmd,
  encodeAmf3Cmd: encodeAMF3Cmd,
  decodeAmf0Cmd: decodeAMF0Cmd,
  encodeAmf0Cmd: encodeAMF0Cmd,
  decodeAmf0Data: decodeAmf0Data,
  encodeAmf0Data: encodeAMF0Data,
  amfType: amfType,
  amf0Encode: amf0Encode,
  amf0EncodeOne: amf0EncodeOne,
  amf0Decode: amf0Decode,
  amf0DecodeOne: amf0DecodeOne,
  amf3Encode: amf3Encode,
  amf3EncodeOne: amf3EncodeOne,
  amf3Decode: amf3Decode,
  amf3DecodeOne: amf3DecodeOne,
  amf0cnvA2O: amf0cnletray2Object,
  amf0cnvO2A: amf0cnvObject2Array,
  amf0markSArray: amf0markSArray,
  amf0decArray: amf0decArray,
  amf0decBool: amf0decBool,
  amf0decDate: amf0decDate,
  amf0decLongString: amf0decLongString,
  amf0decNull: amf0decNull,
  amf0decNumber: amf0decNumber,
  amf0decObject: amf0decObject,
  amf0decRef: amf0decRef,
  amf0decSArray: amf0decSArray,
  amf0decString: amf0decString,
  amf0decTypedObj: amf0decTypedObj,
  amf0decUndefined: amf0decUndefined,
  amf0decXmlDoc: amf0decXmlDoc,
  amf0encArray: amf0encArray,
  amf0encBool: amf0encBool,
  amf0encDate: amf0encDate,
  amf0encLongString: amf0encLongString,
  amf0encNull: amf0encNull,
  amf0encNumber: amf0encNumber,
  amf0encObject: amf0encObject,
  amf0encRef: amf0encRef,
  amf0encSArray: amf0encSArray,
  amf0encString: amf0encString,
  amf0encTypedObj: amf0encTypedObj,
  amf0encUndefined: amf0encUndefined,
  amf0encXmlDoc: amf0encXmlDoc,
  amf3decArray: amf3decArray,
  amf3decByteArray: amf3decByteArray,
  amf3decDate: amf3decDate,
  amf3decDouble: amf3decDouble,
  amf3decFalse: amf3decFalse,
  amf3decInteger: amf3decInteger,
  amf3decNull: amf3decNull,
  amf3decObject: amf3decObject,
  amf3decString: amf3decString,
  amf3decTrue: amf3decTrue,
  amf3decUI29: amf3decUI29,
  amf3decUndefined: amf3decUndefined,
  amf3decXml: amf3decXml,
  amf3decXmlDoc: amf3decXmlDoc,
  amf3encArray: amf3encArray,
  amf3encByteArray: amf3encByteArray,
  amf3encDate: amf3encDate,
  amf3encDouble: amf3encDouble,
  amf3encFalse: amf3encFalse,
  amf3encInteger: amf3encInteger,
  amf3encNull: amf3encNull,
  amf3encObject: amf3encObject,
  amf3encString: amf3encString,
  amf3encTrue: amf3encTrue,
  amf3encUI29: amf3encUI29,
  amf3encUndefined: amf3encUndefined,
  amf3encXml: amf3encXml,
  amf3encXmlDoc: amf3encXmlDoc
};
