/*
 * Parsers version 1.0
 *
 * This is a set of parsers used to parse / format different types of data
 *
 * IMPORTANT: In order to use this script, the page must have the JavaScriptUtil
 * script already included
 *
 * Author: Luis Fernando Planella Gonzalez (lfpg_dev@terra.com.br)
 */

/*****************************************************************************/

///////////////////////////////////////////////////////////////////////////////
/*
 * NumberParser
 *
 * The NumberParser is a class used for parsing, formatting and rounding numbers
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class. All parameters are optional.
 * Parameters: 
 *    decimalDigits: The number of decimal digits. Defaults to -1 (infinite)
 *    decimalSeparator: The decimal separator. Defaults to ","
 *    groupSeparator: The separator between thousands group. If empty, will not be used
 *    currencySymbol: The currency symbol. Defaults to "R$"
 *    negativeParenthesis: Use parenthesis (true) or "-" (false - default) for negative values?
 */
function NumberParser(decimalDigits, decimalSeparator, groupSeparator, useGrouping, currencySymbol, useCurrency, negativeParenthesis) {
    this.decimalDigits = decimalDigits || -1;
    this.decimalSeparator = decimalSeparator || ",";
    this.groupSeparator = groupSeparator || ".";
    this.useGrouping = booleanValue(useGrouping);
    this.currencySymbol = currencySymbol || "R$";
    this.useCurrency = booleanValue(useCurrency);
    this.negativeParenthesis = booleanValue(negativeParenthesis);
    
    /*
     * Parses the String
     */
    this.parse = function(string) {
        string = trim(string);
        
	    if( countChar(string, this.groupSeparator) > 1 )
    		return null;
    		
		if( countChar(string, this.decimalSeparator) > 1 )    		
			return null;
        
        string = replaceAll(string, this.groupSeparator, "");
        string = replaceAll(string, this.decimalSeparator, ".");
        string = replaceAll(string, this.currencySymbol, "");
        var isNegative = (string.indexOf("(") >= 0) || (string.indexOf("-") >= 0);
        string = replaceAll(string, "(", "");
        string = replaceAll(string, ")", "");
        string = replaceAll(string, "-", "");
        string = trim(string);
        //Check the valid characters
        if (!onlySpecified(string, JST_CHARS_NUMBERS + ".")) {
            return null;
        }
        var ret = parseFloat(string);
        ret = isNegative ? (ret * -1) : ret;
        return this.round(ret);
    }
    
	/*
     * Rounds the number to the precision
     */
	this.round = function(number) {
    
        //Checks the trivial cases
        if (this.decimalDigits < 0) {
            return number;
        } else if (this.decimalDigits == 0) {
            return Math.round(number);
        }
        
        var mult = Math.pow(10, this.decimalDigits);
    
		return Math.round(number * mult) / mult;
	}
    
    /*
     * Formats the number
     */
    this.format = function(number) {
        //Check if the number is a String
        if (isNaN(number)) {
            number = this.parse(number);
        }
		if (isNaN(number)) return null;
		
        var isNegative = number < 0;
        number = Math.abs(number);
		var ret = "";
        var parts = String(this.round(number)).split(".");
        var intPart = parts[0];
        var decPart = parts.length > 1 ? parts[1] : "";
        
        //Checks if there's thousand separator
        if ((this.useGrouping) && (!isEmpty(this.groupSeparator))) {
			var group, temp = "";
			for (i = intPart.length; i > 0; i -= 3)
			{
				group = intPart.substring(intPart.length - 3);
				intPart = intPart.substring(0, intPart.length - 3);
				temp = group + this.groupSeparator + temp;
			}
			intPart = temp.substring(0, temp.length-1);
        }
        
        //Starts building the return value
        ret = intPart;
        
        //Checks if there's decimal digits
        if (this.decimalDigits != 0) {
            if (this.decimalDigits > 0) {
                while (decPart.length < this.decimalDigits) {
                    decPart += "0";
                }
            }
            if (!isEmpty(decPart)) {
                ret += this.decimalSeparator + decPart;
            }
        }
        
        //Checks the negative number
        if (isNegative) {
            if (this.negativeParenthesis) {
                ret = "(" + ret + ")";
            }  else {
                ret = "-" + ret;
            }
        }
        
        //Checks the currency symbol
        if (this.useCurrency) {
            ret = this.currencySymbol + " " + ret;
        }
		return ret;
    }
    
    /*
     * Returns if the String represents a valid number
     */
    this.isValid = function(string) {
        return this.parse(string) != null;
    }
}

