'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }

var fs = _interopDefault(require('fs'));

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }

  return obj;
}

// magic number for file type
const MAGIC_NUMBER = '.tbj';
const SIZE_MAGIC_NUMBER = 4; // primitive types

const NULL = 0;
const BOOL = 1;
const UINT8 = 2;
const INT8 = 3;
const UINT16 = 4;
const INT16 = 5;
const UINT32 = 6;
const INT32 = 7;
const FLOAT32 = 8;
const FLOAT64 = 9; // higher-order types

const STRING = 10;
const ARRAY = 11;
const OBJECT = 12;
const NULLABLE = 13;
const TYPED_ARRAY = 14; // primitive sizes
const SIZE_INT8 = 1;
const SIZE_UINT8 = 1;
const SIZE_INT16 = 2;
const SIZE_UINT16 = 2;
const SIZE_INT32 = 4;
const SIZE_UINT32 = 4;
const SIZE_FLOAT32 = 4;
const SIZE_FLOAT64 = 8; // offsets

const NULLABLE_OFFSET = 16;
const TYPED_ARRAY_OFFSET = 32;
const TYPE_OFFSET = 48;
const PROTOTYPE_OFFSET = 64;
const ARRAY_OFFSET = 256;

class Type {
  constructor(reference, serializer, deserializer) {
    this.reference = reference;
    this.serializer = serializer;
    this.deserializer = deserializer;
  }

}

class Prototype {
  constructor(definition, prototype = null, parentCode = null, noInherit = false) {
    this.definition = definition;
    this.prototype = prototype;
    this.parentCode = parentCode;
    this.noInherit = noInherit;
  }

}

const DEFAULT_BUFFER_SIZE = 16384;
const DEFAULT_X_FACTOR = 2;
const DEFAULT_STR_ENCODING = 'utf-8';
class BufferWriter {
  constructor(size = DEFAULT_BUFFER_SIZE, xFactor = DEFAULT_X_FACTOR, strEncoding = DEFAULT_STR_ENCODING) {
    _defineProperty(this, "offset", 0);

    this.buffer = Buffer.allocUnsafe(size);
    this.xFactor = xFactor;
    this.strEncoding = strEncoding;
  }

  get size() {
    return this.buffer.length;
  }

  getBuffer() {
    return this.buffer.slice(0, this.offset);
  }

  grow() {
    this.buffer = Buffer.concat([this.buffer, Buffer.allocUnsafe(this.size * Math.floor(this.xFactor / 2))]);
  }

  writeFixedLengthString(val) {
    this.buffer.write(val, this.offset, val.length, this.strEncoding);
    this.offset += val.length;
  }

  write(type, val) {
    switch (type) {
      case NULL:
        val = 0;

      case BOOL:
      case UINT8:
        this.checkSize(SIZE_UINT8);
        this.buffer.writeUInt8(val, this.offset);
        this.offset += SIZE_UINT8;
        break;

      case INT8:
        this.checkSize(SIZE_INT8);
        this.buffer.writeInt8(val, this.offset);
        this.offset += SIZE_INT8;
        break;

      case UINT16:
        this.checkSize(SIZE_UINT16);
        this.buffer.writeUInt16LE(val, this.offset);
        this.offset += SIZE_UINT16;
        break;

      case INT16:
        this.checkSize(SIZE_INT16);
        this.buffer.writeInt16LE(val, this.offset);
        this.offset += SIZE_INT16;
        break;

      case UINT32:
        this.checkSize(SIZE_UINT32);
        this.buffer.writeUInt32LE(val, this.offset);
        this.offset += SIZE_UINT32;
        break;

      case INT32:
        this.checkSize(SIZE_INT32);
        this.buffer.writeInt32LE(val, this.offset);
        this.offset += SIZE_INT32;
        break;

      case FLOAT32:
        this.checkSize(SIZE_FLOAT32);
        this.buffer.writeFloatLE(val, this.offset);
        this.offset += SIZE_FLOAT32;
        break;

      case FLOAT64:
        this.checkSize(SIZE_FLOAT64);
        this.buffer.writeDoubleLE(val, this.offset);
        this.offset += SIZE_FLOAT64;
        break;

      case STRING:
        this.checkSize(val.length + SIZE_UINT8);
        this.buffer.write(val, this.offset, val.length, this.strEncoding);
        this.offset += val.length;
        this.buffer.writeUInt8(0, this.offset);
        this.offset += SIZE_UINT8;
    }
  }

