// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

define([
    'underscore',
    ], function (_) {
    "use strict";

    var _deserialize_array_buffer = function (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;
    };

    var _deserialize_binary = function(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;
        }
    };

    var deserialize = function (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));
        }
    };

    var _serialize_binary = function (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;
    };

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

    var exports = {
        deserialize : deserialize,
        serialize: serialize
    };
    return exports;
});