/*****************************************************************************/

///////////////////////////////////////////////////////////////////////////////
/*
 * DateParser
 *
 * The DateParser is a class used for parsing and formatting dates
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class
 * Parameters:
 *     mask: The parse mask. Accepts the following characters:
 *        d: Day         M: month        y: year
 *        h: 12 hour     H: 24 hour      m: minute
 *        s: second      S: millisecond
 *        a: am / pm     A: AM / PM
 *        /. -: Separators
 *        The default is "dd/MM/yyyy"
 */
function DateParser(mask) {
    this.mask = mask || "dd/MM/yyyy";
    this.numberParser = new NumberParser(0);
    this.compiledMask = new Array();
    
    //Types of fields
    var LITERAL     =  0;
    var MILLISECOND =  1;
    var SECOND      =  2;
    var MINUTE      =  3;
    var HOUR_12     =  4;
    var HOUR_24     =  5;
    var DAY         =  6;
    var MONTH       =  7;
    var YEAR        =  8;
    var AM_PM_UPPER =  9;
    var AM_PM_LOWER = 10;
    
    //Indexes in the part array
    var MILLISECOND_IDX = 0;
    var SECOND_IDX      = 1;
    var MINUTE_IDX      = 2;
    var HOUR_IDX        = 3;
    var DAY_IDX         = 4;
    var MONTH_IDX       = 5;
    var YEAR_IDX        = 6;
    
    /*
     * The parse function
     */
    this.parse = function(string) {
        if (isEmpty(string) || string.length < 4) {
            return null;
        }
        string = trim(String(string)).toUpperCase();
        
        //Checks PM entries
        var pm = string.indexOf("PM") != -1;
        string = replaceAll(replaceAll(string, "AM", ""), "PM", "");
        
        //Get each field value
        var parts = [0, 0, 0, 0, 0, 0, 0];
        var entries = [null, null, null, null, null, null, null];
        for (var i = 0; i < this.compiledMask.length; i++) {
            var entry = this.compiledMask[i];
            
            var pos = this.getTypeIndex(entry.type);
            
            //Check if is a literal or not
            if (pos == -1) {
                //Check if it's AM/PM or a literal
                if (entry.type == LITERAL) {
                    //Is a literal: skip it
                    string = string.substr(entry.length);
                } else {
                    //It's a AM/PM. All have been already cleared...
                }
            } else {
                var partValue = 0;
                if (i == (this.compiledMask.length - 1)) {
                    partValue = string;
                    string = "";
                } else {
                    var nextEntry = this.compiledMask[i + 1];
                    
                    //Check if the next part is a literal
                    if (nextEntry.type == LITERAL) {
                        var nextPos = string.indexOf(nextEntry.literal);

                        //Check if next literal is missing
                        if (nextPos == -1) {
                            //Probably the last part on the String
                            partValue = string
                            string = "";
                        } else {
                            //Get the value of the part from the string and cuts it
                            partValue = left(string, nextPos);
                            string = string.substr(nextPos);
                        }
                    } else {
                        //Get the value of the part from the string and cuts it
                        partValue = string.substring(0, entry.length);
                        string = string.substr(entry.length);
                    }
                }
                //Validate the part value and store it
                if (!onlyNumbers(partValue)) {
                    return null;
                }
                var a = partValue
                partValue = this.numberParser.parse(partValue);
                parts[pos] = partValue;
                entries[pos] = entry;
            }
        }

        //If there's something else on the String, it's an error!
        if (!isEmpty(string)) {
            return null;
        }
        
        //If was PM, add 12 hours
        if (pm && (parts[HOUR_IDX] < 12)) {
            parts[HOUR_IDX] += 12;
        }
        //The month is from 0 to 11
        if (parts[MONTH_IDX] > 0) {
            parts[MONTH_IDX]--;
        }
        //Check for 2-digit year
        if (parts[YEAR_IDX] < 100) {
            if (parts[YEAR_IDX] < 50) {
                parts[YEAR_IDX] += 2000;
            } else {
                parts[YEAR_IDX] += 1900;
            }
        }
        
        //Validates the parts
        for (var i = 0; i < parts.length; i++) {
            var entry = entries[i]
            var part = parts[i];
            if (part < 0 || ((entry != null) && (part > this.maxValue(parts, entry.type)))) {
                //invalid!!!
                return null;
            }
        }        

        //Builds the return
        return new Date(parts[YEAR_IDX], parts[MONTH_IDX], parts[DAY_IDX], parts[HOUR_IDX], 
            parts[MINUTE_IDX], parts[SECOND_IDX], parts[MILLISECOND_IDX]);
    }
    
    /*
     * Formats the specified date
     */
    this.format = function(date) {
        if (!(date instanceof Date)) {
            date = this.parse(date);
        }
        if (date == null) {
            return "";
        }
        var ret = "";
        var parts = [date.getMilliseconds(), date.getSeconds(), date.getMinutes(), date.getHours(), date.getDate(), date.getMonth(), date.getFullYear()];

        //Iterate through the compiled mask
        for (var i = 0; i < this.compiledMask.length; i++) {
            var entry = this.compiledMask[i];
            switch (entry.type) {
                case LITERAL: 
                    ret += entry.literal;
                    break;
                case AM_PM_LOWER:
                    ret += (parts[HOUR_IDX] < 12) ? "am" : "pm";
                    break;
                case AM_PM_UPPER:
                    ret += (parts[HOUR_IDX] < 12) ? "AM" : "PM";
                    break;
                case MILLISECOND:
                case SECOND:
                case MINUTE:
                case HOUR_24:
                case DAY:
                    ret += lpad(parts[this.getTypeIndex(entry.type)], entry.length, "0");
                    break;
                case HOUR_12:
                    ret += lpad(parts[HOUR_IDX] % 12, entry.length, "0");
                    break;
                case MONTH:
                    ret += lpad(parts[MONTH_IDX] + 1, entry.length, "0");
                    break;
                case YEAR:
                    ret += lpad(right(parts[YEAR_IDX], entry.length), entry.length, "0");
                    break;
            }
        }
        
        //Return the value
        return ret;
    }
    
    /*
     * Returns if the String represents a valid date
     */
    this.isValid = function(string) {
        return isEmpty(string) || this.parse(string) != null;
    }
    
    /*
     * Returns the maximum value of the field
     */
    this.maxValue = function(parts, type) {
        switch (type) {
            case MILLISECOND: return 999;
            case SECOND: return 59;
            case MINUTE: return 59;
            case HOUR_12: case HOUR_24: return 23; //Internal value: both 23
            case DAY: return this.maxDay(parts[MONTH_IDX], parts[YEAR_IDX]);
            case MONTH: return 11;
            case YEAR: return 2999;
            default: return 0;
        }
    }
    
    /*
     * Returns the maximum value for the day, given the month and year
     */
    this.maxDay = function(month, year) {
        month = new Number(month) + 1;
        switch (month) {
            case 1: case 3: case 5: case 7:
            case 8: case 10: case 12:
                return 31;
    	    case 4: case 6: case 9: case 11:
    			return 30;
    		case 2:
    			if ((year % 4) == 0) {
    				return 29;
    			} else {
    				return 28;
    			}
            default:
                return 0;
    	}
    }
    
    /*
     * Returns the field's type
     */
    this.getFieldType = function(field) {
        switch (field.charAt(0)) {
            case "S": return MILLISECOND;
            case "s": return SECOND;
            case "m": return MINUTE;
            case "h": return HOUR_12;
            case "H": return HOUR_24;
            case "d": return DAY;
            case "M": return MONTH;
            case "y": return YEAR;
            case "a": return AM_PM_LOWER;
            case "A": return AM_PM_UPPER;
            default: return LITERAL;
        }
    }
    
    /*
     * Returns the type's index in the field array
     */
    this.getTypeIndex = function(type) {
        switch (type) {
            case MILLISECOND: return MILLISECOND_IDX;
            case SECOND: return SECOND_IDX;
            case MINUTE: return MINUTE_IDX;
            case HOUR_12: case HOUR_24: return HOUR_IDX;
            case DAY: return DAY_IDX;
            case MONTH: return MONTH_IDX;
            case YEAR: return YEAR_IDX;
            default: return -1;
        }
    }
    
    /*
     * Class containing information about a field
     */
    var Entry = function(type, length, literal) {
        this.type = type;
        this.length = length || -1;
        this.literal = literal;
    }
    
    /*
     * Compiles the mask
     */
    this.compile = function() {
        var current = "";
        var old = "";
        var part = "";
        this.compiledMask = new Array();
        for (var i = 0; i < this.mask.length; i++) {
            current = this.mask.charAt(i);
            
            //Checks if still in the same field
            if ((part == "") || (current == part.charAt(0))) {
                part += current;
            } else {
                //Field changed: use the field
                var type = this.getFieldType(part);
                
                //Store the mask entry
                this.compiledMask[this.compiledMask.length] = new Entry(type, part.length, part);

                //Handles the field changing
                part = "";
                i--;
            }
        }
        //Checks if there's another unparsed part
        if (part != "") {
            var type = this.getFieldType(part);
            
            //Store the mask entry
            this.compiledMask[this.compiledMask.length] = new Entry(type, part.length, part);
        }
    }
    
    /*
     * Changes the format mask
     */
    this.setMask = function(mask) {
        this.mask = mask;
        this.compile();
    }
    
    //Initially set the mask
    this.setMask(this.mask);
}