  writeBuffer(buffer) {
    this.checkSize(buffer.length);
    buffer.copy(this.buffer, this.offset);
    this.offset += buffer.length;
  }
  /* private */


  checkSize(size) {
    while (this.offset + size > this.size) {
      this.grow();
    }
  }

}

class BufferReader {
  constructor(buffer) {
    _defineProperty(this, "offset", 0);

    this.buffer = buffer;
  }

  read(type, length = 0) {
    let data;

    switch (type) {
      case BOOL:
        data = !!this.buffer.readUInt8(this.offset);
        this.offset += SIZE_UINT8;
        break;

      case UINT8:
        data = this.buffer.readUInt8(this.offset);
        this.offset += SIZE_UINT8;
        break;

      case INT8:
        data = this.buffer.readInt8(this.offset);
        this.offset += SIZE_INT8;
        break;

      case UINT16:
        data = this.buffer.readUInt16LE(this.offset);
        this.offset += SIZE_UINT16;
        break;

      case INT16:
        data = this.buffer.readInt16LE(this.offset);
        this.offset += SIZE_INT16;
        break;

      case UINT32:
        data = this.buffer.readUInt32LE(this.offset);
        this.offset += SIZE_UINT32;
        break;

      case INT32:
        data = this.buffer.readInt32LE(this.offset);
        this.offset += SIZE_INT32;
        break;

      case FLOAT32:
        data = this.buffer.readFloatLE(this.offset);
        this.offset += SIZE_FLOAT32;
        break;

      case FLOAT64:
        data = this.buffer.readDoubleLE(this.offset);
        this.offset += SIZE_FLOAT64;
        break;

      case STRING:
        if (length) {
          data = this.buffer.toString('utf-8', this.offset, this.offset + length);
          this.offset += length;
        } else {
          length = this.nextNullAt();
          data = this.buffer.toString('utf-8', this.offset, length);
          this.offset = length + 1;
        }

        break;
    }

    return data;
  }

  readTypedArray(type, length) {
    let byteOffset = this.buffer.byteOffset + this.offset;
    let buffer = this.buffer.buffer.slice(byteOffset, byteOffset + length);
    this.offset += length;

    switch (type) {
      case UINT8:
        return new Uint8Array(buffer);

      case INT8:
        return new Int8Array(buffer);

      case UINT16:
        return new Uint16Array(buffer);

      case INT16:
        return new Int16Array(buffer);

      case UINT32:
        return new Uint32Array(buffer);

      case INT32:
        return new Int32Array(buffer);

      case FLOAT32:
        return new Float32Array(buffer);

      case FLOAT64:
        return new Float64Array(buffer);
    }
  }
  /* private */


  nextNullAt() {
    for (let i = this.offset; i < this.buffer.length; ++i) {
      if (!this.buffer[i]) {
        return i;
      }
    }

    throw new Error('BufferReader could not find a null value');
  }

}

class StreamBufferWriter extends BufferWriter {
  constructor(stream, size, xFactor, strEncoding) {
    super(size, xFactor, strEncoding);

    _defineProperty(this, "streamIndex", 0);

    _defineProperty(this, "streamReady", true);

    this.stream = stream;
  }
  /* private */


  flush() {
    this.streamReady = this.stream.write(this.buffer.slice(this.streamIndex, this.offset), () => {
      this.streamReady = true;
    });

    if (this.streamReady) {
      this.offset = 0;
      this.streamIndex = 0;
    } else {
      this.streamIndex = this.offset;
    }

    return this.streamReady;
  }

  checkSize(size) {
    while (this.offset + size > this.size) {
      if (this.streamReady && !this.flush()) {
        this.grow();
      } else {
        this.grow();
      }
    }
  }

}

class StreamBufferReader {
  constructor(stream, size = 8388608) {
    this.stream = stream;
    this.size = size;
    this.tempSize = size;
    this.buffer = Buffer.allocUnsafe(size);
    this.writeOffset = 0;
    this.readOffset = 0;
    this.stream.on('data', chunk => {
      if (this.writeOffset + chunk.length > this.tempSize) {
        this.stream.pause();
      }

      this.buffer.fill(chunk, this.writeOffset, this.writeOffset + chunk.length);
      this.writeOffset += chunk.length;

      if (this.waitingRead) {
        this.waitingRead();
      }
    });
  }

