/*
 * InputMask and NumberMask and DateMask version 1.1
 *
 * The InputMask, NumberMask and DateMask are scripts used to restrict the user's 
 * input on HTML controls.
 *
 * The InputMask script supports any kind of format with a mask that is composed
 * by an array of fields. Each field may be an input of an arbitrary set of possible 
 * characters (may also be uppercase / lowercase input) or a string literal.
 * There are some limitations on the masks, since HTML controls do not supply
 * information about the current cursor position in the text box. As a work-arround,
 * always after an keyboard event, the keyboard cursor will be at the end of the 
 * text.
 *
 * The NumberMask is an specialized mask for decimal number input, and the DateMask,
 * for date input. They both rely on a parser to work. On the NumberMask, the input 
 * is filled from right to left, filling first the decimal part, then the integer part.
 * The DateMask uses the InputMask, automatically creating the fields from its mask.
 *
 * Dependencies: 
 *  - JavaScriptUtil.js
 *  - Parsers.js (for NumberMask and DateMask)
 *
 * Author: Luis Fernando Planella Gonzalez (lfpg_dev@pop.com.br)
 * Home Page: http://javascriptools.sourceforge.net

 * You may freely distribute this file, since you include this header 
 * along with the script
 */

///////////////////////////////////////////////////////////////////////////////
//Temporary variables for the masks
numbers = new Input(JST_CHARS_NUMBERS);
optionalNumbers = new Input(JST_CHARS_NUMBERS);
optionalNumbers.optional = true;
oneToTwoNumbers = new Input(JST_CHARS_NUMBERS, 1, 2);
year = new Input(JST_CHARS_NUMBERS, 1, 4, getFullYear);
dateSep = new Literal("/");
dateTimeSep = new Literal(" ");
timeSep = new Literal(":");

/*
 * Constants
 */
var JST_VALIDATE_ON_BLUR = true;
var JST_DEFAULT_ALLOW_NEGATIVE = true;

/*
 * Some prebuilt masks
 */
var JST_MASK_NUMBERS       = [numbers];
var JST_MASK_DECIMAL       = [numbers, new Literal("."), optionalNumbers];
var JST_MASK_UPPER         = [new Upper(JST_CHARS_LETTERS)];
var JST_MASK_LOWER         = [new Lower(JST_CHARS_LETTERS)];
var JST_MASK_CAPITALIZE    = [new Capitalize(JST_CHARS_LETTERS)];
var JST_MASK_LETTERS       = [new Input(JST_CHARS_LETTERS)];
var JST_MASK_ALPHA         = [new Input(JST_CHARS_ALPHA)];
var JST_MASK_ALPHA_UPPER   = [new Upper(JST_CHARS_ALPHA)];
var JST_MASK_ALPHA_LOWER   = [new Lower(JST_CHARS_ALPHA)];
var JST_MASK_DATE          = [oneToTwoNumbers, dateSep, oneToTwoNumbers, dateSep, year];
var JST_MASK_DATE_TIME     = [oneToTwoNumbers, dateSep, oneToTwoNumbers, dateSep, year, dateTimeSep, oneToTwoNumbers, timeSep, oneToTwoNumbers];
var JST_MASK_DATE_TIME_SEC = [oneToTwoNumbers, dateSep, oneToTwoNumbers, dateSep, year, dateTimeSep, oneToTwoNumbers, timeSep, oneToTwoNumbers, timeSep, oneToTwoNumbers];

//Clear the temporary variables
delete numbers;
delete optionalNumbers;
delete oneToTwoNumbers;
delete year;
delete dateSep;
delete dateTimeSep;
delete timeSep;

///////////////////////////////////////////////////////////////////////////////
/*
 * This is the main InputMask class.
 * Parameters: 
 *    fields: The mask fields
 *    control: The reference to the control that is being masked
 *    keyPressFunction: The additional function instance used on the keyPress event
 *    keyDownFunction: The additional function instance used on the keyDown event
 *    keyUpFunction: The additional function instance used on the keyUp event
 *    blurFunction: The additional function instance used on the blur event
 */
