/**
 * Provides a convenient wrapper for normalized keyboard navigation. KeyNav allows you to bind
 * navigation keys to function calls that will get called when the keys are pressed, providing
 * an easy way to implement custom navigation schemes for any UI component.
 *
 * The following are all of the possible keys that can be implemented: Enter, Space, Left, Right,
 * Up, Down, Tab, Esc, Page Up, Page Down, Delete, Backspace, Home, End.
 *
 * Usage:
 *
 *      var nav = new Ext.util.KeyNav({
 *          target: "my-element",
 *          
 *          left: function(e) {
 *              this.moveLeft(e.ctrlKey);
 *          },
 *          right: function(e) {
 *              this.moveRight(e.ctrlKey);
 *          },
 *          enter: function(e) {
 *              this.save();
 *          },
 *          
 *          // Binding may be a function specifiying fn, scope and defaultEventAction
 *          esc: {
 *              fn: this.onEsc,
 *              defaultEventAction: false
 *          },
 *          
 *          // Binding may be keyed by a single character
 *          A: {
 *              ctrl: true,
 *              fn: selectAll
 *          },
 *          
 *          // Binding may be keyed by a key code (45 = INSERT)
 *          45: {
 *              fn: doInsert
 *          },
 *          
 *          scope: myObject
 *     });
 */