  readUntilNull(fn) {
    for (let i = this.readOffset; i < this.buffer.length; ++i) {
      if (this.buffer[i] == null) {
        fn(this.buffer.slice(this.offset, i));
        this.incReadOffset(i - this.readOffset);
      }
    }
  }

  read(type, length = 0) {
    switch (type) {
      case UINT32:
        this.readBytes(SIZE_UINT32, readOffset => fn(this.buffer.readUInt32(readOffset)));
        break;

      case FLOAT32:
        this.readBytes(SIZE_FLOAT32, readOffset => fn(this.buffer.readFloat32(readOffset)));
        break;

      case STRING:
        if (length) {
          this.readBytes(length, readOffset => fn(this.buffer.toString('utf-8', readOffset, length)));
        } else {
          this.readUntilNull();
        }

    }
  }
  /* private */


  incReadOffset(length) {
    this.readOffset += length;

    if (this.readOffset > this.size) {
      this.writeOffset = this.buffer.length - this.writeOffset;
      this.readOffset = 0;
      this.newBuffer = Buffer.allocUnsafe(this.size);
      this.newBuffer.fill(this.offset, this.buffer.length);
      this.buffer = this.newBuffer;

      if (this.stream.isPaused()) {
        this.stream.resume();
      }
    }
  }

  readBytes(length) {
    if (this.readOffset + length > this.writeOffset) {
      return new Promise((res, rej) => {
        if (this.size < this.readOffset + length) {
          this.tmpSize = this.readOffset + length;
        }

        this.waitingRead = () => {
          this.tempSize = this.size;
          this.readBytes(length, fn);
        };
      });
    } else {
      let readOffset = this.readOffset;
      this.incReadOffset(length);
      return readOffset;
    }
  }

}

const DEFAULT_STR_ENCODING$1 = 'utf-8';
const DEFAULT_NUM_ENCODING = FLOAT64;
const DEFAULT_BUFFER_SIZE$1 = 1048576;
/**
 * Tbjson
 * 
 * A JS TBJSON serializer and parser.
 */

class Tbjson {
  // TODO: for registered types (primitives)
  // for registered prototypes (classes)
  // for plain objects that are inside of known prototypers
  // maybe TODO: objRefs = {};
  // binary definition tree
  // counters for converting types and prototypes to an incrementing numeric value
  // defaults
  constructor(types = [], prototypes = [], options = {}) {
    _defineProperty(this, "typeRefs", {});

    _defineProperty(this, "types", {});

    _defineProperty(this, "protoRefs", {});

    _defineProperty(this, "protos", {});

    _defineProperty(this, "objs", {});

    _defineProperty(this, "root", null);

    _defineProperty(this, "nextTypeCode", TYPE_OFFSET);

    _defineProperty(this, "nextProtoCode", PROTOTYPE_OFFSET);

    _defineProperty(this, "nextObjCode", 0);

    _defineProperty(this, "finalized", false);

    _defineProperty(this, "options", {
      encStringAs: DEFAULT_STR_ENCODING$1,
      encNumberAs: DEFAULT_NUM_ENCODING,
      bufferSize: DEFAULT_BUFFER_SIZE$1
    });

    this.registerTypes(types);
    this.registerPrototypes(prototypes);
    this.options = { ...this.options,
      ...options
    };
  }
  /*-----------------------------------------------------------------------*/

  /* registers */

  /**
   * Register a prototype / class or plain objecct for serilization and deserialization.
   * If using Class.tbjson = { ... } you must call this for each class, and then call finalizePrototypes for inheritance to work.
   * 
   * Example:
   *
   * Tbjson.registerPrototype(Point); // point must have tbjson set on it: Point.tbjson = { definition: ... } 
   * 
   * Tbjson.registerPrototype({
   *     prototype: Point1,
   *     definition: {
   *         x: Tbjson.TYPES.FLOAT32,
   *         y: Tbjson.TYPES.FLOAT32
   *     },
   *     reference: 'Point',
   *     parentReference: Point0
   * });
   * 
   * Tbjson.registerPrototype({
   *     reference: Point,
   *     definition: {
   *         x: Tbjson.TYPES.FLOAT32,
   *         y: Tbjson.TYPES.FLOAT32
   * });
   * 
   * @param { function | object } prototype - class / prototype constructor or a plain object that represents one
   */


