/*! taboverride v4.0.3 | https://github.com/wjbryant/taboverride
(c) 2015 Bill Bryant | http://opensource.org/licenses/mit */

/**
 * @fileOverview taboverride
 * @author       Bill Bryant
 * @version      4.0.3
 */

/*jslint browser: true */
/*global exports, define */

// use CommonJS or AMD if available
(function (factory) {
    'use strict';

    var mod;

    if (typeof exports === 'object') {
        // Node.js/CommonJS
        factory(exports);
    } else if (typeof define === 'function' && define.amd) {
        // AMD - register as an anonymous module
        // files must be concatenated using an AMD-aware tool such as r.js
        define(['exports'], factory);
    } else {
        // no module format - create global variable
        mod = window.tabOverride = {};
        factory(mod);
    }
}(function (tabOverride) {
    'use strict';

    /**
     * The tabOverride namespace object
     *
     * @namespace tabOverride
     */

    var document = window.document,
        listeners,
        aTab = '\t', // the string representing a tab
        tabKey = 9,
        untabKey = 9,
        tabModifierKeys = [],
        untabModifierKeys = ['shiftKey'],
        autoIndent = true, // whether each line should be automatically indented
        inWhitespace = false, // whether the start of the selection is in the leading whitespace on enter
        textareaElem = document.createElement('textarea'), // temp textarea element to get newline character(s)
        newline, // the newline character sequence (\n or \r\n)
        newlineLen, // the number of characters used for a newline (1 or 2)
        hooks = {};

    /**
     * Determines whether the specified modifier keys match the modifier keys
     * that were pressed.
     *
     * @param  {string[]} modifierKeys  the modifier keys to check - ex: ['shiftKey']
     * @param  {Event}    e             the event object for the keydown event
     * @return {boolean}                whether modifierKeys are valid for the event
     *
     * @method tabOverride.utils.isValidModifierKeyCombo
     */
    function isValidModifierKeyCombo(modifierKeys, e) {
        var modifierKeyNames = ['alt', 'ctrl', 'meta', 'shift'],
            numModKeys = modifierKeys.length,
            i,
            j,
            currModifierKey,
            isValid = true;

        // check that all required modifier keys were pressed
        for (i = 0; i < numModKeys; i += 1) {
            if (!e[modifierKeys[i]]) {
                isValid = false;
                break;
            }
        }

        // if the requirements were met, check for additional modifier keys
        if (isValid) {
            for (i = 0; i < modifierKeyNames.length; i += 1) {
                currModifierKey = modifierKeyNames[i] + 'Key';

                // if this key was pressed
                if (e[currModifierKey]) {
                    // if there are required keys, check whether the current key
                    // is required
                    if (numModKeys) {
                        isValid = false;

                        // if this is a required key, continue
                        for (j = 0; j < numModKeys; j += 1) {
                            if (currModifierKey === modifierKeys[j]) {
                                isValid = true;
                                break;
                            }
                        }
                    } else {
                        // no required keys, but one was pressed
                        isValid = false;
                    }
                }

                // an extra key was pressed, don't check anymore
                if (!isValid) {
                    break;
                }
            }
        }

        return isValid;
    }

    /**
     * Determines whether the tab key combination was pressed.
     *
     * @param  {number}  keyCode  the key code of the key that was pressed
     * @param  {Event}   e        the event object for the key event
     * @return {boolean}          whether the tab key combo was pressed
     *
     * @private
     */
    function tabKeyComboPressed(keyCode, e) {
        return keyCode === tabKey && isValidModifierKeyCombo(tabModifierKeys, e);
    }

    /**
     * Determines whether the untab key combination was pressed.
     *
     * @param  {number}  keyCode  the key code of the key that was pressed
     * @param  {Event}   e        the event object for the key event
     * @return {boolean}          whether the untab key combo was pressed
     *
     * @private
     */
    function untabKeyComboPressed(keyCode, e) {
        return keyCode === untabKey && isValidModifierKeyCombo(untabModifierKeys, e);
    }

    /**
     * Creates a function to get and set the specified key combination.
     *
     * @param  {Function} keyFunc       getter/setter function for the key
     * @param  {string[]} modifierKeys  the array of modifier keys to manipulate
     * @return {Function}               a getter/setter function for the specified
     *                                  key combination
     *
     * @private
     */
    function createKeyComboFunction(keyFunc, modifierKeys) {
        return function (keyCode, modifierKeyNames) {
            var i,
                keyCombo = '';

            if (arguments.length) {
                if (typeof keyCode === 'number') {
                    keyFunc(keyCode);

                    modifierKeys.length = 0; // clear the array

                    if (modifierKeyNames && modifierKeyNames.length) {
                        for (i = 0; i < modifierKeyNames.length; i += 1) {
                            modifierKeys.push(modifierKeyNames[i] + 'Key');
                        }
                    }
                }

                return this;
            }

            for (i = 0; i < modifierKeys.length; i += 1) {
                keyCombo += modifierKeys[i].slice(0, -3) + '+';
            }

            return keyCombo + keyFunc();
        };
    }

    /**
     * Event handler to insert or remove tabs and newlines on the keydown event
     * for the tab or enter key.
     *
     * @param {Event} e  the event object
     *
     * @method tabOverride.handlers.keydown
     */
    function overrideKeyDown(e) {
        e = e || event;

        // textarea elements can only contain text nodes which don't receive
        // keydown events, so the event target/srcElement will always be the
        // textarea element, however, prefer currentTarget in order to support
        // delegated events in compliant browsers
        var target = e.currentTarget || e.srcElement, // don't use the "this" keyword (doesn't work in old IE)
            key = e.keyCode, // the key code for the key that was pressed
            tab, // the string representing a tab
            tabLen, // the length of a tab
            text, // initial text in the textarea
            range, // the IE TextRange object
            tempRange, // used to calculate selection start and end positions in IE
            preNewlines, // the number of newline character sequences before the selection start (for IE)
            selNewlines, // the number of newline character sequences within the selection (for IE)
            initScrollTop, // initial scrollTop value used to fix scrolling in Firefox
            selStart, // the selection start position
            selEnd, // the selection end position
            sel, // the selected text
            startLine, // for multi-line selections, the first character position of the first line
            endLine, // for multi-line selections, the last character position of the last line
            numTabs, // the number of tabs inserted / removed in the selection
            startTab, // if a tab was removed from the start of the first line
            preTab, // if a tab was removed before the start of the selection
            whitespace, // the whitespace at the beginning of the first selected line
            whitespaceLen, // the length of the whitespace at the beginning of the first selected line
            CHARACTER = 'character'; // string constant used for the Range.move methods

        // don't do any unnecessary work
        if ((target.nodeName && target.nodeName.toLowerCase() !== 'textarea') ||
                (key !== tabKey && key !== untabKey && (key !== 13 || !autoIndent))) {
            return;
        }

        // initialize variables used for tab and enter keys
        inWhitespace = false; // this will be set to true if enter is pressed in the leading whitespace
        text = target.value;

        // this is really just for Firefox, but will be used by all browsers that support
        // selectionStart and selectionEnd - whenever the textarea value property is reset,
        // Firefox scrolls back to the top - this is used to set it back to the original value
        // scrollTop is nonstandard, but supported by all modern browsers
        initScrollTop = target.scrollTop;

        // get the text selection
        if (typeof target.selectionStart === 'number') {
            selStart = target.selectionStart;
            selEnd = target.selectionEnd;
            sel = text.slice(selStart, selEnd);

        } else if (document.selection) { // IE
            range = document.selection.createRange();
            sel = range.text;
            tempRange = range.duplicate();
            tempRange.moveToElementText(target);
            tempRange.setEndPoint('EndToEnd', range);
            selEnd = tempRange.text.length;
            selStart = selEnd - sel.length;

            // whenever the value of the textarea is changed, the range needs to be reset
            // IE <9 (and Opera) use both \r and \n for newlines - this adds an extra character
            // that needs to be accounted for when doing position calculations with ranges
            // these values are used to offset the selection start and end positions
            if (newlineLen > 1) {
                preNewlines = text.slice(0, selStart).split(newline).length - 1;
                selNewlines = sel.split(newline).length - 1;
            } else {
                preNewlines = selNewlines = 0;
            }
        } else {
            return; // cannot access textarea selection - do nothing
        }

        // tab / untab key - insert / remove tab
        if (key === tabKey || key === untabKey) {

            // initialize tab variables
            tab = aTab;
            tabLen = tab.length;
            numTabs = 0;
            startTab = 0;
            preTab = 0;

            // multi-line selection
            if (selStart !== selEnd && sel.indexOf('\n') !== -1) {
                // for multiple lines, only insert / remove tabs from the beginning of each line

                // find the start of the first selected line
                if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
                    // the selection starts at the beginning of a line
                    startLine = selStart;
                } else {
                    // the selection starts after the beginning of a line
                    // set startLine to the beginning of the first partially selected line
                    // subtract 1 from selStart in case the cursor is at the newline character,
                    // for instance, if the very end of the previous line was selected
                    // add 1 to get the next character after the newline
                    // if there is none before the selection, lastIndexOf returns -1
                    // when 1 is added to that it becomes 0 and the first character is used
                    startLine = text.lastIndexOf('\n', selStart - 1) + 1;
                }

                // find the end of the last selected line
                if (selEnd === text.length || text.charAt(selEnd) === '\n') {
                    // the selection ends at the end of a line
                    endLine = selEnd;
                } else if (text.charAt(selEnd - 1) === '\n') {
                    // the selection ends at the start of a line, but no
                    // characters are selected - don't indent this line
                    endLine = selEnd - 1;
                } else {
                    // the selection ends before the end of a line
                    // set endLine to the end of the last partially selected line
                    endLine = text.indexOf('\n', selEnd);
                    if (endLine === -1) {
                        endLine = text.length;
                    }
                }

                // tab key combo - insert tabs
                if (tabKeyComboPressed(key, e)) {

                    numTabs = 1; // for the first tab

                    // insert tabs at the beginning of each line of the selection
                    target.value = text.slice(0, startLine) + tab +
                        text.slice(startLine, endLine).replace(/\n/g, function () {
                            numTabs += 1;
                            return '\n' + tab;
                        }) + text.slice(endLine);

                    // set start and end points
                    if (range) { // IE
                        range.collapse();
                        range.moveEnd(CHARACTER, selEnd + (numTabs * tabLen) - selNewlines - preNewlines);
                        range.moveStart(CHARACTER, selStart + tabLen - preNewlines);
                        range.select();
                    } else {
                        // the selection start is always moved by 1 character
                        target.selectionStart = selStart + tabLen;
                        // move the selection end over by the total number of tabs inserted
                        target.selectionEnd = selEnd + (numTabs * tabLen);
                        target.scrollTop = initScrollTop;
                    }
                } else if (untabKeyComboPressed(key, e)) {
                    // if the untab key combo was pressed, remove tabs instead of inserting them

                    if (text.slice(startLine).indexOf(tab) === 0) {
                        // is this tab part of the selection?
                        if (startLine === selStart) {
                            // it is, remove it
                            sel = sel.slice(tabLen);
                        } else {
                            // the tab comes before the selection
                            preTab = tabLen;
                        }
                        startTab = tabLen;
                    }

                    target.value = text.slice(0, startLine) + text.slice(startLine + preTab, selStart) +
                        sel.replace(new RegExp('\n' + tab, 'g'), function () {
                            numTabs += 1;
                            return '\n';
                        }) + text.slice(selEnd);

                    // set start and end points
                    if (range) { // IE
                        // setting end first makes calculations easier
                        range.collapse();
                        range.moveEnd(CHARACTER, selEnd - startTab - (numTabs * tabLen) - selNewlines - preNewlines);
                        range.moveStart(CHARACTER, selStart - preTab - preNewlines);
                        range.select();
                    } else {
                        // set start first for Opera
                        target.selectionStart = selStart - preTab; // preTab is 0 or tabLen
                        // move the selection end over by the total number of tabs removed
                        target.selectionEnd = selEnd - startTab - (numTabs * tabLen);
                    }
                } else {
                    return; // do nothing for invalid key combinations
                }

            } else { // single line selection

                // tab key combo - insert a tab
                if (tabKeyComboPressed(key, e)) {
                    if (range) { // IE
                        range.text = tab;
                        range.select();
                    } else {
                        target.value = text.slice(0, selStart) + tab + text.slice(selEnd);
                        target.selectionEnd = target.selectionStart = selStart + tabLen;
                        target.scrollTop = initScrollTop;
                    }
                } else if (untabKeyComboPressed(key, e)) {
                    // if the untab key combo was pressed, remove a tab instead of inserting one

                    // if the character before the selection is a tab, remove it
                    if (text.slice(selStart - tabLen).indexOf(tab) === 0) {
                        target.value = text.slice(0, selStart - tabLen) + text.slice(selStart);

                        // set start and end points
                        if (range) { // IE
                            // collapses range and moves it by -1 tab
                            range.move(CHARACTER, selStart - tabLen - preNewlines);
                            range.select();
                        } else {
                            target.selectionEnd = target.selectionStart = selStart - tabLen;
                            target.scrollTop = initScrollTop;
                        }
                    }
                } else {
                    return; // do nothing for invalid key combinations
                }
            }
        } else if (autoIndent) { // Enter key
            // insert a newline and copy the whitespace from the beginning of the line

            // find the start of the first selected line
            if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
                // the selection starts at the beginning of a line
                // do nothing special
                inWhitespace = true;
                return;
            }

            // see explanation under "multi-line selection" above
            startLine = text.lastIndexOf('\n', selStart - 1) + 1;

            // find the end of the first selected line
            endLine = text.indexOf('\n', selStart);

            // if no newline is found, set endLine to the end of the text
            if (endLine === -1) {
                endLine = text.length;
            }

            // get the whitespace at the beginning of the first selected line (spaces and tabs only)
            whitespace = text.slice(startLine, endLine).match(/^[ \t]*/)[0];
            whitespaceLen = whitespace.length;

            // the cursor (selStart) is in the whitespace at beginning of the line
            // do nothing special
            if (selStart < startLine + whitespaceLen) {
                inWhitespace = true;
                return;
            }

            if (range) { // IE
                // insert the newline and whitespace
                range.text = '\n' + whitespace;
                range.select();
            } else {
                // insert the newline and whitespace
                target.value = text.slice(0, selStart) + '\n' + whitespace + text.slice(selEnd);
                // Opera uses \r\n for a newline, instead of \n,
                // so use newlineLen instead of a hard-coded value
                target.selectionEnd = target.selectionStart = selStart + newlineLen + whitespaceLen;
                target.scrollTop = initScrollTop;
            }
        }

        if (e.preventDefault) {
            e.preventDefault();
        } else {
            e.returnValue = false;
            return false;
        }
    }

    /**
     * Event handler to prevent the default action for the keypress event when
     * tab or enter is pressed. Opera and Firefox also fire a keypress event
     * when the tab or enter key is pressed. Opera requires that the default
     * action be prevented on this event or the textarea will lose focus.
     *
     * @param {Event} e  the event object
     *
     * @method tabOverride.handlers.keypress
     */
    function overrideKeyPress(e) {
        e = e || event;

        var key = e.keyCode;

        if (tabKeyComboPressed(key, e) || untabKeyComboPressed(key, e) ||
                (key === 13 && autoIndent && !inWhitespace)) {

            if (e.preventDefault) {
                e.preventDefault();
            } else {
                e.returnValue = false;
                return false;
            }
        }
    }

    /**
     * Executes all registered extension functions for the specified hook.
     *
     * @param {string} hook    the name of the hook for which the extensions are registered
     * @param {Array}  [args]  the arguments to pass to the extension
     *
     * @method tabOverride.utils.executeExtensions
     */
    function executeExtensions(hook, args) {
        var i,
            extensions = hooks[hook] || [],
            len = extensions.length;

        for (i = 0; i < len; i += 1) {
            extensions[i].apply(null, args);
        }
    }

    /**
     * @typedef {Object} tabOverride.utils~handlerObj
     *
     * @property {string}   type     the event type
     * @property {Function} handler  the handler function - passed an Event object
     */

    /**
     * @typedef {Object} tabOverride.utils~listenersObj
     *
     * @property {Function} add     Adds all the event listeners to the
     *                              specified element
     * @property {Function} remove  Removes all the event listeners from
     *                              the specified element
     */

    /**
     * Creates functions to add and remove event listeners in a cross-browser
     * compatible way.
     *
     * @param  {tabOverride.utils~handlerObj[]} handlerList  an array of {@link tabOverride.utils~handlerObj handlerObj} objects
     * @return {tabOverride.utils~listenersObj}              a listenersObj object used to add and remove the event listeners
     *
     * @method tabOverride.utils.createListeners
     */
    function createListeners(handlerList) {
        var i,
            len = handlerList.length,
            remove,
            add;

        function loop(func) {
            for (i = 0; i < len; i += 1) {
                func(handlerList[i].type, handlerList[i].handler);
            }
        }

        // use the standard event handler registration method when available
        if (document.addEventListener) {
            remove = function (elem) {
                loop(function (type, handler) {
                    elem.removeEventListener(type, handler, false);
                });
            };
            add = function (elem) {
                // remove listeners before adding them to make sure they are not
                // added more than once
                remove(elem);
                loop(function (type, handler) {
                    elem.addEventListener(type, handler, false);
                });
            };
        } else if (document.attachEvent) {
            // support IE 6-8
            remove = function (elem) {
                loop(function (type, handler) {
                    elem.detachEvent('on' + type, handler);
                });
            };
            add = function (elem) {
                remove(elem);
                loop(function (type, handler) {
                    elem.attachEvent('on' + type, handler);
                });
            };
        }

        return {
            add: add,
            remove: remove
        };
    }

    /**
     * Adds the Tab Override event listeners to the specified element.
     *
     * Hooks: addListeners - passed the element to which the listeners will
     * be added.
     *
     * @param {Element} elem  the element to which the listeners will be added
     *
     * @method tabOverride.utils.addListeners
     */
    function addListeners(elem) {
        executeExtensions('addListeners', [elem]);
        listeners.add(elem);
    }

    /**
     * Removes the Tab Override event listeners from the specified element.
     *
     * Hooks: removeListeners - passed the element from which the listeners
     * will be removed.
     *
     * @param {Element} elem  the element from which the listeners will be removed
     *
     * @method tabOverride.utils.removeListeners
     */
    function removeListeners(elem) {
        executeExtensions('removeListeners', [elem]);
        listeners.remove(elem);
    }

    // Initialize Variables

    listeners = createListeners([
        { type: 'keydown', handler: overrideKeyDown },
        { type: 'keypress', handler: overrideKeyPress }
    ]);

    // get the characters used for a newline
    textareaElem.value = '\n';
    newline = textareaElem.value;
    newlineLen = newline.length;
    textareaElem = null;

    // Public Properties and Methods

    /**
     * Namespace for utility methods
     *
     * @namespace
     */
    tabOverride.utils = {
        executeExtensions: executeExtensions,
        isValidModifierKeyCombo: isValidModifierKeyCombo,
        createListeners: createListeners,
        addListeners: addListeners,
        removeListeners: removeListeners
    };

    /**
     * Namespace for event handler functions
     *
     * @namespace
     */
    tabOverride.handlers = {
        keydown: overrideKeyDown,
        keypress: overrideKeyPress
    };

    /**
     * Adds an extension function to be executed when the specified hook is
     * "fired." The extension function is called for each element and is passed
     * any relevant arguments for the hook.
     *
     * @param  {string}   hook  the name of the hook for which the extension
     *                          will be registered
     * @param  {Function} func  the function to be executed when the hook is "fired"
     * @return {Object}         the tabOverride object
     */
    tabOverride.addExtension = function (hook, func) {
        if (hook && typeof hook === 'string' && typeof func === 'function') {
            if (!hooks[hook]) {
                hooks[hook] = [];
            }
            hooks[hook].push(func);
        }

        return this;
    };

    /**
     * Enables or disables Tab Override for the specified textarea element(s).
     *
     * Hooks: set - passed the current element and a boolean indicating whether
     * Tab Override was enabled or disabled.
     *
     * @param  {Element|Element[]} elems          the textarea element(s) for
     *                                            which to enable or disable
     *                                            Tab Override
     * @param  {boolean}           [enable=true]  whether Tab Override should be
     *                                            enabled for the element(s)
     * @return {Object}                           the tabOverride object
     */
    tabOverride.set = function (elems, enable) {
        var enableFlag,
            elemsArr,
            numElems,
            setListeners,
            attrValue,
            i,
            elem;

        if (elems) {
            enableFlag = arguments.length < 2 || enable;

            // don't manipulate param when referencing arguments object
            // this is just a matter of practice
            elemsArr = elems;
            numElems = elemsArr.length;

            if (typeof numElems !== 'number') {
                elemsArr = [elemsArr];
                numElems = 1;
            }

            if (enableFlag) {
                setListeners = addListeners;
                attrValue = 'true';
            } else {
                setListeners = removeListeners;
                attrValue = '';
            }

            for (i = 0; i < numElems; i += 1) {
                elem = elemsArr[i];
                if (elem && elem.nodeName && elem.nodeName.toLowerCase() === 'textarea') {
                    executeExtensions('set', [elem, enableFlag]);
                    elem.setAttribute('data-taboverride-enabled', attrValue);
                    setListeners(elem);
                }
            }
        }

        return this;
    };

    /**
     * Gets or sets the tab size for all elements that have Tab Override enabled.
     * 0 represents the tab character.
     *
     * @param  {number}        [size]  the tab size
     * @return {number|Object}         the tab size or the tabOverride object
     */
    tabOverride.tabSize = function (size) {
        var i;

        if (arguments.length) {
            if (size && typeof size === 'number' && size > 0) {
                aTab = '';
                for (i = 0; i < size; i += 1) {
                    aTab += ' ';
                }
            } else {
                // size is falsy (0), not a number, or a negative number
                aTab = '\t';
            }
            return this;
        }

        return (aTab === '\t') ? 0 : aTab.length;
    };

    /**
     * Gets or sets the auto indent setting. True if each line should be
     * automatically indented (default = true).
     *
     * @param  {boolean}        [enable]  whether auto indent should be enabled
     * @return {boolean|Object}           whether auto indent is enabled or the
     *                                    tabOverride object
     */
    tabOverride.autoIndent = function (enable) {
        if (arguments.length) {
            autoIndent = enable ? true : false;
            return this;
        }

        return autoIndent;
    };

    /**
     * Gets or sets the tab key combination.
     *
     * @param  {number}        keyCode             the key code of the key to use for tab
     * @param  {string[]}      [modifierKeyNames]  the modifier key names - valid names are
     *                                             'alt', 'ctrl', 'meta', and 'shift'
     * @return {string|Object}                     the current tab key combination or the
     *                                             tabOverride object
     *
     * @method
     */
    tabOverride.tabKey = createKeyComboFunction(function (keyCode) {
        if (!arguments.length) {
            return tabKey;
        }
        tabKey = keyCode;
    }, tabModifierKeys);

    /**
     * Gets or sets the untab key combination.
     *
     * @param  {number}        keyCode             the key code of the key to use for untab
     * @param  {string[]}      [modifierKeyNames]  the modifier key names - valid names are
     *                                             'alt', 'ctrl', 'meta', and 'shift'
     * @return {string|Object}                     the current untab key combination or the
     *                                             tabOverride object
     *
     * @method
     */
    tabOverride.untabKey = createKeyComboFunction(function (keyCode) {
        if (!arguments.length) {
            return untabKey;
        }
        untabKey = keyCode;
    }, untabModifierKeys);
}));