function InputMask(fields, control, keyPressFunction, keyDownFunction, keyUpFunction, blurFunction) {
    
    this.fields = fields;
    this.fieldValues = null;

    //Validate the control
    if (!isValidControlToMask(control)) {
        alert("Invalid control to mask");
        return;
    } else {
        this.control = control;
    }
    
    //Set the control's reference to the mask descriptor
    this.control.mask = this;
    this.control.pad = false;
    this.control.keyDownFlag = false;
    this.control.ignore = false;

    //Set the function calls
	this.keyDownFunction = keyDownFunction || null;
	this.keyPressFunction = keyPressFunction || null;
	this.keyUpFunction = keyUpFunction || null;
	this.blurFunction = blurFunction || null;

    //The onKeyDown event will detect special keys
    function onKeyDown (event) {
        if (window.event) {
            event = window.event;
        }
        if (this.keyDownFlag) {
            return false;
        }
        this.pad = false;
        this.ignore = false;
        
        var keyCode = typedCode(event);
/*        if (keyCode == 32) {
            return false;
        } else */if (keyCode <= 46) {
            if (event.ctrlKey && (keyCode == 13 || keyCode == 39)) {
                this.pad = true;
            } else {
                this.ignore = true;
            }
            return true;
        }

        this.keyDownFlag = true;

        //Check for extra function on keydown
        if (this.mask.keyDownFunction != null) {
            var ret = this.mask.keyDownFunction(event, this.mask);
            if (ret == false) {
                return false;
            }
        }
        return true;
    }
    this.control.onkeydown = onKeyDown;
    
    //The KeyUp event will apply the mask
	function onKeyUp (event) {
        if (window.event) {
            event = window.event;
        }
        //Check if it's not an ignored key
        this.keyDownFlag = false;
        if (this.ignore != true) {
            applyMask(this.mask);
        }
        //Check for extra function on keydown
        if (this.mask.keyUpFunction != null) {
            var ret = this.mask.keyUpFunction(event, this.mask);
            if (ret == false) {
                return false;
            }
        }

        return true;
    }
    this.control.onkeyup = onKeyUp;
    
    //The Blur event will apply the mask again, to ensure the user will not paste an invalid value
    if (JST_VALIDATE_ON_BLUR) {
    	function onBlur (event) {
            if (window.event) {
                event = window.event;
            }
            applyMask(this.mask);
            
            //Check for extra function on keydown
            if (this.mask.blurFunction != null) {
                this.mask.blurFunction(event, this.mask);
            }
    
            return true;
        }
        this.control.onblur = onBlur;
    }
    
	if (keyPressFunction != null) {
	    this.control.onkeypress = keyPressFunction;
	}
    
    //Method to determine if the mask is all complete
    this.isComplete = function() {

        //Ensures the field values will be parsed
        applyMask(this);

        //Check if there is some value
        if (this.fieldValues == null && ((this.fields != null) || (this.fields.length > 0))) {
            return false;
        }

        //Check for completed values
        for (var i = 0; i < this.fields.length; i++) {
            var field = this.fields[i];
            if (field.input && !field.isComplete(this.fieldValues[i]) && !field.optional) {
                return false;
            }
        }
        return true;
    }
}

///////////////////////////////////////////////////////////////////////////////
/*
 * This is the main NumberMask class.
 * Parameters: 
 *    parser: The NumberParser instance used by the mask
 *    control: The reference to the control that is being masked
 *    maxIntegerDigits: The limit for integer digits (excluding separators). 
 *                      Default: -1 (no limit)
 *    allowNegative: Should negative values be permitted? Default: see the 
 *                   value of the JST_DEFAULT_ALLOW_NEGATIVE constant.
 *    keyPressFunction: The additional function instance used on the keyPress event
 *    keyDownFunction: The additional function instance used on the keyDown event
 *    keyUpFunction: The additional function instance used on the keyUp event
 *    blurFunction: The additional function instance used on the blur event
 */