  registerPrototype(prototype) {
    if (this.finalized) {
      return;
    } // a prototype


    if (typeof prototype == 'function') {
      // check if it's a known tbjson prototype
      if (prototype.tbjson) {
        // TODO: REMOVE THIS
        if (!prototype.tbjson.definition) {
          throw new Error(`Missing definition for "${prototype.name}"`);
        }

        prototype = {
          prototype: prototype,
          ...prototype.tbjson
        };
      } else {
        prototype = {
          prototype
        };
      }
    } // if the ref is not set, use the constructor's name


    if (!prototype.reference) {
      prototype.reference = prototype.prototype.name;
    }

    let code = this.protoRefs[prototype.reference]; // assign a new reference and definition

    if (!code) {
      code = this.nextProtoCode++;
      this.protoRefs[prototype.reference] = code;
    } // this code has not been defined


    if (!this.protos[code] || !this.protos[code].definition) {
      let parentCode; // get the parent code

      if (prototype.definition) {
        let parent = !prototype.noInherit && prototype.parentReference ? prototype.parentReference : getParent(prototype.prototype);
        parentCode = parent ? this.registerPrototype(parent) : null;
      } // set the prototype


      this.protos[code] = new Prototype(this.fmtDef(prototype.definition), prototype.prototype, parentCode, prototype.noInherit);
    }

    return code;
  }
  /**
   * Register an array of prototypes.
   * 
   * Example:
   * 
   * [{
   *     constructor: Point,
   *     definition: {
   *         x: Tbjson.TYPES.FLOAT32,
   *         y: Tbjson.TYPES.FLOAT32,
   *         z: Tbjson.TYPES.FLOAT32
   *     }
   * }, {
   *     constructor: Line,
   *     reference: 'Line2',
   *     parentReference: 'Line1',
   *     noInherit: true,
   *     definition: {
   *         point1: 'Point',
   *         point2: 'Point'
   *     }
   * }]
   * 
   * @param {[]object} prototypes - array of prototypes 
   */


  registerPrototypes(prototypes = []) {
    for (let prototype of prototypes) {
      this.registerPrototype(prototype);
    }
  }
  /**
   * TODO:
   * Register a type.
   * 
   * Example:
   * 
   * tbjson.registerType('Float48', (data, buffer) => {}, (buffer) => obj);
   * 
   * @param {object} type - type to add
   */


  registerType(type) {}
  /**
   * TODO:
   * Register types.
   * 
   * Example:
   * 
   * [{
   *     ref: 'Float48',
   *     serializer: function(data, buffer) {
   *         buffer.writeUint8(...);
   *     },
   *     deserializer: function(buffer) {
   *         let num = buffer.readUint8(...);
   *         return num;
   *     }
   * }]
   * 
   * @param {[]object} types - array of types to register 
   */


  registerTypes(types = []) {
    for (let type of types) {
      this.registerType(ref, type.serializer, type.deserializer);
    }
  }
  /**
   * If using inheritance, this must be called before serialization to update definitions.
   */


  finalizePrototypes() {
    let finalizedProtos = {};

    while (Object.keys(finalizedProtos).length < Object.keys(this.protos).length) {
      for (let code in this.protos) {
        // don't run on finalized prototypes
        if (finalizedProtos[code]) {
          continue;
        }

        let prototype = this.protos[code]; // finalize if there is no parent code or if the prototype is set to not inherit

        if (!prototype.parentCode || prototype.noInherit) {
          finalizedProtos[code] = true;
          continue;
        } // throw an error if a parent code is missing


        if (!this.protos[prototype.parentCode]) {
          throw new Error('Missing a parent prototype or definition');
        } // parent is finalized, so this can be to


        if (finalizedProtos[prototype.parentCode]) {
          prototype.definition = Object.assign({}, this.protos[prototype.parentCode].definition, prototype.definition);
          finalizedProtos[code] = true;
        }
      }
    }

    this.finalized = true;
  }
  /*-----------------------------------------------------------------------*/

  /* serializers */

