/**
 * This extension allows you to record a sequence using Mousetrap.
 *
 * @author Dan Tao <daniel.tao@gmail.com>
 */
(function(Mousetrap) {
    /**
     * the sequence currently being recorded
     *
     * @type {Array}
     */
    var _recordedSequence = [],

        /**
         * a callback to invoke after recording a sequence
         *
         * @type {Function|null}
         */
        _recordedSequenceCallback = null,

        /**
         * a list of all of the keys currently held down
         *
         * @type {Array}
         */
        _currentRecordedKeys = [],

        /**
         * temporary state where we remember if we've already captured a
         * character key in the current combo
         *
         * @type {boolean}
         */
        _recordedCharacterKey = false,

        /**
         * a handle for the timer of the current recording
         *
         * @type {null|number}
         */
        _recordTimer = null,

        /**
         * the original handleKey method to override when Mousetrap.record() is
         * called
         *
         * @type {Function}
         */
        _origHandleKey = Mousetrap.handleKey;

    /**
     * handles a character key event
     *
     * @param {string} character
     * @param {Array} modifiers
     * @param {Event} e
     * @returns void
     */
    function _handleKey(character, modifiers, e) {
        // remember this character if we're currently recording a sequence
        if (e.type == 'keydown') {
            if (character.length === 1 && _recordedCharacterKey) {
                _recordCurrentCombo();
            }

            for (i = 0; i < modifiers.length; ++i) {
                _recordKey(modifiers[i]);
            }
            _recordKey(character);

        // once a key is released, all keys that were held down at the time
        // count as a keypress
        } else if (e.type == 'keyup' && _currentRecordedKeys.length > 0) {
            _recordCurrentCombo();
        }
    }

    /**
     * marks a character key as held down while recording a sequence
     *
     * @param {string} key
     * @returns void
     */
    function _recordKey(key) {
        var i;

        // one-off implementation of Array.indexOf, since IE6-9 don't support it
        for (i = 0; i < _currentRecordedKeys.length; ++i) {
            if (_currentRecordedKeys[i] === key) {
                return;
            }
        }

        _currentRecordedKeys.push(key);

        if (key.length === 1) {
            _recordedCharacterKey = true;
        }
    }

    /**
     * marks whatever key combination that's been recorded so far as finished
     * and gets ready for the next combo
     *
     * @returns void
     */
    function _recordCurrentCombo() {
        _recordedSequence.push(_currentRecordedKeys);
        _currentRecordedKeys = [];
        _recordedCharacterKey = false;
        _restartRecordTimer();
    }

    /**
     * ensures each combo in a sequence is in a predictable order and formats
     * key combos to be '+'-delimited
     *
     * modifies the sequence in-place
     *
     * @param {Array} sequence
     * @returns void
     */
    function _normalizeSequence(sequence) {
        var i;

        for (i = 0; i < sequence.length; ++i) {
            sequence[i].sort(function(x, y) {
                // modifier keys always come first, in alphabetical order
                if (x.length > 1 && y.length === 1) {
                    return -1;
                } else if (x.length === 1 && y.length > 1) {
                    return 1;
                }

                // character keys come next (list should contain no duplicates,
                // so no need for equality check)
                return x > y ? 1 : -1;
            });

            sequence[i] = sequence[i].join('+');
        }
    }

    /**
     * finishes the current recording, passes the recorded sequence to the stored
     * callback, and sets Mousetrap.handleKey back to its original function
     *
     * @returns void
     */
    function _finishRecording() {
        if (_recordedSequenceCallback) {
            _normalizeSequence(_recordedSequence);
            _recordedSequenceCallback(_recordedSequence);
        }

        // reset all recorded state
        _recordedSequence = [];
        _recordedSequenceCallback = null;
        _currentRecordedKeys = [];

        Mousetrap.handleKey = _origHandleKey;
    }

    /**
     * called to set a 1 second timeout on the current recording
     *
     * this is so after each key press in the sequence the recording will wait for
     * 1 more second before executing the callback
     *
     * @returns void
     */
    function _restartRecordTimer() {
        clearTimeout(_recordTimer);
        _recordTimer = setTimeout(_finishRecording, 1000);
    }

    /**
     * records the next sequence and passes it to a callback once it's
     * completed
     *
     * @param {Function} callback
     * @returns void
     */
    Mousetrap.record = function(callback) {
        Mousetrap.handleKey = _handleKey;
        _recordedSequenceCallback = callback;
    };

})(Mousetrap);
