// Simulate a right Alt key down with the specific sequence of keydown events.

// Simulate holding down Ctrl key and execute a Ctrl shortcut in native shell code

// No need to call the actual Ctrl shortcut since it is not handled in KBM anyway.

/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * 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.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, $, brackets, window */
/*unittests: KeyBindingManager */

/**
 * Manages the mapping of keyboard inputs to commands.
 */
define(function (require, exports, module) {
    "use strict";

    require("utils/Global");

    var AppInit             = require("utils/AppInit"),
        Commands            = require("command/Commands"),
        CommandManager      = require("command/CommandManager"),
        DefaultDialogs      = require("widgets/DefaultDialogs"),
        EventDispatcher     = require("utils/EventDispatcher"),
        FileSystem          = require("filesystem/FileSystem"),
        FileSystemError     = require("filesystem/FileSystemError"),
        FileUtils           = require("file/FileUtils"),
        KeyEvent            = require("utils/KeyEvent"),
        Strings             = require("strings"),
        StringUtils         = require("utils/StringUtils"),
        UrlParams           = require("utils/UrlParams").UrlParams,
        _                   = require("thirdparty/lodash");

    var KeyboardPrefs       = JSON.parse(require("text!base-config/keyboard.json"));

    var KEYMAP_FILENAME     = "keymap.json",
        _userKeyMapFilePath = brackets.app.getApplicationSupportDirectory() + "/" + KEYMAP_FILENAME;

    /**
     * @private
     * Maps normalized shortcut descriptor to key binding info.
     * @type {!Object.<string, {commandID: string, key: string, displayKey: string}>}
     */
    var _keyMap            = {},    // For the actual key bindings including user specified ones
        // For the default factory key bindings, cloned from _keyMap after all extensions are loaded.
        _defaultKeyMap     = {};

    /**
     * @typedef {{shortcut: !string,
     *            commandID: ?string}} UserKeyBinding
     */

    /**
     * @private
     * Maps shortcut descriptor to a command id.
     * @type {UserKeyBinding}
     */
    var _customKeyMap      = {},
        _customKeyMapCache = {};

    /**
     * @private
     * Maps commandID to the list of shortcuts that are bound to it.
     * @type {!Object.<string, Array.<{key: string, displayKey: string}>>}
     */
    var _commandMap  = {},
        _allCommands = [];

    /**
     * @private
     * Maps key names to the corresponding unicode symols
     * @type {{key: string, displayKey: string}}
     */
    var _displayKeyMap      = { "up":    "\u2191",
                                "down":  "\u2193",
                                "left":  "\u2190",
                                "right": "\u2192",
                                "-":     "\u2212" };

    var _specialCommands = [Commands.EDIT_UNDO, Commands.EDIT_REDO, Commands.EDIT_SELECT_ALL,
                            Commands.EDIT_CUT, Commands.EDIT_COPY, Commands.EDIT_PASTE],
        _reservedShortcuts = ["Ctrl-Z", "Ctrl-Y", "Ctrl-A", "Ctrl-X", "Ctrl-C", "Ctrl-V"],
        _macReservedShortcuts = ["Cmd-,", "Cmd-H", "Cmd-Alt-H", "Cmd-M", "Cmd-Shift-Z", "Cmd-Q"],
        _keyNames = ["Up", "Down", "Left", "Right", "Backspace", "Enter", "Space", "Tab"];

    /**
     * @private
     * Flag to show key binding errors in the key map file. Default is true and
     * it will be set to false when reloading without extensions. This flag is not
     * used to suppress errors in loading or parsing the key map file. So if the key
     * map file is corrupt, then the error dialog still shows up.
     *
     * @type {boolean}
     */
    var _showErrors = true;

    /**
     * @private
     * Allow clients to toggle key binding
     * @type {boolean}
     */
    var _enabled = true;

    /**
     * @private
     * Stack of registered global keydown hooks.
     * @type {Array.<function(Event): boolean>}
     */
    var _globalKeydownHooks = [];

    /**
     * @private
     * States of Ctrl key down detection
     * @enum {number}
     */
    var CtrlDownStates = {
        "NOT_YET_DETECTED"    : 0,
        "DETECTED"            : 1,
        "DETECTED_AND_IGNORED": 2   // For consecutive ctrl keydown events while a Ctrl key is being hold down
    };

    /**
     * @private
     * Flags used to determine whether right Alt key is pressed. When it is pressed,
     * the following two keydown events are triggered in that specific order.
     *
     *    1. _ctrlDown - flag used to record { ctrlKey: true, keyIdentifier: "Control", ... } keydown event
     *    2. _altGrDown - flag used to record { ctrlKey: true, altKey: true, keyIdentifier: "Alt", ... } keydown event
     *
     * @type {CtrlDownStates|boolean}
     */
    var _ctrlDown = CtrlDownStates.NOT_YET_DETECTED,
        _altGrDown = false;

    /**
     * @private
     * Used to record the timeStamp property of the last keydown event.
     * @type {number}
     */
    var _lastTimeStamp;

    /**
     * @private
     * Used to record the keyIdentifier property of the last keydown event.
     * @type {string}
     */
    var _lastKeyIdentifier;

    /*
     * @private
     * Constant used for checking the interval between Control keydown event and Alt keydown event.
     * If the right Alt key is down we get Control keydown followed by Alt keydown within 30 ms. if
     * the user is pressing Control key and then Alt key, the interval will be larger than 30 ms.
     * @type {number}
     */
    var MAX_INTERVAL_FOR_CTRL_ALT_KEYS = 30;

    /**
     * @private
     * Forward declaration for JSLint.
     * @type {Function}
     */
    var _onCtrlUp;

    /**
     * @private
     * Resets all the flags and removes _onCtrlUp event listener.
     *
     */
    function _quitAltGrMode() {
        _enabled = true;
        _ctrlDown = CtrlDownStates.NOT_YET_DETECTED;
        _altGrDown = false;
        _lastTimeStamp = null;
        _lastKeyIdentifier = null;
        $(window).off("keyup", _onCtrlUp);
    }

    /**
     * @private
     * Detects the release of AltGr key by checking all keyup events
     * until we receive one with ctrl key code. Once detected, reset
     * all the flags and also remove this event listener.
     *
     * @param {!KeyboardEvent} e keyboard event object
     */
    _onCtrlUp = function (e) {
        var key = e.keyCode || e.which;
        if (_altGrDown && key === KeyEvent.DOM_VK_CONTROL) {
            _quitAltGrMode();
        }
    };

    /**
     * @private
     * Detects whether AltGr key is pressed. When it is pressed, the first keydown event has
     * ctrlKey === true with keyIdentifier === "Control". The next keydown event with
     * altKey === true, ctrlKey === true and keyIdentifier === "Alt" is sent within 30 ms. Then
     * the next keydown event with altKey === true, ctrlKey === true and keyIdentifier === "Control"
     * is sent. If the user keep holding AltGr key down, then the second and third
     * keydown events are repeatedly sent out alternately. If the user is also holding down Ctrl
     * key, then either keyIdentifier === "Control" or keyIdentifier === "Alt" is repeatedly sent
     * but not alternately.
     *
     * Once we detect the AltGr key down, then disable KeyBindingManager and set up a keyup
     * event listener to detect the release of the altGr key so that we can re-enable KeyBindingManager.
     * When we detect the addition of Ctrl key besides AltGr key, we also quit AltGr mode and re-enable
     * KeyBindingManager.
     *
     * @param {!KeyboardEvent} e keyboard event object
     */
    function _detectAltGrKeyDown(e) {
        if (brackets.platform !== "win") {
            return;
        }

        if (!_altGrDown) {
            // Reset _ctrlDown if it was set to be ignored and the shorcuts with Ctrl key was handled
            // by a native shell code.
            if (_ctrlDown === CtrlDownStates.DETECTED_AND_IGNORED &&
                    !e.repeat && e.ctrlKey && e.keyIdentifier === "Control") {
                _ctrlDown = CtrlDownStates.NOT_YET_DETECTED;
            }

            if (_ctrlDown === CtrlDownStates.NOT_YET_DETECTED && e.ctrlKey && e.keyIdentifier === "Control") {
                _ctrlDown = CtrlDownStates.DETECTED;
            } else if (e.ctrlKey && e.keyIdentifier === "Control") {
                // We get here if the user is holding down left/right Control key. Set it to false
                // so that we don't misidentify the combination of Ctrl and Alt keys as AltGr key.
                _ctrlDown = CtrlDownStates.DETECTED_AND_IGNORED;
            } else if (_ctrlDown === CtrlDownStates.DETECTED && e.altKey && e.ctrlKey && e.keyIdentifier === "Alt" &&
                        (e.timeStamp - _lastTimeStamp) < MAX_INTERVAL_FOR_CTRL_ALT_KEYS) {
                _altGrDown = true;
                _lastKeyIdentifier = "Alt";
                _enabled = false;
                $(window).on("keyup", _onCtrlUp);
            } else {
                // Reset _ctrlDown so that we can start over in detecting the two key events
                // required for AltGr key.
                _ctrlDown = CtrlDownStates.NOT_YET_DETECTED;
            }
            _lastTimeStamp = e.timeStamp;
        } else if (e.keyIdentifier === "Control" || e.keyIdentifier === "Alt") {
            // If the user is NOT holding down AltGr key or is also pressing Ctrl key,
            // then _lastKeyIdentifier will be the same as keyIdentifier in the current
            // key event. So we need to quit AltGr mode to re-enable KBM.
            if (e.altKey && e.ctrlKey && e.keyIdentifier === _lastKeyIdentifier) {
                _quitAltGrMode();
            } else {
                _lastKeyIdentifier = e.keyIdentifier;
            }
        }
    }

    /**
     * @private
     */
    function _reset() {
        _keyMap = {};
        _defaultKeyMap = {};
        _customKeyMap = {};
        _customKeyMapCache = {};
        _commandMap = {};
        _globalKeydownHooks = [];
        _userKeyMapFilePath = brackets.app.getApplicationSupportDirectory() + "/" + KEYMAP_FILENAME;
    }

    /**
     * @private
     * Initialize an empty keymap as the current keymap. It overwrites the current keymap if there is one.
     * builds the keyDescriptor string from the given parts
     * @param {boolean} hasCtrl Is Ctrl key enabled
     * @param {boolean} hasAlt Is Alt key enabled
     * @param {boolean} hasShift Is Shift key enabled
     * @param {string} key The key that's pressed
     * @return {string} The normalized key descriptor
     */
    function _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key) {
        if (!key) {
            console.log("KeyBindingManager _buildKeyDescriptor() - No key provided!");
            return "";
        }

        var keyDescriptor = [];

        if (hasMacCtrl) {
            keyDescriptor.push("Ctrl");
        }
        if (hasAlt) {
            keyDescriptor.push("Alt");
        }
        if (hasShift) {
            keyDescriptor.push("Shift");
        }

        if (hasCtrl) {
            // Windows display Ctrl first, Mac displays Command symbol last
            if (brackets.platform === "mac") {
                keyDescriptor.push("Cmd");
            } else {
                keyDescriptor.unshift("Ctrl");
            }
        }

        keyDescriptor.push(key);

        return keyDescriptor.join("-");
    }

    /**
     * normalizes the incoming key descriptor so the modifier keys are always specified in the correct order
     * @param {string} The string for a key descriptor, can be in any order, the result will be Ctrl-Alt-Shift-<Key>
     * @return {string} The normalized key descriptor or null if the descriptor invalid
     */
    function normalizeKeyDescriptorString(origDescriptor) {
        var hasMacCtrl = false,
            hasCtrl = false,
            hasAlt = false,
            hasShift = false,
            key = "",
            error = false;

        function _compareModifierString(left, right) {
            if (!left || !right) {
                return false;
            }
            left = left.trim().toLowerCase();
            right = right.trim().toLowerCase();

            return (left.length > 0 && left === right);
        }

        origDescriptor.split("-").forEach(function parseDescriptor(ele, i, arr) {
            if (_compareModifierString("ctrl", ele)) {
                if (brackets.platform === "mac") {
                    hasMacCtrl = true;
                } else {
                    hasCtrl = true;
                }
            } else if (_compareModifierString("cmd", ele)) {
                if (brackets.platform === "mac") {
                    hasCtrl = true;
                } else {
                    error = true;
                }
            } else if (_compareModifierString("alt", ele)) {
                hasAlt = true;
            } else if (_compareModifierString("opt", ele)) {
                if (brackets.platform === "mac") {
                    hasAlt = true;
                } else {
                    error = true;
                }
            } else if (_compareModifierString("shift", ele)) {
                hasShift = true;
            } else if (key.length > 0) {
                console.log("KeyBindingManager normalizeKeyDescriptorString() - Multiple keys defined. Using key: " + key + " from: " + origDescriptor);
                error = true;
            } else {
                key = ele;
            }
        });

        if (error) {
            return null;
        }

        // Check to see if the binding is for "-".
        if (key === "" && origDescriptor.search(/^.+--$/) !== -1) {
            key = "-";
        }

        // '+' char is valid if it's the only key. Keyboard shortcut strings should use
        // unicode characters (unescaped). Keyboard shortcut display strings may use
        // unicode escape sequences (e.g. \u20AC euro sign)
        if ((key.indexOf("+")) >= 0 && (key.length > 1)) {
            return null;
        }

        // Ensure that the first letter of the key name is in upper case.
        // i.e. 'a' => 'A' and 'up' => 'Up'
        if (/^[a-z]/.test(key)) {
            key = key.toLowerCase().replace(/(^[a-z])/, function (match, p1) {
                return p1.toUpperCase();
            });
        }

        // No restriction on single character key yet, but other key names are restricted to either
        // Function keys or those listed in _keyNames array.
        if (key.length > 1 && !/F\d+/.test(key) &&
                _keyNames.indexOf(key) === -1) {
            return null;
        }

        return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key);
    }

    /**
     * @private
     * Looks for keycodes that have os-inconsistent keys and fixes them.
     * @param {number} The keycode from the keyboard event.
     * @param {string} The current best guess at what the key is.
     * @return {string} If the key is OS-inconsistent, the correct key; otherwise, the original key.
     **/
    function _mapKeycodeToKey(keycode, key) {
        // If keycode represents one of the digit keys (0-9), then return the corresponding digit
        // by subtracting KeyEvent.DOM_VK_0 from keycode. ie. [48-57] --> [0-9]
        if (keycode >= KeyEvent.DOM_VK_0 && keycode <= KeyEvent.DOM_VK_9) {
            return String(keycode - KeyEvent.DOM_VK_0);
        // Do the same with the numpad numbers
        // by subtracting KeyEvent.DOM_VK_NUMPAD0 from keycode. ie. [96-105] --> [0-9]
        } else if (keycode >= KeyEvent.DOM_VK_NUMPAD0 && keycode <= KeyEvent.DOM_VK_NUMPAD9) {
            return String(keycode - KeyEvent.DOM_VK_NUMPAD0);
        }

        switch (keycode) {
        case KeyEvent.DOM_VK_SEMICOLON:
            return ";";
        case KeyEvent.DOM_VK_EQUALS:
            return "=";
        case KeyEvent.DOM_VK_COMMA:
            return ",";
        case KeyEvent.DOM_VK_SUBTRACT:
        case KeyEvent.DOM_VK_DASH:
            return "-";
        case KeyEvent.DOM_VK_ADD:
            return "+";
        case KeyEvent.DOM_VK_DECIMAL:
        case KeyEvent.DOM_VK_PERIOD:
            return ".";
        case KeyEvent.DOM_VK_DIVIDE:
        case KeyEvent.DOM_VK_SLASH:
            return "/";
        case KeyEvent.DOM_VK_BACK_QUOTE:
            return "`";
        case KeyEvent.DOM_VK_OPEN_BRACKET:
            return "[";
        case KeyEvent.DOM_VK_BACK_SLASH:
            return "\\";
        case KeyEvent.DOM_VK_CLOSE_BRACKET:
            return "]";
        case KeyEvent.DOM_VK_QUOTE:
            return "'";
        default:
            return key;
        }
    }

    /**
     * Takes a keyboard event and translates it into a key in a key map
     */
    function _translateKeyboardEvent(event) {
        var hasMacCtrl = (brackets.platform === "mac") ? (event.ctrlKey) : false,
            hasCtrl = (brackets.platform !== "mac") ? (event.ctrlKey) : (event.metaKey),
            hasAlt = (event.altKey),
            hasShift = (event.shiftKey),
            key = String.fromCharCode(event.keyCode);

        //From the W3C, if we can get the KeyboardEvent.keyIdentifier then look here
        //As that will let us use keys like then function keys "F5" for commands. The
        //full set of values we can use is here
        //http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
        var ident = event.keyIdentifier;
        if (ident) {
            if (ident.charAt(0) === "U" && ident.charAt(1) === "+") {
                //This is a unicode code point like "U+002A", get the 002A and use that
                key = String.fromCharCode(parseInt(ident.substring(2), 16));
            } else {
                //This is some non-character key, just use the raw identifier
                key = ident;
            }
        }

        // Translate some keys to their common names
        if (key === "\t") {
            key = "Tab";
        } else if (key === " ") {
            key = "Space";
        } else if (key === "\b") {
            key = "Backspace";
        } else {
            key = _mapKeycodeToKey(event.keyCode, key);
        }

        return _buildKeyDescriptor(hasMacCtrl, hasCtrl, hasAlt, hasShift, key);
    }

    /**
     * Convert normalized key representation to display appropriate for platform.
     * @param {!string} descriptor Normalized key descriptor.
     * @return {!string} Display/Operating system appropriate string
     */
    function formatKeyDescriptor(descriptor) {
        var displayStr;

        if (brackets.platform === "mac") {
            displayStr = descriptor.replace(/-(?!$)/g, "");     // remove dashes
            displayStr = displayStr.replace("Ctrl", "\u2303");  // Ctrl > control symbol
            displayStr = displayStr.replace("Cmd", "\u2318");   // Cmd > command symbol
            displayStr = displayStr.replace("Shift", "\u21E7"); // Shift > shift symbol
            displayStr = displayStr.replace("Alt", "\u2325");   // Alt > option symbol
        } else {
            displayStr = descriptor.replace("Ctrl", Strings.KEYBOARD_CTRL);   // Ctrl
            displayStr = displayStr.replace("Shift", Strings.KEYBOARD_SHIFT); // Shift > shift symbol
            displayStr = displayStr.replace("Space", Strings.KEYBOARD_SPACE); // Alt > option symbol
            displayStr = displayStr.replace(/-(?!$)/g, "+");
        }

        return displayStr;
    }

    /**
     * @private
     * @param {string} A normalized key-description string.
     * @return {boolean} true if the key is already assigned, false otherwise.
     */
    function _isKeyAssigned(key) {
        return (_keyMap[key] !== undefined);
    }

    /**
     * Remove a key binding from _keymap
     *
     * @param {!string} key - a key-description string that may or may not be normalized.
     * @param {?string} platform - OS from which to remove the binding (all platforms if unspecified)
     */
    function removeBinding(key, platform) {
        if (!key || ((platform !== null) && (platform !== undefined) && (platform !== brackets.platform))) {
            return;
        }

        var normalizedKey = normalizeKeyDescriptorString(key);

        if (!normalizedKey) {
            console.log("Fail to nomalize " + key);
        } else if (_isKeyAssigned(normalizedKey)) {
            var binding = _keyMap[normalizedKey],
                command = CommandManager.get(binding.commandID),
                bindings = _commandMap[binding.commandID];

            // delete key binding record
            delete _keyMap[normalizedKey];

            if (bindings) {
                // delete mapping from command to key binding
                _commandMap[binding.commandID] = bindings.filter(function (b) {
                    return (b.key !== normalizedKey);
                });

                if (command) {
                    command.trigger("keyBindingRemoved", {key: normalizedKey, displayKey: binding.displayKey});
                }
            }
        }
    }

    /**
     * @private
     *
     * @param {string} commandID
     * @param {string|{{key: string, displayKey: string}}} keyBinding - a single shortcut.
     * @param {?string} platform
     *     - "all" indicates all platforms, not overridable
     *     - undefined indicates all platforms, overridden by platform-specific binding
     * @return {?{key: string, displayKey:String}} Returns a record for valid key bindings.
     *     Returns null when key binding platform does not match, binding does not normalize,
     *     or is already assigned.
     */
    function _addBinding(commandID, keyBinding, platform) {
        var key,
            result = null,
            normalized,
            normalizedDisplay,
            explicitPlatform = keyBinding.platform || platform,
            targetPlatform,
            command,
            bindingsToDelete = [],
            existing;

        // For platform: "all", use explicit current plaform
        if (explicitPlatform && explicitPlatform !== "all") {
            targetPlatform = explicitPlatform;
        } else {
            targetPlatform = brackets.platform;
        }

        // Skip if the key binding is not for this platform.
        if (explicitPlatform === "mac" && brackets.platform !== "mac") {
            return null;
        }

        // if the request does not specify an explicit platform, and we're
        // currently on a mac, then replace Ctrl with Cmd.
        key = (keyBinding.key) || keyBinding;
        if (brackets.platform === "mac" && (explicitPlatform === undefined || explicitPlatform === "all")) {
            key = key.replace("Ctrl", "Cmd");
            if (keyBinding.displayKey !== undefined) {
                keyBinding.displayKey = keyBinding.displayKey.replace("Ctrl", "Cmd");
            }
        }

        normalized = normalizeKeyDescriptorString(key);

        // skip if the key binding is invalid
        if (!normalized) {
            console.error("Unable to parse key binding " + key + ". Permitted modifiers: Ctrl, Cmd, Alt, Opt, Shift; separated by '-' (not '+').");
            return null;
        }

        // check for duplicate key bindings
        existing = _keyMap[normalized];

        // for cross-platform compatibility
        if (exports.useWindowsCompatibleBindings) {
            // windows-only key bindings are used as the default binding
            // only if a default binding wasn't already defined
            if (explicitPlatform === "win") {
                // search for a generic or platform-specific binding if it
                // already exists
                if (existing && (!existing.explicitPlatform ||
                                 existing.explicitPlatform === brackets.platform ||
                                 existing.explicitPlatform === "all")) {
                    // do not clobber existing binding with windows-only binding
                    return null;
                }

                // target this windows binding for the current platform
                targetPlatform = brackets.platform;
            }
        }

        // skip if this binding doesn't match the current platform
        if (targetPlatform !== brackets.platform) {
            return null;
        }

        // skip if the key is already assigned
        if (existing) {
            if (!existing.explicitPlatform && explicitPlatform) {
                // remove the the generic binding to replace with this new platform-specific binding
                removeBinding(normalized);
                existing = false;
            }
        }

        // delete existing bindings when
        // (1) replacing a windows-compatible binding with a generic or
        //     platform-specific binding
        // (2) replacing a generic binding with a platform-specific binding
        var existingBindings = _commandMap[commandID] || [],
            isWindowsCompatible,
            isReplaceGeneric,
            ignoreGeneric;

        existingBindings.forEach(function (binding) {
            // remove windows-only bindings in _commandMap
            isWindowsCompatible = exports.useWindowsCompatibleBindings &&
                binding.explicitPlatform === "win";

            // remove existing generic binding
            isReplaceGeneric = !binding.explicitPlatform &&
                explicitPlatform;

            if (isWindowsCompatible || isReplaceGeneric) {
                bindingsToDelete.push(binding);
            } else {
                // existing binding is platform-specific and the requested binding is generic
                ignoreGeneric = binding.explicitPlatform && !explicitPlatform;
            }
        });

        if (ignoreGeneric) {
            // explicit command binding overrides this one
            return null;
        }

        if (existing) {
            // do not re-assign a key binding
            console.error("Cannot assign " + normalized + " to " + commandID + ". It is already assigned to " + _keyMap[normalized].commandID);
            return null;
        }

        // remove generic or windows-compatible bindings
        bindingsToDelete.forEach(function (binding) {
            removeBinding(binding.key);
        });

        // optional display-friendly string (e.g. CMD-+ instead of CMD-=)
        normalizedDisplay = (keyBinding.displayKey) ? normalizeKeyDescriptorString(keyBinding.displayKey) : normalized;

        // 1-to-many commandID mapping to key binding
        if (!_commandMap[commandID]) {
            _commandMap[commandID] = [];
        }

        result = {
            key                 : normalized,
            displayKey          : normalizedDisplay,
            explicitPlatform    : explicitPlatform
        };

        _commandMap[commandID].push(result);

        // 1-to-1 key binding to commandID
        _keyMap[normalized] = {
            commandID           : commandID,
            key                 : normalized,
            displayKey          : normalizedDisplay,
            explicitPlatform    : explicitPlatform
        };

        // notify listeners
        command = CommandManager.get(commandID);

        if (command) {
            command.trigger("keyBindingAdded", result);
        }

        return result;
    }

    /**
     * Returns a copy of the current key map. If the optional 'defaults' parameter is true,
     * then a copy of the default key map is returned.
     * @param {boolean=} defaults true if the caller wants a copy of the default key map.
     *                            Otherwise, the current active key map is returned.
     * @return {!Object.<string, {commandID: string, key: string, displayKey: string}>}
     */
    function getKeymap(defaults) {
        return $.extend({}, defaults ? _defaultKeyMap : _keyMap);
    }

    /**
     * Process the keybinding for the current key.
     *
     * @param {string} A key-description string.
     * @return {boolean} true if the key was processed, false otherwise
     */
    function _handleKey(key) {
        if (_enabled && _keyMap[key]) {
            // The execute() function returns a promise because some commands are async.
            // Generally, commands decide whether they can run or not synchronously,
            // and reject immediately, so we can test for that synchronously.
            var promise = CommandManager.execute(_keyMap[key].commandID);
            return (promise.state() !== "rejected");
        }
        return false;
    }

    /**
     * @private
     *
     * Sort objects by platform property. Objects with a platform property come
     * before objects without a platform property.
     */
    function _sortByPlatform(a, b) {
        var a1 = (a.platform) ? 1 : 0,
            b1 = (b.platform) ? 1 : 0;
        return b1 - a1;
    }

    /**
     * Add one or more key bindings to a particular Command.
     *
     * @param {!string | Command} command - A command ID or command object
     * @param {?({key: string, displayKey: string}|Array.<{key: string, displayKey: string, platform: string}>)} keyBindings
     *     A single key binding or an array of keybindings. Example:
     *     "Shift-Cmd-F". Mac and Win key equivalents are automatically
     *     mapped to each other. Use displayKey property to display a different
     *     string (e.g. "CMD+" instead of "CMD=").
     * @param {?string} platform The target OS of the keyBindings either
     *     "mac", "win" or "linux". If undefined, all platforms not explicitly
     *     defined will use the key binding.
     * @return {{key: string, displayKey:String}|Array.<{key: string, displayKey:String}>}
     *     Returns record(s) for valid key binding(s)
     */
    function addBinding(command, keyBindings, platform) {
        var commandID = "",
            results;

        if (!command) {
            console.error("addBinding(): missing required parameter: command");
            return;
        }

        if (!keyBindings) { return; }

        if (typeof (command) === "string") {
            commandID = command;
        } else {
            commandID = command.getID();
        }

        if (Array.isArray(keyBindings)) {
            var keyBinding;
            results = [];

            // process platform-specific bindings first
            keyBindings.sort(_sortByPlatform);

            keyBindings.forEach(function addSingleBinding(keyBindingRequest) {
                // attempt to add keybinding
                keyBinding = _addBinding(commandID, keyBindingRequest, keyBindingRequest.platform);

                if (keyBinding) {
                    results.push(keyBinding);
                }
            });
        } else {
            results = _addBinding(commandID, keyBindings, platform);
        }

        return results;
    }

    /**
     * Retrieve key bindings currently associated with a command
     *
     * @param {!string | Command} command - A command ID or command object
     * @return {!Array.<{{key: string, displayKey: string}}>} An array of associated key bindings.
     */
    function getKeyBindings(command) {
        var bindings    = [],
            commandID   = "";

        if (!command) {
            console.error("getKeyBindings(): missing required parameter: command");
            return [];
        }

        if (typeof (command) === "string") {
            commandID = command;
        } else {
            commandID = command.getID();
        }

        bindings = _commandMap[commandID];
        return bindings || [];
    }

    /**
     * Adds default key bindings when commands are registered to CommandManager
     * @param {$.Event} event jQuery event
     * @param {Command} command Newly registered command
     */
    function _handleCommandRegistered(event, command) {
        var commandId   = command.getID(),
            defaults    = KeyboardPrefs[commandId];

        if (defaults) {
            addBinding(commandId, defaults);
        }
    }

    /**
     * Adds a global keydown hook that gets first crack at keydown events
     * before standard keybindings do. This is intended for use by modal or
     * semi-modal UI elements like dialogs or the code hint list that should
     * execute before normal command bindings are run.
     *
     * The hook is passed one parameter, the original keyboard event. If the
     * hook handles the event (or wants to block other global hooks from
     * handling the event), it should return true. Note that this will *only*
     * stop other global hooks and KeyBindingManager from handling the
     * event; to prevent further event propagation, you will need to call
     * stopPropagation(), stopImmediatePropagation(), and/or preventDefault()
     * as usual.
     *
     * Multiple keydown hooks can be registered, and are executed in order,
     * most-recently-added first.
     *
     * (We have to have a special API for this because (1) handlers are normally
     * called in least-recently-added order, and we want most-recently-added;
     * (2) native DOM events don't have a way for us to find out if
     * stopImmediatePropagation()/stopPropagation() has been called on the
     * event, so we have to have some other way for one of the hooks to
     * indicate that it wants to block the other hooks from running.)
     *
     * @param {function(Event): boolean} hook The global hook to add.
     */
    function addGlobalKeydownHook(hook) {
        _globalKeydownHooks.push(hook);
    }

    /**
     * Removes a global keydown hook added by `addGlobalKeydownHook`.
     * Does not need to be the most recently added hook.
     *
     * @param {function(Event): boolean} hook The global hook to remove.
     */
    function removeGlobalKeydownHook(hook) {
        var index = _globalKeydownHooks.indexOf(hook);
        if (index !== -1) {
            _globalKeydownHooks.splice(index, 1);
        }
    }

    /**
     * Handles a given keydown event, checking global hooks first before
     * deciding to handle it ourselves.
     * @param {Event} The keydown event to handle.
     */
    function _handleKeyEvent(event) {
        var i, handled = false;
        for (i = _globalKeydownHooks.length - 1; i >= 0; i--) {
            if (_globalKeydownHooks[i](event)) {
                handled = true;
                break;
            }
        }
        _detectAltGrKeyDown(event);
        if (!handled && _handleKey(_translateKeyboardEvent(event))) {
            event.stopPropagation();
            event.preventDefault();
        }
    }

    AppInit.htmlReady(function () {
        // Install keydown event listener.
        window.document.body.addEventListener(
            "keydown",
            _handleKeyEvent,
            true
        );

        exports.useWindowsCompatibleBindings = (brackets.platform !== "mac") &&
            (brackets.platform !== "win");
    });

    /**
     * @private
     * Displays an error dialog and also opens the user key map file for editing only if
     * the error is not the loading file error.
     *
     * @param {?string} err Error type returned from JSON parser or open file operation
     * @param {string=} message Error message to be displayed in the dialog
     */
    function _showErrorsAndOpenKeyMap(err, message) {
        // Asynchronously loading Dialogs module to avoid the circular dependency
        require(["widgets/Dialogs"], function (Dialogs) {
            var errorMessage = Strings.ERROR_KEYMAP_CORRUPT;

            if (err === FileSystemError.UNSUPPORTED_ENCODING) {
                errorMessage = Strings.ERROR_LOADING_KEYMAP;
            } else if (message) {
                errorMessage = message;
            }

            Dialogs.showModalDialog(
                DefaultDialogs.DIALOG_ID_ERROR,
                Strings.ERROR_KEYMAP_TITLE,
                errorMessage
            )
                .done(function () {
                    if (err !== FileSystemError.UNSUPPORTED_ENCODING) {
                        CommandManager.execute(Commands.FILE_OPEN_KEYMAP);
                    }
                });
        });
    }

    /**
     * @private
     *
     * Checks whether the given command ID is a special command that the user can't bind
     * to another shortcut.
     * @param {!string} commandID A string referring to a specific command
     * @return {boolean} true if normalizedKey is a special command, false otherwise.
     */
    function _isSpecialCommand(commandID) {
        if (brackets.platform === "mac" && commandID === "file.quit") {
            return true;
        }

        return (_specialCommands.indexOf(commandID) > -1);
    }

    /**
     * @private
     *
     * Checks whether the given key combination is a shortcut of a special command
     * or a Mac system command that the user can't reassign to another command.
     * @param {!string} normalizedKey A key combination string used for a keyboard shortcut
     * @return {boolean} true if normalizedKey is a restricted shortcut, false otherwise.
     */
    function _isReservedShortcuts(normalizedKey) {
        if (!normalizedKey) {
            return false;
        }

        if (_reservedShortcuts.indexOf(normalizedKey) > -1 ||
                _reservedShortcuts.indexOf(normalizedKey.replace("Cmd", "Ctrl")) > -1) {
            return true;
        }

        if (brackets.platform === "mac" && _macReservedShortcuts.indexOf(normalizedKey) > -1) {
            return true;
        }

        return false;
    }

    /**
     * @private
     *
     * Creates a bullet list item for any item in the given list.
     * @param {Array.<string>} list An array of strings to be converted into a
     * message string with a bullet list.
     * @return {string} the html text version of the list
     */
    function _getBulletList(list) {
        var message = "<ul class='dialog-list'>";
        list.forEach(function (info) {
            message += "<li>" + info + "</li>";
        });
        message += "</ul>";
        return message;
    }

    /**
     * @private
     *
     * Gets the corresponding unicode symbol of an arrow key for display in the menu.
     * @param {string} key The non-modifier key used in the shortcut. It does not need to be normalized.
     * @return {string} An empty string if key is not one of those we want to show with the unicode symbol.
     *                  Otherwise, the corresponding unicode symbol is returned.
     */
    function _getDisplayKey(key) {
        var displayKey = "",
            match = key ? key.match(/(Up|Down|Left|Right|\-)$/i) : null;
        if (match) {
            displayKey = key.substr(0, match.index) + _displayKeyMap[match[0].toLowerCase()];
        }
        return displayKey;
    }

    /**
     * @private
     *
     * Applies each user key binding to all the affected commands and updates _keyMap.
     * Shows errors in a dialog and then opens the user key map file if any of the following
     * is detected while applying the user key bindings.
     *     - A key binding is attempting to modify a special command.
     *     - A key binding is attempting to assign a shortcut of a special command to another one.
     *     - Multiple key bindings are specified for the same command ID.
     *     - The same key combination is listed for multiple key bindings.
     *     - A key binding has any invalid key syntax.
     *     - A key binding is referring to a non-existent command ID.
     */
    function _applyUserKeyBindings() {
        var remappedCommands   = [],
            remappedKeys       = [],
            restrictedCommands = [],
            restrictedKeys     = [],
            invalidKeys        = [],
            invalidCommands    = [],
            multipleKeys       = [],
            duplicateBindings  = [],
            errorMessage       = "";

        _.forEach(_customKeyMap, function (commandID, key) {
            var normalizedKey    = normalizeKeyDescriptorString(key),
                existingBindings = _commandMap[commandID] || [];

            // Skip this since we don't allow user to update key binding of a special
            // command like cut, copy, paste, undo, redo and select all.
            if (_isSpecialCommand(commandID)) {
                restrictedCommands.push(commandID);
                return;
            }

            // Skip this since we don't allow user to update a shortcut used in
            // a special command or any Mac system command.
            if (_isReservedShortcuts(normalizedKey)) {
                restrictedKeys.push(key);
                return;
            }

            // Skip this if the key is invalid.
            if (!normalizedKey) {
                invalidKeys.push(key);
                return;
            }

            if (_isKeyAssigned(normalizedKey)) {
                if (remappedKeys.indexOf(normalizedKey) !== -1) {
                    // JSON parser already removed all the duplicates that have the exact
                    // same case or order in their keys. So we're only detecting duplicate
                    // bindings that have different orders or different cases used in the key.
                    duplicateBindings.push(key);
                    return;
                }
                // The same key binding already exists, so skip this.
                if (_keyMap[normalizedKey].commandID === commandID) {
                    // Still need to add it to the remappedCommands so that
                    // we can detect any duplicate later on.
                    remappedCommands.push(commandID);
                    return;
                }
                removeBinding(normalizedKey);
            }

            if (remappedKeys.indexOf(normalizedKey) === -1) {
                remappedKeys.push(normalizedKey);
            }

            // Remove another key binding if the new key binding is for a command
            // that has a different key binding. e.g. "Ctrl-W": "edit.selectLine"
            // requires us to remove "Ctrl-W" from "file.close" command, but we
            // also need to remove "Ctrl-L" from "edit.selectLine".
            if (existingBindings.length) {
                existingBindings.forEach(function (binding) {
                    removeBinding(binding.key);
                });
            }

            if (commandID) {
                if (_allCommands.indexOf(commandID) !== -1) {
                    if (remappedCommands.indexOf(commandID) === -1) {
                        var keybinding = { key: normalizedKey };

                        keybinding.displayKey = _getDisplayKey(normalizedKey);
                        addBinding(commandID, keybinding.displayKey ? keybinding : normalizedKey, brackets.platform);
                        remappedCommands.push(commandID);
                    } else {
                        multipleKeys.push(commandID);
                    }
                } else {
                    invalidCommands.push(commandID);
                }
            }
        });

        if (restrictedCommands.length) {
            errorMessage = StringUtils.format(Strings.ERROR_RESTRICTED_COMMANDS, _getBulletList(restrictedCommands));
        }

        if (restrictedKeys.length) {
            errorMessage += StringUtils.format(Strings.ERROR_RESTRICTED_SHORTCUTS, _getBulletList(restrictedKeys));
        }

        if (multipleKeys.length) {
            errorMessage += StringUtils.format(Strings.ERROR_MULTIPLE_SHORTCUTS, _getBulletList(multipleKeys));
        }

        if (duplicateBindings.length) {
            errorMessage += StringUtils.format(Strings.ERROR_DUPLICATE_SHORTCUTS, _getBulletList(duplicateBindings));
        }

        if (invalidKeys.length) {
            errorMessage += StringUtils.format(Strings.ERROR_INVALID_SHORTCUTS, _getBulletList(invalidKeys));
        }

        if (invalidCommands.length) {
            errorMessage += StringUtils.format(Strings.ERROR_NONEXISTENT_COMMANDS, _getBulletList(invalidCommands));
        }

        if (_showErrors && errorMessage) {
            _showErrorsAndOpenKeyMap("", errorMessage);
        }
    }

    /**
     * @private
     *
     * Restores the default key bindings for all the commands that are modified by each key binding
     * specified in _customKeyMapCache (old version) but no longer specified in _customKeyMap (new version).
     */
    function _undoPriorUserKeyBindings() {
        _.forEach(_customKeyMapCache, function (commandID, key) {
            var normalizedKey  = normalizeKeyDescriptorString(key),
                defaults       = _.find(_.toArray(_defaultKeyMap), { "commandID": commandID }),
                defaultCommand = _defaultKeyMap[normalizedKey];

            // We didn't modified this before, so skip it.
            if (_isSpecialCommand(commandID) ||
                    _isReservedShortcuts(normalizedKey)) {
                return;
            }

            if (_isKeyAssigned(normalizedKey) &&
                    _customKeyMap[key] !== commandID && _customKeyMap[normalizedKey] !== commandID) {
                // Unassign the key from any command. e.g. "Cmd-W": "file.open" in _customKeyMapCache
                // will require us to remove Cmd-W shortcut from file.open command.
                removeBinding(normalizedKey);

                // Reassign the default key binding. e.g. "Cmd-W": "file.open" in _customKeyMapCache
                // will require us to reassign Cmd-O shortcut to file.open command.
                if (defaults) {
                    addBinding(commandID, defaults, brackets.platform);
                }

                // Reassign the default key binding of the previously modified command.
                // e.g. "Cmd-W": "file.open" in _customKeyMapCache will require us to reassign Cmd-W
                // shortcut to file.close command.
                if (defaultCommand && defaultCommand.key) {
                    addBinding(defaultCommand.commandID, defaultCommand.key, brackets.platform);
                }
            }
        });
    }

    /**
     * @private
     *
     * Gets the full file path to the user key map file. In testing environment
     * a different file path is returned so that running integration tests won't
     * pop up the error dialog showing the errors from the actual user key map file.
     *
     * @return {string} full file path to the user key map file.
     */
    function _getUserKeyMapFilePath() {
        if (window.isBracketsTestWindow) {
            return brackets.app.getApplicationSupportDirectory() + "/_test_/" + KEYMAP_FILENAME;
        }
        return _userKeyMapFilePath;
    }

    /**
     * @private
     *
     * Reads in the user key map file and parses its content into JSON.
     * Returns the user key bindings if JSON has "overrides".
     * Otherwise, returns an empty object or an error if the file
     * cannot be parsed or loaded.
     *
     * @return {$.Promise} a jQuery promise that will be resolved with the JSON
     * object if the user key map file has "overrides" property or an empty JSON.
     * If the key map file cannot be read or cannot be parsed by the JSON parser,
     * then the promise is rejected with an error.
     */
    function _readUserKeyMap() {
        var file   = FileSystem.getFileForPath(_getUserKeyMapFilePath()),
            result = new $.Deferred();

        file.exists(function (err, doesExist) {
            if (doesExist) {
                FileUtils.readAsText(file)
                    .done(function (text) {
                        var keyMap = {};
                        try {
                            if (text) {
                                var json = JSON.parse(text);
                                // If no overrides, return an empty key map.
                                result.resolve((json && json.overrides) || keyMap);
                            } else {
                                // The file is empty, so return an empty key map.
                                result.resolve(keyMap);
                            }
                        } catch (err) {
                            // Cannot parse the text read from the key map file.
                            result.reject(err);
                        }
                    })
                    .fail(function (err) {
                        // Key map file cannot be loaded.
                        result.reject(err);
                    });
            } else {
                // Just resolve if no user key map file
                result.resolve();
            }
        });
        return result.promise();
    }

    /**
     * @private
     *
     * Reads in the user key bindings and updates the key map with each user key
     * binding by removing the existing one assigned to each key and adding
     * new one for the specified command id. Shows errors and opens the user
     * key map file if it cannot be parsed.
     */
    function _loadUserKeyMap() {
        _readUserKeyMap()
            .then(function (keyMap) {
                _customKeyMapCache = _.cloneDeep(_customKeyMap);
                _customKeyMap = keyMap;
                _undoPriorUserKeyBindings();
                _applyUserKeyBindings();
            }, function (err) {
                _showErrorsAndOpenKeyMap(err);
            });
    }

    /**
     * @private
     *
     * Opens the existing key map file or creates a new one with default content
     * if it does not exist.
     */
    function _openUserKeyMap() {
        var userKeyMapPath = _getUserKeyMapFilePath(),
            file = FileSystem.getFileForPath(userKeyMapPath);
        file.exists(function (err, doesExist) {
            if (doesExist) {
                CommandManager.execute(Commands.FILE_OPEN, { fullPath: userKeyMapPath });
            } else {
                var defaultContent = "{\n    \"documentation\": \"https://github.com/adobe/brackets/wiki/User-Key-Bindings\"," +
                                     "\n    \"overrides\": {" +
                                     "\n        \n    }\n}\n";

                FileUtils.writeText(file, defaultContent, true)
                    .done(function () {
                        CommandManager.execute(Commands.FILE_OPEN, { fullPath: userKeyMapPath });
                    });
            }
        });
    }

    // Due to circular dependencies, not safe to call on() directly
    EventDispatcher.on_duringInit(CommandManager, "commandRegistered", _handleCommandRegistered);
    CommandManager.register(Strings.CMD_OPEN_KEYMAP, Commands.FILE_OPEN_KEYMAP, _openUserKeyMap);

    // Asynchronously loading DocumentManager to avoid the circular dependency
    require(["document/DocumentManager"], function (DocumentManager) {
        DocumentManager.on("documentSaved", function checkKeyMapUpdates(e, doc) {
            if (doc && doc.file.fullPath === _userKeyMapFilePath) {
                _loadUserKeyMap();
            }
        });
    });

    /**
     * @private
     *
     * Initializes _allCommands array and _defaultKeyMap so that we can use them for
     * detecting non-existent commands and restoring the original key binding.
     *
     * @param {string} fullPath file path to the user key map file.
     */
    function _initCommandAndKeyMaps() {
        _allCommands = CommandManager.getAll();
        // Keep a copy of the default key bindings before loading user key bindings.
        _defaultKeyMap = _.cloneDeep(_keyMap);
    }

    /**
     * @private
     *
     * Sets the full file path to the user key map file. Only used by unit tests
     * to load a test file instead of the actual user key map file.
     *
     * @param {string} fullPath file path to the user key map file.
     */
    function _setUserKeyMapFilePath(fullPath) {
        _userKeyMapFilePath = fullPath;
    }

    AppInit.extensionsLoaded(function () {
        var params  = new UrlParams();
        params.parse();
        if (params.get("reloadWithoutUserExts") === "true") {
            _showErrors = false;
        }

        _initCommandAndKeyMaps();
        _loadUserKeyMap();
    });

    // unit test only
    exports._reset = _reset;
    exports._setUserKeyMapFilePath = _setUserKeyMapFilePath;
    exports._getDisplayKey = _getDisplayKey;
    exports._loadUserKeyMap = _loadUserKeyMap;
    exports._initCommandAndKeyMaps = _initCommandAndKeyMaps;
    exports._onCtrlUp = _onCtrlUp;

    // Define public API
    exports.getKeymap = getKeymap;
    exports.addBinding = addBinding;
    exports.removeBinding = removeBinding;
    exports.formatKeyDescriptor = formatKeyDescriptor;
    exports.getKeyBindings = getKeyBindings;
    exports.addGlobalKeydownHook = addGlobalKeydownHook;
    exports.removeGlobalKeydownHook = removeGlobalKeydownHook;

    /**
     * Use windows-specific bindings if no other are found (e.g. Linux).
     * Core Brackets modules that use key bindings should always define at
     * least a generic keybinding that is applied for all platforms. This
     * setting effectively creates a compatibility mode for third party
     * extensions that define explicit key bindings for Windows and Mac, but
     * not Linux.
     */
    exports.useWindowsCompatibleBindings = false;

    // For unit testing only
    exports._handleKey = _handleKey;
    exports._handleKeyEvent = _handleKeyEvent;
});


