﻿/**
* Removes leading and trailing spaces from string
*/
String.prototype.trim = function () {
    return this.replace(/&nbsp;/g, " ").replace(/(^[ \t]*)|([ \t]*$)/g, "");
};

/************Array extensions************/
/**
*	Removes array element from specified location
*/
Array.prototype.removeAt = function (index) {
    if (Number(index) < 0 || this.length == 0)
        return false;
    for (var i = index; i < this.length; i++) {
        this[i] = this[i + 1];
    }
    this.length--;
};
/**
*	Removes specified element(s) from array
*/
Array.prototype.remove = function (value) {
    for (var i = 0; i < this.length; i++)
        if (this[i] == value) {
            this.removeAt(i);
            i--;
        }
};
/**
*	Inserts new value at specified location
*/
Array.prototype.insertAt = function (value, index) {
    if (typeof index == 'undefined' || index == this.length) this.push(value);
    else if (index == 0) this.unshift(value);
    else {
        var i = this.length;
        while (i > index) {
            this[i] = this[i - 1];
            i--;
        }
        this[index] = value;
    }
};
/**
*	Moves array element from one position (fromIndex) to another(toIndex)
*/
Array.prototype.moveTo = function (fromIndex, toIndex) {
    if (fromIndex == toIndex) return;
    var temp = this[fromIndex];
    this.removeAt(fromIndex);
    this.insertAt(temp, toIndex);
};
/**
*	Calculates number of elements that satisfy specified condition
*/
Array.prototype.countBy = function (testFunction) {
    var count = 0;
    for (var i = 0; i < this.length; i++)
        if (testFunction(this[i], i)) count++;
    return count;
};

/**
* Creates new collection of function results received after applying argument function on each element of source collection
* @param fSelector function to apply on each element of source collection
* @return collection of function results received after applying argument function on each element of source collection
*/
Array.prototype.select = function (fSelector) {
    var result = new Array();
    for (var i = 0; i < this.length; i++)
        result.push(fSelector(this[i], i));
    return result;
};

/**
* Creates new collection of elements of source collection that satisfy argument function
* @param fFilter filter function to test elements of source collection by
* @return collection of elements of source collection that satisfy argument function
*/
Array.prototype.where = function (fFilter) {
    var result = new Array();
    for (var i = 0; i < this.length; i++)
        if (fFilter(this[i], i))
            result.push(this[i]);
    return result;
};

/**
* Applies function given to each array element. Source collection will be modified.
* @param {Function} modifier Function to apply to array elements
* @return {Array} reference to array modified
*/
Array.prototype.each = function (modifier) {
    for (var i = 0; i < this.length; i++) {
        modifier(this[i]);
    }
    return this;
};

/**
* Gets first element of collection
* @return first element of collection
*/
Array.prototype.first = function () {
    return this[0];
};

/**
* Gets last element of collection
* @return last element of collection
*/
Array.prototype.last = function () {
    return this[this.length - 1];
};

/**
* Finds location of value in source collection
* @param value Value to find in collection
* @param {function} {optional} compare Function to compare source value to array elements
* @return -1 if element was not found, or its index otherwise
*/
Array.prototype.indexOf = function (value, compare) {
    if (!compare) {
        compare = function (v1, v2) {
            return v1 == v2;
        };
    }
    var i = this.length;
    while (i--) {
        if (compare(this[i], value)) {
            return i;
        }
    }
    return -1;
};

/**
* Converts simple indexed array to associative array
* @return {Object} that represents hashtable where keys and values are the same
*/
Array.prototype.toHashTable = function () {
    var result = {};
    for (var i = 0; i < this.length; i++) {
        result[this[i]] = this[i];
    }
    return result;
};

/**
* Checks if element is in collection
* @param value Element to check
* @param {function} compare Function to compare source value to array elements
* @return True if element was found
*/
Array.prototype.contains = function (value, compare) {
    return this.indexOf(value, compare) != -1;
};

/**
* Checks if any of elements provided is in collection
* @param values Elements to check
* @param {function} compare Function to compare source value to array elements
* @return True if element was found
*/
Array.prototype.containsAny = function (values, compare) {
    if (!values) {
        return false;
    }
    for (var i = 0; i < values.length; i++) {
        if (this.contains(values[i], compare))
            return true;
    }
    return false;
};