///////////////////////////////////////////////////////////////////////////////
/*
 * StringParser
 *
 * The StringParser is a convenience parser to use the same interface as other parsers
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class
 */
function StringParser() {

    /*
     * Parses the String
     */
    this.parse = function(string) {
        return String(string);
    }
    
    /*
     * Formats the String
     */
    this.format = function(string) {
		return string;
    }
}

///////////////////////////////////////////////////////////////////////////////
/*
 * BooleanParser
 *
 * The BooleanParser is parser that converts from boolean to String values
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class
 */
function BooleanParser(trueValue, falseValue) {

    this.trueValue = trueValue || "true";
    this.falseValue = falseValue || "true";

    /*
     * Parses the String
     */
    this.parse = function(string) {
        return booleanValue(string);
    }
    
    /*
     * Formats the boolean
     */
    this.format = function(bool) {
		return booleanValue(bool) ? this.trueValue : this.falseValue;
    }
}

///////////////////////////////////////////////////////////////////////////////
/*
 * MapParser
 *
 * The MapParser maps between String values from objects.
 * To define the values, an object is user, where the properties are the internal
 * values and the property values are the formatted value. 
 * ie: var parser = new MapParser(map)
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class
 * Parameters:
 *     values: The Map with the values
 *     directParse: If set to true, the parse will not map, interpreting the internal value
 */