function NumberMask(parser, control, maxIntegerDigits, allowNegative, keyPressFunction, keyDownFunction, keyUpFunction, blurFunction) {
    //Validate the parser
    if (!isInstance(parser, NumberParser)) {
        alert("Illegal NumberParser instance");
        return;
    }
    if (parser.decimalDigits < 0) {
        alert("A NumberParser with unlimited decimal digits is not supported on NumberMask");
        return;
    }
    this.parser = parser;
    
    //Validate the control
    if (!isValidControlToMask(control)) {
        alert("Invalid control to mask");
        return;
    } else {
        this.control = control;
    }

    //Get the additional properties
    this.maxIntegerDigits = maxIntegerDigits || -1;
    this.allowNegative = allowNegative || JST_DEFAULT_ALLOW_NEGATIVE;

    //Set the control's reference to the mask descriptor
    this.control.mask = this;
    this.control.keyDownFlag = false;
    this.control.ignore = false;
    this.control.swapSign = false;
    this.control.oldValue = this.control.value;
    
    //The onKeyDown event will detect special keys
    function onKeyDown (event) {
        if (window.event) {
            event = window.event;
        }
        if (this.keyDownFlag) {
            return false;
        }
        this.pad = false;
        this.ignore = false;
        
        var keyCode = typedCode(event);
        if (keyCode == 32) {
            return false;
        } else if (keyCode <= 46) {
            this.ignore = true;
            return true;
        }

        this.keyDownFlag = true;

        //Check for extra function on keydown
        if (this.mask.keyDownFunction != null) {
            var ret = this.mask.keyDownFunction(event, this.mask);
            if (ret == false) {
                return false;
            }
        }
        
        //Store the old value
        this.oldValue = this.value;
        
        return true;
    }
    this.control.onkeydown = onKeyDown;
    
    //The KeyUp event will apply the mask
	function onKeyUp (event) {
        if (window.event) {
            event = window.event;
        }
        //Check if it's not an ignored key
        this.keyDownFlag = false;
        if (this.ignore != true) {
            applyNumberMask(this.mask);
        }
        //Check for extra function on keyup
        if (this.mask.keyUpFunction != null) {
            var ret = this.mask.keyUpFunction(event, this.mask);
            if (ret == false) {
                return false;
            }
        }

        return true;
    }
    this.control.onkeyup = onKeyUp;

    //The KeyPress event will filter the keys
	function onKeyPress (event) {
        if (window.event) {
            event = window.event;
        }

        //Check for extra function on keypress
        if (this.mask.keyPressFunction != null) {
            var ret = this.mask.keyPressFunction(event, this.mask);
            if (ret == false) {
                return false;
            }
        }
        if (this.ignore) {
            return true;
        } else {
            //Check for the minus sign
            if (String.fromCharCode(typedCode(event)) == '-') {
                if (this.mask.allowNegative) {
                    this.swapSign = true;
                }
                return false;
            }
            this.swapSign = false;
            return onlyNumbers(String.fromCharCode(typedCode(event)));
        }
	}
    this.control.onkeypress = onKeyPress;
    
    //The Blur event will apply the mask again, to ensure the user will not paste an invalid value
	function onBlur (event) {
        if (window.event) {
            event = window.event;
        }
        applyNumberMask(this.mask);
        
        //Check for extra function on keydown
        if (this.mask.blurFunction != null) {
            this.mask.blurFunction(event, this.mask);
        }

        return true;
    }
    this.control.onblur = onBlur;
    
    //Method to determine if the mask is all complete
    this.isComplete = function() {
        return this.control.value != "";
    }
}

///////////////////////////////////////////////////////////////////////////////
/*
 * This is the main DateMask class.
 * Parameters: 
 *    parser: The DateParser instance used by the mask
 *    control: The reference to the control that is being masked
 *    validate: Validate the control on the onblur event? Default: true
 *    validationMessage: Message alerted on validation on fail. The {0} placeholder may
 *                   be used as a substituition for the field value, and {0} for the
 *                   mask. Default: ""
 *    keyPressFunction: The additional function instance used on the keyPress event
 *    keyDownFunction: The additional function instance used on the keyDown event
 *    keyUpFunction: The additional function instance used on the keyUp event
 *    blurFunction: The additional function instance used on the blur event
 */
function DateMask(parser, control, validate, validationMessage, keyPressFunction, keyDownFunction, keyUpFunction, blurFunction) {
    
    //Validate the parser
    if (!isInstance(parser, DateParser)) {
        alert("Illegal DateParser instance");
        return;
    }
    this.parser = parser;
    
    //Add some functionality to the original onblur event
    this.extraBlurFunction = blurFunction || null;
    function maskBlurFunction (event, dateMask) {
        var control = dateMask.control;
        if (dateMask.validate) {
            if (!dateMask.parser.isValid(control.value)) {
                var msg = dateMask.validationMessage;
                if (!isEmpty(msg)) {
                    msg = replaceAll(msg, "{0}", control.value);
                    msg = replaceAll(msg, "{1}", dateMask.parser.mask);
                    alert(msg);
                }
                control.value = "";
                control.focus();
            }
        }
        if (dateMask.extraBlurFunction != null) {
            return dateMask.extraBlurFunction(event, dateMask);
        }
        return true;
    }
    
    //Build the fields array
    var fields = new Array();
    var old = '';
    var mask = this.parser.mask;
    while (mask.length > 0) {
        var field = mask.charAt(0);
        var size = 1;
        var maxSize = -1;
        var padFunction = null;
        while (mask.charAt(size) == field) {
            size++;
        }
        mask = mid(mask, size);
        switch (field) {
            case 'd': case 'M': case 'h': case 'H': case 'm': case 's': 
                maxSize = 2;
                break;
            case 'y':
                padFunction = getFullYear;
                if (size == 2) {
                    maxSize = 2;
                } else {
                    maxSize = 4;
                }
                break;
            case 'S':
                maxSize = 3;
                break;
        }
        var input;
        if (maxSize == -1) {
            input = new Literal(field);
        } else {
            input = new Input(JST_CHARS_NUMBERS, size, maxSize);
            input.padFunction = padFunction;
        }
        fields[fields.length] = input;
    }
    
    //Initialize the superclass
    this.base = InputMask;
    this.base(fields, control, keyPressFunction, keyDownFunction, keyUpFunction, maskBlurFunction);
    
    //Store the additional variables
    this.validate = validate == null ? true : booleanValue(validate);
    this.validationMessage = validationMessage || "";
    this.control.dateMask = this;
}