/*global threesixty:true*/

function generateImagesToPreload(totalImages) {
  for (var i = 0, images = [], index; i < totalImages; i++) {
    index = (i < 10) ?  '0' + i : i
    images.push('https://github.com/rbartoli/threesixty/raw/master/example/images/sequence-' + index + '.png')
  }

  return images
}

document.addEventListener('DOMContentLoaded', function(){
  var instance = threesixty(
    document.querySelector('#one'),
    generateImagesToPreload(27)
  )
  instance.init()
})


'use strict';

var util            = require('util');
var Imap            = require('imap');
var EventEmitter    = require('events').EventEmitter;
var Q               = require('q');
var Message         = require('./message');

/**
 * Helper function that asynchronously converts ImapMessage instance
 * to instance of Message class.
 *
 * @param  {ImapMessage} imapMessage    Instance of ImapMessage class obtained from Imap server call
 * @return {Promise}                    Promise resolved with instance of Message class
 */
var convertImapMessageToMessage = function(imapMessage) {
    var deferred = Q.defer();
    var message = new Message();

    imapMessage.on('body', function(stream, info) {
        var buffer = '';

        stream.on('data', function(chunk) {
            buffer += chunk.toString('utf8');
        });

        stream.on('end', function() {
            if (info.which === 'TEXT') {
                message.body = buffer;
            } else {
                message.headers = Imap.parseHeader(buffer);
            }
        });
    });

    imapMessage.on('attributes', function(attrs) {
        message.attributes = attrs;
    });

    imapMessage.on('end', function() {
        deferred.resolve(message);
    });

    return deferred.promise;
};

