/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

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

// If you're developing a new keymapping and want to get an idea what's going
// on, then enable debugging.
    var DEBUG = false;

    function StateHandler(keymapping) {
        this.keymapping = this.$buildKeymappingRegex(keymapping);
    }

    StateHandler.prototype = {
        /*
         * Build the RegExp from the keymapping as RegExp can't stored directly
         * in the metadata JSON and as the RegExp used to match the keys/buffer
         * need to be adapted.
         */
        $buildKeymappingRegex: function (keymapping) {
            for (var state in keymapping) {
                this.$buildBindingsRegex(keymapping[state]);
            }
            return keymapping;
        },

        $buildBindingsRegex: function (bindings) {
            // Escape a given Regex string.
            bindings.forEach(function (binding) {
                if (binding.key) {
                    binding.key = new RegExp('^' + binding.key + '$');
                } else if (Array.isArray(binding.regex)) {
                    if (!('key' in binding))
                        binding.key = new RegExp('^' + binding.regex[1] + '$');
                    binding.regex = new RegExp(binding.regex.join('') + '$');
                } else if (binding.regex) {
                    binding.regex = new RegExp(binding.regex + '$');
                }
            });
        },

        $composeBuffer: function (data, hashId, key, e) {
            // Initialize the data object.
            if (data.state == null || data.buffer == null) {
                data.state = "start";
                data.buffer = "";
            }

            var keyArray = [];
            if (hashId & 1) keyArray.push("ctrl");
            if (hashId & 8) keyArray.push("command");
            if (hashId & 2) keyArray.push("option");
            if (hashId & 4) keyArray.push("shift");
            if (key) keyArray.push(key);

            var symbolicName = keyArray.join("-");
            var bufferToUse = data.buffer + symbolicName;

            // Don't add the symbolic name to the key buffer if the alt_ key is
            // part of the symbolic name. If it starts with alt_, this means
            // that the user hit an alt keycombo and there will be a single,
            // new character detected after this event, which then will be
            // added to the buffer (e.g. alt_j will result in ∆).
            //
            // We test for 2 and not for & 2 as we only want to exclude the case where
            // the option key is pressed alone.
            if (hashId != 2) {
                data.buffer = bufferToUse;
            }

            var bufferObj = {
                bufferToUse: bufferToUse,
                symbolicName: symbolicName
            };

            if (e) {
                bufferObj.keyIdentifier = e.keyIdentifier;
            }

            return bufferObj;
        },

        $find: function (data, buffer, symbolicName, hashId, key, keyIdentifier) {
            // Holds the command to execute and the args if a command matched.
            var result = {};

            // Loop over all the bindings of the keymap until a match is found.
            this.keymapping[data.state].some(function (binding) {
                var match;

                // Check if the key matches.
                if (binding.key && !binding.key.test(symbolicName)) {
                    return false;
                }

                // Check if the regex matches.
                if (binding.regex && !(match = binding.regex.exec(buffer))) {
                    return false;
                }

                // Check if the match function matches.
                if (binding.match && !binding.match(buffer, hashId, key, symbolicName, keyIdentifier)) {
                    return false;
                }

                // Check for disallowed matches.
                if (binding.disallowMatches) {
                    for (var i = 0; i < binding.disallowMatches.length; i++) {
                        if (!!match[binding.disallowMatches[i]]) {
                            return false;
                        }
                    }
                }

                // If there is a command to execute, then figure out the
                // command and the arguments.
                if (binding.exec) {
                    result.command = binding.exec;

                    // Build the arguments.
                    if (binding.params) {
                        var value;
                        result.args = {};
                        binding.params.forEach(function (param) {
                            if (param.match != null && match != null) {
                                value = match[param.match] || param.defaultValue;
                            } else {
                                value = param.defaultValue;
                            }

                            if (param.type === 'number') {
                                value = parseInt(value);
                            }

                            result.args[param.name] = value;
                        });
                    }
                    data.buffer = "";
                }

                // Handle the 'then' property.
                if (binding.then) {
                    data.state = binding.then;
                    data.buffer = "";
                }

                // If no command is set, then execute the "null" fake command.
                if (result.command == null) {
                    result.command = "null";
                }

                if (DEBUG) {
                    console.log("KeyboardStateMapper#find", binding);
                }
                return true;
            });

            if (result.command) {
                return result;
            } else {
                data.buffer = "";
                return false;
            }
        },

        /*
         * This function is called by keyBinding.
         */
        handleKeyboard: function (data, hashId, key, keyCode, e) {
            if (hashId == -1)
                hashId = 0
            // If we pressed any command key but no other key, then ignore the input.
            // Otherwise "shift-" is added to the buffer, and later on "shift-g"
            // which results in "shift-shift-g" which doesn't make sense.
            if (hashId != 0 && (key == "" || key == String.fromCharCode(0))) {
                return null;
            }

            // Compute the current value of the keyboard input buffer.
            var r = this.$composeBuffer(data, hashId, key, e);
            var buffer = r.bufferToUse;
            var symbolicName = r.symbolicName;
            var keyId = r.keyIdentifier;

            r = this.$find(data, buffer, symbolicName, hashId, key, keyId);
            if (DEBUG) {
                console.log("KeyboardStateMapper#match", buffer, symbolicName, r);
            }

            return r;
        }
    }

    /*
     * This is a useful matching function and therefore is defined here so that
     * users of KeyboardStateMapper can use it.
     *
     * @return {Boolean} If no command key (Command|Option|Shift|Ctrl) is pressed, it
     *          returns true. If the only the Shift key is pressed + a character
     *          true is returned as well. Otherwise, false is returned.
     *          Summing up, the function returns true whenever the user typed
     *          a normal character on the keyboard and no shortcut.
     */
    exports.matchCharacterOnly = function (buffer, hashId, key, symbolicName) {
        // If no command keys are pressed, then catch the input.
        if (hashId == 0) {
            return true;
        }
        // If only the shift key is pressed and a character key, then
        // catch that input as well.
        else if ((hashId == 4) && key.length == 1) {
            return true;
        }
        // Otherwise, we let the input got through.
        else {
            return false;
        }
    };

    exports.StateHandler = StateHandler;
});