/**
* Summarizes all numbers found in collection
* @return Sum of all numeric values in array
*/
Array.prototype.sum = function () {
    var sum = 0;
    for (var i = 0; i < this.length; i++) {
        var val = parseFloat(this[i]);
        if (val != Number.NaN)
            sum += val;
    }
    return sum;
};

/**
* Returns new array that contains only unique values from the first one
* @param {Function} {optional} compare Function of 2 arguments to compare array values
* @return Array of unique values from the source array
*/
Array.prototype.distinct = function (compare) {
    if (!compare) {
        compare = function (v1, v2) {
            return v1 === v2;
        };
    }
    var result = [];
    var i;
    for (i = 0; i < this.length; i++) {
        if (!result.contains(this[i], compare)) {
            result.push(this[i]);
        }
    }
    return result;
};

/**
*	Sorts elements by one or more property names that each collection element is expected to have
*	E.g.:
*	var arr = new Array(.....);
*	arr.sortByMany(['stopTime']);
*	//or
*	arr.sortByMany(['stopTime ASC']);
*	//or
*	arr.sortByMany(['stopTime DESC']);
*	//or
*	arr.sortByMany(['startTime ASC,finishTime ASC,registerDate DESC']);
*/
Array.prototype.sortByMany = function (arrFields, arrComparers) {
    var comparers = new Array();
    for (var i = 0; i < arrFields.length; i++) {
        var desc = arrFields[i].toUpperCase().indexOf(' DESC') != -1;
        var sortField = arrFields[i].replace(/( DESC)|( ASC)$/ig, '');
        var comparerName = arrComparers[i];

        //TODO Optimization candidate 2
        //var funcStr = "if (v1."+sortField+"==v2."+sortField+") return 0;else if (v1."+sortField+" < v2."+sortField+") return " + (desc?'':'-')+ "1;else return " + (desc?'-':'')+ "1;";
        var funcStr = "return " + (desc ? "-1*" : "") + "window.Utils.Compare(v1." + sortField + ", v2." + sortField + ", '" + comparerName + "');";
        comparers[i] = new Function("v1,v2", funcStr);
    }
    var comparer = function (v1, v2) {
        for (var i = 0; i < comparers.length; i++) {
            var result = comparers[i](v1, v2);
            if (result != 0) return result;
        }
        return 0;
    };
    return this.sort(comparer);
};

/**
* Filters collection of objects by filterExpression given
* @param filterExpression string or object to search in objects collection
* @param arrCheckForProperties array of properties values of which will be checked while filtering collection
* @param funcDataObjectExtractor optional. Function to format element before applying filter rules
* @return new collection of elements that satisfy given criteria
*/
Array.prototype.filter = function (filterExpression, arrCheckForProperties, funcDataObjectExtractor) {
    if (filterExpression == null || filterExpression == undefined)
        return this;
    else {
        var result = new Array();
        for (var i = 0; i < this.length; i++)
            for (var j = 0; j < arrCheckForProperties.length; j++) {
                var dataObj = (funcDataObjectExtractor ? funcDataObjectExtractor(this[i]) : this[i]);
                var val = dataObj[arrCheckForProperties[j]];
                if (!(val == null || val == undefined || (val != filterExpression && val.toString().toLowerCase().indexOf(filterExpression.toString().toLowerCase()) == -1))) {
                    result.push(this[i]);
                    break;
                }
            }
        return result;
    }
};

Array.prototype.removeEmptyItems = function () {
    return this.where(function (value) {
        return value !== null && value !== undefined && value !== "";
    });
};


String.prototype.htmlEscape = function () {
    return this.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
};

/**
* Sets cookie value
* @param name Cookie name
* @param value Cookie value
* @param expires Expiration time (days)
* @param path
* @param domain
* @param secure
*/
function setCookie(name, value, expires, path, domain, secure) {
    var today = new Date();
    today.setTime(today.getTime());
    if (expires) { expires = expires * 1000 * 60 * 60 * 24; }
    var expires_date = new Date(today.getTime() + (expires));
    document.cookie = name + "=" + escape(value) +
		((expires) ? ";expires=" + expires_date.toGMTString() : "") +
		((path) ? ";path=" + path : "") +
		((domain) ? ";domain=" + domain : "") +
		((secure) ? ";secure" : "");
}