Ext.define('Ext.util.KeyNav', {
    alternateClassName: 'Ext.KeyNav',
 
    requires: ['Ext.util.KeyMap'],
 
    /**
     * @cfg {Boolean} disabled
     * True to disable this KeyNav instance.
     */
    disabled: false,
 
    /**
     * @cfg {String} [defaultEventAction=false]
     * The method to call on the {@link Ext.event.Event} after this KeyNav intercepts a key.
     * Valid values are {@link Ext.event.Event#stopEvent}{@link Ext.event.Event#preventDefault}
     * and {@link Ext.event.Event#stopPropagation}.
     *
     * If a falsy value is specified, no method is called on the key event.
     */
    defaultEventAction: false,
 
    /**
     * @cfg {Boolean} forceKeyDown
     *
     * Handle the keydown event instead of keypress. KeyNav automatically does this for IE
     * since IE does not propagate special keys on keypress, but setting this to true will force
     * other browsers to also handle keydown instead of keypress.
     */
    forceKeyDown: false,
 
    /**
     * @cfg {Ext.Component/Ext.dom.Element/HTMLElement/String} target
     * The object on which to listen for the event specified by the {@link #eventName}
     * config option.
     */
 
    /**
     * @cfg {String} eventName
     * The event to listen for to pick up key events.
     */
    eventName: 'keypress',
 
    /**
     * @cfg {Function} processEvent
     * An optional event processor function which accepts the argument list provided by the
     * {@link #eventName configured event} of the {@link #target}, and returns a keyEvent
     * for processing by the KeyMap.
     *
     * This may be useful when the {@link #target} is a Component with s complex event signature.
     * Extra information from the event arguments may be injected into the event for use
     * by the handler functions before returning it.
     */
 
    /**
     * @cfg {Object} [processEventScope=this]
     * The scope (`this` context) in which the {@link #processEvent} method is executed.
     */
 
    /**
     * @cfg {Boolean} [ignoreInputFields=false]
     * Configure this as `true` if there are any input fields within the {@link #target}, and this
     * KeyNav should not process events from input fields (`<input>`, `<textarea>` and elements
     * with `contentEditable="true"`)
     */
 
    /**
     * @cfg {Ext.util.KeyMap} [keyMap]
     * An optional pre-existing {@link Ext.util.KeyMap KeyMap} to use to listen for key events.
     * If not specified, one is created.
     */
 
    /**
     * @property {Ext.event.Event} lastKeyEvent
     * The last key event that this KeyMap handled.
     */
 
    /**
     * @cfg {Number} [priority]
     * The priority to set on this KeyNav's listener. Listeners with a higher priority are fired
     * before those with lower priority.
     */
 
    statics: {
        keyOptions: {
            left: 37,
            right: 39,
            up: 38,
            down: 40,
            space: 32,
            pageUp: 33,
            pageDown: 34,
            del: 46,
            backspace: 8,
            home: 36,
            end: 35,
            enter: 13,
            esc: 27,
            tab: 9
        }
    },
 
    constructor: function(config) {
        var me = this,
            keymapCfg, map;
        
        //<debug>
        if (arguments.length === 2) {
            Ext.raise("2-argument KeyNav constructor is removed. Use a config object instead.");
        }
        //</debug>
        
        config = config || {};
        
        keymapCfg = {
            target: config.target,
            ignoreInputFields: config.ignoreInputFields,
            eventName: me.getKeyEvent(
                'forceKeyDown' in config ? config.forceKeyDown : me.forceKeyDown,
                config.eventName
            ),
            capture: config.capture
        };
        
        if (me.map) {
            me.map.destroy();
        }
 
        // Ensure config system configs are set
        me.initConfig(config);
 
        if (config.processEvent) {
            keymapCfg.processEvent = config.processEvent;
            keymapCfg.processEventScope = config.processEventScope || me;
        }
        
        if (config.priority) {
            keymapCfg.priority = config.priority;
        }
 
        // If they specified a KeyMap to use, use it
        if (config.keyMap) {
            map = me.map = config.keyMap;
        }
        // Otherwise, create one, and remember to destroy it on destroy
        else {
            map = me.map = new Ext.util.KeyMap(keymapCfg);
            me.destroyKeyMap = true;
        }
 
        me.addBindings(config);
 
        map.disable();
        
        if (!config.disabled) {
            map.enable();
        }
    },
 
    addBindings: function(bindings) {
        var me = this,
            map = me.map,
            keyCodes = Ext.util.KeyNav.keyOptions,
            Event = Ext.event.Event,
            defaultScope = bindings.scope || me,
            binding, keyName, keyCode;
 
        for (keyName in bindings) {
            binding = bindings[keyName];
            
            // There is a property named after a key name.
            // It may be a function or an binding spec containing handler, scope and
            // defaultEventAction configs
            // Allow { A: { ctrl: true, handler: onCtrlA } }
            // Allow { 45: doInsert } to use key codes directly
            // Allow { F2: onKeyF2 }
            keyCode = keyName.length === 1
                ? keyName.charCodeAt(0)
                : (keyCodes[keyName] || Event[keyName.toUpperCase()]);
            
            if (keyCode != null) {
                keyName = keyCode;
            }
            
            if (binding && (keyName.length === 1 || !isNaN(keyName = parseInt(keyName, 10)))) {
                if (typeof binding === 'function') {
                    binding = {
                        handler: binding,
                        defaultEventAction: (bindings.defaultEventAction !== undefined)
                            ? bindings.defaultEventAction
                            : me.defaultEventAction
                    };
                }
                
                map.addBinding({
                    key: keyName,
                    ctrl: binding.ctrl,
                    shift: binding.shift,
                    alt: binding.alt,
                    handler: Ext.Function.bind(me.handleEvent, binding.scope || defaultScope,
                                               [binding.handler || binding.fn, me], true),
                    defaultEventAction: (binding.defaultEventAction !== undefined)
                        ? binding.defaultEventAction
                        : me.defaultEventAction
                });
            }
        }
    },
 
    /**
     * Method for filtering out the map argument
     * @private
     * @param {Number} keyCode 
     * @param {Ext.event.Event} event 
     * @param {Function} handler The function to call
     * @param {Ext.util.KeyNav} keyNav The owning KeyNav
     */
    handleEvent: function(keyCode, event, handler, keyNav) {
        keyNav.lastKeyEvent = event;
 
        return handler.call(this, event);
    },
 
    /**
     * Destroy this KeyNav.
     * @param {Boolean} removeEl Pass `true` to remove the element associated with this KeyNav.
     */
    destroy: function(removeEl) {
        var me = this;
        
        if (removeEl) {
            Ext.raise("removeEl argument in KeyNav destructor is not supported anymore.");
        }
        
        if (me.destroyKeyMap) {
            me.map.destroy(removeEl);
        }
        
        me.map = null;
        
        me.callParent();
    },
 
    /**
     * Enables this KeyNav.
     */
    enable: function() {
        // this.map will be removed if destroyed
        if (this.map) {
            this.map.enable();
            this.disabled = false;
        }
    },
 
    /**
     * Disables this KeyNav.
     */
    disable: function() {
        // this.map will be removed if destroyed
        if (this.map) {
            this.map.disable();
        }
        
        this.disabled = true;
    },
 
    /**
     * Convenience function for setting disabled/enabled by boolean.
     * @param {Boolean} disabled 
     */
    setDisabled: function(disabled) {
        this.map.setDisabled(disabled);
        this.disabled = disabled;
    },
    
    isEnabled: function() {
        return !this.disabled;
    },
 
    /**
     * @private
     * Determines the event to bind to listen for keys. Defaults to the {@link #eventName} value,
     * but may be overridden the {@link #forceKeyDown} setting.
     *
     * @return {String} The type of event to listen for.
     */
    getKeyEvent: function(forceKeyDown, configuredEventName) {
        if (forceKeyDown || (Ext.supports.SpecialKeyDownRepeat && !configuredEventName)) {
            return 'keydown';
        }
        else {
            return configuredEventName || this.eventName;
        }
    }
});