"use strict"

function isObject(obj) { // Adapted from underscore
    var type = typeof obj;
    return type === 'function' || type === 'object' && !!obj;
}

var isArray = Array.isArray || function(obj) { // Adapted from underscore
    return toString.call(obj) === '[object Array]';
}

function clone(obj) { // Adapted from underscore
    // Previously used _.extend({}, obj);
  if (!isObject(obj)) return obj;
  return isArray(obj) ? obj.slice() : Object.assign({}, obj);
}

// Adapted from notebook/static/services/kernels/serialize.js

function _deserialize_array_buffer(buf) {
   var data = new DataView(buf);
   // read the header: 1 + nbufs 32b integers
   var nbufs = data.getUint32(0);
   var offsets = [];
   var i;
   for (i = 1; i <= nbufs; i++) {
     offsets.push(data.getUint32(i * 4));
   }
   var json_bytes = new Uint8Array(buf.slice(offsets[0], offsets[1]));
   var msg = JSON.parse(
     (new TextDecoder('utf8')).decode(json_bytes)
   );
   // the remaining chunks are stored as DataViews in msg.buffers
   msg.buffers = [];
   var start, stop;
   for (i = 1; i < nbufs; i++) {
     start = offsets[i];
     stop = offsets[i+1] || buf.byteLength;
     msg.buffers.push(new DataView(buf.slice(start, stop)));
   }
   return msg;
 }

export function _deserialize_binary(data) {
  /**
   * deserialize the binary message format
   * callback will be called with a message whose buffers attribute
   * will be an array of DataViews.
   */
  if (data instanceof Blob) {
    // data is Blob, have to deserialize from ArrayBuffer in reader callback
    var reader = new FileReader();
    var promise = new Promise(function(resolve, reject) {
      reader.onload = function () {
        var msg = _deserialize_array_buffer(this.result);
        resolve(msg);
      };
    });
    reader.readAsArrayBuffer(data);
    return promise;
  } else {
    // data is ArrayBuffer, can deserialize directly
    var msg = _deserialize_array_buffer(data);
    return msg;
  }
}

export function deserialize(data) {
  /**
   * deserialize a message and return a promise for the unpacked message
   */
  if (typeof data === "string") {
    // text JSON message
    return Promise.resolve(JSON.parse(data));
  } else {
    // binary message
    return Promise.resolve(_deserialize_binary(data));
  }
}

function _serialize_binary(msg) {
  /**
   * implement the binary serialization protocol
   * serializes JSON message to ArrayBuffer
   */
  msg = clone(msg);
  var offsets = [];
  var buffers = [];
  var i;
  for (i = 0; i < msg.buffers.length; i++) {
    // msg.buffers elements could be either views or ArrayBuffers
    // buffers elements are ArrayBuffers
    var b = msg.buffers[i];
    buffers.push(b.buffer instanceof ArrayBuffer ? b.buffer : b);
  }
  delete msg.buffers;
  var json_utf8 = (new TextEncoder('utf8')).encode(JSON.stringify(msg));
  buffers.unshift(json_utf8);
  var nbufs = buffers.length;
  offsets.push(4 * (nbufs + 1));
  for (i = 0; i + 1 < buffers.length; i++) {
    offsets.push(offsets[offsets.length-1] + buffers[i].byteLength);
  }
  var msg_buf = new Uint8Array(
    offsets[offsets.length-1] + buffers[buffers.length-1].byteLength
  );
  // use DataView.setUint32 for network byte-order
  var view = new DataView(msg_buf.buffer);
  // write nbufs to first 4 bytes
  view.setUint32(0, nbufs);
  // write offsets to next 4 * nbufs bytes
  for (i = 0; i < offsets.length; i++) {
    view.setUint32(4 * (i+1), offsets[i]);
  }
  // write all the buffers at their respective offsets
  for (i = 0; i < buffers.length; i++) {
    msg_buf.set(new Uint8Array(buffers[i]), offsets[i]);
  }
  // return raw ArrayBuffer
  return msg_buf.buffer;
}

export function serialize(msg) {
  if (msg.buffers && msg.buffers.length) {
    return _serialize_binary(msg);
  } else {
    return JSON.stringify(msg);
  }
}