/**
* Gets cookie value
* @param name Cookie name to retrieve
* @return cookie value or null
*/
function getCookie(name) {
    var prefix = name + "=";
    var cookieStartIndex = document.cookie.indexOf(prefix);
    if (cookieStartIndex == -1) {
        return null;
    }
    var cookieEndIndex = document.cookie.indexOf(";", cookieStartIndex + prefix.length);
    if (cookieEndIndex == -1) {
        cookieEndIndex = document.cookie.length;
    }
    return unescape(document.cookie.substring(cookieStartIndex + prefix.length, cookieEndIndex));
}

/**
* Removes cookie by name
* @param name Cookie name
* @param path
* @param domain
* @return undefined
*/
function deleteCookie(name, path, domain) {
    if (getCookie(name)) {
        setCookie(name, '', -10000, path, domain, null);
    }
}

var DatesAPI;
var GroupsAPI;
var ConverterAPI;

// The set of functions for dates. InitDates to make it working
function DatesAPIObject() {
    var self = this;
    // make start and end date connected to avoid incorrect date interval.
    this.initDates = function (startDateID, endDateID) {
        $('#' + endDateID).datepicker({ duration: 0, dateFormat: ConverterAPI.options.dateFormat, beforeShow: function (input) { self._backupData("end", input.value); } });
        $('#' + startDateID).datepicker({ duration: 0, dateFormat: ConverterAPI.options.dateFormat, beforeShow: function (input) { self._backupData("start", input.value); } });
        this.startDateID = startDateID;
        this.endDateID = endDateID;
        this.insertDate(startDateID, _previousDateStart);
        this.insertDate(endDateID, _previousDateEnd);
        $('#' + startDateID).change(function () { self.fixDateAfterChange(this, "start"); });
        $('#' + endDateID).change(function () { self.fixDateAfterChange(this, "end"); });
        /*
        $('#'+startDateID).bind(
        'dpClosed',
        function(e, selectedDates){
        var d = selectedDates[0];
        if (d) {
        d = new Date(d);
        $('#'+endDateID).dpSetStartDate(d.addDays(1).asString());
        }
        }
        );
        $('#'+endDateID).bind(
        'dpClosed',
        function(e, selectedDates){
        var d = selectedDates[0];
        if (d) {
        d = new Date(d);
        $('#'+startDateID).dpSetEndDate(d.addDays(-1).asString());
        }
        }
        );
        */
    };
    this.initDatesQuickSelection = function (id, startDateID, endDateID) {
        $('#' + id).change(function (o) {
            var sDate = new Date();
            var eDate = new Date();
            sDate.setHours("0");
            sDate.setMinutes("0");
            eDate.setHours("23");
            eDate.setMinutes("59");
            sDate.setSeconds("0");
            eDate.setSeconds("0");
            switch (this.value) {
                case "today":
                    break;
                case "yesterday":
                    sDate.addDays(-1);
                    eDate.addDays(-1);
                    break;
                case "thisweek":
                    var d = sDate.getDay();
                    sDate.addDays(-d);
                    eDate.addDays(6 - d);
                    break;
                case "lastweek":
                    var d = sDate.getDay();
                    sDate.addDays(-d - 7);
                    eDate.addDays(6 - d - 7);
                    break;
                case "currentmonth":
                    sDate.setDate(1);
                    eDate.setDate(1);

                    eDate.addMonths(1);
                    eDate.setHours("0");
                    eDate.setMinutes("0");
                    eDate.addMinutes(-1);
                    break;
                case "lastmonth":
                    sDate.addMonths(-1);
                    eDate.addMonths(-1);
                    sDate.setDate(1);
                    eDate.setDate(1);

                    eDate.addMonths(1);
                    eDate.setHours("0");
                    eDate.setMinutes("0");
                    eDate.addMinutes(-1);
                    break;
            }
            self.insertDate(startDateID, sDate);
            self.insertDate(endDateID, eDate);
        });
    };
    //////////////////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////////
    _previousDateStart = new Date();
    _previousDateEnd = new Date();
    _previousDateStart.setSeconds(0);
    _previousDateStart.setMinutes(0);
    _previousDateStart.setHours(0);
    _previousDateEnd.setSeconds(0);
    _previousDateEnd.setMinutes(59);
    _previousDateEnd.setHours(23);
    _numberFormat = function (n) {
        if (n.toString().length == 1) return "0" + n;
        return n;
    };
    this._backupData = function (type, v) {
        if (type == "start")
            _previousDateStart = ConverterAPI.stringToDate(v, ConverterAPI.options.dateFormat + " HH:nn");
        if (type == "end")
            _previousDateEnd = ConverterAPI.stringToDate(v, ConverterAPI.options.dateFormat + " HH:nn");
    };
    this.fixDateAfterChange = function (node, dateType) {
        var v = node.value + (node.value.indexOf(":") > -1 ? "" : (dateType == "end" ? " 23:59" : " 00:00"));
        var dtm = ConverterAPI.stringToDate(v, ConverterAPI.options.dateFormat + " HH:nn");
        if (v == ConverterAPI.dateTimeFormatSmall(dtm)) {
            _previousDate = dtm;
        }
        this.insertDate(node.id, _previousDate);
    };
    this.insertDate = function (id, date) {
        $('#' + id).val(ConverterAPI.dateTimeFormatSmall(date));
    };
    _getDateById = function (id) {
        var v = $('#' + id).val();
        var date = ConverterAPI.stringToDate(v);
        return date;
    };
    this.getStartDate = function () {
        return _getDateById(this.startDateID);
    };
    this.getEndDate = function () {
        return _getDateById(this.endDateID);
    };
    this.getStartDateValue = function () {
        return $('#' + this.startDateID).val();
    };
    this.getEndDateValue = function () {
        return $('#' + this.endDateID).val();
    };
}
DatesAPI = new DatesAPIObject();