var mkdir = require('./mkdir')
  , path = require('path')
  , fs = require('fs')
  , exists = fs.exists || path.exists
  , existsSync = fs.existsSync || path.existsSync

function outputFile (file, data, encoding, callback) {
  if (typeof encoding === 'function') {
    callback = encoding
    encoding = 'utf8'
  }

  var dir = path.dirname(file)
  exists(dir, function(itDoes) {
    if (itDoes) return fs.writeFile(file, data, encoding, callback)

    mkdir.mkdirs(dir, function(err) {
      if (err) return callback(err)

      fs.writeFile(file, data, encoding, callback)
    })
  })
}

function outputFileSync (file, data, encoding) {
  var dir = path.dirname(file)
  if (existsSync(dir)) return fs.writeFileSync.apply(fs, arguments)
  mkdir.mkdirsSync(dir)
  fs.writeFileSync.apply(fs, arguments)
}

module.exports.outputFile = outputFile;
module.exports.outputFileSync = outputFileSync;


If you want to write an option parser, and have it be good, there are
two ways to do it.  The Right Way, and the Wrong Way.

The Wrong Way is to sit down and write an option parser.  We've all done
that.

The Right Way is to write some complex configurable program with so many
options that you go half-insane just trying to manage them all, and put
it off with duct-tape solutions until you see exactly to the core of the
problem, and finally snap and write an awesome option parser.