var Inbox = function(options) {
    var _imap = null;
    var _this = this;
    var _inbox = null;

    Object.defineProperties(this, {
        /**
         * Underlying Imap instance.
         * It is lazy-created when first needed to enable methods like useGmail
         * change options.
         *
         * @type {Imap}
         */
        imap: {
            enumerable: true,
            get: function() {
                if (!_imap) {
                    _imap = new Imap(options);

                    // Attach error handler that will emit error events on Inbox instance
                    _imap.on('error', function(error) {
                        _this.emit('error', error);
                    });
                }

                return _imap;
            }
        },

        /**
         * Last open inbox.
         *
         * @type {Box}
         */
        inbox: {
            enumerable: true,
            get: function() {
                return _inbox;
            }
        }
    });

    /**
     * Apply default GMail settings.
     * This method should be called before you connect to the IMAP server,
     * otherwise it has no effect.
     *
     * @return {Inbox} This instance
     */
    this.useGmail = function() {
        options.host = 'imap.gmail.com';
        options.port = 993;
        options.tls = true;
        options.tlsOptions = {
            rejectUnauthorized: false
        };

        return this;
    };

    /**
     * Destroy this object and Imap instance.
     * After destroyed, it can be recreated again by simply accessing it by inbox.imap
     *
     * @return {Inbox} This instance
     */
    this.destroy = function() {
        if (_imap) {
            _imap.destroy();
            _imap = null;
            _inbox = null;
        }

        return this;
    };

    // This event is fired from connect method
    // of this object. We make inbox object accessible as a read-only
    // property by Object.defineProperty above
    this.on('connected', function(instance, inbox) {
        _inbox = inbox;
    });

    // This event is fired from disconnect method
    this.on('disconnected', function() {
        _inbox = null;
    });
};

util.inherits(Inbox, EventEmitter);

/**
 * Connects to IMAP server
 *
 * @param {Boolean} readOnly [optional] Whether to open inbox in read-only mode
 * @return {Promise} promise resolved with this inbox
 */
Inbox.prototype.connect = function(readOnly) {
    var imap = this.imap;
    var deferred = Q.defer();

    imap.on('error', function(error) {
        this.emit('error', error);
    }.bind(this));

    imap.once('ready', function() {
        // Open Imap inbox after the connection has been made
        // `true` as the second argument stands for read-only
        Q.ninvoke(imap, 'openBox', 'INBOX', !!readOnly)
            .then(function(box) {
                this.emit('connected', this, box);

                return this;
            }.bind(this))
            .then(deferred.resolve, deferred.reject);
    }.bind(this));

    imap.connect();

    return deferred.promise;
};

/**
 * Disconnects from IMAP server
 * Doesn't destroy Imap instance.
 *
 * @return {Promise} promise resolved with this inbox
 */
Inbox.prototype.disconnect = function() {
    var imap = this.imap;
    var deferred = Q.defer();

    imap.once('close', function() {
        this.emit('disconnected', this);

        imap.removeAllListeners('error');

        deferred.resolve(this);
    }.bind(this));

    this.imap.end();

    return deferred.promise;
};

/**
 * Start watching inbox for new messages
 *
 * @return {Inbox} This instance
 */
Inbox.prototype.watch = function(options) {
    this.imap.on('mail', function(count) {
        this.fetch(count, options).then(function(messages) {
            this.emit('messages', messages, this);
        }.bind(this));
    }.bind(this));

    return this;
};

Inbox.prototype.unwatch = function() {
    this.imap.removeAllListeners('mail');

    return this;
};

Inbox.prototype.fetch = function(offset, limit, options) {
    if (arguments.length === 2) {
        options = limit;
        limit = offset;
        offset = 0;
    }

    if (arguments.length === 1) {
        limit = offset;
        offset = 0;
    }

    var source = (offset + 1) + ':' + limit;

    return this.fetchByUID(source, options);
};

Inbox.prototype.search = function(criteria, options) {
    return this.searchForUID(criteria).then(function(uids) {
        if (!uids.length) {
            return [];
        }

        return this.fetchByUID(uids, options);
    }.bind(this));
};

Inbox.prototype.searchForUID = function(criteria) {
    criteria = criteria || [];

    return Q.ninvoke(this.imap, 'search', criteria);
};

Inbox.prototype.fetchByUID = function(uids, options) {
    var deferred = Q.defer();
    var messagePromises = [];

    options = options || {};
    options.bodies = options.bodies || ['HEADER.FIELDS (FROM TO SUBJECT DATE)', 'TEXT'];
    options.struct = true;

    var imapFetch = this.imap.fetch(uids, options);

    imapFetch.on('message', function(imapMessage) {
        messagePromises.push(convertImapMessageToMessage(imapMessage));
    }.bind(this));

    imapFetch.on('error', deferred.reject);

    imapFetch.on('end', function() {
        Q.all(messagePromises).then(deferred.resolve, deferred.reject);
    });

    return deferred.promise;
};

module.exports = Inbox;


var crc32 = require( 'cyclic-32' )
var uint64 = require( './uint64' )

/**
 * GUID Partition Table
 * @constructor
 * @param {Object} [options]
 * @param {Number} [options.blockSize=512]
 * @returns {GPT}
 */
function GPT( options ) {

  if( !(this instanceof GPT) ) {
    return new GPT( options )
  }

  options = options != null ? options : {}

  /** @type {Number} Storage device's block size in bytes */
  this.blockSize = options.blockSize || 512

  /** @type {String} GUID of the GUID Partition Table */
  this.guid = options.guid || GPT.GUID.ZERO
  /** @type {Number} GPT format revision (?) */
  this.revision = options.revision || 0
  /** @type {Number} Size of the GPT header in bytes */
  this.headerSize = options.headerSize || GPT.HEADER_SIZE
  /** @type {Number} GPT header's CRC32 checksum */
  this.headerChecksum = 0

  /** @type {Number} Logical block address of *this* GPT */
  this.currentLBA = options.currentLBA || 1
  /** @type {Number} Logical block address of the secondary GPT */
  this.backupLBA = options.backupLBA || 0
  /** @type {Number} Address of the first user-space usable logical block */
  this.firstLBA = options.firstLBA || 34
  /** @type {Number} Address of the last user-space usable logical block */
  this.lastLBA = options.lastLBA || 0

  /** @type {Number} LBA of partition table */
  this.tableOffset = options.tableOffset || GPT.TABLE_OFFSET
  /** @type {Number} Number of partition table entries */
  this.entries = options.entries || GPT.TABLE_ENTRIES
  /** @type {Number} Partition entry's size in bytes */
  this.entrySize = options.entrySize || GPT.TABLE_ENTRY_SIZE
  /** @type {Number} Partition table's CRC32 checksum */
  this.tableChecksum = 0

  // Array of partition entries
  this.partitions = []

}