///////////////////////////////////////////////////////
// Some converter fucntioanlity
function ConverterAPIObject() {
    var self = this;
    this.options = {
        isMetric: true, // {km,mile}
        distanceRound: "0.01",
        dateFormat: "dd M yy",
        timeFormat: "hh:nn pp", //"HH:nn:ss",
        timeFormatSmall: "HH:nn",
        jsonDateFormat: "yy-mm-ddTHH:nn:ssZ"
    };

    // Init options
    this.initOptions = function (options) {
        for (var i in options) {
            this.options[i] = options[i];
        }
    };
    this._zeroPad = function (v) {
        return v < 10 ? '0' + v : v;
    };
    this._round = function (v) {
        var r = 1 / this.options.distanceRound;
        v = Math.round(v * r) / r;
        return v;
    };
    this._getAMPMHours = function (h) {
        if (h > 12) h -= 12;
        return h;
    };
    this._getAMPM = function (h) {
        var res = (h > 12) ? "PM" : "AM";
        return res;
    };


    // convert string to minutes
    this.strToMinutes = function (v) {
        var res = Math.round(this.strToSeconds(v) / 60);
        return res;
    };
    // convert string to seconds
    this.strToSeconds = function (v) {
        var s = v.split(":");
        var res = Math.round(s[2] * 1 + s[1] * 60 + s[0] * 3600);
        return res;
    };

    // get datetimeFormat for the grid sort.
    this.dateTimeFormatForGrid = function () {
        var str = this.options.dateFormat + " " + this.options.timeFormat;
        str = str.replace(/[p]+/gi, "");
        str = str.replace(/n/gi, "i");
        str = str.replace(/([ymdhis]+)/gi, function ($1) { return $1.substr(0, 1); });
        return str.toLowerCase();
    };
    this.timeFormatForGrid = function () {
        var str = this.options.timeFormat;
        str = str.replace(/[p]+/gi, "");
        str = str.replace(/n/gi, "i");
        str = str.replace(/([his]+)/gi, function ($1) { return $1.substr(0, 1); });
        return str.toLowerCase();
    };

    /**
    * Converts internal string representation of duration into UI duration string + surrounds result with DIV tags with proper set title
    * @param value - duration to convert(string)
    * @return formatted duration
    * @type string
    */
    this.durationToDecoratedString = function (value) {
        var result = "<div title='" + value + "'>";
        if (!value || value == '' || value == '00:00:00')
            result += "<span style='display:none;'>00:00:00</span>&nbsp;";
        else {
            result += this.durationToPlainString(value);
        }
        return result + "</div>";
    };

    /**
    * Converts internal distance value into UI distance string + surrounds result with DIV tags with proper set title
    * @param value - distance to convert
    * @return string
    */
    this.distanceToDecoratedString = function (value) {
        if (value === undefined || value === null) {
            value = 0;
        }

        var result = "<div title='" + value + "'>";

        if (value != 0 && value < 1) {
            value = 1;
        } else {
            value = Math.round(value);
        }
        result += value;
        return result + "</div>";
    };
    /**
    * Converts internal string representation of duration into UI duration string
    * @param value - duration to convert(string)
    * @return formatted duration
    * @type string
    */
    this.durationToPlainString = function (value) {
        var result = "";
        if (!value || value == '' || value == '00:00:00')
            result = "&nbsp;";
        else {
            var parser = /((\d*)[.])?(\d*):(\d*):(\d*)/;
            parser.exec(value);
            var days = RegExp.$2 == "" ? "0" : RegExp.$2;
            var hours = RegExp.$3 == "" ? "0" : RegExp.$3;
            var minutes = RegExp.$4 == "" ? "0" : RegExp.$4;
            var seconds = RegExp.$5 == "" ? "0" : RegExp.$5;

            days = parseInt(days.replace(/^0*/ig, ''));
            hours = parseInt(hours.replace(/^0*/ig, ''));
            minutes = parseInt(minutes.replace(/^0*/ig, ''));
            seconds = parseInt(seconds.replace(/^0*/ig, ''));

            if (days) result += days + "d ";
            if (hours) result += hours + "h ";
            if (minutes) result += minutes + "m ";
            if (!days && !hours && !minutes && seconds) result += "<1m";
        }
        return result;
    };

    // Date functions.
    this.dateFormat = function (d, format, isUTC) {
        var getFullMonth = function (month) {
            switch (month) {
                case 0: return "Jan";
                case 1: return "Feb";
                case 2: return "Mar";
                case 3: return "Apr";
                case 4: return "May";
                case 5: return "Jun";
                case 6: return "Jul";
                case 7: return "Aug";
                case 8: return "Sep";
                case 9: return "Oct";
                case 10: return "Nov";
                case 11: return "Dec";
            }
        }
        var str = format || this.options.dateFormat;
        str = str
			.split('yy').join(isUTC ? d.getUTCFullYear() : d.getFullYear())
			.split('y').join(((isUTC ? d.getUTCFullYear() : d.getFullYear()) + '').substring(2))
			.split('M').join(getFullMonth(isUTC ? d.getUTCMonth() : d.getMonth()))
			.split('mm').join(this._zeroPad((isUTC ? d.getUTCMonth() : d.getMonth()) + 1))
			.split('m').join((isUTC ? d.getUTCMonth() : d.getMonth()) + 1)
			.split('dd').join(this._zeroPad(isUTC ? d.getUTCDate() : d.getDate()))
			.split('d').join(isUTC ? d.getUTCDate() : d.getDate());
        return str;
    };
    this.timeFormat = function (d, format, isUTC) {
        var str = format || this.options.timeFormat;
        str = str
			.split('HH').join(this._zeroPad(isUTC ? d.getUTCHours() : d.getHours()))
			.split('hh').join(this._zeroPad(this._getAMPMHours(isUTC ? d.getUTCHours() : d.getHours())))
			.split('pp').join(this._zeroPad(this._getAMPM(isUTC ? d.getUTCHours() : d.getHours())))
			.split('nn').join(this._zeroPad(isUTC ? d.getUTCMinutes() : d.getMinutes()))
			.split('ss').join(this._zeroPad(isUTC ? d.getUTCSeconds() : d.getSeconds()));
        return str;
    };
    //date time format that is created as a result of concatenating of date and time formats
    this.dateTimeFormat = function (d) {
        return this.dateFormat(d) + " " + this.timeFormat(d);
    };
    // date time format in small format( may be without seconds)
    this.dateTimeFormatSmall = function (d) {
        return this.dateFormat(d) + " " + this.timeFormat(d, this.options.timeFormatSmall);
    };
    //convert string to date
    this.stringToDate = function (str, format, isUTC) {
        var f = format || (this.options.dateFormat + " " + this.options.timeFormat);
        var d = new Date('01/01/1977');
        var arr = str.split(/[\\\/\.\:\-\, TZ]/g);
        var arrF = f.split(/[\\\/\.\:\-\, TZ]/g);
        if (arr.length > arrF.length) return d;
        for (var i = 0; i < arr.length; i++) {
            switch (arrF[i]) {
                case "yy":
                    isUTC ? d.setUTCFullYear(Number(arr[i])) : d.setFullYear(Number(arr[i]));
                    break;
                case "M":
                    for (var j = 0; j < Date.abbrMonthNames.length; j++)
                        if (Date.abbrMonthNames[j].toLowerCase() == arr[i].toLowerCase()) {
                            arr[i] = j + 1;
                            break;
                        }
                case "mm":
                case "m":
                    isUTC ? d.setUTCMonth(Number(arr[i] - 1)) : d.setMonth(Number(arr[i] - 1));
                    break;
                case "dd":
                case "d":
                    isUTC ? d.setUTCDate(Number(arr[i])) : d.setDate(Number(arr[i]));
                    break;
                case "HH":
                    isUTC ? d.setUTCHours(Number(arr[i])) : d.setHours(Number(arr[i]));
                    break;
                case "hh":
                    var h = Number(arr[i]);
                    h += (str.toLowerCase().indexOf("am") > -1) ? 0 : (str.toLowerCase().indexOf("pm") > -1 ? 12 : 0);
                    isUTC ? d.setUTCHours(h) : d.setHours(h);
                    break;
                case "nn":
                    isUTC ? d.setUTCMinutes(Number(arr[i])) : d.setMinutes(Number(arr[i]));
                    break;
                case "ss":
                    isUTC ? d.setUTCSeconds(Number(arr[i])) : d.setSeconds(Number(arr[i]));
                    break;
            }
        }
        return d;
    };

    //JSON functions
    this.parseJSONDate = function (str) {
        //var d =  this.stringToDate(str,"yyyy-mm-ddTHH:nn:ssZ",true)
        var arr = str.split(/[\\\/\.\:\- TZ]/gi);
        var d = new Date();
        d.setTime(Date.UTC(Number(arr[0]), Number(arr[1]) - 1, Number(arr[2]), Number(arr[3]), Number(arr[4]), Number(arr[5])));
        return d;
    };
    this.parseJSONTime = function (str) {
        var d = str.replace(/^.*T/ig, "").split(":");
        if (d[0].split(".").length == 2) d[0] = d[0].split(".")[1] + d[0].split(".")[0] * 24;
        d[2] = Math.round(parseFloat(d[2]));
        var res = this._zeroPad(d[0] * 1) + ":" + this._zeroPad(d[1] * 1) + ":" + this._zeroPad(d[2] * 1);
        return res;
    };
    this.dateToJSON = function (d) {
        return this.dateFormat(d, "yy-mm-dd", true) + "T" + this.timeFormat(d, "HH:nn:ss", true) + "Z";
    };

    //Distance functions
    this.milesToKm = function (n) {
        return this._round(n * 1.609344);
    };
    this.kmToMiles = function (n) {
        return this._round(n * 0.621371192);
    };
    this.fixDistance = function (v) {
        if (this.options.isMetric) return this._round(v);
        return this.kmToMiles(v);
    };
    this.setDistance = function (isMetric) {
        this.options.isMetric = isMetric;
    };
    this.setDateFormat = function (dateFormat) {
        this.options.dateFormat = dateFormat;
    };
}

