/*global Blob,File*/

/**
 * Module requirements
 */

var isArray = require('isarray');
var isBuf = require('./is-buffer');
var toString = Object.prototype.toString;
var withNativeBlob = typeof Blob === 'function' || (typeof Blob !== 'undefined' && toString.call(Blob) === '[object BlobConstructor]');
var withNativeFile = typeof File === 'function' || (typeof File !== 'undefined' && toString.call(File) === '[object FileConstructor]');

/**
 * Replaces every Buffer | ArrayBuffer in packet with a numbered placeholder.
 * Anything with blobs or files should be fed through removeBlobs before coming
 * here.
 *
 * @param {Object} packet - socket.io event packet
 * @return {Object} with deconstructed packet and list of buffers
 * @api public
 */

exports.deconstructPacket = function(packet) {
    var buffers = [];
    var packetData = packet.data;
    var pack = packet;
    pack.data = _deconstructPacket(packetData, buffers);
    pack.attachments = buffers.length; // number of binary 'attachments'
    return {
        packet: pack,
        buffers: buffers
    };
};

function _deconstructPacket(data, buffers) {
    if (!data) return data;

    if (isBuf(data)) {
        var placeholder = {
            _placeholder: true,
            num: buffers.length
        };
        buffers.push(data);
        return placeholder;
    } else if (isArray(data)) {
        var newData = new Array(data.length);
        for (var i = 0; i < data.length; i++) {
            newData[i] = _deconstructPacket(data[i], buffers);
        }
        return newData;
    } else if (typeof data === 'object' && !(data instanceof Date)) {
        var newData = {};
        for (var key in data) {
            newData[key] = _deconstructPacket(data[key], buffers);
        }
        return newData;
    }
    return data;
}

/**
 * Reconstructs a binary packet from its placeholder packet and buffers
 *
 * @param {Object} packet - event packet with placeholders
 * @param {Array} buffers - binary buffers to put in placeholder positions
 * @return {Object} reconstructed packet
 * @api public
 */

exports.reconstructPacket = function(packet, buffers) {
    packet.data = _reconstructPacket(packet.data, buffers);
    packet.attachments = undefined; // no longer useful
    return packet;
};

function _reconstructPacket(data, buffers) {
    if (!data) return data;

    if (data && data._placeholder === true) {
        var isIndexValid =
            typeof data.num === "number" &&
            data.num >= 0 &&
            data.num < buffers.length;
        if (isIndexValid) {
            return buffers[data.num]; // appropriate buffer (should be natural order anyway)
        } else {
            throw new Error("illegal attachments");
        }
    } else if (isArray(data)) {
        for (var i = 0; i < data.length; i++) {
            data[i] = _reconstructPacket(data[i], buffers);
        }
    } else if (typeof data === 'object') {
        for (var key in data) {
            data[key] = _reconstructPacket(data[key], buffers);
        }
    }

    return data;
}

/**
 * Asynchronously removes Blobs or Files from data via
 * FileReader's readAsArrayBuffer method. Used before encoding
 * data as msgpack. Calls callback with the blobless data.
 *
 * @param {Object} data
 * @param {Function} callback
 * @api private
 */

exports.removeBlobs = function(data, callback) {
    function _removeBlobs(obj, curKey, containingObject) {
        if (!obj) return obj;

        // convert any blob
        if ((withNativeBlob && obj instanceof Blob) ||
            (withNativeFile && obj instanceof File)) {
            pendingBlobs++;

            // async filereader
            var fileReader = new FileReader();
            fileReader.onload = function() { // this.result == arraybuffer
                if (containingObject) {
                    containingObject[curKey] = this.result;
                } else {
                    bloblessData = this.result;
                }

                // if nothing pending its callback time
                if (!--pendingBlobs) {
                    callback(bloblessData);
                }
            };

            fileReader.readAsArrayBuffer(obj); // blob -> arraybuffer
        } else if (isArray(obj)) { // handle array
            for (var i = 0; i < obj.length; i++) {
                _removeBlobs(obj[i], i, obj);
            }
        } else if (typeof obj === 'object' && !isBuf(obj)) { // and object
            for (var key in obj) {
                _removeBlobs(obj[key], key, obj);
            }
        }
    }

    var pendingBlobs = 0;
    var bloblessData = data;
    _removeBlobs(bloblessData);
    if (!pendingBlobs) {
        callback(bloblessData);
    }
};