// Exports
module.exports = GPT

/**
 * GPT magic signature
 * @type {String}
 */
GPT.SIGNATURE = 'EFI PART'

/**
 * Default GPT header size in bytes
 * @type {Number}
 */
GPT.HEADER_SIZE = 92

/**
 * Offset of the partition table in logical blocks
 * @type {Number}
 */
GPT.TABLE_OFFSET = 2

/**
 * Number of partition table entries
 * @type {Number}
 */
GPT.TABLE_ENTRIES = 128

/**
 * Size of a partition table entry in bytes
 * @type {Number}
 */
GPT.TABLE_ENTRY_SIZE = 128

/**
 * GPT GUID structure constructor
 * @type {Function}
 */
GPT.GUID = require( './guid' )

/**
 * GPT PartitionEntry constructor
 * @type {Function}
 */
GPT.PartitionEntry = require( './partition' )

/**
 * Parse a GUID Partition Table from a buffer
 * @param {Buffer} buffer
 * @param {Number} [offset=0]
 * @returns {GPT}
 */
GPT.parse = function( buffer, offset ) {
  return new GPT().parse( buffer, offset )
}

/**
 * Parse a GPT from a buffer in its backup order
 * @param {Buffer} buffer
 * @param {Number} [offset=0]
 * @returns {GPT}
 */
GPT.parseBackup = function( buffer, offset ) {
  return new GPT().parseBackup( buffer, offset )
}

// TODO: Should this verify a buffer, or an instance of a GPT?
// GPT.verify = function( gpt ) {
//   return gpt.verify()
// }

/**
 * Compare two GPTs against each other
 * @param {GPT} primary
 * @param {GPT} secondary
 * @returns {Boolean}
 */
// TODO:
// GPT.compare = function( primary, secondary ) {
//   return Boolean
// }

/**
 * Align a value to a given boundary value
 * @param {Number} value
 * @param {Number} boundary
 * @returns {Number}
 * @example
 * align( 512 * 1.5, 512 ) // => 1024
 */
function align( value, boundary ) {
  return value + ( boundary - ( value % boundary ) ) % boundary
}

/**
 * GUID Partition Table prototype
 * @type {Object}
 * @ignore
 */
GPT.prototype = {

  constructor: GPT,

  /**
   * @property {Number} tableSize
   * @readOnly
   */
  get tableSize() {
    return align( this.entrySize * 4, this.blockSize ) +
      Math.max( align( this.entrySize * ( this.entries - 4 ), this.blockSize ), this.blockSize * 31 )
  },

  /**
   * Verify integrity of the GPT
   * @returns {Boolean}
   */
  verify() {
    return this.verifyHeader() && this.verifyTable()
  },

  /**
   * Verify integrity of the header
   * @returns {Boolean}
   */
  verifyHeader() {
    return this.checksumHeader() === this.headerChecksum
  },

  /**
   * Verify integrity of the partition table
   * @returns {Boolean}
   */
  verifyTable() {
    return this.checksumTable() === this.tableChecksum
  },

  /**
   * Calculate the header checksum (CRC32)
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {Number}
   */
  checksumHeader( buffer, offset ) {

    offset = offset || 0
    buffer = buffer || this.writeHeader( null, 0, true )

    // Read & clear stored checksum from buffer
    var storedChecksum =  buffer.readUInt32LE( offset + 16 )
    buffer.writeUInt32LE( 0, offset + 16 )

    // Calculate checksum
    var checksum = crc32( buffer ) >>> 0

    // Restore stored checksum
    buffer.writeUInt32LE( storedChecksum, offset + 16 )

    return checksum

  },

  /**
   * Calculate the partition table checksum (CRC32)
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {Number}
   */
  checksumTable( buffer, offset ) {

    var tableSize = this.tableSize

    offset = offset || 0

    if( buffer == null ) {

      buffer = Buffer.alloc( tableSize )

      this.writeTable( this.partitions.slice( 0, 4 ), buffer, offset )
      this.writeTable( this.partitions.slice( 4 ), buffer, offset + this.blockSize )

    } else if( ( buffer.length - offset ) >= tableSize ) {

      buffer = buffer.length !== tableSize ?
        buffer.slice( offset, offset + tableSize ) :
        buffer

    } else {
      throw new Error( `Buffer smaller than table size (${ buffer.length - offset } < ${ tableSize })` )
    }

    return crc32( buffer ) >>> 0

  },

  /**
   * Parse a GUID Partition Table from a buffer
   * @param {Buffer} buffer
   * @param {Number} [offset=0]
   * @returns {GPT}
   */
  parse( buffer, offset ) {

    offset = offset || 0

    // Clean out our partition array
    this.partitions.length = 0

    // Parse header
    this.parseHeader( buffer, offset )

    // Calculate table offset in bytes from currentLBA & tableOffset
    var tableOffset = ( this.tableOffset - this.currentLBA ) * this.blockSize

    // Parse partition tables
    this.parseTable( buffer, offset + tableOffset, offset + tableOffset + this.blockSize )
    this.parseTable( buffer, offset + tableOffset + this.blockSize, offset + tableOffset + this.tableSize )

    return this

  },

  /**
   * Parse a GPT from a buffer in its backup order
   * @param {Buffer} buffer
   * @param {Number} [offset=0]
   * @returns {GPT}
   */
  parseBackup( buffer, offset ) {

    offset = offset || 0

    // Clean out our partition array
    this.partitions.length = 0

    // Parse header, adding 32 blocks to the offset, as the backup
    // has the header behind the partition table
    this.parseHeader( buffer, buffer.length - this.blockSize )

    // Parse partition tables
    this.parseTable( buffer, offset, offset + this.blockSize )
    this.parseTable( buffer, offset + this.blockSize, offset + this.tableSize )

    return this

  },

  /**
   * Parse a GPT's header from a given buffer
   * @param {Buffer} buffer
   * @param {Number} [offset=0]
   * @returns {GPT}
   */
  parseHeader( buffer, offset ) {

    offset = offset || 0

    if( buffer.length - offset < GPT.HEADER_SIZE ) {
      throw new Error( `Buffer too small: Minimum header size is ${GPT.HEADER_SIZE} bytes, got ${buffer.length - offset} bytes` )
    }

    var signature = buffer.toString( 'ascii', offset + 0, offset + 8 )
    if( signature !== GPT.SIGNATURE ) {
      throw new Error( `Invalid signature: Expected "${GPT.SIGNATURE}", saw "${signature}"` )
    }

    // Header descriptors
    this.revision = buffer.readUInt32LE( offset + 8 )
    this.headerSize = buffer.readUInt32LE( offset + 12 )
    this.headerChecksum = buffer.readUInt32LE( offset + 16 )

    // Location descriptors
    this.currentLBA = uint64.readLE( buffer, offset + 24 )
    this.backupLBA = uint64.readLE( buffer, offset + 32 )
    this.firstLBA = uint64.readLE( buffer, offset + 40 )
    this.lastLBA = uint64.readLE( buffer, offset + 48 )

    // GUID
    this.guid = GPT.GUID.toString( buffer, offset + 56 )

    // Partition table descriptors
    this.tableOffset = uint64.readLE( buffer, offset + 72 )
    this.entries = buffer.readUInt32LE( offset + 80 )
    this.entrySize = buffer.readUInt32LE( offset + 84 )
    this.tableChecksum = buffer.readUInt32LE( offset + 88 )

    return this

  },

  /**
   * Parse a GPT's partition table entries from a given buffer
   * @param {Buffer} buffer
   * @param {Number} [offset=0]
   * @param {Number} [end]
   * @returns {GPT}
   */
  parseTable( buffer, offset, end ) {

    offset = offset || 0
    end = Math.min( end || buffer.length, buffer.length )

    var part = null

    while( ( offset + this.entrySize ) <= end ) {
      part = GPT.PartitionEntry.parse( buffer, offset )
      if( !part.isEmpty() ) {
        this.partitions.push( part )
      }
      offset += this.entrySize
    }

    return this

  },

  /**
   * Write the GPT to a buffer
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {Buffer}
   */
  write( buffer, offset ) {

    // Calculate the table offset from its LBA & the block size
    // NOTE: What'd happen if tableOffset == 0?
    // Prob. good idea to add a test for expected behavior
    var tableOffset = ( this.tableOffset - this.currentLBA ) * this.blockSize
    var length = this.blockSize + this.tableSize

    offset = offset || 0
    buffer = buffer || Buffer.alloc( offset + length, 0 )

    this.writeTable( this.partitions.slice( 0, 4 ), buffer, offset + tableOffset )
    this.writeTable( this.partitions.slice( 4 ), buffer, offset + tableOffset + this.blockSize )

    // Recalculate partition table checksum before writing header,
    // as the header's checksum includes the table checksum
    this.tableChecksum = this.checksumTable( buffer, offset + tableOffset )

    this.writeHeader( buffer, offset )

    return buffer

  },

  /**
   * Write the GPT to a buffer in its backup order
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {Buffer}
   */
  writeBackup( buffer, offset ) {

    // NOTE: How is .tableOffset to be interpreted for backup GPTs?
    var tableSize = this.tableSize
    var length = this.blockSize + tableSize

    offset = offset || 0
    buffer = buffer || Buffer.alloc( offset + length )

    // For the backup, the partition table comes first, followed by the header
    this.writeTable( this.partitions.slice( 0, 4 ), buffer, offset )
    this.writeTable( this.partitions.slice( 4 ), buffer, offset + this.blockSize )

    // Recalculate partition table checksum before writing header,
    // as the header's checksum includes the table checksum
    this.tableChecksum = this.checksumTable( buffer, offset )

    this.writeHeader( buffer, offset + tableSize )

    return buffer

  },

  /**
   * Write a backup order GPT from a primary (or vice-versa) to a buffer
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {Buffer}
   */
  writeBackupFromPrimary( buffer, offset ) {

    // Swap currentLBA, backupLBA & tableOffset for backup order
    var currentLBA = this.currentLBA
    var backupLBA = this.backupLBA
    var tableOffset = this.tableOffset

    this.backupLBA = currentLBA
    this.currentLBA = backupLBA
    this.tableOffset = this.currentLBA - ( this.tableSize / this.blockSize )

    var output = this.writeBackup( buffer, offset )

    // Restore currentLBA, backupLBA & tableOffset
    this.backupLBA = backupLBA
    this.currentLBA = currentLBA
    this.tableOffset = tableOffset

    return output

  },

  /**
   * Write the GPT header to a buffer
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @param {Boolean} [noChecksum=false] - don't calculate & update the header checksum
   * @returns {Buffer}
   */
  writeHeader( buffer, offset, noChecksum ) {

    offset = offset || 0
    buffer = buffer || Buffer.alloc( offset + this.headerSize )

    buffer.write( GPT.SIGNATURE, offset + 0, offset + 8, 'ascii' )

    // Header descriptors
    buffer.writeUInt32LE( this.revision, offset + 8 )
    buffer.writeUInt32LE( this.headerSize, offset + 12 )
    buffer.writeUInt32LE( this.headerChecksum, offset + 16 )

    // Location descriptors
    uint64.writeLE( buffer, this.currentLBA, offset + 24 )
    uint64.writeLE( buffer, this.backupLBA, offset + 32 )
    uint64.writeLE( buffer, this.firstLBA, offset + 40 )
    uint64.writeLE( buffer, this.lastLBA, offset + 48 )

    // GUID
    GPT.GUID.write( this.guid, buffer, offset + 56 )

    // Partition table descriptors
    uint64.writeLE( buffer, this.tableOffset, offset + 72 )
    buffer.writeUInt32LE( this.entries, offset + 80 )
    buffer.writeUInt32LE( this.entrySize, offset + 84 )
    buffer.writeUInt32LE( this.tableChecksum, offset + 88 )

    if( !noChecksum ) {
      this.headerChecksum = this.checksumHeader( buffer.slice( offset, offset + this.headerSize ) )
      buffer.writeUInt32LE( this.headerChecksum, offset + 16 )
    }

    return buffer

  },

  /**
   * Write partition entries to a buffer
   * @param {Array<GPT.Partition>} entries
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {Buffer}
   */
  writeTable( entries, buffer, offset ) {

    offset = offset || 0
    buffer = buffer || Buffer.alloc( offset + ( this.entrySize * entries.length ) )

    for( var i = 0; i < entries.length; i++ ) {
      entries[i].write( buffer, offset + ( i * this.entrySize ) )
    }

    return buffer

  }

}


// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

var sketchSpecifics = require('./sketch-specifics')

// we only expose the posix implementation since Sketch only runs on macOS

var CHAR_FORWARD_SLASH = 47
var CHAR_DOT = 46

// Resolves . and .. elements in a path with directory names
function normalizeString(path, allowAboveRoot) {
  var res = ''
  var lastSegmentLength = 0
  var lastSlash = -1
  var dots = 0
  var code
  for (var i = 0; i <= path.length; i += 1) {
    if (i < path.length) code = path.charCodeAt(i)
    else if (code === CHAR_FORWARD_SLASH) break
    else code = CHAR_FORWARD_SLASH
    if (code === CHAR_FORWARD_SLASH) {
      if (lastSlash === i - 1 || dots === 1) {
        // NOOP
      } else if (lastSlash !== i - 1 && dots === 2) {
        if (
          res.length < 2 ||
          lastSegmentLength !== 2 ||
          res.charCodeAt(res.length - 1) !== CHAR_DOT ||
          res.charCodeAt(res.length - 2) !== CHAR_DOT
        ) {
          if (res.length > 2) {
            var lastSlashIndex = res.lastIndexOf('/')
            if (lastSlashIndex !== res.length - 1) {
              if (lastSlashIndex === -1) {
                res = ''
                lastSegmentLength = 0
              } else {
                res = res.slice(0, lastSlashIndex)
                lastSegmentLength = res.length - 1 - res.lastIndexOf('/')
              }
              lastSlash = i
              dots = 0
              continue
            }
          } else if (res.length === 2 || res.length === 1) {
            res = ''
            lastSegmentLength = 0
            lastSlash = i
            dots = 0
            continue
          }
        }
        if (allowAboveRoot) {
          if (res.length > 0) res += '/..'
          else res = '..'
          lastSegmentLength = 2
        }
      } else {
        if (res.length > 0) res += '/' + path.slice(lastSlash + 1, i)
        else res = path.slice(lastSlash + 1, i)
        lastSegmentLength = i - lastSlash - 1
      }
      lastSlash = i
      dots = 0
    } else if (code === CHAR_DOT && dots !== -1) {
      ++dots
    } else {
      dots = -1
    }
  }
  return res
}

function _format(sep, pathObject) {
  var dir = pathObject.dir || pathObject.root
  var base =
    pathObject.base || (pathObject.name || '') + (pathObject.ext || '')
  if (!dir) {
    return base
  }
  if (dir === pathObject.root) {
    return dir + base
  }
  return dir + sep + base
}