If you want to write an option parser, don't write an option parser.
Write a package manager, or a source control system, or a service
restarter, or an operating system.  You probably won't end up with a
good one of those, but if you don't give up, and you are relentless and
diligent enough in your procrastination, you may just end up with a very
nice option parser.

## USAGE

    // my-program.js
    var nopt = require("nopt")
      , Stream = require("stream").Stream
      , path = require("path")
      , knownOpts = { "foo" : [String, null]
                    , "bar" : [Stream, Number]
                    , "baz" : path
                    , "bloo" : [ "big", "medium", "small" ]
                    , "flag" : Boolean
                    , "pick" : Boolean
                    , "many" : [String, Array]
                    }
      , shortHands = { "foofoo" : ["--foo", "Mr. Foo"]
                     , "b7" : ["--bar", "7"]
                     , "m" : ["--bloo", "medium"]
                     , "p" : ["--pick"]
                     , "f" : ["--flag"]
                     }
                 // everything is optional.
                 // knownOpts and shorthands default to {}
                 // arg list defaults to process.argv
                 // slice defaults to 2
      , parsed = nopt(knownOpts, shortHands, process.argv, 2)
    console.log(parsed)

This would give you support for any of the following:

```bash
$ node my-program.js --foo "blerp" --no-flag
{ "foo" : "blerp", "flag" : false }

$ node my-program.js ---bar 7 --foo "Mr. Hand" --flag
{ bar: 7, foo: "Mr. Hand", flag: true }

$ node my-program.js --foo "blerp" -f -----p
{ foo: "blerp", flag: true, pick: true }

$ node my-program.js -fp --foofoo
{ foo: "Mr. Foo", flag: true, pick: true }

$ node my-program.js --foofoo -- -fp  # -- stops the flag parsing.
{ foo: "Mr. Foo", argv: { remain: ["-fp"] } }

$ node my-program.js --blatzk 1000 -fp # unknown opts are ok.
{ blatzk: 1000, flag: true, pick: true }

$ node my-program.js --blatzk true -fp # but they need a value
{ blatzk: true, flag: true, pick: true }

$ node my-program.js --no-blatzk -fp # unless they start with "no-"
{ blatzk: false, flag: true, pick: true }

$ node my-program.js --baz b/a/z # known paths are resolved.
{ baz: "/Users/isaacs/b/a/z" }

# if Array is one of the types, then it can take many
# values, and will always be an array.  The other types provided
# specify what types are allowed in the list.

$ node my-program.js --many 1 --many null --many foo
{ many: ["1", "null", "foo"] }

$ node my-program.js --many foo
{ many: ["foo"] }
```