  /**
   * Serialize the obj to a buffer.  Fastest, but uses the most memory.
   * 
   * @param {object} obj - object to serialize 
   */


  serializeToBuffer(obj) {
    try {
      // make a writer
      this.writer = new BufferWriter(this.options.bufferSize); // process the obj

      this.root = this.serialize(obj); // add the header to the front

      return Buffer.concat([this.getHeaderAsBuffer(), this.writer.getBuffer()]);
    } catch (e) {
      e.message = 'Tbjson failed to serialize to the buffer: ' + e.message;
      throw e;
    }
  }
  /**
   * Serialize the object to the stream.  Slower, but uses the least memory.
   * 
   * @param {stream} stream - stream to serialize to
   * @param {object} obj - object to serialize 
   */


  serializeToStream(stream, obj) {
    try {
      // make a writer
      this.writer = new StreamBufferWriter(stream, this.options.bufferSize); // process the obj

      this.root = this.serialize(obj); // flush and cleanup

      this.writer.flush();
      this.writer = null;
    } catch (e) {
      e.message = 'Tbjson failed to serialize to the stream: ' + e.message;
      throw e;
    }
  }
  /**
   * Serialize the object to a file. Opens as a write stream, so it's slower and uses less memory.
   * 
   * @param {string} filename - filename / path to write to
   * @param {object} obj - object to serialize
   */


  serializeToFile(filename, obj) {
    return new Promise((res, rej) => {
      try {
        let tempFilename = `${filename}.tmp`; // write the data to a tmp file

        let writeStream = fs.createWriteStream(tempFilename, 'binary');
        this.serializeToStream(writeStream, obj);
        writeStream.end(); // write the final file

        writeStream = fs.createWriteStream(filename, 'binary'); // write the header

        writeStream.write(this.getHeaderAsBuffer()); // pipe the tmp file to the final file

        let readStream = fs.createReadStream(tempFilename, 'binary');
        readStream.pipe(writeStream);
        readStream.on('end', () => {
          // cleanup
          fs.unlinkSync(tempFilename);
          res();
        });
      } catch (e) {
        e.message = `Tbjson Failed to serialize object to "${filename}": ` + e.message;
        rej(e);
      }
    });
  }
  /*-----------------------------------------------------------------------*/

  /* parsers */

  /**
   * Parse a TBJSON containing buffer into ab object. Fastest, but uses the most memory.
   * 
   * @param {buffer} buffer - buffer to read from
   */


  parseBuffer(buffer) {
    try {
      if (!buffer) {
        throw new Error('Null buffer passed in');
      }

      this.reader = new BufferReader(buffer); // validate the buffer type

      if (this.reader.read(STRING, SIZE_MAGIC_NUMBER) != MAGIC_NUMBER) {
        throw new Error('Buffer is not a Typed Binary JSON format');
      } // get the header length


      let headerLength = this.reader.read(UINT32); // read and parse the header

      this.parseHeader(this.reader.read(STRING, headerLength));
      console.log(this.protoRefs);
      console.log(this.protos); // construct the object

      return this.parse(this.root);
    } catch (e) {
      e.message = 'Tbjson failed to parse the buffer: ' + e.message;
      throw e;
    }
  }
  /**
   * TODO:
   * Parse a TBJSON containing stream into an object. Slower, but uses the least memory.
   * 
   * @param {stream} stream - stream to read from
   */


  parseStream(stream) {
    return new Promise(async (res, rej) => {
      this.reader = new StreamBufferReader(stream); // validate the stream type

      if ((await this.reader.read(STRING, SIZE_MAGIC_NUMBER)) != MAGIC_NUMBER) {
        rej(new Error('Stream is not a Typed Binary JSON format'));
      } // get the header length


      let headerLength = await this.reader.read(UINT32); // read and parse the header

      this.parseHeader((await this.reader.read(STRING, headerLength))); // construct the object

      res((await this.parse(this.root)));
    });
  }
  /**
   * Parse a TBJSON file into the object it represents. Faster, but uses more memory.
   * 
   * @param {string} filename - filename / path to read from 
   */


  parseFileAsBuffer(filename) {
    try {
      return this.parseBuffer(fs.readFileSync(filename));
    } catch (e) {
      e.message = `Tbjson failed to parse "${filename}": ` + e.message;
      throw e;
    }
  }
  /**
   * Parse a TBJSON file into the object it represents. Slower, but uses less memory.
   * 
   * @param {string} filename - filename / path to read from
   */