ConverterAPI = new ConverterAPIObject();


/**
* 	Clones given object
* 	@param Object to clone
* 	@return shallow copy of given object
*/
window.cloneObject = function (prototype) {
    var clone = {};
    for (var prop in prototype)
        if (typeof prototype[prop] != 'function')
            clone[prop] = prototype[prop];
    return clone;
};

function component() {
    this.loadInto = function () {
        alert("'loadInto' method must be overwritten in descendant classes!");
    };

    this.load = function (containerId, url, callback) {
        this.loaded = false;
        var self = this;
        $("#" + containerId).load(url + "?un=" + Date.parse(new Date()), null, function () {
            self.loaded = true;
            self.initialize();
            if (callback)
                callback();
        });
    };

    this.initialize = function () {
        alert("'initialize' method must be overwritten in descendant classes!");
    };

    this.resize = function () {
        alert("'resize' method must be overwritten in descendant classes!");
    };
}

/**
* Event handling helper class.
* @return undefined
*/
function eventsCollectionObj() {
    var eventsCache = {};
    var self = this;

    /**
    * Attaches event  to event handler.
    * @param eventName event name.
    * @param handler callback that is called when the event is fired.
    * @return undefined
    */
    this.attachEvent = function (eventName, handler) {
        if (!handler) {
            throw ('Null Reference in attachEvent');
        }
        if (!eventsCache[eventName])
            eventsCache[eventName] = new Array();
        if (!eventsCache[eventName].contains(handler))
            eventsCache[eventName].push(handler);
    };

    /**
    * Replaces event handlers.
    * @param eventName event name.
    * @param handler callback that is called when the event is fired.
    * @return undefined
    */
    this.prependEvent = function (eventName, handler) {
        if (!handler) {
            throw ('Null Reference in prependEvent');
        }
        if (!eventsCache[eventName])
            eventsCache[eventName] = new Array();
        if (!eventsCache[eventName].contains(handler))
            eventsCache[eventName].insertAt(handler, 0);
    };

    /**
    * Detaches event  to event handler.
    * @param eventName event name.
    * @param handler callback that is called when the event is fired.
    * @return undefined
    */
    this.detachEvent = function (eventName, handler) {
        if (eventsCache[eventName] && eventsCache[eventName].contains(handler))
            eventsCache[eventName].remove(handler);
    };

    /**
    * Clears all event handlers of a particular kind.
    * @param eventName event name.
    * @return undefined
    */
    this.clear = function (eventName) {
        eventsCache[eventName] = [];
    };

    /**
    * Fires event handlers of a particular kind.
    * @param eventName event name.
    * @param sender the event sender.
    * @param args the event args.
    * @return undefined
    */
    this.fireEvent = function (eventName, sender, args) {
        if (self.hasEventHandler(eventName))
            for (var i = 0; i < eventsCache[eventName].length && eventsCache[eventName][i].apply(sender, (args instanceof Array ? args : (args ? [args] : []))) != false; i++);
    };

    /**
    * Checks if event handlers of a particular kind have been attached.
    * @param eventName event name.
    * @return bool
    */
    this.hasEventHandler = function (eventName) {
        var handlers = eventsCache[eventName];
        return handlers && handlers.length > 0;
    };
}