function MapParser(values, directParse) {
    this.values = values || new Map();
    this.directParse = (directParse == null ? false : booleanValue(directParse));
    
    /*
     * Parses the value
     */
    this.parse = function(value) {
        if (directParse) {
            return value;
        }
        var keys = (values instanceof Map) ? values.getKeys() : new Array();
        for (var k = 0; k < keys.length; k++) {
            var key = keys[k];
            if (value = values.get(key)) {
                return key;
            }
        }
    }
    
    /*
     * Formats the value
     */
    this.format = function(value) {
		return this.values.get(value);
    }
}

///////////////////////////////////////////////////////////////////////////////
/*
 * EscapeParser
 *
 * The EscapeParser formats / parses values using the escapeCharacters()
 * and unescapeCharacters() functions respectively
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class
 */
function EscapeParser() {
    
    /*
     * Parses the value
     */
    this.parse = function(value) {
        if (value == null) {
            return null;
        }
        return unescapeCharacters(String(value));
    }
    
    /*
     * Formats the value
     */
    this.format = function(value) {
        if (value == null) {
            return null;
        }
        return escapeCharacters(String(value));
    }
}

///////////////////////////////////////////////////////////////////////////////
/*
 * CustomParser
 *
 * The CustomParser formats / parses values using custom functions
 * Parameters:
 *     formatFunction: The function that will format the data
 *     parseFunction: The function that will parse the data
 */
///////////////////////////////////////////////////////////////////////////////

/*
 * This is the main class
 */
function CustomParser(formatFunction, parseFunction) {
    
    this.formatFunction = formatFunction || function(value) { return value; };
    this.parseFunction = parseFunction || function(value) { return value; };
    
    /*
     * Parses the value
     */
    this.parse = function(value) {
        return parseFunction(value);
    }
    
    /*
     * Formats the value
     */
    this.format = function(value) {
        return formatFunction(value);
    }
}