  async parseFileAsStream(filename) {
    try {
      return await this.parseStream(fs.createReadStream(filename));
    } catch (e) {
      e.message = `Tbjson failed to parse "${filename}": ` + e.message;
      throw e;
    }
  }
  /*-----------------------------------------------------------------------*/

  /* helpers */

  /**
   * Get the header object after serialization.
   * Useful if you are writing your custom own stream.
   */


  getHeader() {
    // get the type serializers / deserializers
    let typeDefs = {};

    for (let code in this.types) {
      typeDefs[code] = {
        serializer: type.serializer ? this.types[code].serializer.toString() : null,
        deserializer: type.deserializer ? this.types[code].deserializer.toString() : null
      };
    } // get the prototype definitions


    let protoDefs = {};

    for (let code in this.protos) {
      protoDefs[code] = this.protos[code].definition ? this.protos[code].definition : null;
    }

    return {
      typeRefs: this.typeRefs,
      typeDefs: typeDefs,
      protoRefs: this.protoRefs,
      protoDefs: protoDefs,
      objs: this.objs,
      root: this.root
    };
  }
  /**
   * Get the header object as a buffer.
   * Useful if you are writing your custom format.
   */


  getHeaderAsBuffer() {
    try {
      // header string
      let headerStr = JSON.stringify(this.getHeader()); // make a new buffer, add the header, append the binary

      let buffer = new BufferWriter(SIZE_MAGIC_NUMBER + SIZE_UINT32 + headerStr.length); // str - magic number

      buffer.writeFixedLengthString(MAGIC_NUMBER); // uint32 - header length

      buffer.write(UINT32, headerStr.length); // str - header

      buffer.writeFixedLengthString(headerStr);
      return buffer.buffer;
    } catch (e) {
      e.message = 'Tbjson failed to create a buffer for the header: ' + e.message;
      throw e;
    }
  }
  /**
   * Parse a TBJSON header from a string.
   * Useful if you are writing your own deserializer.
   * 
   * @param {string} headerStr - string containing the encoded JSON header 
   */


  parseHeader(headerStr) {
    try {
      let header = JSON.parse(headerStr); // types

      this.typeRefs = header.typeRefs;
      this.types = {};

      for (let code in header.typeDefs) {
        this.types[code] = new Type(Function(header.typeDefs[code].serializer), Function(header.typeDefs[code].deserializer));
      } // prototypes (preserve proto constructors for typed parsing)


      this.protoRefs = header.protoRefs;

      for (let code in header.protoDefs) {
        if (this.protos[code]) {
          this.protos[code].definition = header.protoDefs[code];
        } else {
          this.protos[code] = new Prototype(header.protoDefs[code]);
        }
      } // unknown objects


      this.objs = header.objs; // set the root

      this.root = header.root;
    } catch (e) {
      e.message = 'Tbjson failed to parse header string: ' + e.message;
      throw e;
    }
  }
  /*-----------------------------------------------------------------------*/

  /* private */

  /**
   * Format the definition to its number representations.
   * 
   * Converts the more verbose array definitions to simpler numeric ones:
   * 
   * [Tbjson.TYPES.ARRAY, Tbjson.TYPES.FLOAT32] -> ARRAY + FLOAT32 = 12 + 9 = 21
   * [Tbjson.TYPES.Array, Class] ->                ARRAY + NUM_CLASS = 12 + x
   * [Tbjson.TYPES.Array, "class"] ->              ARRAY + NUM_CLASS = 12 + x
   * 
   * @param { object | array | number } def - the definition specifying how to decode the binary data
   */