Read the tests at the bottom of `lib/nopt.js` for more examples of
what this puppy can do.

## Types

The following types are supported, and defined on `nopt.typeDefs`

* String: A normal string.  No parsing is done.
* path: A file system path.  Gets resolved against cwd if not absolute.
* url: A url.  If it doesn't parse, it isn't accepted.
* Number: Must be numeric.
* Date: Must parse as a date. If it does, and `Date` is one of the options,
  then it will return a Date object, not a string.
* Boolean: Must be either `true` or `false`.  If an option is a boolean,
  then it does not need a value, and its presence will imply `true` as
  the value.  To negate boolean flags, do `--no-whatever` or `--whatever
  false`
* NaN: Means that the option is strictly not allowed.  Any value will
  fail.
* Stream: An object matching the "Stream" class in node.  Valuable
  for use when validating programmatically.  (npm uses this to let you
  supply any WriteStream on the `outfd` and `logfd` config options.)
* Array: If `Array` is specified as one of the types, then the value
  will be parsed as a list of options.  This means that multiple values
  can be specified, and that the value will always be an array.

If a type is an array of values not on this list, then those are
considered valid values.  For instance, in the example above, the
`--bloo` option can only be one of `"big"`, `"medium"`, or `"small"`,
and any other value will be rejected.