/**
* Base super-class for event handling classes
* @return undefined
*/
function iEventsContainer() {
    var self = this;
    /**
    * Collection of events and their handlers
    */
    var events = new eventsCollectionObj();

    /**
    * Fires event handlers of a particular kind.
    * @param eventName event name.
    * @param sender the event sender.
    * @param args the event args.
    * @return undefined
    */
    this.fireEvent = function (eventName, sender, args) {
        events.fireEvent(eventName, sender, args);
    };

    /**
    * Attaches handler to specified event
    * @param eventName Event name to attach handler to
    * @param handler function to be executed when event fires
    * @return undefined
    */
    this.attachEvent = function (eventName, handler) {
        events.attachEvent(eventName, handler);
    };

    /**
    * Replaces event handlers.
    * @param eventName event name.
    * @param handler callback that is called when the event is fired.
    * @return undefined
    */
    this.prependEvent = function (eventName, handler) {
        events.prependEvent(eventName, handler);
    };

    /**
    * Detaches event  to event handler.
    * @param eventName event name.
    * @param handler callback that is called when the event is fired.
    * @return undefined
    */
    this.detachEvent = function (eventName, handler) {
        events.detachEvent(eventName, handler);
    };

    /**
    * Checks if event handlers of a particular kind have been attached.
    * @param eventName event name.
    * @return bool
    */
    this.hasEventHandler = function (eventName) {
        return events.hasEventHandler(eventName);
    };
}