  fmtDef(def) {
    switch (typeof def) {
      // already in number form, just return it
      case 'number':
        return def;
      // string referencing a prototype, add the string to the reference lookup table

      case 'string':
        if (this.protoRefs[def]) {
          return this.protoRefs[def];
        }

        this.protoRefs[def] = this.nextProtoCode++;
        return this.protoRefs[def];
      // prototype (class)

      case 'function':
        return this.registerPrototype(def);
      // object or array

      case 'object':
        // null
        if (!def) {
          return null; // array
        } else if (Array.isArray(def)) {
          // typed array
          if (def.length == 2 && def[0] == ARRAY) {
            return ARRAY_OFFSET + this.fmtDef(def[1]); // nullable primitive
          } else if (def.length == 2 && def[0] == NULLABLE) {
            return NULLABLE_OFFSET + this.fmtDef(def[1]); // primitive typed array
          } else if (def.length == 2 && def[0] == TYPED_ARRAY) {
            return TYPED_ARRAY_OFFSET + this.fmtDef(def[1]); // fixed length array
          } else {
            let fmtDef = [];

            for (let i = 0; i < def.length; ++i) {
              fmtDef.push(this.fmtDef(def[i]));
            }

            return fmtDef;
          } // simple object

        } else {
          let fmtDef = {};

          for (let key in def) {
            fmtDef[key] = this.fmtDef(def[key]);
          }

          return fmtDef;
        }

      // invalid type

      case 'boolean':
        break;
    }
  }
  /**
   * Serialize the object based on its definition. Only run for known prototypes.
   * 
   * @param { object } obj - the object to serialize
   * @param { object | array | number } def - the definition specifying how to decode the binary data
   */


  serializeDef(obj, def) {
    // no def, could be a known but undefined prototype, or a plain object, kick back to the serializer
    if (!def) {
      // write the code
      let code = this.nextObjCode++;
      this.writer.write(UINT32, code); // write the obj

      let ref = {};

      for (let key in obj) {
        ref[key] = this.serialize(obj[key]);
      }

      this.objs[code] = ref;
      return;
    } // typed


    if (typeof def == 'number') {
      // primitive
      if (def < NULLABLE_OFFSET) {
        // an unknown object
        if (def == OBJECT) {
          this.serializeDef(obj); // primitive
        } else {
          this.writer.write(def, obj);
        } // nullable primitive

      } else if (def < TYPED_ARRAY_OFFSET) {
        if (obj == null) {
          this.writer.write(UINT8, 0);
        } else {
          this.writer.write(UINT8, 1);
          this.writer.write(def - NULLABLE_OFFSET, obj);
        } // primitive typed array

      } else if (def < TYPE_OFFSET) {
        this.writer.write(UINT32, obj.buffer.byteLength);
        this.writer.writeBuffer(Buffer.from(obj.buffer)); // custom type
      } else if (def < PROTOTYPE_OFFSET) ; else if (def < ARRAY_OFFSET) {
        // register the prototype if needed
        if (obj.constructor.tbjson) {
          this.registerPrototype(obj.constructor);
        }

        this.serializeDef(obj, this.protos[def].definition); //variable-length fixed typed array 
      } else {
        // write out the length
        this.writer.write(UINT32, obj.length);

        for (let i = 0; i < obj.length; ++i) {
          this.serializeDef(obj[i], def - ARRAY_OFFSET);
        }
      } // oject or array

    } else {
      // fixed-length variable type array
      if (Array.isArray(def)) {
        for (let i = 0; i < def.length; ++i) {
          this.serializeDef(obj[i], def[i]);
        } // object

      } else {
        for (let key in def) {
          this.serializeDef(obj[key], def[key]);
        }
      }
    }
  }
  /**
   * Serialize an object. Can be known (TBJSON has a definition for it) or plain (Class or object that TBJSON doesn't have a definition for).
   * Calls serializeDef() if a known type is found.
   * 
   * @param {object} obj - the object to serialize
   */


