define(function(require, exports, module) {
    main.consumes = ["Plugin", "settings"];
    main.provides = ["commands"];
    return main;
    
    function main(options, imports, register) {
        var Plugin = imports.Plugin;
        var settings = imports.settings;
        
        var lang = require("ace/lib/lang");
        var event = require("ace/lib/event");
        var keyUtil = require("ace/lib/keys");
        var KeyBinding = require("ace/keyboard/keybinding").KeyBinding;
        var CommandManager = require("ace/commands/command_manager").CommandManager;
        
        /***** Initialization *****/
        
        var nav = navigator.platform.toLowerCase();
        var platform = nav.indexOf("mac") > -1 ? "mac" : "win";
        
        var commandManager = new CommandManager(platform);
        var commands = commandManager.commands;
        
        var plugin = new Plugin("Ajax.org", main.consumes);
        var emit = plugin.getEmitter();
        
        var execSequenceID = 0;
        
        // Use our exec function
        commandManager.exec = exec;
        
        var loaded = false;
        function load(){
            if (loaded) return false;
            loaded = true;
            
            var kb = new KeyBinding({
                commands: commandManager,
                fake: true
            });
            event.addCommandKeyListener(document.documentElement, kb.onCommandKey.bind(kb));
            event.addListener(document.documentElement, "keyup", function(e) {
                if (e.keyCode === 18) // do not trigger browser menu on windows
                    e.preventDefault();
            });
            
            settings.on("read", function(e) {
                settings.setDefaults("user/key-bindings", [
                    ["preset", "default"],
                    ["platform", "auto"]
                ]);
                
                var platform = settings.get("user/key-bindings/@platform");
                if (platform && platform != "auto")
                    changePlatform(platform);
            });
            
            addCommands([{
                name: "passKeysToBrowser",
                group: "ignore",
                bindKey: {
                    win: "F12|Ctrl-Shift-I",
                    mac: "F12|Cmd-`|Cmd-Option-I|Cmd-H|Cmd-M"
                },
                exec: function(){},
                passEvent: true,
                hint: "Allow keys to be handled by the browser"
            }, {
                name: "cancelBrowserAction",
                group: "ignore",
                bindKey: {
                    mac: "Cmd-S|Cmd-R|Cmd-[|Cmd-]",
                    win: "Ctrl-S|Ctrl-R|Alt-Left|Alt-Right",
                    position: -10000
                },
                exec: function(){},
                hint: "This cancels some native browser keybindings that can be annoying if triggered accidentally"
            }], plugin);
        }
        
        /***** Methods *****/
        
        function changePlatform(value) {
            platform = value == "auto"
                ? (apf.isMac ? "mac" : "win")
                : value;
            
            commandManager.platform = platform;
            Object.keys(commands).forEach(function(name) {
                var command = commands[name];
                var displayKey = command.bindKey || command.nativeKey;
                if (displayKey)
                    plugin.commandManager
                        .setProperty(command.name, displayKey[platform]);
            });
        }
    
        function getHotkey(command) {
            if (!commands[command] || !commands[command].bindKey)
                return "";
            return commands[command].bindKey[platform];
        }
        
        function getPrettyHotkey(command) {
            var key = getHotkey(command);
            if (platform == "mac")
                key = apf.hotkeys.toMacNotation(key);
            return key;
        }
        
        var markDirty = lang.delayedCall(function(){
            emit("update");
        }, 500);
        
        function exec(command, editor, args, e) {
            var sCommand = command;
            
            if (!editor || editor.fake)
                editor = emit("getEditor");
            
            if (Array.isArray(command)) {
                for (var i = command.length; i--; ) {
                    if (this.exec(command[i], editor, args, e))
                        return true;
                }
                return false;
            }
            
                
            if (typeof command === 'string')
                command = commands[command];
            
            if (!command) {
                console.warn("Could not find command ", sCommand);
                return false;
            }
            
            if (command.isAvailable && !command.isAvailable(editor, args, e))
                return; // Disable commands for other contexts

            if (command.findEditor)
                editor = command.findEditor(editor, e);
            
            if (editor && editor.$readOnly && !command.readOnly)
                return false;
                
            var execEvent = {
                editor: editor,
                command: command,
                args: args,
                sequenceID: ++execSequenceID
            };
            
            emit("beforeExec", execEvent);
            
            var retvalue;
            if (editor && editor.commands) {
                retvalue = editor.commands._emit("exec", execEvent);
                editor.commands._signal("afterExec", execEvent);
            } else {
                retvalue = commandManager._emit("exec", execEvent);
            }

            if (retvalue !== false && args) {
                // e.returnValue = false;
                // e.preventDefault();
                if (typeof apf != "undefined")
                    apf.queue.empty();
            }
            
            execEvent.returnValue = retvalue;
            emit("afterExec", execEvent);
            
            return retvalue !== false;
        }
        
        function addCommand(command, hostPlugin, asDefault) {
            if (!command.name)
                return console.error("trying to add a command without name", command);
            plugin.commandManager[command.name] = "";
            
            if (command.readOnly === undefined)
                command.readOnly = true;
            
            if (typeof command.bindKey == "string")
                command.bindKey = {win: command.bindKey, mac: command.bindKey};
            
            if (asDefault)
                command.isDefault = asDefault;
            
            commandManager.addCommand.apply(commandManager, arguments);
            var displayKey = command.bindKey || command.nativeKey;
            if (displayKey)
                plugin.commandManager
                    .setProperty(command.name, displayKey[platform]);

            if (!command.originalBindKey)
                command.originalBindKey = command.bindKey 
                    || (command.bindKey = { mac: "", win: "" });
            
            hostPlugin.addOther(function(){
                removeCommand(command);
            });
            
            markDirty.schedule();
            return command;
        }
        
        function addCommands(list, hostPlugin, asDefault) {
            list && Object.keys(list).forEach(function(name) {
                var command = list[name];
                if (typeof command === "string")
                    return bindKey(command, name, asDefault);
    
                if (typeof command === "function")
                    command = { exec: command };
    
                if (!command.name)
                    command.name = name;
                    
                if (typeof command.bindKey == "string")
                    command.bindKey = {mac: command.bindKey, win: command.bindKey};
                    
                if (asDefault && commands[command.name])
                    return;
                    
                command.isDefault = asDefault;
                
    
                addCommand(command, hostPlugin, asDefault);
            });
        }
        
        function removeCommands(commands) {
            Object.keys(commands).forEach(function(name) {
                removeCommand(commands[name]);
            });
        }
        
        function removeCommand(command, context, clean) {
            if (!command)
                return;

            var name = (typeof command === 'string' ? command : command.name);

            if (name) {
                if (plugin.commandManager[name])
                    plugin.commandManager.setProperty(name, "");
    
                command = commands[name];
                if (!clean)
                    delete commands[name];
            }
            
            commandManager.removeCommand(command, clean);
            markDirty.schedule();
        }
        
        function setDefault(name, keys) {
            var command = commands[name];
            if (!command) return;
            // If bind key is not yet overridden by a custom one
            if (plugin.commandManager[name] == command.bindKey[platform])
                bindKey(keys[platform], command);
            
            command.bindKey = keys;
        }
        
        function bindKey(key, command, asDefault) {
            removeCommand(command, null, true);
            
            if (!command)
                return;
            
            if (typeof key == "string" || !key) {
                command.bindKey = {};
                command.bindKey[commandManager.platform] = key;
            } else
                command.bindKey = key;
            
            if (command.bindKey.position == undefined)
                command.bindKey.position = command.originalBindKey.position;
            
            commandManager.bindKey(command.bindKey, command, asDefault);
            plugin.commandManager.setProperty(command.name, 
                command.bindKey[commandManager.platform]);
        }
        
        function findKey(key, scope) {
            var commands = commandManager.commandKeyBinding[key];
            if (!commands)
                commands = commandManager.commandKeyBinding[key.toLowerCase()];
            if (!commands) return [];
            if (!Array.isArray(commands)) commands = [commands];
            if (scope == "global") {
                var exceptions = getExceptionList();
                commands = commands.filter(function(c) {
                    return exceptions.indexOf(c) != -1;
                });
            }
            return commands;
        }
        
        function reset(noReload, toDefault){
            commandManager.commandKeyBinding = {};
            
            Object.keys(commands).forEach(function(name) {
                var cmd = commands[name];
                bindKey(toDefault ? cmd.originalBindKey : cmd.bindKey, cmd);
            });
            
            if (noReload)
                markDirty.cancel();
            else
                markDirty.schedule();
        }
        
        function flushUpdateQueue() {
            markDirty.call();
        }
        
        function getExceptionList(){
            // Whitelist certain IDE keys for use from terminal and preview
            return [
                {
                    bindKey: { win: null, mac: "Command-O" },
                    name: "navigateAlt",
                    passEvent: true,
                    exec: function(){}
                },
                commands.openpreferences,
                commands.passKeysToBrowser,
                commands.commands,
                commands.find,
                commands.openterminal,
                commands.navigate,
                commands.searchinfiles,
                commands.close_term_pane,
                commands.closeallbutme,
                commands.closealltabs,
                commands.closealltotheleft,
                commands.closealltotheright,
                commands.closepane,
                commands.closetab,
                commands.gototabright,
                commands.gototableft,
                commands.movetabright,
                commands.movetableft,
                commands.movetabup,
                commands.movetabdown,
                commands.nexttab,
                commands.previoustab,
                commands.nextpane,
                commands.previouspane,
                commands.exit,
                commands.toggleconsole,
                commands.runlast,
                commands.run,
                commands.resume,
                commands.stepinto,
                commands.stepover,
                commands.stepout,
                commands.devtools,
                commands.open,
                commands.settings,
                commands.new,
                commands.build,
                commands.switchterminal,
                commands.findinfiles,
                commands.tab1,
                commands.tab2,
                commands.tab3,
                commands.tab4,
                commands.tab5,
                commands.tab6,
                commands.tab7,
                commands.tab8,
                commands.tab9,
                commands.tab0,
                commands.revealtab,
                commands.reopenLastTab,
                commands.gotopaneleft,
                commands.gotopaneright,
                commands.gotopaneup,
                commands.gotopanedown,
            ].filter(Boolean);
        }
        
        function getExceptionBindings(){
            var list = [];
            getExceptionList().forEach(function(cmd) {
                var m = cmd && cmd.bindKey && cmd.bindKey[platform];
                if (!m) return;
                
                list.push.apply(list, m.split("|").map(function(keyPart) {
                    var binding = commandManager.parseKeys(keyPart, cmd);
                    binding.key = keyUtil[binding.key];
                    return {
                        binding: binding,
                        command: cmd.name
                    };
                }));
            });
            return list;
        }
        
        // commandManager.bindKey = bindKey;
        
        /***** Lifecycle *****/
        
        plugin.on("load", function(){
            load();
        });
        plugin.on("enable", function(){
            
        });
        plugin.on("disable", function(){
            
        });
        plugin.on("unload", function(){
            loaded = false;
        });
        
        /***** Register and define API *****/
        
        /**
         * Manages the commands and the key bindings for Cloud9.
         * 
         * Commands are named items that represent functionality that can be 
         * triggered by a user by pressing a combination of keys at the same
         * time. Commands can also be executed via code:
         * 
         *     // Saves the currently focussed tab (if any)
         *     commands.exec("save");
         * 
         * You can also specify arguments to commands
         * 
         *     // Runs the file /example.js with argument --test
         *     var editor = tabs.findPage("/example.js").editor;
         *     commands.exec("run", editor, ["--test"]);
         * 
         * This example shows how you can define a command. This command is
         * available when there is a current editor and it's an {@link ace.Ace Ace} 
         * editor.
         * 
         *     commands.addCommand({
         *         name        : "gotoline",
         *         group       : "ace",
         *         hint        : "Triggers the goto line dialog",
         *         bindKey     : { mac: "Command-L", win: "Ctrl-G" },
         *         isAvailable : function(editor) {
         *             return editor && editor.type == "ace";
         *         },
         *         exec : function() {
         *             gotoline();
         *         }
         *     }, plugin);
         * 
         * This example is also from the gotoline plugin. In this example the
         * bind key is a commonly used key: Escape. There are many commands that
         * bind this key and the isAvailable function determines which command 
         * will be executed. The first command found for which the isAvailable
         * function returns true will be used. In this case it returns true when
         * the gotoline dialog is visible (which is only visible when it has 
         * focus).
         * 
         *     commands.addCommand({
         *         bindKey     : { mac: "ESC", win: "ESC" },
         *         isAvailable : function(editor){ return win && win.visible; },
         *         exec        : function() {
         *             hide();
         *             var tab = tabs.focussedTab;
         *             tab && tabs.focusTab(tab);
         *             
         *             if (originalLine) {
         *                 execGotoLine(originalLine, originalColumn, true);
         *                 originalPath = originalColumn = originalLine = undefined;
         *             }
         *         }
         *     }, plugin);
         * 
         * Commands are generally used by buttons and menu items to attach
         * functionality to them:
         * 
         *     menus.addItemByPath("Edit/Cut", new MenuItem({ command : "cut" }), 400, plugin);
         *     menus.addItemByPath("Edit/Copy", new MenuItem({ command : "copy" }), 500, plugin);
         *     menus.addItemByPath("Edit/Paste", new MenuItem({ command : "paste" }), 600, plugin);
         * 
         * Or to ui.button elements:
         * 
         *     var button = new ui.button({ caption: "Save", command: "save" });
         * 
         * Users are able to change the key bindings of each command in the
         * key bindings editor. They are also able to set
         * key bindings for commands that have been defined without a key
         * binding.
         * 
         * @singleton
         */
        plugin.freezePublicAPI({
            /**
             * @ignore
             */
            flushUpdateQueue: flushUpdateQueue,
            /**
             * @ignore
             */
            get commandKeyBinding() { commandManager.commandKeyBinding },
            /**
             * @ignore
             */
            commandManager: (typeof apf != "undefined" 
                ? new apf.Class().$init() : 
                { setProperty : function(x,y){ this[x] = y } }),
            
            /**
             * A hash table of all the commands. The index is the name of the
             * command.
             * 
             * See {@link #addCommand} for a description of the command object.
             * 
             * @property {Object[]} commands
             * @readonly
             */
            get commands(){ return commands; },
            
            /**
             * The operating system that is being run.
             * @property {String} platform  Possible values are "mac", "win".
             * @readonly
             */
            get platform(){ return platform; },
            
            /**
             * By default the key bindings for the platform that the user is
             * currently running are choosen. Using this function the chosen
             * platform can be changed.
             * @param {String} platform  The platform to change the key binding 
             *   set to. Possible values are "auto", "mac" or "win".
             */
            changePlatform: changePlatform,
            
            /**
             * Retrieves a string that specifies the current hotkey for a 
             * command. The (modifier) keys are space or dash (-) separated.
             * Special named keys are: 
             * 
             * * Ctrl
             * * Command
             * * Alt
             * * Option
             * * Shift
             * * Meta
             * * Tab
             * * Esc
             * * Enter
             * * F1-F12
             * * Up
             * * Down
             * * Left
             * * Right
             * * PgUp
             * * PgDown
             * * Home
             * * End
             * 
             * @param {String} name  the name of the command.
             * @return {String}
             */
            getHotkey: getHotkey,
            
            /**
             * returns result of getHotkey formatted for displaying in menus
             * 
             * @param {String} name  the name of the command.
             * @return {String}
             */
            getPrettyHotkey: getPrettyHotkey,
            
            /**
             * Executes the action tied to a command. This method will call
             * the `isAvailable` method for a command and will not execute if 
             * the command is not available. If there are multiple commands
             * with the same name, the command for which the `isAvailable`
             * method returns true first will be executed.
             * @param {String} command  the name of the command to execute
             * @param {Editor} editor   the editor that is the context for the
             *   command.
             * @param {Array}  args     a list of arguments to pass to the
             *   command.
             * @return {Boolean} Specifies whether the command was executed 
             *   successfully.
             */
            exec: exec,
            
            /**
             * Adds a command to the list of available commands.
             * 
             * @param {Object}   command                The command definition 
             *   to add.
             * @param {String}   command.name           The name of this command
             * @param {Object}   [command.bindKey]      Object containing an entry 
             *   for each platform. The (modifier) keys are space or dash (-) 
             *   separated. Special named keys are: 
             * 
             * Ctrl, Command, Alt, Option, Shift, Meta, Tab, Esc, Enter, F1-F12, 
             * Up, Down, Left, Right, PgUp, PgDown, Home, End
             * 
             * Example:
             * 
             *     bindKey : { mac: "Command-Option-Z", win: "Ctrl-Alt-Z" }
             * 
             * @param {String}   [command.bindKey.win]  The bind key for windows 
             *   and unix.
             * @param {String}   [command.bindKey.mac]  The bind key for mac. 
             * @param {String}   [command.hint]         A description of this 
             *   command. This is displayed in the key bindings editor.
             * @param {String}   [command.group]        The group to which this 
             *   command belongs. This is used by the key bindings editor to 
             *   group the commands.This function should return true when the 
             *   command is available and otherwise return false. Make sure that 
             *   you implement this to be as exact as possible.
             * @param {Function} [command.isAvailable]  This function should 
             *   return true when the command is available and otherwise return 
             *   false. Make sure that you implement this to be as exact as 
             *   possible.
             * @param {Function} command.exec           This function is called 
             *   when the command is triggered for execution.
             * @param {Plugin}   plugin   The plugin responsible for adding the 
             *   command.
             * @return {Object}                         The  command definition
             */
            addCommand: addCommand,
            
            /**
             * Adds multiple commands to the list of available commands.
             * @param {Object[]} list    The list of commands to add. 
             *   See {@link #addCommand} for a description of the object 
             *   definition.
             * @param {Plugin}   plugin  The plugin responsible for adding the 
             *   commands.
             */
            addCommands: addCommands,
            
            /**
             * Remove multiple commands from the list of available commans.
             * @param {String[]} list    The list of names of commands to remove. 
             */
            removeCommands: removeCommands,
            
            /**
             * Remove a command from the list of available commands
             * @param {String} name  the name of the command to remove.
             */
            removeCommand: removeCommand,
            
            /**
             * Set a new bind key for a command
             * @param {String} key      the description of the keys to press. 
             *   See {@link #getHotkey} for the way to construct this string.
             * @param {String} command  The command object as described by
             *   {@link #addCommand}
             */
            bindKey: bindKey,
            
            /**
             * 
             */
            findKey: findKey,
            
            /**
             * 
             */
            reset: reset,
            
            /**
             * 
             */
            setDefault: setDefault,
            
            /**
             * 
             */
            getExceptionList: getExceptionList,
            
            /**
             * 
             */
            getExceptionBindings: getExceptionBindings
        });
        
        register(null, {
            commands: plugin
        });
    }
});