/**
* Set of data converters, comparers and summary values calculators
* @return Nothing
*/
function Utilities() {
    this.Converters = {
        "date": function (value) { if (!value) return '&nbsp;'; else return ConverterAPI.dateFormat(ConverterAPI.parseJSONDate(value)); },
        "time": function (value) { if (!value) return '&nbsp;'; else if (value.getHours) return ConverterAPI.timeFormat(value); else return ConverterAPI.timeFormat(ConverterAPI.parseJSONDate(value)) },
        "duration": function (value) { return ConverterAPI.durationToDecoratedString(value); },
        "distance": function (value) { return ConverterAPI.distanceToDecoratedString(ConverterAPI.fixDistance(value)); },
        "number": function (value) { return value; },
        "datetime": function (value) { if (!value) return '&nbsp;'; else return ConverterAPI.dateTimeFormat(ConverterAPI.parseJSONDate(value)); }
    };
    this.Summarizers = {
        "date": function (arrValues) {
            return null;
        },
        "time": function (arrValues) {
            return null;
        },
        "datetime": function (arrValues) {
            return null;
        },
        "duration":
				function (arrValues) {
				    var parser = /((\d*)[.])?(\d*):(\d*):(\d*)/;
				    var resultArr = [0, 0, 0, 0];
				    for (var i = 0; i < arrValues.length; i++) {
				        parser.exec(arrValues[i]);
				        resultArr[0] += (RegExp.$2 == "" ? 0 : parseInt(RegExp.$2));
				        resultArr[1] += (RegExp.$3 == "" ? 0 : parseInt(RegExp.$3));
				        resultArr[2] += (RegExp.$4 == "" ? 0 : parseInt(RegExp.$4));
				        resultArr[3] += (RegExp.$5 == "" ? 0 : parseInt(RegExp.$5));
				    }
				    resultArr[2] += Math.floor(resultArr[3] / 60);
				    resultArr[3] %= 60;

				    resultArr[1] += Math.floor(resultArr[2] / 60);
				    resultArr[2] %= 60;

				    resultArr[0] += Math.floor(resultArr[1] / 24);
				    resultArr[1] %= 24;
				    return resultArr[0] + "." + resultArr[1] + ":" + resultArr[2] + ":" + resultArr[3];
				},
        "distance":
				function (arrValues) {
				    return arrValues.sum();
				},
        "number":
				function (arrValues) {
				    return arrValues.sum();
				}
    };
    this.Comparers = {
        "date": function (v1, v2) {
            v1 = v1.replace(/T.*$/ig, 'T00:00:00Z');
            v2 = v2.replace(/T.*$/ig, 'T00:00:00Z');
            if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1;
        },
        "time": function (v1, v2) {
            v1 = v1.replace(/^.*T/ig, '');
            v2 = v2.replace(/^.*T/ig, '');
            if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1;
        },
        "datetime": function (v1, v2) {
            if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1;
        },
        "duration": function (v1, v2) { if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1; },
        "distance": function (v1, v2) { if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1; },
        "number": function (v1, v2) { if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1; },
        "default": function (v1, v2) { if (v1 == v2) return 0; else if (v1 > v2) return 1; else return -1; }
    };
    var self = this;
    this.Format = function (value, formatName) {
        if (!formatName) return ((value != undefined && value != null) ? value : '').toString();
        else if (!self.Converters[formatName]) { alert('Converter named ' + formatName + ' has not been implemented yet! Using default converter.'); return value; };
        return self.Converters[formatName](value);
    };
    this.Summarize = function (arrValues, formatName) {
        if (!formatName) return '';
        else if (!self.Summarizers[formatName]) { alert('Summarizer named ' + formatName + ' has not been implemented yet! Using default summarizer.'); return ''; };
        return self.Summarizers[formatName](arrValues);
    };
    this.Compare = function (v1, v2, formatName) {
        if (!formatName || !this.Comparers[formatName])
            return this.Comparers["default"](v1, v2);
        else
            return this.Comparers[formatName](v1, v2);

    };
}
window.Utils = new Utilities();