When parsing unknown fields, `"true"`, `"false"`, and `"null"` will be
interpreted as their JavaScript equivalents, and numeric values will be
interpreted as a number.

You can also mix types and values, or multiple types, in a list.  For
instance `{ blah: [Number, null] }` would allow a value to be set to
either a Number or null.  When types are ordered, this implies a
preference, and the first type that can be used to properly interpret
the value will be used.

To define a new type, add it to `nopt.typeDefs`.  Each item in that
hash is an object with a `type` member and a `validate` method.  The
`type` member is an object that matches what goes in the type list.  The
`validate` method is a function that gets called with `validate(data,
key, val)`.  Validate methods should assign `data[key]` to the valid
value of `val` if it can be handled properly, or return boolean
`false` if it cannot.

You can also call `nopt.clean(data, types, typeDefs)` to clean up a
config object and remove its invalid properties.

## Error Handling

By default, nopt outputs a warning to standard error when invalid
options are found.  You can change this behavior by assigning a method
to `nopt.invalidHandler`.  This method will be called with
the offending `nopt.invalidHandler(key, val, types)`.

If no `nopt.invalidHandler` is assigned, then it will console.error
its whining.  If it is assigned to boolean `false` then the warning is
suppressed.

## Abbreviations

Yes, they are supported.  If you define options like this:

```javascript
{ "foolhardyelephants" : Boolean
, "pileofmonkeys" : Boolean }
```

Then this will work:

```bash
node program.js --foolhar --pil
node program.js --no-f --pileofmon
# etc.
```

## Shorthands

Shorthands are a hash of shorter option names to a snippet of args that
they expand to.

If multiple one-character shorthands are all combined, and the
combination does not unambiguously match any other option or shorthand,
then they will be broken up into their constituent parts.  For example:

```json
{ "s" : ["--loglevel", "silent"]
, "g" : "--global"
, "f" : "--force"
, "p" : "--parseable"
, "l" : "--long"
}
```

```bash
npm ls -sgflp
# just like doing this:
npm ls --loglevel silent --global --force --long --parseable
```

## The Rest of the args

The config object returned by nopt is given a special member called
`argv`, which is an object with the following fields:

* `remain`: The remaining args after all the parsing has occurred.
* `original`: The args as they originally appeared.
* `cooked`: The args after flags and shorthands are expanded.

## Slicing

Node programs are called with more or less the exact argv as it appears
in C land, after the v8 and node-specific options have been plucked off.
As such, `argv[0]` is always `node` and `argv[1]` is always the
JavaScript program being run.

That's usually not very useful to you.  So they're sliced off by
default.  If you want them, then you can pass in `0` as the last
argument, or any other number that you'd like to slice off the start of
the list.