  serialize(obj) {
    switch (typeof obj) {
      // bool
      case 'boolean':
        this.writer.write(BOOL, obj);
        return BOOL;
      // number, use the default number type

      case 'number':
        this.writer.write(FLOAT32, obj);
        return FLOAT32;
      // string

      case 'string':
        this.writer.write(STRING, obj);
        return STRING;
      // null, object, or array

      case 'object':
        // null
        if (!obj) {
          return NULL; // array
        } else if (Array.isArray(obj)) {
          let refs = [];

          for (let i = 0; i < obj.length; ++i) {
            refs.push(this.serialize(obj[i]));
          }

          return refs; // primitive typed array
        } else if (ArrayBuffer.isView(obj)) {
          let ref = NULL;

          if (obj instanceof Uint8Array) {
            ref = TYPED_ARRAY_OFFSET + UINT8;
          } else if (obj instanceof Int8Array) {
            ref = TYPED_ARRAY_OFFSET + INT8;
          } else if (obj instanceof Uint16Array) {
            ref = TYPED_ARRAY_OFFSET + UINT16;
          } else if (obj instanceof Int16Array) {
            ref = TYPED_ARRAY_OFFSET + INT16;
          } else if (obj instanceof Uint32Array) {
            ref = TYPED_ARRAY_OFFSET + UINT32;
          } else if (obj instanceof Int32Array) {
            ref = TYPED_ARRAY_OFFSET + INT32;
          } else if (obj instanceof Float32Array) {
            ref = TYPED_ARRAY_OFFSET + FLOAT32;
          } else if (obj instanceof Float64Array) {
            ref = TYPED_ARRAY_OFFSET + FLOAT64;
          }

          if (ref) {
            this.writer.write(UINT32, obj.buffer.byteLength);
            this.writer.writeBuffer(Buffer.from(obj.buffer));
          }

          return ref; // object or known prototype
        } else {
          // the object is prototype
          if (obj.constructor) {
            // a known tbjson prototype
            if (obj.constructor.tbjson) {
              // add this object type to the known prototypes
              let code = this.registerPrototype(obj.constructor); // process the prototype definition

              this.serializeDef(obj, this.protos[code].definition);
              return code; // might be a known tbjson prototype
            } else {
              let code = this.protoRefs[obj.constructor.name];

              if (code) {
                // process the prototype
                this.serializeDef(obj, this.protos[code].definition);
                return code;
              }
            }
          } // simple object, traverse accordingly


          let ref = {};

          for (let key in obj) {
            ref[key] = this.serialize(obj[key]);
          }

          return ref;
        }

    }
  }
  /**
   * Parse a definition.
   * 
   * @param { object | array | number } def - the definition specifying how to decode the binary data
   * @param {function} prototype - [optional] create this type during object instantiation
   */


  parse(def, prototype) {
    // type
    if (typeof def == 'number') {
      // primitive
      if (def < NULLABLE_OFFSET) {
        // null
        if (def == NULL) {
          return null; // unknown object
        } else if (def == OBJECT) {
          return this.parse(this.objs[this.reader.read(UINT32)]); // primitive
        } else {
          return this.reader.read(def);
        } // nullable primitive

      } else if (def < TYPED_ARRAY_OFFSET) {
        // non null
        if (this.reader.read(UINT8)) {
          return this.reader.read(def - NULLABLE_OFFSET); // null
        } else {
          return null;
        } // primitive typed array

      } else if (def < TYPE_OFFSET) {
        return this.reader.readTypedArray(def - TYPED_ARRAY_OFFSET, this.reader.read(UINT32)); // custom type
      } else if (def < PROTOTYPE_OFFSET) {
        return this.reader.read(def); // known prototype
      } else if (def < ARRAY_OFFSET) {
        let proto = this.protos[def]; // defined

        if (proto.definition) {
          return this.parse(proto.definition, proto.prototype); // undefined
        } else {
          return this.parse(this.objs[this.reader.read(UINT32)], proto.prototype);
        } // variable-length fixed typed array 

      } else {
        let length = this.reader.read(UINT32);
        let objs = [];

        for (let i = 0; i < length; ++i) {
          objs.push(this.parse(def - ARRAY_OFFSET));
        }

        return objs;
      } // fixed-length array

    } else if (Array.isArray(def)) {
      let objs = [];

      for (let i = 0; i < def.length; ++i) {
        objs.push(this.parse(def[i]));
      }

      return objs; // object
    } else {
      let obj = prototype ? new prototype() : {};

      for (let key in def) {
        obj[key] = this.parse(def[key]);
      }

      return obj;
    }
  }

}
Tbjson.TYPES = {
  NULL,
  BOOL,
  INT8,
  UINT8,
  INT16,
  UINT16,
  INT32,
  UINT32,
  FLOAT32,
  FLOAT64,
  STRING,
  ARRAY,
  OBJECT,
  NULLABLE,
  TYPED_ARRAY
};
/* internal */

/**
 * Return the parent of a prototype.
 * 
 * @param {function} prototype - prototype to check for parent of 
 */

function getParent(prototype) {
  let parent = prototype ? Object.getPrototypeOf(prototype) : null;
  return parent && parent.name ? parent : null;
}

module.exports = Tbjson;