///////////////////////////////////////////////////////////////////////////////
/*
 * This class represents a mask type
 */
function MaskDescriptor(fields) {
    if (fields instanceof Array) {
	    this.fields = fields;
    } else {
        this.fields = null;
	}
}

//Function to return the typed key code
function typedCode(event) {
    var asc = 0;
    if (event.keyCode) {
        asc = event.keyCode;
    } else if (event.which) {
        asc = event.which;
    }
    return asc;
}

//Function to determine if a given object is a valid control to mask
function isValidControlToMask(control) {
    if (control == null) {
        return false;
    } else if (!(control.type) || (!inArray(control.type, ["text", "textarea", "password"]))) {
        return false;
    } else {
        return true;
    }
}

//Function to handle the mask format
function applyMask(mask) {
    var fields = mask.fields;

    //Return if there are fields to process
    if ((fields == null) || (fields.length == 0)) {
        return;
    }

    var control = mask.control;
    var out = "";
    var fieldValues = [];
    
    //Ensures the values are the same size as the fields
    for (var i = 0; i < fields.length; i++) {
        fieldValues[i] = "";
    }
        
    //Return if there are fields to process
    var currentField = null;
    var currentFieldIndex = -1;
    var value = control.value;
    var fixedPositionLiterals = [];
    
    //Remove all literals
    for (var i = 0; i < fields.length; i++) {
        var field = fields[i];
        if (field.literal) {
            if (i > 0) {
                //Check for this field's fixed position (if any)
                if (fields[i - 1].max == -1) {
                    var descriptor = {};
                    descriptor.field = field;
                    descriptor.position = value.indexOf(field.text);
                    if (descriptor.position >= 0) {
                        fixedPositionLiterals[fixedPositionLiterals.length] = descriptor;
                    }
                }
            }
            value = replaceAll(value, field.text, "");
        }
    }
    
    //Build the output
    var pos = 0;
    for (var i = 0; (i < fields.length) && (pos < value.length); i++) {
        var field = fields[i];
        if (field.literal) {
            var fixed = false;
            for (var j = 0; j < fixedPositionLiterals.length; j++) {
                var descriptor = fixedPositionLiterals[j];
                if (descriptor.field.text == field.text) {
                    fixed = true;
                    break;
                }
            }
            if (!fixed) {
                out += field.text;
            }
        } else {
            var upTo = field.upTo(value, pos);

            //Not accepted
            if (upTo == -1) {
                break;
            } else {
                var fieldValue = field.transformValue(value.substring(pos, upTo + 1));
                if (control.pad) {
                    fieldValue = field.pad(fieldValue);
                }
                fieldValues[i] = fieldValue;
                out += fieldValue;
                pos = upTo + 1;
                if (!field.isComplete(fieldValue)) {
                    break;
                }
            }
        }
    }

    //Insert for this field's fixed position (if any)
    for (var i = 0; i < fixedPositionLiterals.length; i++) {
        var descriptor = fixedPositionLiterals[i];
        out = insertString(out, descriptor.position, descriptor.field.text);
    }

    //Completes the value
    if (control.maxLength > 0) {
        out = left(out, control.maxLength);
    }
    mask.fieldValues = fieldValues;
    control.value = out;
}