/*global define, $ */

define(function (require, exports, module) {
    "use strict";

    exports.initExtension = function () {
        // promise is rejected immediately
        return new $.Deferred().reject();
    };
});


keep keep keep add keep keep keep keep keep


/*
 * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved.
 *
 * 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.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define */

define(function (require, exports, module) {
    "use strict";

    console.log("loading foo!");

    require("secondary").bar();

    exports.bar = function bar() {
        console.log("in bar in foo!");
    };

});


exports['rfc1867'] =
  { boundary: 'AaB03x',
    raw:
      '--AaB03x\r\n'+
      'content-disposition: form-data; name="field1"\r\n'+
      '\r\n'+
      'Joe Blow\r\nalmost tricked you!\r\n'+
      '--AaB03x\r\n'+
      'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+
      'Content-Type: text/plain\r\n'+
      '\r\n'+
      '... contents of file1.txt ...\r\r\n'+
      '--AaB03x--\r\n',
    parts:
    [ { headers: {
          'content-disposition': 'form-data; name="field1"',
        },
        data: 'Joe Blow\r\nalmost tricked you!',
      },
      { headers: {
          'content-disposition': 'form-data; name="pics"; filename="file1.txt"',
          'Content-Type': 'text/plain',
        },
        data: '... contents of file1.txt ...\r',
      }
    ]
  };

