(function($) {
    var $t = $.telerik;
    var dateRegExp = /"\\\/Date\((.*?)\)\\\/"/g;
    var dateFormatTokenRegExp = /d{1,4}|M{1,4}|yy(?:yy)?|([Hhmstf])\1*|"[^"]*"|'[^']*'/g;
    
    $.extend($t, {

        grid: function(element, options) {
            this.element = element;
            this.sorted = [];

            $.extend(this, options);

            $('.t-pager .t-state-disabled', this.element)
                .live('click', $t.preventDefault);

            $('.t-pager .t-link:not(.t-state-disabled)', this.element)
                .live('mouseenter', $t.hover)
                .live('mouseleave', $t.leave);

            $('.t-pager input[type=text]', this.element)
                .live('keydown', $t.delegate(this, this.pagerKeyDown));

            if (this.sort) {
                $('.t-header .t-link', this.element)
					.live('mouseenter', $t.hover)
					.live('mouseleave', $t.leave);
            }

            for (var i = 0; i < this.plugins.length; i++)
                $t[this.plugins[i]].initialize(this);

            var headerWrap = this.$headerWrap = $('.t-grid-header-wrap', this.element);

            var $content = $('.t-grid-content', this.element)
                .bind('scroll', function() {
                    headerWrap.scrollLeft(this.scrollLeft);
                });

            this.$tbody = $('.t-grid-content table tbody', this.element);

            if (!this.$tbody.length)
                this.$tbody = $('table tbody', this.element);

            $('.t-refresh', this.element)
                .live('click', $t.delegate(this, this.refreshClick));

            if (this.onError)
                $(element).bind('error.Grid', this.onError);

            if (this.onDataBinding)
                $(element).bind('dataBinding.Grid', this.onDataBinding);

            if (this.onRowDataBound)
                $(element).bind('rowDataBound.Grid', this.onRowDataBound);

            if (this.isAjax()) {
                $('.t-pager .t-link:not(.t-state-disabled)', this.element)
                    .live('click', $t.delegate(this, this.pagerClick));

                if (this.sort)
                    $('.t-header > .t-link', this.element)
					    .live('click', $t.delegate(this, this.headerClick));
            }

            if (this.onLoad)
                $(this.element).bind('load.Grid', this, this.onLoad).trigger('load.Grid');
        },

        formatString: function() {
            var s = arguments[0];

            for (var i = 0, l = arguments.length - 1; i < l; i++) {
                var reg = new RegExp('\\{' + i + '(:([^\\}]+))?\\}', 'gm');

                var argument = arguments[i + 1];

                var formatter = this.formatters[getType(argument)];
                if (formatter) {
                    var match = reg.exec(s);
                    argument = formatter(argument, match[2]);
                }
                s = s.replace(reg, argument);
            }
            return s;
        },

        formatters: {
            date: formatDate
        },

        cultureInfo: {
            longDate: 'dddd, MMMM dd, yyyy',
            shortDate: 'M/d/yyyy',
            fullDateTime: 'dddd, MMMM dd, yyyy h:mm:ss tt',
            generalDateShortTime: 'M/d/yyyy h:mm tt',
            generalDateTime: 'M/d/yyyy h:mm:ss tt',
            sortableDateTime: "yyyy'-'MM'-'ddTHH':'mm':'ss",
            universalSortableDateTime: "yyyy'-'MM'-'dd HH':'mm':'ss'Z'",
            monthYear: 'MMMM, yyyy',
            monthDay: 'MMMM dd',
            days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
            abbrDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
            abbrMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
            am: 'AM',
            pm: 'PM',
            dateSeparator: '/',
            timeSeparator: ':'
        }
    });

    // Returns the type as a string. Not full. Used in string formatting
    function getType(obj) {
        if (obj instanceof Date)
            return 'date';
        return 'object';
    }

    function pad(value) {
        if (value < 10)
            return '0' + value;
        return value;
    }

    function formatDate(date, format) {
        var l = $t.cultureInfo;

        var standardFormats = {
            d: l.shortDate,
            D: l.longDate,
            F: l.fullDateTime,
            g: l.generalDateShortTime,
            G: l.generalDateTime,
            m: l.monthDay,
            M: l.monthDay,
            s: l.sortableDateTime,
            u: l.universalSortableDateTime,
            y: l.monthYear,
            Y: l.monthYear
        };
        
        var d = date.getDate();
        var day = date.getDay();
        var M = date.getMonth();
        var y = date.getFullYear();
        var h = date.getHours();
        var m = date.getMinutes();
        var s = date.getSeconds();
        var f = date.getMilliseconds();
        
        var dateFormatters = {
            d: d,
            dd: pad(d),
            ddd: l.abbrDays[day],
            dddd: l.days[day],
            
            M: M + 1,
            MM: pad(M + 1),
            MMM: l.abbrMonths[M],
            MMMM: l.months[M],

            yy: pad(y % 100),
            yyyy: y,
            
            h: h % 12 || 12,
            hh: pad(h % 12 || 12),
            H: h,
            HH: pad(h),
            
            m: m,
            mm: pad(m),
            
            s: s,
            ss: pad(s),
            
            f: Math.floor(f / 100),
            ff: Math.floor(f / 10),
            fff: f,
            
            tt: h < 12 ? l.am : l.pm
        };

        format = format || 'G';
        format = standardFormats[format] ? standardFormats[format] : format;

        return format.replace(dateFormatTokenRegExp, function(match) {
            return match in dateFormatters ? dateFormatters[match] : match.slice(1, match.length - 1);
        });
    }

    $.extend($t.grid.prototype, {
        headerClick: function(e, element) {
            e.preventDefault();
            this.sort(element.parentNode.cellIndex);
        },

        refreshClick: function(e, element) {
            e.preventDefault();
            if ($(element).is('.t-loading'))
                return;
            if (this.isAjax())
                this.ajaxRequest(true);
            else
                location.reload();
        },

        sort: function(columnIndex) {
            this.orderBy = this.sortExpr(columnIndex);
            this.ajaxRequest();
        },

        sortExpr: function(columnIndex) {
            var column = this.columns[columnIndex];

            var direction = 'asc';

            if (column.sortDirection == 'asc')
                direction = 'desc';
            else if (column.sortDirection == 'desc')
                direction = null;

            column.sortDirection = direction;

            var sortedIndex = $.inArray(column, this.sorted);

            if (this.sortMode == 'single' && sortedIndex < 0) {
                for (var i = 0; i < this.sorted.length; i++)
                    this.sorted[i].sortDirection = null;

                this.sorted = [];
            }
            if (sortedIndex < 0 && direction)
                this.sorted.push(column);

            if (!direction)
                this.sorted.splice(sortedIndex, 1);

            var expr = [];
            for (var i = 0, length = this.sorted.length; i < length; i++)
                expr.push(this.sorted[i].name + '-' + this.sorted[i].sortDirection);

            return expr.join('~');
        },

        pagerKeyDown: function(e, element) {
            if (e.keyCode == 13) {
                var page = this.sanitizePage($(element).val());
                if (page != this.currentPage)
                    this.pageTo(page);
                else
                    $(element).val(page);
            }
        },

        isAjax: function() {
            return this.ajaxUrl || this.onDataBinding;
        },

        pagerClick: function(e, element) {
            e.preventDefault();

            var page = this.currentPage;
            var pagerButton = $(element).find('.t-icon');

            if (pagerButton.hasClass('t-arrow-next'))
                page++;
            else if (pagerButton.hasClass('t-arrow-last'))
                page = this.totalPages();
            else if (pagerButton.hasClass('t-arrow-prev'))
                page--;
            else if (pagerButton.hasClass('t-arrow-first'))
                page = 1;
            else {
                var linkText = $(element).text();

                if (linkText == '...') {
                    var elementIndex = $(element).parent().children().index(element);

                    if (elementIndex == 0) {
                        page = parseInt($(element).next().text(), 10) - 1;
                    } else {
                        page = parseInt($(element).prev().text(), 10) + 1;
                    }

                } else {
                    page = parseInt(linkText, 10);
                }
            }

            this.pageTo(page);
        },

        pageTo: function(page) {
            this.currentPage = page;
            this.ajaxRequest();
        },

        ajaxRequest: function(forceLoading) {
            if ($t.trigger(this.element, 'dataBinding.Grid', {
                page: this.currentPage,
                sortedColumns: this.sorted,
                filteredColumns: $.grep(this.columns, function(column) {
                    return column.filters;
                })
            }))
                return;

            if (!this.ajaxUrl)
                return;

            var data = {};

            data[this.queryString.page] = this.currentPage;
            data[this.queryString.size] = this.pageSize;
            data[this.queryString.orderBy] = this.orderBy || '';
            data[this.queryString.filter] = (this.filterExpression || '').replace(/\"/g, '\\"');

            var statusIcon = $('.t-status .t-icon', this.element);
            var showLoading = function() {
                statusIcon.addClass('t-loading');
            };

            if (forceLoading)
                showLoading();

            var loadingIconTimeout = setTimeout(showLoading, 100);

            var options = {
                type: 'POST',
                url: this.ajaxUrl,
                dataType: 'json',
                dataFilter: function(data, dataType) {
                    return data.replace(dateRegExp, 'new Date($1)');
                },
                error: $t.bind(this, function(xhr, status) {
                    if ($t.ajaxError(this.element, 'error.Grid', xhr, status))
                        return;

                    if (status == 'parsererror')
                        alert('Error! The requested URL "' + this.ajaxUrl + '" did not return JSON.');
                    if (status == 'error')
                        alert('Error! The requested URL "' + this.ajaxUrl + '" returned ' + xhr.status + ' - ' + xhr.statusText);
                    if (status == 'timeout')
                        alert('Error! Server timeout.');
                }),
                complete: function() {
                    clearTimeout(loadingIconTimeout);
                    statusIcon.removeClass('t-loading');
                },

                success: $t.bind(this, function(data) {
                    data = data.d || data; //Support the `d` returned by MS Web Services 
                    this.total = data.total || data.Total || 0;
                    this.dataBind(data.data || data.Data);
                })
            };

            if (this.ws) {
                var json = [];

                for (var key in data)
                    json.push($t.formatString('"{0}":"{1}"', key, data[key]));
                options.data = '{' + json.join(',') + '}';
                options.contentType = 'application/json; charset=utf-8';
            } else {
                options.data = data;
            }

            $.ajax(options);
        },

        createMapping: function(column) {
            var functionBody = new $t.stringBuilder();
            
            functionBody.cat('var result = data.')
                        .cat(column.name)
                        .cat(';');
            
            if (column.format || column.type == 'Date')
                functionBody.cat('result = $.telerik.formatString("')
                            .cat(column.format || '{0:G}')
                            .cat('", result);');
                        
            functionBody.cat('return result;')
            
            return new Function('data', functionBody.string());
        },

        createColumnMappings: function(data) {
            for (var key in data) {
                for (var i = 0, l = this.columns.length; i < l; i++) {
                    var column = this.columns[i];

                    if (!column.name)
                        continue;

                    if (column.name.indexOf(key) > -1)
                        column.mapping = this.createMapping(column);
                }
            }
        },

        bindTo: function(data) {
            this.$tbody.find('tr.t-no-data').remove();

            this.createColumnMappings(data[0]);

            var rows = this.$tbody[0].rows;
            var rowLength = rows.length;
            var dataLength = Math.min(this.pageSize, data.length);

            if (this.pageSize == 0)
                dataLength = data.length;

            if (rowLength < dataLength) {
                var rowHtml = new $t.stringBuilder();

                for (var rowIndex = rowLength; rowIndex < dataLength; rowIndex++) {
                    if (rowIndex % 2 == 1)
                        rowHtml.cat('<tr class="t-alt">');
                    else
                        rowHtml.cat('<tr>');

                    for (var cellIndex = 0; cellIndex < this.columns.length; cellIndex++)
                        rowHtml.cat('<td/>');

                    rowHtml.cat('</tr>');
                }

                $(rowHtml.string()).appendTo(this.$tbody);
            }

            for (var rowIndex = 0; rowIndex < dataLength; rowIndex++) {
                var row = rows[rowIndex];
                for (var i = 0, l = this.columns.length; i < l; i++) {
                    var evaluate = this.columns[i].mapping;
                    if (evaluate)
                        row.cells[i].innerHTML = evaluate(data[rowIndex]);
                }

                if (this.onRowDataBound)
                    $t.trigger(this.element, 'rowDataBound.Grid', { row: row, dataItem: data[rowIndex] });
            }

            for (; rowIndex < rowLength; rowIndex++)
                $(rows[rows.length - 1]).remove();
        },

        updatePager: function() {
            var totalPages = this.totalPages(this.total);
            var currentPage = this.currentPage;
            var $pager = $('.t-pager', this.element);

            // nextPrevious
            // work-around for weird issue in IE, when using comma-based selector
            $pager.find('.t-arrow-next').parent().add($pager.find('.t-arrow-last').parent())
	            .toggleClass('t-state-disabled', currentPage >= totalPages)
	            .removeClass('t-state-hover');

            $pager.find('.t-arrow-prev').parent().add($pager.find('.t-arrow-first').parent())
	            .toggleClass('t-state-disabled', currentPage == 1)
	            .removeClass('t-state-hover');

            // pageInput
            $pager.find('.t-page-i-of-n').each(function() {
                this.innerHTML = $t.formatString('Page <input type="text" value="{0}" /> of {1}', currentPage, totalPages);
            });

            // numeric
            $pager.find('.t-numeric').each($t.bind(this, function(index, element) {
                this.numericPager(element, currentPage, totalPages);
            }));

            // status
            $('.t-status-text', this.element)
                .text($t.formatString('Displaying items {0} - {1} of {2}',
                    this.firstItemInPage(),
	                this.lastItemInPage(),
	                this.total));
        },

        numericPager: function(pagerElement, currentPage, totalPages) {
            var numericLinkSize = 10;
            var numericStart = 1;

            if (currentPage > numericLinkSize) {
                var reminder = (currentPage % numericLinkSize);

                numericStart = (reminder == 0) ? (currentPage - numericLinkSize) + 1 : (currentPage - reminder) + 1;
            }

            var numericEnd = (numericStart + numericLinkSize) - 1;

            numericEnd = Math.min(numericEnd, totalPages);

            var pagerHtml = new $t.stringBuilder();
            if (numericStart > 1)
                pagerHtml.cat('<a class="t-link">...</a>');

            for (var page = numericStart; page <= numericEnd; page++) {
                if (page == currentPage) {
                    pagerHtml.cat('<span class="t-state-active">')
                        .cat(page)
                        .cat('</span>');
                } else {
                    pagerHtml.cat('<a class="t-link">')
	                .cat(page)
	                .cat('</a>');
                }
            }

            if (numericEnd < totalPages)
                pagerHtml.cat('<a class="t-link">...</a>');

            pagerElement.innerHTML = pagerHtml.string();
        },

        updateSorting: function() {
            $('.t-header', this.element).each($t.bind(this, function(i, header) {
                var direction = this.columns[i].sortDirection;
                var $link = $(header).children('.t-link');
                var $icon = $link.children('.t-icon');

                if (!direction) {
                    $icon.hide();
                } else {
                    if ($icon.length == 0)
                        $icon = $('<span class="t-icon"/>').appendTo($link);

                    $icon.toggleClass('t-arrow-up', direction == 'asc')
                        .toggleClass('t-arrow-down', direction == 'desc')
                        .show();
                }
            }));
        },

        sanitizePage: function(value) {
            var result = parseInt(value, 10);
            if (isNaN(result) || result < 1)
                return 1
            return Math.min(result, this.totalPages());
        },

        totalPages: function() {
            return Math.ceil(this.total / this.pageSize);
        },

        firstItemInPage: function() {
            return this.total > 0 ? (this.currentPage - 1) * this.pageSize + 1 : 0;
        },

        lastItemInPage: function() {
            return Math.min(this.currentPage * this.pageSize, this.total);
        },

        dataBind: function(data) {
            this.bindTo(data);
            this.updatePager();
            this.updateSorting();
        },

        rebind: function(args) {
            this.sorted = [];
            this.currentPage = 1;

            $.each(this.columns, function() {
                this.sortDirection = null;
                this.filters = [];
            });

            for (var key in args) {
                var regExp = new RegExp($t.formatString('({0})=([^&]*)', key), 'g');
                this.ajaxUrl = this.ajaxUrl.replace(regExp, '$1=' + args[key]);
            }

            this.ajaxRequest();
        }
    });

    $.fn.tGrid = function(options) {
        options = $.extend({}, $.fn.tGrid.defaults, options);

        return this.each(function() {
            options = $.meta ? $.extend({}, options, $(this).data()) : options;

            if (!$(this).data('tGrid')) {
                var grid = new $t.grid(this, options);

                $(this).data('tGrid', grid);

                if (grid.$tbody.find('tr.t-no-data').length)
                    grid.ajaxRequest();
            }
        });
    }

    // default options

    $.fn.tGrid.defaults = {
        columns: [],
        plugins: [],
        currentPage: 1,
        pageSize: 10,
        queryString: {
            page: 'page',
            size: 'size',
            orderBy: 'orderBy',
            filter: 'filter'
        }
    };
})(jQuery);