var posix = {
  // path.resolve([from ...], to)
  resolve: function resolve() {
    var resolvedPath = ''
    var resolvedAbsolute = false
    var cwd

    for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i -= 1) {
      var path
      if (i >= 0) {
        path = arguments[i]
      } else {
        if (cwd === undefined) {
          cwd = posix.dirname(sketchSpecifics.cwd())
        }
        path = cwd
      }

      path = sketchSpecifics.getString(path, 'path')

      // Skip empty entries
      if (path.length === 0) {
        continue
      }

      resolvedPath = path + '/' + resolvedPath
      resolvedAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH
    }

    // At this point the path should be resolved to a full absolute path, but
    // handle relative paths to be safe (might happen when process.cwd() fails)

    // Normalize the path
    resolvedPath = normalizeString(resolvedPath, !resolvedAbsolute)

    if (resolvedAbsolute) {
      if (resolvedPath.length > 0) return '/' + resolvedPath
      else return '/'
    } else if (resolvedPath.length > 0) {
      return resolvedPath
    } else {
      return '.'
    }
  },

  normalize: function normalize(path) {
    path = sketchSpecifics.getString(path, 'path')

    if (path.length === 0) return '.'

    var isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH
    var trailingSeparator =
      path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH

    // Normalize the path
    path = normalizeString(path, !isAbsolute)

    if (path.length === 0 && !isAbsolute) path = '.'
    if (path.length > 0 && trailingSeparator) path += '/'

    if (isAbsolute) return '/' + path
    return path
  },

  isAbsolute: function isAbsolute(path) {
    path = sketchSpecifics.getString(path, 'path')
    return path.length > 0 && path.charCodeAt(0) === CHAR_FORWARD_SLASH
  },

  join: function join() {
    if (arguments.length === 0) return '.'
    var joined
    for (var i = 0; i < arguments.length; i += 1) {
      var arg = arguments[i]
      arg = sketchSpecifics.getString(arg, 'path')
      if (arg.length > 0) {
        if (joined === undefined) joined = arg
        else joined += '/' + arg
      }
    }
    if (joined === undefined) return '.'
    return posix.normalize(joined)
  },

  relative: function relative(from, to) {
    from = sketchSpecifics.getString(from, 'from path')
    to = sketchSpecifics.getString(to, 'to path')

    if (from === to) return ''

    from = posix.resolve(from)
    to = posix.resolve(to)

    if (from === to) return ''

    // Trim any leading backslashes
    var fromStart = 1
    for (; fromStart < from.length; fromStart += 1) {
      if (from.charCodeAt(fromStart) !== CHAR_FORWARD_SLASH) break
    }
    var fromEnd = from.length
    var fromLen = fromEnd - fromStart

    // Trim any leading backslashes
    var toStart = 1
    for (; toStart < to.length; toStart += 1) {
      if (to.charCodeAt(toStart) !== CHAR_FORWARD_SLASH) break
    }
    var toEnd = to.length
    var toLen = toEnd - toStart

    // Compare paths to find the longest common path from root
    var length = fromLen < toLen ? fromLen : toLen
    var lastCommonSep = -1
    var i = 0
    for (; i <= length; i += 1) {
      if (i === length) {
        if (toLen > length) {
          if (to.charCodeAt(toStart + i) === CHAR_FORWARD_SLASH) {
            // We get here if `from` is the exact base path for `to`.
            // For example: from='/foo/bar'; to='/foo/bar/baz'
            return to.slice(toStart + i + 1)
          } else if (i === 0) {
            // We get here if `from` is the root
            // For example: from='/'; to='/foo'
            return to.slice(toStart + i)
          }
        } else if (fromLen > length) {
          if (from.charCodeAt(fromStart + i) === CHAR_FORWARD_SLASH) {
            // We get here if `to` is the exact base path for `from`.
            // For example: from='/foo/bar/baz'; to='/foo/bar'
            lastCommonSep = i
          } else if (i === 0) {
            // We get here if `to` is the root.
            // For example: from='/foo'; to='/'
            lastCommonSep = 0
          }
        }
        break
      }
      var fromCode = from.charCodeAt(fromStart + i)
      var toCode = to.charCodeAt(toStart + i)
      if (fromCode !== toCode) break
      else if (fromCode === CHAR_FORWARD_SLASH) lastCommonSep = i
    }

    var out = ''
    // Generate the relative path based on the path difference between `to`
    // and `from`
    for (i = fromStart + lastCommonSep + 1; i <= fromEnd; i += 1) {
      if (i === fromEnd || from.charCodeAt(i) === CHAR_FORWARD_SLASH) {
        if (out.length === 0) out += '..'
        else out += '/..'
      }
    }

    // Lastly, append the rest of the destination (`to`) path that comes after
    // the common path parts
    if (out.length > 0) return out + to.slice(toStart + lastCommonSep)
    else {
      toStart += lastCommonSep
      if (to.charCodeAt(toStart) === CHAR_FORWARD_SLASH) toStart += 1
      return to.slice(toStart)
    }
  },

  toNamespacedPath: function toNamespacedPath(path) {
    // Non-op on posix systems
    return path
  },

  dirname: function dirname(path) {
    path = sketchSpecifics.getString(path, 'path')
    if (path.length === 0) return '.'
    var code = path.charCodeAt(0)
    var hasRoot = code === CHAR_FORWARD_SLASH
    var end = -1
    var matchedSlash = true
    for (var i = path.length - 1; i >= 1; i -= 1) {
      code = path.charCodeAt(i)
      if (code === CHAR_FORWARD_SLASH) {
        if (!matchedSlash) {
          end = i
          break
        }
      } else {
        // We saw the first non-path separator
        matchedSlash = false
      }
    }

    if (end === -1) return hasRoot ? '/' : '.'
    if (hasRoot && end === 1) return '//'
    return path.slice(0, end)
  },

  basename: function basename(path, ext) {
    if (ext !== undefined)
      ext = sketchSpecifics.getString(ext, 'ext')
    path = sketchSpecifics.getString(path, 'path')

    var start = 0
    var end = -1
    var matchedSlash = true
    var i

    if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
      if (ext.length === path.length && ext === path) return ''
      var extIdx = ext.length - 1
      var firstNonSlashEnd = -1
      for (i = path.length - 1; i >= 0; i -= 1) {
        var code = path.charCodeAt(i)
        if (code === CHAR_FORWARD_SLASH) {
          // If we reached a path separator that was not part of a set of path
          // separators at the end of the string, stop now
          if (!matchedSlash) {
            start = i + 1
            break
          }
        } else {
          if (firstNonSlashEnd === -1) {
            // We saw the first non-path separator, remember this index in case
            // we need it if the extension ends up not matching
            matchedSlash = false
            firstNonSlashEnd = i + 1
          }
          if (extIdx >= 0) {
            // Try to match the explicit extension
            if (code === ext.charCodeAt(extIdx)) {
              if (--extIdx === -1) {
                // We matched the extension, so mark this as the end of our path
                // component
                end = i
              }
            } else {
              // Extension does not match, so our result is the entire path
              // component
              extIdx = -1
              end = firstNonSlashEnd
            }
          }
        }
      }

      if (start === end) end = firstNonSlashEnd
      else if (end === -1) end = path.length
      return path.slice(start, end)
    } else {
      for (i = path.length - 1; i >= 0; --i) {
        if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {
          // If we reached a path separator that was not part of a set of path
          // separators at the end of the string, stop now
          if (!matchedSlash) {
            start = i + 1
            break
          }
        } else if (end === -1) {
          // We saw the first non-path separator, mark this as the end of our
          // path component
          matchedSlash = false
          end = i + 1
        }
      }

      if (end === -1) return ''
      return path.slice(start, end)
    }
  },

  extname: function extname(path) {
    path = sketchSpecifics.getString(path, 'path')
    var startDot = -1
    var startPart = 0
    var end = -1
    var matchedSlash = true
    // Track the state of characters (if any) we see before our first dot and
    // after any path separator we find
    var preDotState = 0
    for (var i = path.length - 1; i >= 0; --i) {
      var code = path.charCodeAt(i)
      if (code === CHAR_FORWARD_SLASH) {
        // If we reached a path separator that was not part of a set of path
        // separators at the end of the string, stop now
        if (!matchedSlash) {
          startPart = i + 1
          break
        }
        continue
      }
      if (end === -1) {
        // We saw the first non-path separator, mark this as the end of our
        // extension
        matchedSlash = false
        end = i + 1
      }
      if (code === CHAR_DOT) {
        // If this is our first dot, mark it as the start of our extension
        if (startDot === -1) startDot = i
        else if (preDotState !== 1) preDotState = 1
      } else if (startDot !== -1) {
        // We saw a non-dot and non-path separator before our dot, so we should
        // have a good chance at having a non-empty extension
        preDotState = -1
      }
    }

    if (
      startDot === -1 ||
      end === -1 ||
      // We saw a non-dot character immediately before the dot
      preDotState === 0 ||
      // The (right-most) trimmed path component is exactly '..'
      (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
    ) {
      return ''
    }
    return path.slice(startDot, end)
  },

  format: function format(pathObject) {
    if (pathObject === null || typeof pathObject !== 'object') {
      throw new Error('pathObject should be an Object')
    }
    return _format('/', pathObject)
  },

  parse: function parse(path) {
    path = sketchSpecifics.getString(path, 'path')

    var ret = { root: '', dir: '', base: '', ext: '', name: '' }
    if (path.length === 0) return ret
    var code = path.charCodeAt(0)
    var isAbsolute = code === CHAR_FORWARD_SLASH
    var start
    if (isAbsolute) {
      ret.root = '/'
      start = 1
    } else {
      start = 0
    }
    var startDot = -1
    var startPart = 0
    var end = -1
    var matchedSlash = true
    var i = path.length - 1

    // Track the state of characters (if any) we see before our first dot and
    // after any path separator we find
    var preDotState = 0

    // Get non-dir info
    for (; i >= start; --i) {
      code = path.charCodeAt(i)
      if (code === CHAR_FORWARD_SLASH) {
        // If we reached a path separator that was not part of a set of path
        // separators at the end of the string, stop now
        if (!matchedSlash) {
          startPart = i + 1
          break
        }
        continue
      }
      if (end === -1) {
        // We saw the first non-path separator, mark this as the end of our
        // extension
        matchedSlash = false
        end = i + 1
      }
      if (code === CHAR_DOT) {
        // If this is our first dot, mark it as the start of our extension
        if (startDot === -1) startDot = i
        else if (preDotState !== 1) preDotState = 1
      } else if (startDot !== -1) {
        // We saw a non-dot and non-path separator before our dot, so we should
        // have a good chance at having a non-empty extension
        preDotState = -1
      }
    }

    if (
      startDot === -1 ||
      end === -1 ||
      // We saw a non-dot character immediately before the dot
      preDotState === 0 ||
      // The (right-most) trimmed path component is exactly '..'
      (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
    ) {
      if (end !== -1) {
        if (startPart === 0 && isAbsolute)
          ret.base = ret.name = path.slice(1, end)
        else ret.base = ret.name = path.slice(startPart, end)
      }
    } else {
      if (startPart === 0 && isAbsolute) {
        ret.name = path.slice(1, startDot)
        ret.base = path.slice(1, end)
      } else {
        ret.name = path.slice(startPart, startDot)
        ret.base = path.slice(startPart, end)
      }
      ret.ext = path.slice(startDot, end)
    }

    if (startPart > 0) ret.dir = path.slice(0, startPart - 1)
    else if (isAbsolute) ret.dir = '/'

    return ret
  },

  sep: '/',
  delimiter: ':',
  win32: null,
  posix: null,

  resourcePath: sketchSpecifics.resourcePath,
}

module.exports = posix
module.exports.posix = posix


var fs = require( 'fs' )
var MBR = require( 'mbr' )
var GPT = require( '..' )
var inspect = require( '../test/inspect' )

var argv = process.argv.slice( 2 )
var devicePath = argv.shift()

if( !devicePath ) {
  console.error(`
  Usage: node example/verify <device>

  Examples:
    - Linux / Mac OS: node example/verify /dev/rdisk2
    - Windows: node example/verify \\.\PhysicalDrive0

  NOTE: This assumes a block size of 512 bytes
`)
  process.exit(1)
}

console.log( '' )

var blockSize = 512
var fd = null

try {
  fd = fs.openSync( devicePath, 'r' )
} catch( error ) {
  console.error( 'Couldn\'t open device for reading:\n', error.message )
  process.exit( 1 )
}

function readMBR() {
  var buffer = Buffer.alloc( 512 )
  fs.readSync( fd, buffer, 0, buffer.length, 0 )
  return MBR.parse( buffer )
}

function readPrimaryGPT(efiPart) {

  // NOTE: You'll need to know / determine the logical block size of the storage device;
  // For the sake of brevity, we'll just go with the still most common 512 bytes
  var gpt = new GPT({ blockSize: 512 })
  var offset = efiPart.firstLBA * gpt.blockSize
  // The default GPT is 33 blocks in length (1 block header, 32 block table)
  var buffer = Buffer.alloc( 33 * gpt.blockSize )

  fs.readSync( fd, buffer, 0, buffer.length, offset )

  return GPT.parse( buffer )

}

// function readPrimaryGPT(efiPart) {

//   // NOTE: You'll need to know / determine the logical block size of the storage device;
//   // For the sake of brevity, we'll just go with the still most common 512 bytes
//   var gpt = new GPT({ blockSize: 512 })

//   // First, we need to read & parse the GPT header, which will declare various
//   // sizes and offsets for us to calculate where & how long the table and backup are
//   var offset = efiPart.firstLBA * gpt.blockSize
//   var headerBuffer = Buffer.alloc( gpt.blockSize )

//   fs.readSync( fd, headerBuffer, 0, headerBuffer.length, offset )
//   gpt.parseHeader( headerBuffer )

//   // Now on to reading the actual partition table
//   var tableBuffer = Buffer.alloc( gpt.tableSize )
//   var tableOffset = gpt.tableOffset * gpt.blockSize
//   fs.readSync( fd, tableBuffer, 0, tableBuffer.length, tableOffset )

//   // We need to parse the first 4 partition entries & the rest separately
//   // as the first 4 table entries always occupy one block,
//   // with the rest following in subsequent blocks
//   gpt.parseTable( tableBuffer, 0, gpt.blockSize )
//   gpt.parseTable( tableBuffer, gpt.blockSize, gpt.tableSize )

//   return gpt

// }

function readBackupGPT(primaryGPT) {

  var backupGPT = new GPT({ blockSize: primaryGPT.blockSize })
  var buffer = Buffer.alloc( 33 * primaryGPT.blockSize )
  var offset = ( ( primaryGPT.backupLBA - 32 ) * blockSize )

  fs.readSync( fd, buffer, 0, buffer.length, offset )
  backupGPT.parseBackup( buffer )

  return backupGPT

}

var mbr = readMBR()

console.log( 'Master Boot Record:', inspect( mbr ) )
console.log( '' )

var efiPart = mbr.getEFIPart()

if( efiPart == null ) {
  return console.error( 'No EFI partition found' )
}

console.log( 'EFI Parition:', inspect( efiPart ) )
console.log( '' )

var primaryGPT = readPrimaryGPT(efiPart)

console.log( 'Primary:', inspect( primaryGPT ) )
console.log( '' )

var backupGPT = readBackupGPT(primaryGPT)

console.log( 'Backup:', inspect( backupGPT ) )
console.log( '' )

// Check header & table checksums for primary and backup GPT
var isPrimaryValid = primaryGPT.verify() // true
var isBackupValid = backupGPT.verify() // true

// Compare primary & backup GPT table checksums to
// verify both are in the same state
// NOTE: If there's a `gpt.headerChecksum` match, something's wrong,
// as the primary and backup should refer to each other in offsets
var checksumsMatch = primaryGPT.tableChecksum === backupGPT.tableChecksum &&
  primaryGPT.headerChecksum !== backupGPT.headerChecksum

if( isPrimaryValid && isBackupValid && checksumsMatch ) {
  return console.log('[OK]')
}

if( !isPrimaryValid ) {
  console.error( `[ERROR]: Primary GPT checksum mismatch` )
  process.exit( 1 )
}

if( !isBackupValid ) {
  console.error( `[ERROR]: Backup GPT checksum mismatch` )
  process.exit( 1 )
}

if( !checksumsMatch ) {
  console.error( `[ERROR]: Primary & Backup GPT mismatch` )
  process.exit( 1 )
}


var GUID = require( './guid' )
var GPT = require( './gpt' )
var uint64 = require( './uint64' )

/**
 * PartitionEntry
 * @constructor
 * @param {Object} [options]
 * @returns {PartitionEntry}
 */
function PartitionEntry( options ) {

  if( !(this instanceof PartitionEntry) ) {
    return new PartitionEntry( options )
  }

  options = options != null ? options : {}

  /** @type {String} Type GUID */
  this.type = options.type || GUID.ZERO
  /** @type {String} GUID */
  this.guid = options.guid || GUID.ZERO
  /** @type {String} Partition label */
  this.name = options.name || ''

  /** @type {Number} First addressable block address */
  this.firstLBA = options.firstLBA || 0
  /** @type {Number} Last addressable block address */
  this.lastLBA = options.lastLBA || 0
  // NOTE: Can't use this as uint64 as logical ops will cast to uint32;
  // Maybe revert to buffer, or use bitfield module?
  /** @type {Number} Attribute flags */
  this.attr = options.attr || 0

}

/**
 * Parse a partition entry from a given buffer
 * @param {Buffer} buffer
 * @param {Number} [offset=0]
 * @returns {PartitionEntry}
 */
PartitionEntry.parse = function( buffer, offset ) {
  return new PartitionEntry().parse( buffer, offset )
}

/**
 * PartitionEntry prototype
 * @ignore
 */
PartitionEntry.prototype = {

  constructor: PartitionEntry,

  /**
   * Determine whether this partition entry is marked empty
   * @returns {Boolean}
   */
  isEmpty() {
    return this.type.toString() === GUID.ZERO
  },

  /**
   * Parse a partition entry from a given buffer
   * @param {Buffer} buffer
   * @param {Number} [offset=0]
   * @returns {PartitionEntry}
   */
  parse( buffer, offset ) {

    offset = offset || 0

    this.type = GUID.toString( buffer, offset + 0 )
    this.guid = GUID.toString( buffer, offset + 16 )

    this.firstLBA = uint64.readLE( buffer, offset + 32 )
    this.lastLBA = uint64.readLE( buffer, offset + 40 )
    this.attr = uint64.readLE( buffer, offset + 48 )

    this.name = buffer.toString( 'ucs2', offset + 56, offset + GPT.TABLE_ENTRY_SIZE )
      .replace( /\x00+/g, '' )

    return this

  },

  /**
   * Write a partition entry to a given buffer
   * @param {Buffer} [buffer]
   * @param {Number} [offset=0]
   * @returns {PartitionEntry}
   */
  write( buffer, offset ) {

    buffer = buffer || Buffer.alloc( GPT.TABLE_ENTRY_SIZE, 0 )
    offset = offset || 0

    GPT.GUID.write( this.type, buffer, offset + 0 )
    GPT.GUID.write( this.guid, buffer, offset + 16 )

    uint64.writeLE( buffer, this.firstLBA, offset + 32 )
    uint64.writeLE( buffer, this.lastLBA, offset + 40 )
    uint64.writeLE( buffer, this.attr, offset + 48 )

    buffer.fill( 0, offset + 56, offset + GPT.TABLE_ENTRY_SIZE )
    buffer.write( this.name, offset + 56, offset + GPT.TABLE_ENTRY_SIZE, 'ucs2' )

    return buffer

  },

}

// Exports
module.exports = PartitionEntry


'use strict';

/**
 * Class representing a token bucket.
 */
class TokenBucket {
  /**
   * Create a new `TokenBucket`.
   *
   * @param {Object} options Options object
   * @param {Number} options.limit The tokens to add per `interval`
   * @param {Number} options.interval The interval at which tokens are added
   * @param {Number} options.bucketSize The capacity of the bucket
   */
  constructor(options) {
    this.tokens = this.capacity = options.bucketSize;
    this.interval = options.interval;
    this.limit = options.limit;
    this.last = Date.now();
  }

  /**
   * Refill the bucket with the proper amount of tokens.
   */
  refill() {
    const now = Date.now();
    const tokens = Math.floor(((now - this.last) * this.limit) / this.interval);

    this.tokens += tokens;

    //
    // `tokens` is rounded downward, so we only add the actual time required by
    // those tokens.
    //
    this.last += Math.ceil((tokens * this.interval) / this.limit);

    if (this.tokens > this.capacity) {
      this.tokens = this.capacity;
      this.last = now;
    }
  }

  /**
   * Remove a token from the bucket.
   *
   * @return {Number} The amount of time to wait for a token to be available
   */
  consume() {
    this.refill();

    if (this.tokens) {
      this.tokens--;
      return 0;
    }

    return Math.ceil(this.interval / this.limit - (Date.now() - this.last));
  }
}

/**
 * Limit the execution rate of a function using a leaky bucket algorithm.
 *
 * @param {Function} fn The function to rate limit calls to
 * @param {Object} options Options object
 * @param {Number} options.limit The number of allowed calls per `interval`
 * @param {Number} options.interval The timespan where `limit` is calculated
 * @param {Number} options.queueSize The maximum size of the queue
 * @param {Number} options.bucketSize The capacity of the bucket
 * @return {Function}
 * @public
 */
function stopcock(fn, options) {
  options = Object.assign(
    {
      queueSize: Math.pow(2, 32) - 1,
      bucketSize: 40,
      interval: 1000,
      limit: 2
    },
    options
  );

  const bucket = new TokenBucket(options);
  const queue = [];
  let timer = null;

  function shift() {
    clearTimeout(timer);
    while (queue.length) {
      const delay = bucket.consume();

      if (delay > 0) {
        timer = setTimeout(shift, delay);
        break;
      }

      const data = queue.shift();
      data[2](fn.apply(data[0], data[1]));
    }
  }

  function limiter() {
    const args = arguments;

    return new Promise((resolve, reject) => {
      if (queue.length === options.queueSize) {
        return reject(new Error('Queue is full'));
      }

      queue.push([this, args, resolve]);
      shift();
    });
  }

  Object.defineProperty(limiter, 'size', { get: () => queue.length });

  return limiter;
}

module.exports = stopcock;


const schema = require('ethjs-schema');
const util = require('ethjs-util');
const numberToBN = require('number-to-bn');
const stripHexPrefix = require('strip-hex-prefix');
const BN = require('bn.js');
const padToEven = util.padToEven;
const arrayContainsArray = util.arrayContainsArray;
const getBinarySize = util.getBinarySize;
const ten = new BN('10', 10);

/**
 * Format quantity values, either encode to hex or decode to BigNumber
 * should intake null, stringNumber, number, BN
 *
 * @method formatQuantity
 * @param {String|BigNumber|Number} value quantity or tag to convert
 * @param {Boolean} encode to hex or decode to BigNumber
 * @returns {Optional} output to BigNumber or string
 * @throws error if value is a float
 */
function formatQuantity(value, encode, pad) {
  if (['string', 'number', 'object'].indexOf(typeof value) === -1 || value === null) {
    return value;
  }

  const numberValue = numberToBN(value);
  const numPadding = numberValue.lt(ten) && pad === true && !numberValue.isZero() ? '0' : '';

  if (numberToBN(value).isNeg()) { throw new Error(`[ethjs-format] while formatting quantity '${numberValue.toString(10)}', invalid negative number. Number must be positive or zero.`); }

  return encode ? `0x${numPadding}${numberValue.toString(16)}` : numberValue;
}

/**
 * Format quantity or tag, if tag bypass return, else format quantity
 * should intake null, stringNumber, number, BN, string tag
 *
 * @method formatQuantityOrTag
 * @param {String|BigNumber|Number} value quantity or tag to convert
 * @param {Boolean} encode encode the number to hex or decode to BigNumber
 * @returns {Object|String} output to BigNumber or string
 * @throws error if value is a float
 */
function formatQuantityOrTag(value, encode) {
  var output = value; // eslint-disable-line

  // if the value is a tag, bypass
  if (schema.tags.indexOf(value) === -1) {
    output = formatQuantity(value, encode);
  }

  return output;
}

/**
 * FormatData under strict conditions hex prefix
 *
 * @method formatData
 * @param {String} value the bytes data to be formatted
 * @param {Number} byteLength the required byte length (usually 20 or 32)
 * @returns {String} output output formatted data
 * @throws error if minimum length isnt met
 */
function formatData(value, byteLength) {
  var output = value; // eslint-disable-line
  var outputByteLength = 0; // eslint-disable-line

  // prefix only under strict conditions, else bypass
  if (typeof value === 'string') {
    output = `0x${padToEven(stripHexPrefix(value))}`;
    outputByteLength = getBinarySize(output);
  }

  // format double padded zeros.
  if (output === '0x00') { output = '0x0'; }

  // throw if bytelength is not correct
  if (typeof byteLength === 'number' && value !== null && output !== '0x' && output !== '0x0' // support empty values
    && (!/^[0-9A-Fa-f]+$/.test(stripHexPrefix(output)) || outputByteLength !== 2 + byteLength * 2)) {
    throw new Error(`[ethjs-format] hex string '${output}' must be an alphanumeric ${2 + byteLength * 2} utf8 byte hex (chars: a-fA-F) string, is ${outputByteLength} bytes`);
  }

  return output;
}

/**
 * Format object, even with random RPC caviets
 *
 * @method formatObject
 * @param {String|Array} formatter the unit to convert to, default ether
 * @param {Object} value the object value
 * @param {Boolean} encode encode to hex or decode to BigNumber
 * @returns {Object} output object
 * @throws error if value is a float
 */
function formatObject(formatter, value, encode) {
  var output = Object.assign({}, value); // eslint-disable-line
  var formatObject = null; // eslint-disable-line

  // if the object is a string flag, then retreive the object
  if (typeof formatter === 'string') {
    if (formatter === 'Boolean|EthSyncing') {
      formatObject = Object.assign({}, schema.objects.EthSyncing);
    } else if (formatter === 'DATA|Transaction') {
      formatObject = Object.assign({}, schema.objects.Transaction);
    } else {
      formatObject = Object.assign({}, schema.objects[formatter]);
    }
  }

  // check if all required data keys are fulfilled
  if (!arrayContainsArray(Object.keys(value), formatObject.__required)) { // eslint-disable-line
    throw new Error(`[ethjs-format] object ${JSON.stringify(value)} must contain properties: ${formatObject.__required.join(', ')}`); // eslint-disable-line
  }

  // assume formatObject is an object, go through keys and format each
  Object.keys(formatObject).forEach((valueKey) => {
    if (valueKey !== '__required' && typeof value[valueKey] !== 'undefined') {
      output[valueKey] = format(formatObject[valueKey], value[valueKey], encode);
    }
  });

  return output;
}

/**
 * Format array
 *
 * @method formatArray
 * @param {String|Array} formatter the unit to convert to, default ether
 * @param {Object} value the value in question
 * @param {Boolean} encode encode to hex or decode to BigNumber
 * @param {Number} lengthRequirement the required minimum array length
 * @returns {Object} output object
 * @throws error if minimum length isnt met
 */
function formatArray(formatter, value, encode, lengthRequirement) {
  var output = value.slice(); // eslint-disable-line
  var formatObject = formatter; // eslint-disable-line

  // if the formatter is an array or data, then make format object an array data
  if (formatter === 'Array|DATA') {
    formatObject = ['D'];
  }

  // if formatter is a FilterChange and acts like a BlockFilter
  // or PendingTx change format object to tx hash array
  if (formatter === 'FilterChange' && typeof value[0] === 'string') {
    formatObject = ['D32'];
  }

  // enforce minimum value length requirements
  if (encode === true
    && typeof lengthRequirement === 'number'
    && value.length < lengthRequirement) {
    throw new Error(`array ${JSON.stringify(value)} must contain at least ${lengthRequirement} params, but only contains ${value.length}.`); // eslint-disable-line
  }

  // make new array, avoid mutation
  formatObject = formatObject.slice();

  // assume formatObject is an object, go through keys and format each
  value.forEach((valueKey, valueIndex) => {
    // use key zero as formatter for all values, unless otherwise specified
    var formatObjectKey = 0; // eslint-disable-line

    // if format array is exact, check each argument against formatter argument
    if (formatObject.length > 1) {
      formatObjectKey = valueIndex;
    }

    output[valueIndex] = format(formatObject[formatObjectKey], valueKey, encode);
  });

  return output;
}

/**
 * Format various kinds of data to RPC spec or into digestable JS objects
 *
 * @method format
 * @param {String|Array} formatter the data formatter
 * @param {String|Array|Object|Null|Number} value the data value input
 * @param {Boolean} encode encode to hex or decode to BigNumbers, Strings, Booleans, Null
 * @param {Number} lengthRequirement the minimum data length requirement
 * @throws error if minimum length isnt met
 */
function format(formatter, value, encode, lengthRequirement) {
  var output = value; // eslint-disable-line

  // if formatter is quantity or quantity or tag
  if (formatter === 'Q') {
    output = formatQuantity(value, encode);
  } else if (formatter === 'QP') {
    output = formatQuantity(value, encode, true);
  } else if (formatter === 'Q|T') {
    output = formatQuantityOrTag(value, encode);
  } else if (formatter === 'D') {
    output = formatData(value); // dont format data flagged objects like compiler output
  } else if (formatter === 'D20') {
    output = formatData(value, 20); // dont format data flagged objects like compiler output
  } else if (formatter === 'D32') {
    output = formatData(value, 32); // dont format data flagged objects like compiler output
  } else {
    // if value is an object or array
    if (typeof value === 'object'
      && value !== null
      && Array.isArray(value) === false) {
      output = formatObject(formatter, value, encode);
    } else if (Array.isArray(value)) {
      output = formatArray(formatter, value, encode, lengthRequirement);
    }
  }

  return output;
}

/**
 * Format RPC inputs generally to the node or TestRPC
 *
 * @method formatInputs
 * @param {Object} method the data formatter
 * @param {Array} inputs the data inputs
 * @returns {Array} output the formatted inputs array
 * @throws error if minimum length isnt met
 */
function formatInputs(method, inputs) {
  return format(schema.methods[method][0], inputs, true, schema.methods[method][2]);
}

/**
 * Format RPC outputs generally from the node or TestRPC
 *
 * @method formatOutputs
 * @param {Object} method the data formatter
 * @param {Array|String|Null|Boolean|Object} outputs the data inputs
 * @returns {Array|String|Null|Boolean|Object} output the formatted data
 */
function formatOutputs(method, outputs) {
  return format(schema.methods[method][1], outputs, false);
}

// export formatters
module.exports = {
  schema,
  formatQuantity,
  formatQuantityOrTag,
  formatObject,
  formatArray,
  format,
  formatInputs,
  formatOutputs,
};


/**
 * Created by jorgecuesta on 6/6/16.
 */
let mongoose = require('mongoose'),
    utils    = require('mongoose/lib/utils'),
    _        = require('lodash');

mongoose.Model.discriminator = function discriminator(name, schema) {
  // Using this because if you want register tenant plugins we need know
  // base schema before register same plugin to root level model.
  // This baseSchema is just an schema.clone() native of mongoose.
  // Is set on mongoose.mtModel('Foo', schema);
  let baseSchema = this.baseSchema ? this.baseSchema : this.schema;

  if (!(schema && schema.instanceOfSchema)) {
    throw new Error('You must pass a valid discriminator Schema');
  }

  if (baseSchema.discriminatorMapping &&
    !baseSchema.discriminatorMapping.isRoot) {
    throw new Error('Discriminator "' + name +
      '" can only be a discriminator of the root model');
  }

  let key = baseSchema.options.discriminatorKey;
  if (schema.path(key)) {
    throw new Error('Discriminator "' + name +
      '" cannot have field with name "' + key + '"');
  }

  function clean(a, b) {
    a = utils.clone(a);
    b = utils.clone(b);
    delete a.toJSON;
    delete a.toObject;
    delete b.toJSON;
    delete b.toObject;
    // Prevent errors by collection option.
    delete a.collection;
    delete b.collection;
    delete a._id;
    delete b._id;

    if (!utils.deepEqual(a, b)) {
      throw new Error('Discriminator options are not customizable ' +
        '(except toJSON, toObject, _id)');
    }
  }

  function merge(schema, baseSchema) {
    utils.merge(schema, baseSchema);

    var obj = {};
    obj[key] = {type: String, default: name};
    schema.add(obj);
    schema.discriminatorMapping = {key: key, value: name, isRoot: false};

    var col = schema.options.collection || baseSchema.options.collection;

    // Prevent baseSchema collection name override on new schema.
    // if (baseSchema.options.collection && !schema.options.collection) {
    //   schema.options.collection = baseSchema.options.collection;
    // }

    // throws error if options are invalid
    clean(schema.options, baseSchema.options);

    var toJSON = schema.options.toJSON;
    var toObject = schema.options.toObject;
    var _id = schema.options._id;

    schema.options = utils.clone(baseSchema.options);

    if (toJSON) schema.options.toJSON = toJSON;
    if (toObject) schema.options.toObject = toObject;

    // Set collection again or add new collection name.
    schema.options.collection = col;

    if (typeof _id !== 'undefined') {
      schema.options._id = _id;
    }

    schema.callQueue = baseSchema.callQueue.concat(
      schema.callQueue.slice(schema._defaultMiddleware.length));
    schema._requiredpaths = undefined; // reset just in case
                                       // Schema#requiredPaths() was called on
                                       // either schema
  }

  // merges base schema into new discriminator schema and sets new type field.
  merge(schema, baseSchema);

  if (this.schema.options.tenantPlugins &&
    _.isArray(this.schema.options.tenantPlugins)) {
    this.schema.options.tenantPlugins.forEach(function(plugin) {
      if (!_.isFunction(plugin.register)) {
        throw new Error([
          'tenantPlugins should be an array with object. Those objects',
          ' should have register key with plugin function and optionally',
          ' option key with options to send plugin.',
        ].join());
      }

      schema.plugin(plugin.register, plugin.options);
    });
  } else if (this.schema.options.tenantPlugins) {
    throw new Error([
      'tenantPlugins should be an array.',
    ].join());
  }

  if (!this.discriminators) {
    this.discriminators = {};
  }

  if (!baseSchema.discriminatorMapping) {
    baseSchema.discriminatorMapping = {key: key, value: null, isRoot: true};
  }

  if (this.discriminators[name]) {
    throw new Error('Discriminator with name "' + name + '" already exists');
  }
  if (this.db.models[name]) {
    throw new mongoose.MongooseError.OverwriteModelError(name);
  }

  // Use schema collection name set on merge process.
  this.discriminators[name] = this.db.model(name, schema,
    schema.options.collection);
  this.discriminators[name].prototype.__proto__ = this.prototype;
  Object.defineProperty(this.discriminators[name], 'baseModelName', {
    value: this.modelName,
    configurable: true,
    writable: false,
  });

  // apply methods and statics
  applyMethods(this.discriminators[name], schema);
  applyStatics(this.discriminators[name], schema);

  return this.discriminators[name];
};

/*
 * Register methods for this model
 *
 * @param {Model} model
 * @param {Schema} schema
 */
var applyMethods = function(model, schema) {
  function apply(method, schema) {
    Object.defineProperty(model.prototype, method, {
      get: function() {
        var h = {};
        for (var k in schema.methods[method]) {
          h[k] = schema.methods[method][k].bind(this);
        }
        return h;
      },
      configurable: true,
    });
  }

  for (var method in schema.methods) {
    if (typeof schema.methods[method] === 'function') {
      model.prototype[method] = schema.methods[method];
    } else {
      apply(method, schema);
    }
  }
};

/*
 * Register statics for this model
 * @param {Model} model
 * @param {Schema} schema
 */
var applyStatics = function(model, schema) {
  for (var i in schema.statics) {
    model[i] = schema.statics[i];
  }
};

module.exports = mongoose.Model.discriminator;


(function () {
    'use strict';

    var _ = require('lodash');
    var gruntFile = require('grunt/lib/grunt/file');

    /**
     * Returns a merged set of yaml files
     * @param {array} files - array of file paths to merge together into one object.
     * @return {object} Merged javascript object containing converted data from the merged yaml files
     */
    var mergeFiles = function (files) {
        if (!_.isArray(files)) {
            throw new Error('Arguments to config-helper.mergeConfig should be an array');
        }

        var appConfig = {};
        files.forEach(function (filePath) {
            if (gruntFile.exists(filePath)) {
                var fileConfig = gruntFile.readYAML(filePath);
                // Use lodash to do a 'deep merge' which only overwrites the properties
                // specified in previous config files, without wiping out their child properties.
                _.merge(appConfig, fileConfig);
            }
        });

        return appConfig;
    };

    module.exports = {
        mergeFiles: mergeFiles
    };

})();