(function( $ ) {

    var example = " e.g. $('#mydiv').volta({columns: ['Name'], keys: ['name']})";
    var counter = 0;

    function getSort(config, keys)
    {
        var sorters = {};

        if(!keys) return sorters;

        for(var i=0; i<keys.length; i++)
        {
            if(config && config[keys[i]]) sorters[keys[i]] = config[keys[i]];
            else if(!config || config[keys[i]] !== false) sorters[keys[i]] = {direction: 'both'};
        }

        return sorters;
    }

    function getStyle(config)
    {
        var style = {
            even: "even",
            odd: "odd",
            table: "display",
            header: "header",
            ascending: "sort_asc",
            descending: "sort_desc",
            sortable: "sortable"
        };

        if(!config) return style;

        for(var i in config)
        {
            if(config.hasOwnProperty(i))
            {
                style[i] = config[i];
            }
        }

        return style;
    }

    function defaultRenderer(state)
    {
        if(state.value) $(state.cell).text(state.value);
    }
    
    function volta( options ) {
        var data = options ? options.data : null;
        var filtered = data;
        var filter;
        var filterMapping = {};
        var table = $('<table border="0" cellspacing="0" cellpadding="0">');
        var settings = {
            page: 0,
            pageSize: 10,
            sortBy: -1,
            style: getStyle(options && options.style),
            sort: getSort(options && options.sort, options && options.keys),
            noRecordsText: "No data available"
        };

        var sortCache = {};
        var sorting = false;
        
        if(this.data("volta")) return this.data("volta");
        
        if(!options) $.error("A configuration object must be passed to initialize a Volta table." + example);
        if(!options.columns || !options.columns.length) $.error("Attribute \"columns\" must be passed to configure the Volta table." + example);
        if(!options.keys || !options.keys.length) $.error("Attribute \"keys\" must be passed to configure the Volta table." + example);

        if(options.border) table.attr("border", options.border);
        if(options.cellspacing) table.attr("cellspacing", options.cellspacing);
        if(options.cellpadding) table.attr("cellpadding", options.cellpadding);

        this.data("volta", {
            keys: function() {
                return settings.keys;
            },
            filter: function(token, newFilter) {
                var that = this;
                var length;

                if(token instanceof Function)
                {
                    newFilter = token;
                    token = null;
                }

                if(token && !newFilter)
                {
                    delete filterMapping[token];
                }

                if(token || newFilter)
                {
                    if(!token) filter = newFilter;
                    else if(newFilter) filterMapping[token] = newFilter;

                    if(token)
                    {
                        filter = [];

                        for(var i in filterMapping)
                        {
                            if(filterMapping.hasOwnProperty(i)) filter.push(filterMapping[i]);
                        }
                    }
                    
                    if(settings.sortBy >= 0 && sortCache[(settings.sort[settings.keys[settings.sortBy]].key || settings.keys[settings.sortBy])])
                    {
                        filtered = sortCache[(settings.sort[settings.keys[settings.sortBy]].key || settings.keys[settings.sortBy])][settings.sort[settings.keys[settings.sortBy]].order];
                    }
                    else
                    {
                        filtered = null;
                    }

                    if(!filtered || sorting)
                    {
                        filtered = $.jsonFilter(data, filter);
                        length = filtered.length;

                        if(settings.sortBy >= 0) sorting = filtered;
                        if(settings.sortBy >= 0) filtered = $.sort(filtered, {
                            order: settings.sort[settings.keys[settings.sortBy]].order ? 'asc' : 'desc',
                            property: (settings.sort[settings.keys[settings.sortBy]].key || settings.keys[settings.sortBy]),
                            finished: function() {
                                var key = this.property;
                                var order = settings.sort[settings.keys[settings.sortBy]].order;

                                sorting = false;

                                that.render();
                                table.trigger("paged", that, settings.page);
                                table.trigger("sorting", {sorted: filtered.length, total: length});

                                if(!filter || filter.length < 1)
                                {
                                    if(!sortCache[key]) sortCache[key] = {};

                                    sortCache[key][order] = filtered;
                                }
                            },
                            notification: function(index) {
                                that.render();
                                table.trigger("paged", that, settings.page);
                                table.trigger("sorting", {index: index, total: length});
                            }
                        });
                    }
                    else
                    {
                        filtered = $.jsonFilter(filtered, filter);
                    }

                    this.page(0);
                }

                return filter;
            },
            first: function() {
                this.page(0);
            },
            last: function() {
                this.page(this.totalPages() - 1);
            },
            next: function() {
                if((settings.page + 1) * settings.pageSize < filtered.length)
                {
                    this.page(++settings.page);
                }

                return settings.page;
            },
            previous: function() {
                if(settings.page > 0)
                {
                    this.page(--settings.page);
                }

                return settings.page;
            },
            page: function(newPage) {

                if(newPage) newPage = Number(newPage);

                if((newPage || newPage === 0) && newPage > -1 && (newPage < this.totalPages() || (newPage == 0 && this.totalPages() == 0)))
                {
                    settings.page = newPage;
                    this.render();
                    table.trigger("paged", this, settings.page);
                }

                return settings.page;
            },
            totalPages: function() {
                return (Math.floor(filtered.length / settings.pageSize) + Math.min(1, filtered.length % settings.pageSize));
            },
            pageSize: function(newPageSize) {
                if(newPageSize) newPageSize = Number(newPageSize);

                if(newPageSize && newPageSize > 0)
                {
                    settings.pageSize = newPageSize;
                    this.page(0);
                }

                return settings.pageSize;
            },
            render: function() {
                var offset = settings.page * settings.pageSize;
                var renderState = {};
                var that = this;
                var head = table.find("thead");
                var body = table.find("tbody");
                
                var row;
                var cell;
                var style;
                var length;

                if(!head.length) table.append(head = $("<thead>"));
                if(!body.length) table.append(body = $("<tbody>"));
                
                head.children().remove();
                body.children().remove();

                head.append(row = $("<tr>"));

                // Render Header
                for(var i=0; i<settings.columns.length; i++)
                {
                    row.append(cell = $("<th>"));

                    renderState = {
                        row: 0,
                        column: i,
                        cell: cell,
                        value: settings.columns[i]
                    };
                    
                    if(settings.sort[settings.keys[i]])
                    {
                        (function(key, column, sorter) {
                            cell.click(function() {
                                var order = 'asc';

                                if(sorter.direction == 'both')
                                {
                                    sorter.order = !sorter.order;
                                    order = sorter.order ? 'asc' : 'desc';
                                }
                                else if(sorter.direction == 'desc')
                                {
                                    sorter.order = false;
                                    order = 'desc';
                                }
                                else
                                {
                                    sorter.order = true;
                                }

                                // Already sorted
                                if(column == settings.sortBy && sorter.direction != 'both') return;
                                else if(settings.sortBy >= 0 && column != settings.sortBy) delete settings.sort[settings.keys[settings.sortBy]].order;

                                // We're currently sorting, so let's undo the progress
                                if(sorting) filtered = sorting;

                                settings.sortBy = column;
                                length = filtered.length;
                                
                                if((!filter || filter.length < 1) && sortCache[(sorter.key || key)] && sortCache[(sorter.key || key)][sorter.order])
                                {
                                    filtered = sortCache[(sorter.key || key)][sorter.order];
                                }
                                else
                                {
                                    sorting = filtered;
                                    filtered = $.sort(filtered, {
                                        order: order,
                                        property: (sorter.key || key),
                                        finished: function() {
                                            var key = this.property;
                                            var order = sorter.order;

                                            sorting = false;

                                            that.render();
                                            table.trigger("paged", that, settings.page);
                                            table.trigger("sorting", {sorted: filtered.length, total: length});

                                            if(!filter || filter.length < 1)
                                            {
                                                if(!sortCache[key]) sortCache[key] = {};

                                                sortCache[key][order] = filtered;
                                            }
                                        },
                                        notification: function(index) {
                                            that.render();
                                            table.trigger("paged", that, settings.page);
                                            table.trigger("sorting", {sorted: index, total: length});
                                        }
                                    });
                                }

                                that.page(0);
                            });
                        })(settings.keys[i], i, settings.sort[settings.keys[i]]);
                        
                        if(settings.sortBy == i) cell.addClass(settings.sort[settings.keys[i]].order ? settings.style.ascending : settings.style.descending);
                        else cell.addClass(settings.style.sortable);
                    }

                    if(settings.widths && settings.widths[i])
                    {
                        cell.css("width", (typeof settings.widths[i] == 'number') ? settings.widths[i]+"px" : settings.widths[i]);
                    }

                    if(settings.headerRenderers && settings.headerRenderers[settings.keys[i]]) settings.headerRenderers[settings.keys[i]](renderState);
                    else defaultRenderer(renderState);
                }

                if(!filtered || !filtered.length)
                {
                    body.append(row = $("<tr>"));
                    row.append(cell = $("<td>")
                        .text(settings.noRecordsText)
                        .attr("colspan", settings.columns.length)
                        .attr("align", "center"));
                        
                    return;
                }

                // Render Rows
                for(var j=0; (j+offset)<filtered.length && j<settings.pageSize; j++)
                {
                    style = j%2 ? settings.style.odd: settings.style.even;
                    body.append(row = $("<tr>").addClass(style));

                    for(var i=0; i<settings.columns.length; i++)
                    {
                        row.append(cell = $("<td>"));
                        
                        renderState = {
                            row: j+offset,
                            column: i,
                            cell: cell,
                            record: filtered[j+offset],
                            value: filtered[j+offset][settings.keys[i]]
                        };
                        
                        if(settings.renderers && settings.renderers[settings.keys[i]]) settings.renderers[settings.keys[i]](renderState);
                        else defaultRenderer(renderState);
                    }
                }
            },
            data: function(newData) {
                
                if(newData)
                {
                    data = newData;
                    filtered = data;
                    sortCache = {};
                    settings.page = 0;
                    this.render();
                }

                return data;
            },
            sortBy: function(newSort) {
                if(!(typeof newSort == 'undefined') && newSort >= 0)
                {
                    var order = 'asc';
                    var that = this;
                    var key = settings.keys[newSort];
                    var sorter = settings.sort[key];
                    var length;

                    if(sorter.direction == 'both')
                    {
                        sorter.order = !sorter.order;
                        order = sorter.order ? 'asc' : 'desc';
                    }
                    else if(sorter.direction == 'desc')
                    {
                        sorter.order = false;
                        order = 'desc';
                    }
                    else
                    {
                        sorter.order = true;
                    }

                    // Already sorted
                    if(newSort == settings.sortBy && sorter.direction != 'both') return settings.sortBy;
                    else if(settings.sortBy >= 0 && newSort != settings.sortBy) delete settings.sort[settings.keys[settings.sortBy]].order;

                    if(sorting) filtered = sorting;

                    settings.sortBy = newSort;
                    length = filtered.length;

                    if((!filter || filter.length < 1) && sortCache[(sorter.key || key)] && sortCache[(sorter.key || key)][sorter.order])
                    {
                        filtered = sortCache[(sorter.key || key)][sorter.order];
                    }
                    else
                    {
                        sorting = filtered;
                        filtered = $.sort(filtered, {
                            order: order,
                            property: (sorter.key || key),
                            finished: function() {
                                var key = this.property;
                                var order = sorter.order;

                                sorting = false;

                                that.render();
                                table.trigger("paged", that, settings.page);
                                table.trigger("sorting", {sorted: filtered.length, total: length});

                                if(!filter || filter.length < 1)
                                {
                                    if(!sortCache[key]) sortCache[key] = {};

                                    sortCache[key][order] = filtered;
                                }
                            },
                            notification: function(index) {
                                that.render();
                                table.trigger("paged", that, settings.page);
                                table.trigger("sorting", {sorted: index, total: length});
                            }
                        });
                    }

                    this.page(0);
                }

                return settings.sortBy;
            },
            paged: function(listener) {
                table.bind("paged", listener);
            },
            sorting: function(listener) {
                table.bind("sorting", listener);
            }
        });

        for(var i in options)
        {
            if(options.hasOwnProperty(i) && i != "style" && i != "sort")
            {
                settings[i] = options[i];
            }
        }

        table.addClass(settings.style.table);

        if(settings.sortBy >= 0 && settings.data)
        {
            var sortBy = settings.sortBy;

            delete settings.sortBy; // Ensure that the table doesn't think it's already sorted.
            this.data("volta").sortBy(sortBy);
        }
        
        this.append(table);
        this.data("volta").render();

        return this.data("volta");
    };

    function paginate(table, options)
    {
        var maxPages = (options && options.maxPages) || 5;
        var first = (options && options.first) || "First";
        var previous = (options && options.previous) || "Prev.";
        var next = (options && options.next) || "Next";
        var last = (options && options.last) || "Last";
        var buttonClass = (options && options.style && options.style.button) || "paginate_button";
        var activeClass = (options && options.style && options.style.active) || "paginate_active";
        var paginateClass = (options && options.style && options.style.paginate) || "paginate";
        var firstClass = (options && options.style && options.style.first) || "first";
        var previousClass = (options && options.style && options.style.previous) || "previous";
        var nextClass = (options && options.style && options.style.next) || "next";
        var lastClass = (options && options.style && options.style.last) || "last";
        var paginateId = (options && options.prefix && options.prefix + "_paginate") || "paginate" + (counter++);
        var firstId = (options && options.prefix && options.prefix + "_first") || "first" + (counter++);
        var previousId = (options && options.prefix && options.prefix + "_previous") || "previous" + (counter++);
        var nextId = (options && options.prefix && options.prefix + "_next") || "next" + (counter++);
        var lastId = (options && options.prefix && options.prefix + "_last") || "last" + (counter++);

        var container = $("<div>").addClass(paginateClass).attr("id", paginateId);
        var pages = [];

        if(this.length > 1)
        {
            for(var i=0; i<this.length; i++)
            {
                $(this.get(i)).volta('paginate', table, options);
            }
            
            return this;
        }

        if(!table || !table.page) $.error("A volta table instance must be passed to volta('paginate')");

        function createNav(text, className, id, method)
        {
            container.append($("<span>")
                .text(text)
                .addClass(className)
                .addClass(buttonClass)
                .attr("id", id)
                .click(function(event) {
                    table[method]();
                    event.preventDefault();
                })
            );
        }

        function createPage(page)
        {
            var item = $("<span>")
                .text((page + 1))
                .addClass((page == table.page()) ? activeClass : buttonClass)
                .click(function(event) {
                    table.page(page);
                    event.preventDefault();
                });

            pages.push(item);
            container.append(item);
        }

        function render()
        {
            var mid = Math.max(Math.floor(maxPages/2), 1);
            var pageOffset = Math.min(Math.max(0, table.page() - mid), table.totalPages() - maxPages);

            if(pageOffset < 0) pageOffset = 0;

            container.children().remove();

            createNav(first, firstClass, firstId, "first");
            createNav(previous, previousClass, previousId, "previous");

            for(var i=0; i<maxPages && i+pageOffset < table.totalPages(); i++)
            {
                createPage(i + pageOffset);
            }

            createNav(next, nextClass, nextId, "next");
            createNav(last, lastClass, lastId, "last");

            // Prevent highlighting
            container.children().each(function() {
                this.onselectstart = function() {return false;};
                this.unselectable = "on";
                $(this).css('-moz-user-select', 'none');
            });
        }

        render();
        table.paged(render);
        
        this.append(container);

        return this;
    }

    function search(table, options)
    {
        var fields = (options && options.fields) || table.keys();
        var filterName = (options && options.filterName) || "search";
        var liveSearch = (options && typeof options.liveSearch != 'undefined') ? options.liveSearch : true;
        var goButton = (options && typeof options.goButton != 'undefined') ? options.goButton : true;
        var goButtonText = (options && typeof options.goButtonText != 'undefined') ? options.goButtonText : "Go";
        var inputClass = (options && options.inputClass);
        var goButtonClass = (options && options.inputClass);

        var inputfield = $('<input type="text">');
        var button = $('<button>').text(goButtonText);
        var timer;

        function getFilters()
        {
            var filters = [];
            
            if(!inputfield.val()) return null;

            for(var i=0; i<fields.length; i++)
            {
                filters[i] = $.jsonFilter('contains', fields[i], inputfield.val());
            }
            
            if(filters.length == 1) return filters[0];
            else return $.jsonFilter('or', filters);
        }

        this.append(inputfield);

        if(goButton) this.append(button.click(function(){
            timer = 0;
            table.filter(filterName, getFilters());
        }));

        if(liveSearch) inputfield.keydown(function() {
            if(timer) clearTimeout(timer);

            timer = setTimeout(function() {
                timer = 0;
                table.filter(filterName, getFilters());
            }, 250);
        });

        if(inputClass) inputfield.addClass(inputClass);
        if(goButtonClass) inputfield.addClass(goButtonClass);
    }

    function selector(table, options)
    {
        var field = (options && options.field);
        var filterName = (options && options.filterName) || String(field) + "Selector";
        var displayField = (options && options.displayField) || field;
        var defaultText = (options && options.defaultText) || "All Values";
        var selectorClass = (options && options.selectorClass);
        
        var select = $("<select>");
        var selectOptions = select.attr('options');
        var values = table.data();

        if(!field || (field  instanceof Array && !field.length)) $.error("Selector requires the 'field' option to be set. e.g. $('#selectorDiv').volta($('#voltaDiv).volta(), {field: 'name'})");

        if(field instanceof Array)
        {
            if(field.length != displayField.length) $.error("Volta Selector Error: Identifier field and display field must be sibling fields.");
            
            for(var i=0; i<field.length-1; i++)
            {
                if(field[i] != displayField[i]) $.error("Volta Selector Error: Identifier field and display field must be sibling fields.");
            }
        }

        function findValues(path, displayPath, values, exists)
        {
            var isPathArray = (path instanceof Array);
            var results = [];
            var value;

            if(isPathArray && path.length == 1)
            {
                path = path[0];
                displayPath = displayPath[0];
                isPathArray = false;
            }
            
            if(!values) return results;
            
            if(values instanceof Array)
            {
                for(var i=0; i<values.length; i++)
                {
                    value = values[i];

                    if(isPathArray)
                    {
                        results = results.concat(findValues(path, displayPath, values[i], exists));
                    }
                    else if(typeof value[path] != 'undefined' && !exists[value[path]])
                    {
                        results.push({id: value[path], display: value[displayPath]});
                        exists[value[path]] = true;
                    }
                }
            }
            else
            {
                if(isPathArray)
                {
                    results = results.concat(findValues(path.slice(1), displayPath.slice(1), values[path[0]], exists));
                }
                else if(typeof values[path] != 'undefined' && !exists[values[path]])
                {
                    results.push({id: values[path], display: values[displayPath]});
                    exists[values[path]] = true;
                }
            }
            
            return results;
        }

        values = findValues(field, displayField, values, {});
        values.sort(function(value1, value2) {
            if(value1.id < value2.id) return -1;
            if(value1.id > value2.id) return 1;

            return 0;
        });

        // Create the list of facilities dropdown.
        selectOptions[0] = new Option(defaultText, null);
        this.append(select);
        $.each(values, function(index, value) {
            selectOptions[index+1] = new Option(value.display, value.id);
        });
        
        select.change(function() {
            var value = ($(this).val() == "null") ? null : $(this).val();

            if(!value) table.filter(filterName, null);
            else table.filter(filterName, $.jsonFilter('exact', field, value));
        });

        if(selectorClass) select.addClass(selectorClass);
    }

    function progress(table, options)
    {
        var progressClass = (options && options.progressClass) || "progress";
        var fullClass = (options && options.fullClass) || "progress-full";

        var fullBar = $('<div>').addClass(fullClass);
        var progressBar = $('<div>')
                            .addClass(progressClass)
                            .append(fullBar);

        table.sorting(function(event, details) {
            if(details.sorted == details.total)
            {
                fullBar.css("width", "100%");
                progressBar.fadeOut();
            }
            else
            {
                progressBar.fadeIn();
                fullBar.css("width", Math.floor(details.sorted/details.total * 100)+"%");
            }
        });

        progressBar.css("position", "relative");
        fullBar.css("position", "absolute");
        progressBar.hide();
        
        this.append(progressBar);
    }

    var methods = {
        paginate: paginate,
        search: search,
        selector: selector,
        progress: progress
    };

    $.fn.volta = function(method)
    {
        if(methods[method]) return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
        else return volta.apply(this, arguments);
    }

})(jQuery);