//Function to handle the number mask format
function applyNumberMask(numberMask) {
    var control = numberMask.control;
    var parser = numberMask.parser;
    var maxIntegerDigits = numberMask.maxIntegerDigits;
    var swapSign = false;
    if (control.swapSign == true) {
        swapSign = true;
        control.swapSign = false;
    }
    var value = control.value;
    value = replaceAll(value, parser.groupSeparator, '');
    value = replaceAll(value, parser.decimalSeparator, '');
    var value = parser.parse(value);
    if (isNaN(value)) {
        control.value = "";
        return true;
    }
    var isNegative = (value < 0)
    if (swapSign) {
        isNegative = !isNegative;
    }
    value = Math.abs(value) / Math.pow(10, parser.decimalDigits)
    
    var intPart = Math.floor(value)
    var decPart = value - intPart;
    if (maxIntegerDigits >= 0 && String(intPart).length > maxIntegerDigits) {
        control.value = control.oldValue;
        return false;
    }
    intPart = parseInt(intPart, 10);
    value = parser.round(intPart + decPart);
    if (isNegative) {
        value = -value;
    }
    control.value = parser.format(value);
    return false;
}

///////////////////////////////////////////////////////////////////////////////
// Field Type Classes

/*
 * General input field type
 */
function FieldType() {
    this.literal = false;
    this.input = false;
}

/*
 * Literal field type
 */
function Literal(text) {
    this.base = FieldType;
    this.base();
    this.text = text;
    this.literal = true;
    
    //Return if the character is in the text
    this.isAccepted = function(chr) {
        return onlySpecified(chr, this.text);
    }
}

/*
 * User input field type
 */
function Input(accepted, min, max, padFunction, optional) {
    this.base = FieldType;
    this.base();
    this.accepted = accepted;
    this.min = min || 1;
    this.max = max || -1;
    this.padFunction = padFunction || null;
    this.input = true;
    this.upper = false;
    this.lower = false;
    this.capitalize = false;
    this.optional = booleanValue(optional);

    //Ensures the min/max consistencies
    if (this.min < 1) {
        this.min = 1;
    }
    if (this.max == 0) {
        this.max = -1;
    }
    if ((this.min > this.max) && (this.max > 0)) {
        this.min = this.max;
    }
    
    //Returns the index up to where the texts matches this input
    this.upTo = function(text, fromIndex) {
        text = text || "";
        fromIndex = fromIndex || 0;
        if (text.length < fromIndex) {
            return -1;
        }
        var toIndex = -1;
        for (var i = fromIndex; i < text.length; i++) {
            if (this.isAccepted(text.substring(fromIndex, i + 1))) {
                toIndex = i;
            } else {
                break;
            }
        }
        return toIndex;
    }
    
    //Return if the text is accepted
    this.isAccepted = function(text) {
        return onlySpecified(text, this.accepted) && ((text.length <= this.max) || (this.max < 0));
    }

    //Return if the text length is ok
    this.checkLength = function(text) {
        return (text.length >= this.min) && ((this.max < 0) || (text.length <= this.max));
    }
    
    //Return if the text is complete
    this.isComplete = function(text) {
        text = String(text);
        if (text.length < this.min) {
            return false;
        }
        return (this.max < 0) || (text.length == this.max);
    }

    //Apply the case transformations when necessary
    this.transformValue = function(text) {
        text = String(text);
        if (this.upper) {
            return text.toUpperCase();
        } else if (this.lower) {
            return text.toLowerCase();
        } else if (this.capitalize) {
            if (text.length > 0) {
                return text.charAt(0).toUpperCase() + ((text.length > 1) ? mid(text, 1).toLowerCase() : "");
            }
        } else {
            return text;
        }
    }
    
    //Pads the text
    this.pad = function(text) {
        text = String(text);
        if (!this.checkLength(text)) {
            var value;
            if (this.padFunction != null) {
                value = this.padFunction(text, this.min, this.max);
            } else {
                value = text;
            }
            //Enforces padding
            if (this.max > 0) {
                var padChar = ' ';
                if (this.accepted.indexOf(' ') > 0) {
                    padChar = ' ';
                } else if (this.accepted.indexOf('0') > 0) {
                    padChar = '0';
                } else {
                    padChar = this.accepted.charAt(0);
                }
                return left(lpad(value, this.max, padChar), this.max);
            } else {
                //If has no max limit
                return value;
            }
        } else {
            return text;
        }
    }
}

/*
 * Lowercased input field type
 */
function Lower(accepted, min, max, padFunction) {
    this.base = Input;
    this.base(accepted, min, max, padFunction);
    this.lower = true;
}

/*
 * Uppercased input field type
 */
function Upper(accepted, min, max, padFunction) {
    this.base = Input;
    this.base(accepted, min, max, padFunction);
    this.upper = true;
}

/*
 * Capitalized input field type
 */
function Capitalize(accepted, min, max, padFunction) {
    this.base = Input;
    this.base(accepted, min, max, padFunction);
    this.capitalize = true;
}