exports['noTrailing\r\n'] =
  { boundary: 'AaB03x',
    raw:
      '--AaB03x\r\n'+
      'content-disposition: form-data; name="field1"\r\n'+
      '\r\n'+
      'Joe Blow\r\nalmost tricked you!\r\n'+
      '--AaB03x\r\n'+
      'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+
      'Content-Type: text/plain\r\n'+
      '\r\n'+
      '... contents of file1.txt ...\r\r\n'+
      '--AaB03x--',
    parts:
    [ { headers: {
          'content-disposition': 'form-data; name="field1"',
        },
        data: 'Joe Blow\r\nalmost tricked you!',
      },
      { headers: {
          'content-disposition': 'form-data; name="pics"; filename="file1.txt"',
          'Content-Type': 'text/plain',
        },
        data: '... contents of file1.txt ...\r',
      }
    ]
  };

exports['emptyHeader'] =
  { boundary: 'AaB03x',
    raw:
      '--AaB03x\r\n'+
      'content-disposition: form-data; name="field1"\r\n'+
      ': foo\r\n'+
      '\r\n'+
      'Joe Blow\r\nalmost tricked you!\r\n'+
      '--AaB03x\r\n'+
      'content-disposition: form-data; name="pics"; filename="file1.txt"\r\n'+
      'Content-Type: text/plain\r\n'+
      '\r\n'+
      '... contents of file1.txt ...\r\r\n'+
      '--AaB03x--\r\n',
    expectError: true,
  };


UnitTestReporter        = require("test/UnitTestReporter").UnitTestReporter,

BootstrapReporterView   = require("test/BootstrapReporterView").BootstrapReporterView;

// Load modules that self-register and just need to get included in the main project

// TODO (#2155): These are used by extensions via brackets.getModule(), so tests that run those

// extensions need these to be required up front. We need a better solution for this eventually.

keep keep keep keep keep replace keep keep keep keep keep

/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * 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.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, describe, it, expect, beforeEach, afterEach, waitsFor, runs, window, $, jasmine, setTimeout */
/*unittests: QuickOpen*/

define(function (require, exports, module) {
    'use strict';

    var Commands              = require("command/Commands"),
        KeyEvent              = require("utils/KeyEvent"),
        SpecRunnerUtils       = require("spec/SpecRunnerUtils");

    describe("QuickOpen", function () {
        var testPath = SpecRunnerUtils.getTestPath("/spec/QuickOpen-test-files");
        var brackets, test$, executeCommand, EditorManager, DocumentManager;

        beforeEach(function () {

            runs(function () {
                SpecRunnerUtils.createTestWindowAndRun(this, function (testWindow) {
                    brackets = testWindow.brackets;
                    test$ = testWindow.$;
                    executeCommand = testWindow.executeCommand;
                    EditorManager = brackets.test.EditorManager;
                    DocumentManager = brackets.test.DocumentManager;
                });
            });

        });

        afterEach(function () {
            SpecRunnerUtils.closeTestWindow();
        });

        function getSearchBar() {
            return test$(".modal-bar");
        }
        function getSearchField() {
            return test$(".modal-bar input[type='text']");
        }

        function expectSearchBarOpen() {
            expect(getSearchBar()[0]).toBeDefined();
        }
        function expectSearchBarClosed() {
            expect(getSearchBar()[0]).not.toBeDefined();
        }

        function enterSearchText(str, timeoutLength) {
            timeoutLength = timeoutLength || 10;

            expectSearchBarOpen();
            var input = getSearchField()[0];

            // We need to simulate typing into the field in order to get the QuickOpen matching code to
            // run.
            var i;
            for (i = 0; i < str.length; i++) {
                SpecRunnerUtils.simulateKeyEvent(KeyEvent["DOM_VK_" + str[i]], "keydown", input);
            }

            // We still set the input value as well because that is read by other code.
            // setTimeout is used to turn the event loop so that the search text is not placed in
            // the text field until after the keydown code has received the events.
            //
            // timeoutLength is configurable because some uses may take longer than others
            setTimeout(function () {
                getSearchField().val(str);
            }, timeoutLength);
        }

        function pressEnter() {
            expectSearchBarOpen();

            // Using keyup here because of inside knowledge of how the events are processed
            // on the QuickOpen input.
            SpecRunnerUtils.simulateKeyEvent(KeyEvent.DOM_VK_RETURN, "keyup", getSearchField()[0]);
        }

        it("can open a file and jump to a line, centering that line on the screen", function () {
            var hostOpened = false;
            var err = false;
            var workingSet = [];

            SpecRunnerUtils.loadProjectInTestWindow(testPath);

            runs(function () {
                SpecRunnerUtils.openProjectFiles(workingSet).done(function (documents) {
                    hostOpened = true;
                }).fail(function () {
                    err = true;
                });
            });

            waitsFor(function () { return hostOpened && !err; }, "window opening timeout", 1000);

            runs(function () {
                executeCommand(Commands.NAVIGATE_QUICK_OPEN);

                // need to set the timeout length here to ensure that it has a chance to load the file
                // list.
                enterSearchText("lines", 100);
            });

            waitsFor(function () {
                return getSearchField().val() === "lines";
            }, "filename entry timeout", 1000);

            runs(function () {
                pressEnter();
            });

            waitsFor(function () {
                return EditorManager.getCurrentFullEditor() !== null;
            }, "file opening timeout", 3000);

            runs(function () {
                // Make sure we've opened the right file. It should open the longer one, because
                // of the scoring in the StringMatch algorithm.
                expect(DocumentManager.getCurrentDocument().file.name).toEqual("lotsOfLines.html");
                executeCommand(Commands.NAVIGATE_GOTO_LINE);
                enterSearchText(":50");
            });

            waitsFor(function () {
                return getSearchField().val() === ":50";
            }, "goto line entry timeout", 1000);

            var eventLooped = false;
            runs(function () {
                pressEnter();
                setTimeout(function () {
                    eventLooped = true;
                }, 10);
            });

            waitsFor(function () { return eventLooped; });

            runs(function () {
                var editor = EditorManager.getCurrentFullEditor();
                var scrollPos = editor.getScrollPos();

                // The user enters a 1-based number, but the reported position
                // is 0 based, so we check for 49.
                expect(editor).toHaveCursorPosition(49, 0);

                // We expect the result to be scrolled roughly to the middle of the window.
                expect(scrollPos.y).toBeGreaterThan(400);
                expect(scrollPos.y).toBeLessThan(500);

                eventLooped = false;
                setTimeout(function () {
                    eventLooped = true;
                }, 10);
            });

            waitsFor(function () { return eventLooped; });
        });

    });
});