function TimeSpan() {
    var value = 0;
    var self = this;
    var timeComponents = [
        { name: "Seconds", rateToDefault: 1, delimiterToPrevious: "", length: 2 },
        { name: "Minutes", rateToDefault: 60, delimiterToPrevious: ":", length: 2 },
        { name: "Hours", rateToDefault: 60 * 60, delimiterToPrevious: ":", length: 2 },
        { name: "Days", rateToDefault: 24 * 60 * 60, delimiterToPrevious: ".", length: 200 }
    ];
    for (var i = 0; i < timeComponents.length; i++) {
        value += parseInt(arguments[arguments.length - 1 - i] || 0) * timeComponents[i].rateToDefault;
        (function (componentIndex) {
            self["get" + timeComponents[componentIndex].name] = function () {
                return getTimeComponent(componentIndex);
            };
            self["getAll" + timeComponents[componentIndex].name] = function () {
                return getAllTimeComponent(componentIndex);
            }
        })(i);
    }

    function getTimeComponent(componentIndex) {
        return getAllTimeComponent(componentIndex) % ((timeComponents[componentIndex + 1] ? timeComponents[componentIndex + 1].rateToDefault : 1) / timeComponents[componentIndex].rateToDefault);
    }

    function getAllTimeComponent(componentIndex) {
        return Math.floor(value / timeComponents[componentIndex].rateToDefault);
    }

    this.toString = function () {
        var result = "";
        for (var i = timeComponents.length - 1; i >= 0; i--) {
            result += getTimeComponent(i) + timeComponents[i].delimiterToPrevious;
        }
        return result;
    };
}
RegExp.escape = function (str) {
    return (str + '').replace(/([?!^$.(){}:|=[\]+\-\/\\*])/g, '\\$1');
};
