// Last time updated: 2017-08-27 5:48:35 AM UTC

// ________________
// FileBufferReader

// Open-Sourced: https://github.com/muaz-khan/FileBufferReader

// --------------------------------------------------
// Muaz Khan     - www.MuazKhan.com
// MIT License   - www.WebRTC-Experiment.com/licence
// --------------------------------------------------

'use strict';

(function() {

    function FileBufferReader() {
        var fbr = this;
        var fbrHelper = new FileBufferReaderHelper();

        fbr.chunks = {};
        fbr.users = {};

        fbr.readAsArrayBuffer = function(file, callback, extra) {
            var options = {
                file: file,
                earlyCallback: function(chunk) {
                    callback(fbrClone(chunk, {
                        currentPosition: -1
                    }));
                },
                extra: extra || {
                    userid: 0
                }
            };

            if (file.extra && Object.keys(file.extra).length) {
                Object.keys(file.extra).forEach(function(key) {
                    options.extra[key] = file.extra[key];
                });
            }

            fbrHelper.readAsArrayBuffer(fbr, options);
        };

        fbr.getNextChunk = function(fileUUID, callback, userid) {
            var currentPosition;

            if (typeof fileUUID.currentPosition !== 'undefined') {
                currentPosition = fileUUID.currentPosition;
                fileUUID = fileUUID.uuid;
            }

            var allFileChunks = fbr.chunks[fileUUID];
            if (!allFileChunks) {
                return;
            }

            if (typeof userid !== 'undefined') {
                if (!fbr.users[userid + '']) {
                    fbr.users[userid + ''] = {
                        fileUUID: fileUUID,
                        userid: userid,
                        currentPosition: -1
                    };
                }

                if (typeof currentPosition !== 'undefined') {
                    fbr.users[userid + ''].currentPosition = currentPosition;
                }

                fbr.users[userid + ''].currentPosition++;
                currentPosition = fbr.users[userid + ''].currentPosition;
            } else {
                if (typeof currentPosition !== 'undefined') {
                    fbr.chunks[fileUUID].currentPosition = currentPosition;
                }

                fbr.chunks[fileUUID].currentPosition++;
                currentPosition = fbr.chunks[fileUUID].currentPosition;
            }

            var nextChunk = allFileChunks[currentPosition];
            if (!nextChunk) {
                delete fbr.chunks[fileUUID];
                fbr.convertToArrayBuffer({
                    chunkMissing: true,
                    currentPosition: currentPosition,
                    uuid: fileUUID
                }, callback);
                return;
            }

            nextChunk = fbrClone(nextChunk);

            if (typeof userid !== 'undefined') {
                nextChunk.remoteUserId = userid + '';
            }

            if (!!nextChunk.start) {
                fbr.onBegin(nextChunk);
            }

            if (!!nextChunk.end) {
                fbr.onEnd(nextChunk);
            }

            fbr.onProgress(nextChunk);

            fbr.convertToArrayBuffer(nextChunk, function(buffer) {
                if (nextChunk.currentPosition == nextChunk.maxChunks) {
                    callback(buffer, true);
                    return;
                }

                callback(buffer, false);
            });
        };

        var fbReceiver = new FileBufferReceiver(fbr);

        fbr.addChunk = function(chunk, callback) {
            if (!chunk) {
                return;
            }

            fbReceiver.receive(chunk, function(chunk) {
                fbr.convertToArrayBuffer({
                    readyForNextChunk: true,
                    currentPosition: chunk.currentPosition,
                    uuid: chunk.uuid
                }, callback);
            });
        };

        fbr.chunkMissing = function(chunk) {
            delete fbReceiver.chunks[chunk.uuid];
            delete fbReceiver.chunksWaiters[chunk.uuid];
        };

        fbr.onBegin = function() {};
        fbr.onEnd = function() {};
        fbr.onProgress = function() {};

        fbr.convertToObject = FileConverter.ConvertToObject;
        fbr.convertToArrayBuffer = FileConverter.ConvertToArrayBuffer

        // for backward compatibility----it is redundant.
        fbr.setMultipleUsers = function() {};

        // extends 'from' object with members from 'to'. If 'to' is null, a deep clone of 'from' is returned
        function fbrClone(from, to) {
            if (from == null || typeof from != "object") return from;
            if (from.constructor != Object && from.constructor != Array) return from;
            if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function ||
                from.constructor == String || from.constructor == Number || from.constructor == Boolean)
                return new from.constructor(from);

            to = to || new from.constructor();

            for (var name in from) {
                to[name] = typeof to[name] == "undefined" ? fbrClone(from[name], null) : to[name];
            }

            return to;
        }
    }

    function FileBufferReaderHelper() {
        var fbrHelper = this;

        function processInWebWorker(_function) {
            var blob = URL.createObjectURL(new Blob([_function.toString(),
                'this.onmessage =  function (e) {' + _function.name + '(e.data);}'
            ], {
                type: 'application/javascript'
            }));

            var worker = new Worker(blob);
            return worker;
        }

        fbrHelper.readAsArrayBuffer = function(fbr, options) {
            var earlyCallback = options.earlyCallback;
            delete options.earlyCallback;

            function processChunk(chunk) {
                if (!fbr.chunks[chunk.uuid]) {
                    fbr.chunks[chunk.uuid] = {
                        currentPosition: -1
                    };
                }

                options.extra = options.extra || {
                    userid: 0
                };

                chunk.userid = options.userid || options.extra.userid || 0;
                chunk.extra = options.extra;

                fbr.chunks[chunk.uuid][chunk.currentPosition] = chunk;

                if (chunk.end && earlyCallback) {
                    earlyCallback(chunk.uuid);
                    earlyCallback = null;
                }

                // for huge files
                if ((chunk.maxChunks > 200 && chunk.currentPosition == 200) && earlyCallback) {
                    earlyCallback(chunk.uuid);
                    earlyCallback = null;
                }
            }
            if (false && typeof Worker !== 'undefined') {
                var webWorker = processInWebWorker(fileReaderWrapper);

                webWorker.onmessage = function(event) {
                    processChunk(event.data);
                };

                webWorker.postMessage(options);
            } else {
                fileReaderWrapper(options, processChunk);
            }
        };

        function fileReaderWrapper(options, callback) {
            callback = callback || function(chunk) {
                postMessage(chunk);
            };

            var file = options.file;
            if (!file.uuid) {
                file.uuid = (Math.random() * 100).toString().replace(/\./g, '');
            }

            var chunkSize = options.chunkSize || 15 * 1000;
            if (options.extra && options.extra.chunkSize) {
                chunkSize = options.extra.chunkSize;
            }

            var sliceId = 0;
            var cacheSize = chunkSize;

            var chunksPerSlice = Math.floor(Math.min(100000000, cacheSize) / chunkSize);
            var sliceSize = chunksPerSlice * chunkSize;
            var maxChunks = Math.ceil(file.size / chunkSize);

            file.maxChunks = maxChunks;

            var numOfChunksInSlice;
            var currentPosition = 0;
            var hasEntireFile;
            var chunks = [];

            callback({
                currentPosition: currentPosition,
                uuid: file.uuid,
                maxChunks: maxChunks,
                size: file.size,
                name: file.name,
                type: file.type,
                lastModifiedDate: (file.lastModifiedDate || new Date()).toString(),
                start: true
            });

            var blob, reader = new FileReader();

            reader.onloadend = function(evt) {
                if (evt.target.readyState == FileReader.DONE) {
                    addChunks(file.name, evt.target.result, function() {
                        sliceId++;
                        if ((sliceId + 1) * sliceSize < file.size) {
                            blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize);
                            reader.readAsArrayBuffer(blob);
                        } else if (sliceId * sliceSize < file.size) {
                            blob = file.slice(sliceId * sliceSize, file.size);
                            reader.readAsArrayBuffer(blob);
                        } else {
                            file.url = URL.createObjectURL(file);
                            callback({
                                currentPosition: currentPosition,
                                uuid: file.uuid,
                                maxChunks: maxChunks,
                                size: file.size,
                                name: file.name,
                                lastModifiedDate: (file.lastModifiedDate || new Date()).toString(),
                                url: URL.createObjectURL(file),
                                type: file.type,
                                end: true
                            });
                        }
                    });
                }
            };

            currentPosition += 1;

            blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize);
            reader.readAsArrayBuffer(blob);

            function addChunks(fileName, binarySlice, addChunkCallback) {
                numOfChunksInSlice = Math.ceil(binarySlice.byteLength / chunkSize);
                for (var i = 0; i < numOfChunksInSlice; i++) {
                    var start = i * chunkSize;
                    chunks[currentPosition] = binarySlice.slice(start, Math.min(start + chunkSize, binarySlice.byteLength));

                    callback({
                        uuid: file.uuid,
                        buffer: chunks[currentPosition],
                        currentPosition: currentPosition,
                        maxChunks: maxChunks,

                        size: file.size,
                        name: file.name,
                        lastModifiedDate: (file.lastModifiedDate || new Date()).toString(),
                        type: file.type
                    });

                    currentPosition++;
                }

                if (currentPosition == maxChunks) {
                    hasEntireFile = true;
                }

                addChunkCallback();
            }
        }
    }

    function FileSelector() {
        var selector = this;

        var noFileSelectedCallback = function() {};

        selector.selectSingleFile = function(callback, failure) {
            if (failure) {
                noFileSelectedCallback = failure;
            }

            selectFile(callback);
        };
        selector.selectMultipleFiles = function(callback, failure) {
            if (failure) {
                noFileSelectedCallback = failure;
            }

            selectFile(callback, true);
        };
        selector.selectDirectory = function(callback, failure) {
            if (failure) {
                noFileSelectedCallback = failure;
            }

            selectFile(callback, true, true);
        };

        selector.accept = '*.*';

        function selectFile(callback, multiple, directory) {
            callback = callback || function() {};

            var file = document.createElement('input');
            file.type = 'file';

            if (multiple) {
                file.multiple = true;
            }

            if (directory) {
                file.webkitdirectory = true;
            }

            file.accept = selector.accept;

            file.onclick = function() {
                file.clickStarted = true;
            };

            document.body.onfocus = function() {
                setTimeout(function() {
                    if (!file.clickStarted) return;
                    file.clickStarted = false;

                    if (!file.value) {
                        noFileSelectedCallback();
                    }
                }, 500);
            };

            file.onchange = function() {
                if (multiple) {
                    if (!file.files.length) {
                        console.error('No file selected.');
                        return;
                    }

                    var arr = [];
                    Array.from(file.files).forEach(function(file) {
                        file.url = file.webkitRelativePath;
                        arr.push(file);
                    });
                    callback(arr);
                    return;
                }

                if (!file.files[0]) {
                    console.error('No file selected.');
                    return;
                }

                callback(file.files[0]);

                file.parentNode.removeChild(file);
            };
            file.style.display = 'none';
            (document.body || document.documentElement).appendChild(file);
            fireClickEvent(file);
        }

        function getValidFileName(fileName) {
            if (!fileName) {
                fileName = 'file' + (new Date).toISOString().replace(/:|\.|-/g, '')
            }

            var a = fileName;
            a = a.replace(/^.*[\\\/]([^\\\/]*)$/i, "$1");
            a = a.replace(/\s/g, "_");
            a = a.replace(/,/g, '');
            a = a.toLowerCase();
            return a;
        }

        function fireClickEvent(element) {
            if (typeof element.click === 'function') {
                element.click();
                return;
            }

            if (typeof element.change === 'function') {
                element.change();
                return;
            }

            if (typeof document.createEvent('Event') !== 'undefined') {
                var event = document.createEvent('Event');

                if (typeof event.initEvent === 'function' && typeof element.dispatchEvent === 'function') {
                    event.initEvent('click', true, true);
                    element.dispatchEvent(event);
                    return;
                }
            }

            var event = new MouseEvent('click', {
                view: window,
                bubbles: true,
                cancelable: true
            });

            element.dispatchEvent(event);
        }
    }

    function FileBufferReceiver(fbr) {
        var fbReceiver = this;

        fbReceiver.chunks = {};
        fbReceiver.chunksWaiters = {};

        function receive(chunk, callback) {
            if (!chunk.uuid) {
                fbr.convertToObject(chunk, function(object) {
                    receive(object);
                });
                return;
            }

            if (chunk.start && !fbReceiver.chunks[chunk.uuid]) {
                fbReceiver.chunks[chunk.uuid] = {};
                if (fbr.onBegin) fbr.onBegin(chunk);
            }

            if (!chunk.end && chunk.buffer) {
                fbReceiver.chunks[chunk.uuid][chunk.currentPosition] = chunk.buffer;
            }

            if (chunk.end) {
                var chunksObject = fbReceiver.chunks[chunk.uuid];
                var chunksArray = [];
                Object.keys(chunksObject).forEach(function(item, idx) {
                    chunksArray.push(chunksObject[item]);
                });

                var blob = new Blob(chunksArray, {
                    type: chunk.type
                });
                blob = merge(blob, chunk);
                blob.url = URL.createObjectURL(blob);
                blob.uuid = chunk.uuid;

                if (!blob.size) console.error('Something went wrong. Blob Size is 0.');

                if (fbr.onEnd) fbr.onEnd(blob);

                // clear system memory
                delete fbReceiver.chunks[chunk.uuid];
                delete fbReceiver.chunksWaiters[chunk.uuid];
            }

            if (chunk.buffer && fbr.onProgress) fbr.onProgress(chunk);

            if (!chunk.end) {
                callback(chunk);

                fbReceiver.chunksWaiters[chunk.uuid] = function() {
                    function looper() {
                        if (!chunk.buffer) {
                            return;
                        }

                        if (!fbReceiver.chunks[chunk.uuid]) {
                            return;
                        }

                        if (chunk.currentPosition != chunk.maxChunks && !fbReceiver.chunks[chunk.uuid][chunk.currentPosition]) {
                            callback(chunk);
                            setTimeout(looper, 5000);
                        }
                    }
                    setTimeout(looper, 5000);
                };

                fbReceiver.chunksWaiters[chunk.uuid]();
            }
        }

        fbReceiver.receive = receive;
    }

    var FileConverter = {
        ConvertToArrayBuffer: function(object, callback) {
            binarize.pack(object, function(dataView) {
                callback(dataView.buffer);
            });
        },
        ConvertToObject: function(buffer, callback) {
            binarize.unpack(buffer, callback);
        }
    };

    function merge(mergein, mergeto) {
        if (!mergein) mergein = {};
        if (!mergeto) return mergein;

        for (var item in mergeto) {
            try {
                mergein[item] = mergeto[item];
            } catch (e) {}
        }
        return mergein;
    }

    var debug = false;

    var BIG_ENDIAN = false,
        LITTLE_ENDIAN = true,
        TYPE_LENGTH = Uint8Array.BYTES_PER_ELEMENT,
        LENGTH_LENGTH = Uint16Array.BYTES_PER_ELEMENT,
        BYTES_LENGTH = Uint32Array.BYTES_PER_ELEMENT;

    var Types = {
        NULL: 0,
        UNDEFINED: 1,
        STRING: 2,
        NUMBER: 3,
        BOOLEAN: 4,
        ARRAY: 5,
        OBJECT: 6,
        INT8ARRAY: 7,
        INT16ARRAY: 8,
        INT32ARRAY: 9,
        UINT8ARRAY: 10,
        UINT16ARRAY: 11,
        UINT32ARRAY: 12,
        FLOAT32ARRAY: 13,
        FLOAT64ARRAY: 14,
        ARRAYBUFFER: 15,
        BLOB: 16,
        FILE: 16,
        BUFFER: 17 // Special type for node.js
    };

    if (debug) {
        var TypeNames = [
            'NULL',
            'UNDEFINED',
            'STRING',
            'NUMBER',
            'BOOLEAN',
            'ARRAY',
            'OBJECT',
            'INT8ARRAY',
            'INT16ARRAY',
            'INT32ARRAY',
            'UINT8ARRAY',
            'UINT16ARRAY',
            'UINT32ARRAY',
            'FLOAT32ARRAY',
            'FLOAT64ARRAY',
            'ARRAYBUFFER',
            'BLOB',
            'BUFFER'
        ];
    }

    var Length = [
        null, // Types.NULL
        null, // Types.UNDEFINED
        'Uint16', // Types.STRING
        'Float64', // Types.NUMBER
        'Uint8', // Types.BOOLEAN
        null, // Types.ARRAY
        null, // Types.OBJECT
        'Int8', // Types.INT8ARRAY
        'Int16', // Types.INT16ARRAY
        'Int32', // Types.INT32ARRAY
        'Uint8', // Types.UINT8ARRAY
        'Uint16', // Types.UINT16ARRAY
        'Uint32', // Types.UINT32ARRAY
        'Float32', // Types.FLOAT32ARRAY
        'Float64', // Types.FLOAT64ARRAY
        'Uint8', // Types.ARRAYBUFFER
        'Uint8', // Types.BLOB, Types.FILE
        'Uint8' // Types.BUFFER
    ];

    var binary_dump = function(view, start, length) {
        var table = [],
            endianness = BIG_ENDIAN,
            ROW_LENGTH = 40;
        table[0] = [];
        for (var i = 0; i < ROW_LENGTH; i++) {
            table[0][i] = i < 10 ? '0' + i.toString(10) : i.toString(10);
        }
        for (i = 0; i < length; i++) {
            var code = view.getUint8(start + i, endianness);
            var index = ~~(i / ROW_LENGTH) + 1;
            if (typeof table[index] === 'undefined') table[index] = [];
            table[index][i % ROW_LENGTH] = code < 16 ? '0' + code.toString(16) : code.toString(16);
        }
        console.log('%c' + table[0].join(' '), 'font-weight: bold;');
        for (i = 1; i < table.length; i++) {
            console.log(table[i].join(' '));
        }
    };

    var find_type = function(obj) {
        var type = undefined;

        if (obj === undefined) {
            type = Types.UNDEFINED;

        } else if (obj === null) {
            type = Types.NULL;

        } else {
            var const_name = obj.constructor.name;
            var const_name_reflection = obj.constructor.toString().match(/\w+/g)[1];
            if (const_name !== undefined && Types[const_name.toUpperCase()] !== undefined) {
                // return type by .constructor.name if possible
                type = Types[const_name.toUpperCase()];

            } else if (const_name_reflection !== undefined && Types[const_name_reflection.toUpperCase()] !== undefined) {
                type = Types[const_name_reflection.toUpperCase()];

            } else {
                // Work around when constructor.name is not defined
                switch (typeof obj) {
                    case 'string':
                        type = Types.STRING;
                        break;

                    case 'number':
                        type = Types.NUMBER;
                        break;

                    case 'boolean':
                        type = Types.BOOLEAN;
                        break;

                    case 'object':
                        if (obj instanceof Array) {
                            type = Types.ARRAY;

                        } else if (obj instanceof Int8Array) {
                            type = Types.INT8ARRAY;

                        } else if (obj instanceof Int16Array) {
                            type = Types.INT16ARRAY;

                        } else if (obj instanceof Int32Array) {
                            type = Types.INT32ARRAY;

                        } else if (obj instanceof Uint8Array) {
                            type = Types.UINT8ARRAY;

                        } else if (obj instanceof Uint16Array) {
                            type = Types.UINT16ARRAY;

                        } else if (obj instanceof Uint32Array) {
                            type = Types.UINT32ARRAY;

                        } else if (obj instanceof Float32Array) {
                            type = Types.FLOAT32ARRAY;

                        } else if (obj instanceof Float64Array) {
                            type = Types.FLOAT64ARRAY;

                        } else if (obj instanceof ArrayBuffer) {
                            type = Types.ARRAYBUFFER;

                        } else if (obj instanceof Blob) { // including File
                            type = Types.BLOB;

                        } else if (obj instanceof Buffer) { // node.js only
                            type = Types.BUFFER;

                        } else if (obj instanceof Object) {
                            type = Types.OBJECT;

                        }
                        break;

                    default:
                        break;
                }
            }
        }
        return type;
    };

    var utf16_utf8 = function(string) {
        return unescape(encodeURIComponent(string));
    };

    var utf8_utf16 = function(bytes) {
        return decodeURIComponent(escape(bytes));
    };

    /**
     * packs seriarized elements array into a packed ArrayBuffer
     * @param  {Array} serialized Serialized array of elements.
     * @return {DataView} view of packed binary
     */
    var pack = function(serialized) {
        var cursor = 0,
            i = 0,
            j = 0,
            endianness = BIG_ENDIAN;

        var ab = new ArrayBuffer(serialized[0].byte_length + serialized[0].header_size);
        var view = new DataView(ab);

        for (i = 0; i < serialized.length; i++) {
            var start = cursor,
                header_size = serialized[i].header_size,
                type = serialized[i].type,
                length = serialized[i].length,
                value = serialized[i].value,
                byte_length = serialized[i].byte_length,
                type_name = Length[type],
                unit = type_name === null ? 0 : window[type_name + 'Array'].BYTES_PER_ELEMENT;

            // Set type
            if (type === Types.BUFFER) {
                // on node.js Blob is emulated using Buffer type
                view.setUint8(cursor, Types.BLOB, endianness);
            } else {
                view.setUint8(cursor, type, endianness);
            }
            cursor += TYPE_LENGTH;

            if (debug) {
                console.info('Packing', type, TypeNames[type]);
            }

            // Set length if required
            if (type === Types.ARRAY || type === Types.OBJECT) {
                view.setUint16(cursor, length, endianness);
                cursor += LENGTH_LENGTH;

                if (debug) {
                    console.info('Content Length', length);
                }
            }

            // Set byte length
            view.setUint32(cursor, byte_length, endianness);
            cursor += BYTES_LENGTH;

            if (debug) {
                console.info('Header Size', header_size, 'bytes');
                console.info('Byte Length', byte_length, 'bytes');
            }

            switch (type) {
                case Types.NULL:
                case Types.UNDEFINED:
                    // NULL and UNDEFINED doesn't have any payload
                    break;

                case Types.STRING:
                    if (debug) {
                        console.info('Actual Content %c"' + value + '"', 'font-weight:bold;');
                    }
                    for (j = 0; j < length; j++, cursor += unit) {
                        view.setUint16(cursor, value.charCodeAt(j), endianness);
                    }
                    break;

                case Types.NUMBER:
                case Types.BOOLEAN:
                    if (debug) {
                        console.info('%c' + value.toString(), 'font-weight:bold;');
                    }
                    view['set' + type_name](cursor, value, endianness);
                    cursor += unit;
                    break;

                case Types.INT8ARRAY:
                case Types.INT16ARRAY:
                case Types.INT32ARRAY:
                case Types.UINT8ARRAY:
                case Types.UINT16ARRAY:
                case Types.UINT32ARRAY:
                case Types.FLOAT32ARRAY:
                case Types.FLOAT64ARRAY:
                    var _view = new Uint8Array(view.buffer, cursor, byte_length);
                    _view.set(new Uint8Array(value.buffer));
                    cursor += byte_length;
                    break;

                case Types.ARRAYBUFFER:
                case Types.BUFFER:
                    var _view = new Uint8Array(view.buffer, cursor, byte_length);
                    _view.set(new Uint8Array(value));
                    cursor += byte_length;
                    break;

                case Types.BLOB:
                case Types.ARRAY:
                case Types.OBJECT:
                    break;

                default:
                    throw 'TypeError: Unexpected type found.';
            }

            if (debug) {
                binary_dump(view, start, cursor - start);
            }
        }

        return view;
    };

    /**
     * Unpack binary data into an object with value and cursor
     * @param  {DataView} view [description]
     * @param  {Number} cursor [description]
     * @return {Object}
     */
    var unpack = function(view, cursor) {
        var i = 0,
            endianness = BIG_ENDIAN,
            start = cursor;
        var type, length, byte_length, value, elem;

        // Retrieve "type"
        type = view.getUint8(cursor, endianness);
        cursor += TYPE_LENGTH;

        if (debug) {
            console.info('Unpacking', type, TypeNames[type]);
        }

        // Retrieve "length"
        if (type === Types.ARRAY || type === Types.OBJECT) {
            length = view.getUint16(cursor, endianness);
            cursor += LENGTH_LENGTH;

            if (debug) {
                console.info('Content Length', length);
            }
        }

        // Retrieve "byte_length"
        byte_length = view.getUint32(cursor, endianness);
        cursor += BYTES_LENGTH;

        if (debug) {
            console.info('Byte Length', byte_length, 'bytes');
        }

        var type_name = Length[type];
        var unit = type_name === null ? 0 : window[type_name + 'Array'].BYTES_PER_ELEMENT;

        switch (type) {
            case Types.NULL:
            case Types.UNDEFINED:
                if (debug) {
                    binary_dump(view, start, cursor - start);
                }
                // NULL and UNDEFINED doesn't have any octet
                value = null;
                break;

            case Types.STRING:
                length = byte_length / unit;
                var string = [];
                for (i = 0; i < length; i++) {
                    var code = view.getUint16(cursor, endianness);
                    cursor += unit;
                    string.push(String.fromCharCode(code));
                }
                value = string.join('');
                if (debug) {
                    console.info('Actual Content %c"' + value + '"', 'font-weight:bold;');
                    binary_dump(view, start, cursor - start);
                }
                break;

            case Types.NUMBER:
                value = view.getFloat64(cursor, endianness);
                cursor += unit;
                if (debug) {
                    console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;');
                    binary_dump(view, start, cursor - start);
                }
                break;

            case Types.BOOLEAN:
                value = view.getUint8(cursor, endianness) === 1 ? true : false;
                cursor += unit;
                if (debug) {
                    console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;');
                    binary_dump(view, start, cursor - start);
                }
                break;

            case Types.INT8ARRAY:
            case Types.INT16ARRAY:
            case Types.INT32ARRAY:
            case Types.UINT8ARRAY:
            case Types.UINT16ARRAY:
            case Types.UINT32ARRAY:
            case Types.FLOAT32ARRAY:
            case Types.FLOAT64ARRAY:
            case Types.ARRAYBUFFER:
                elem = view.buffer.slice(cursor, cursor + byte_length);
                cursor += byte_length;

                // If ArrayBuffer
                if (type === Types.ARRAYBUFFER) {
                    value = elem;

                    // If other TypedArray
                } else {
                    value = new window[type_name + 'Array'](elem);
                }

                if (debug) {
                    binary_dump(view, start, cursor - start);
                }
                break;

            case Types.BLOB:
                if (debug) {
                    binary_dump(view, start, cursor - start);
                }
                // If Blob is available (on browser)
                if (window.Blob) {
                    var mime = unpack(view, cursor);
                    var buffer = unpack(view, mime.cursor);
                    cursor = buffer.cursor;
                    value = new Blob([buffer.value], {
                        type: mime.value
                    });
                } else {
                    // node.js implementation goes here
                    elem = view.buffer.slice(cursor, cursor + byte_length);
                    cursor += byte_length;
                    // node.js implementatino uses Buffer to help Blob
                    value = new Buffer(elem);
                }
                break;

            case Types.ARRAY:
                if (debug) {
                    binary_dump(view, start, cursor - start);
                }
                value = [];
                for (i = 0; i < length; i++) {
                    // Retrieve array element
                    elem = unpack(view, cursor);
                    cursor = elem.cursor;
                    value.push(elem.value);
                }
                break;

            case Types.OBJECT:
                if (debug) {
                    binary_dump(view, start, cursor - start);
                }
                value = {};
                for (i = 0; i < length; i++) {
                    // Retrieve object key and value in sequence
                    var key = unpack(view, cursor);
                    var val = unpack(view, key.cursor);
                    cursor = val.cursor;
                    value[key.value] = val.value;
                }
                break;

            default:
                throw 'TypeError: Type not supported.';
        }
        return {
            value: value,
            cursor: cursor
        };
    };

    /**
     * deferred function to process multiple serialization in order
     * @param  {array}   array    [description]
     * @param  {Function} callback [description]
     * @return {void} no return value
     */
    var deferredSerialize = function(array, callback) {
        var length = array.length,
            results = [],
            count = 0,
            byte_length = 0;
        for (var i = 0; i < array.length; i++) {
            (function(index) {
                serialize(array[index], function(result) {
                    // store results in order
                    results[index] = result;
                    // count byte length
                    byte_length += result[0].header_size + result[0].byte_length;
                    // when all results are on table
                    if (++count === length) {
                        // finally concatenate all reuslts into a single array in order
                        var array = [];
                        for (var j = 0; j < results.length; j++) {
                            array = array.concat(results[j]);
                        }
                        callback(array, byte_length);
                    }
                });
            })(i);
        }
    };

    /**
     * Serializes object and return byte_length
     * @param  {mixed} obj JavaScript object you want to serialize
     * @return {Array} Serialized array object
     */
    var serialize = function(obj, callback) {
        var subarray = [],
            unit = 1,
            header_size = TYPE_LENGTH + BYTES_LENGTH,
            type, byte_length = 0,
            length = 0,
            value = obj;

        type = find_type(obj);

        unit = Length[type] === undefined || Length[type] === null ? 0 :
            window[Length[type] + 'Array'].BYTES_PER_ELEMENT;

        switch (type) {
            case Types.UNDEFINED:
            case Types.NULL:
                break;

            case Types.NUMBER:
            case Types.BOOLEAN:
                byte_length = unit;
                break;

            case Types.STRING:
                length = obj.length;
                byte_length += length * unit;
                break;

            case Types.INT8ARRAY:
            case Types.INT16ARRAY:
            case Types.INT32ARRAY:
            case Types.UINT8ARRAY:
            case Types.UINT16ARRAY:
            case Types.UINT32ARRAY:
            case Types.FLOAT32ARRAY:
            case Types.FLOAT64ARRAY:
                length = obj.length;
                byte_length += length * unit;
                break;

            case Types.ARRAY:
                deferredSerialize(obj, function(subarray, byte_length) {
                    callback([{
                        type: type,
                        length: obj.length,
                        header_size: header_size + LENGTH_LENGTH,
                        byte_length: byte_length,
                        value: null
                    }].concat(subarray));
                });
                return;

            case Types.OBJECT:
                var deferred = [];
                for (var key in obj) {
                    if (obj.hasOwnProperty(key)) {
                        deferred.push(key);
                        deferred.push(obj[key]);
                        length++;
                    }
                }
                deferredSerialize(deferred, function(subarray, byte_length) {
                    callback([{
                        type: type,
                        length: length,
                        header_size: header_size + LENGTH_LENGTH,
                        byte_length: byte_length,
                        value: null
                    }].concat(subarray));
                });
                return;

            case Types.ARRAYBUFFER:
                byte_length += obj.byteLength;
                break;

            case Types.BLOB:
                var mime_type = obj.type;
                var reader = new FileReader();
                reader.onload = function(e) {
                    deferredSerialize([mime_type, e.target.result], function(subarray, byte_length) {
                        callback([{
                            type: type,
                            length: length,
                            header_size: header_size,
                            byte_length: byte_length,
                            value: null
                        }].concat(subarray));
                    });
                };
                reader.onerror = function(e) {
                    throw 'FileReader Error: ' + e;
                };
                reader.readAsArrayBuffer(obj);
                return;

            case Types.BUFFER:
                byte_length += obj.length;
                break;

            default:
                throw 'TypeError: Type "' + obj.constructor.name + '" not supported.';
        }

        callback([{
            type: type,
            length: length,
            header_size: header_size,
            byte_length: byte_length,
            value: value
        }].concat(subarray));
    };

    /**
     * Deserialize binary and return JavaScript object
     * @param  ArrayBuffer buffer ArrayBuffer you want to deserialize
     * @return mixed              Retrieved JavaScript object
     */
    var deserialize = function(buffer, callback) {
        var view = buffer instanceof DataView ? buffer : new DataView(buffer);
        var result = unpack(view, 0);
        return result.value;
    };

    if (debug) {
        window.Test = {
            BIG_ENDIAN: BIG_ENDIAN,
            LITTLE_ENDIAN: LITTLE_ENDIAN,
            Types: Types,
            pack: pack,
            unpack: unpack,
            serialize: serialize,
            deserialize: deserialize
        };
    }

    var binarize = {
        pack: function(obj, callback) {
            try {
                if (debug) console.info('%cPacking Start', 'font-weight: bold; color: red;', obj);
                serialize(obj, function(array) {
                    if (debug) console.info('Serialized Object', array);
                    callback(pack(array));
                });
            } catch (e) {
                throw e;
            }
        },
        unpack: function(buffer, callback) {
            try {
                if (debug) console.info('%cUnpacking Start', 'font-weight: bold; color: red;', buffer);
                var result = deserialize(buffer);
                if (debug) console.info('Deserialized Object', result);
                callback(result);
            } catch (e) {
                throw e;
            }
        }
    };

    window.FileConverter = FileConverter;
    window.FileSelector = FileSelector;
    window.FileBufferReader = FileBufferReader;
})();
