/*
 * Copyright (c) 2007 Josh Bush (digitalbush.com)
 * 
 * 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. 
 */

/*
 * Version: 1.0
 * Release: 2007-07-25
 */
(function ($) {
    //Helper Functions for Caret positioning
    function getCaretPosition(ctl) {
        var res = {begin: 0, end: 0};
        if (ctl.setSelectionRange) {
            res.begin = ctl.selectionStart;
            res.end = ctl.selectionEnd;
        } else if (document.selection && document.selection.createRange) {
            var range = document.selection.createRange();
            res.begin = 0 - range.duplicate().moveStart('character', -100000);
            res.end = res.begin + range.text.length;
        }
        return res;
    };

    function setCaretPosition(ctl, pos) {
        if (ctl.setSelectionRange) {
            ctl.focus();
            ctl.setSelectionRange(pos, pos);
        } else if (ctl.createTextRange) {
            var range = ctl.createTextRange();
            range.collapse(true);
            range.moveEnd('character', pos);
            range.moveStart('character', pos);
            range.select();
        }
    };

    //Predefined character definitions
    var charMap = {
        '9': "[0-9]",
        'a': "[A-Za-z]",
        '*': "[A-Za-z0-9]"
    };

    //Helper method to inject character definitions
    $.mask = {
        addPlaceholder: function (c, r) {
            charMap[c] = r;
        }
    };

    //Main Method
    $.fn.mask = function (mask, settings) {
        settings = $.extend({
            placeholder: "_",
            completed: null
        }, settings);

        //Build Regex for format validation
        var reString = "^";
        for (var i = 0; i < mask.length; i++)
            reString += (charMap[mask.charAt(i)] || ("\\" + mask.charAt(i)));
        reString += "$";
        var re = new RegExp(reString);

        return this.each(function () {
            var input = $(this);
            var buffer = new Array(mask.length);
            var locked = new Array(mask.length);

            //Build buffer layout from mask
            for (var i = 0; i < mask.length; i++) {
                locked[i] = charMap[mask.charAt(i)] == null;
                buffer[i] = locked[i] ? mask.charAt(i) : settings.placeholder;
            }

            /*Event Bindings*/
            input.focus(function () {
                checkVal();
                writeBuffer();
                setCaretPosition(this, 0);
            });

            input.blur(checkVal);

            //Paste events for IE and Mozilla thanks to Kristinn Sigmundsson
            if ($.browser.msie)
                this.onpaste = function () {
                    setTimeout(checkVal, 0);
                };
            else if ($.browser.mozilla)
                this.addEventListener('input', checkVal, false);

            var ignore = false;  //Variable for ignoring control keys

            input.keydown(function (e) {
                var pos = getCaretPosition(this);
                var k = e.keyCode;
                ignore = (k < 16 || (k > 16 && k < 32 ) || (k > 32 && k < 41));

                //delete selection before proceeding
                if ((pos.begin - pos.end) != 0 && (!ignore || k == 8 || k == 46)) {
                    clearBuffer(pos.begin, pos.end);
                }
                //backspace and delete get special treatment
                if (k == 8) {//backspace
                    while (pos.begin-- >= 0) {
                        if (!locked[pos.begin]) {
                            buffer[pos.begin] = settings.placeholder;
                            if ($.browser.opera) {
                                //Opera won't let you cancel the backspace, so we'll let it backspace over a dummy character.
                                writeBuffer(pos.begin);
                                setCaretPosition(this, pos.begin + 1);
                            } else {
                                writeBuffer();
                                setCaretPosition(this, pos.begin);
                            }
                            return false;
                        }
                    }
                } else if (k == 46) {//delete
                    clearBuffer(pos.begin, pos.begin + 1);
                    writeBuffer();
                    setCaretPosition(this, pos.begin);
                    return false;
                } else if (k == 27) {
                    clearBuffer(0, mask.length);
                    writeBuffer();
                    setCaretPosition(this, 0);
                    return false;
                }

            });

            input.keypress(function (e) {
                if (ignore) {
                    ignore = false;
                    return;
                }
                e = e || window.event;
                var k = e.charCode || e.keyCode || e.which;

                var pos = getCaretPosition(this);
                var caretPos = pos.begin;

                if (e.ctrlKey || e.altKey) {//Ignore
                    return true;
                } else if ((k >= 41 && k <= 122) || k == 32 || k > 186) {//typeable characters
                    while (pos.begin < mask.length) {
                        var reString = charMap[mask.charAt(pos.begin)];
                        var match;
                        if (reString) {
                            var reChar = new RegExp(reString);
                            match = String.fromCharCode(k).match(reChar);
                        } else {//we're on a mask char, go forward and try again
                            pos.begin += 1;
                            pos.end = pos.begin;
                            caretPos += 1;
                            continue;
                        }

                        if (match)
                            buffer[pos.begin] = String.fromCharCode(k);
                        else
                            return false;//reject char

                        while (++caretPos < mask.length) {//seek forward to next typable position
                            if (!locked[caretPos])
                                break;
                        }
                        break;
                    }
                } else
                    return false;

                writeBuffer();
                if (settings.completed && caretPos >= buffer.length)
                    settings.completed.call(input);
                else
                    setCaretPosition(this, caretPos);

                return false;
            });

            /*Helper Methods*/
            function clearBuffer(start, end) {
                for (var i = start; i < end; i++) {
                    if (!locked[i])
                        buffer[i] = settings.placeholder;
                }
            };

            function writeBuffer(pos) {
                var s = "";
                for (var i = 0; i < mask.length; i++) {
                    s += buffer[i];
                    if (i == pos)
                        s += settings.placeholder;
                }
                input.val(s);
                return s;
            };

            function checkVal() {
                //try to place charcters where they belong
                var test = input.val();
                var pos = 0;
                for (var i = 0; i < mask.length; i++) {
                    if (!locked[i]) {
                        while (pos++ < test.length) {
                            //Regex Test each char here.
                            var reChar = new RegExp(charMap[mask.charAt(i)]);
                            if (test.charAt(pos - 1).match(reChar)) {
                                buffer[i] = test.charAt(pos - 1);
                                break;
                            }
                        }
                    }
                }
                var s = writeBuffer();
                if (!s.match(re)) {
                    input.val("");
                    clearBuffer(0, mask.length);
                }
            };
        });
    };
})(jQuery);