
(function ($) {
    $.fn.sorttable = function (sortFns) {
        return this.each(function () {
            var $table = $(this);
            sortFns = sortFns || {};
            sortFns = $.extend({}, $.fn.sorttable.default_sort_fns, sortFns);
            $table.data('sortFns', sortFns);

            $table.on("click.sorttable", "th", function () {
                $(this).sortThis();
            });
        });
    };


    // Expects $("#mytable").sorttable() to have already been called.
    // Call on a table header.
    $.fn.sortThis = function (force_direction) {
        var $this_th = $(this);
        var th_index = 0; // we'll increment this soon
        var dir = $.fn.sorttable.dir;
        var $table = $this_th.closest("table");
        var datatype = $this_th.data("sort") || null;

        // No datatype? Nothing to do.
        if (datatype === null) {
            return;
        }

        // Account for colspans
        $this_th.parents("tr").find("th").slice(0, $(this).index()).each(function () {
            var cols = $(this).attr("colspan") || 1;
            th_index += parseInt(cols, 10);
        });

        var sort_dir;
        if (arguments.length == 1) {
            sort_dir = force_direction;
        } else {
            sort_dir = force_direction || $this_th.data("sort-default") || dir.ASC;
            if ($this_th.data("sort-dir"))
                sort_dir = $this_th.data("sort-dir") === dir.ASC ? dir.DESC : dir.ASC;
        }


        $table.trigger("beforetablesort", {
            column: th_index,
            direction: sort_dir
        });

        // More reliable method of forcing a redraw
        $table.css("display");

        // Run sorting asynchronously on a timout to force browser redraw after
        // `beforetablesort` callback. Also avoids locking up the browser too much.
        setTimeout(function () {
            // Gather the elements for this column
            var column = [];
            var sortFns = $table.data('sortFns');
            var sortMethod = sortFns[datatype];
            var trs = $table.children("tbody").children("tr");

            // Extract the data for the column that needs to be sorted and pair it up
            // with the TR itself into a tuple. This way sorting the values will
            // incidentally sort the trs.
            trs.each(function (index, tr) {
                var $e = $(tr).children().eq(th_index);
                var sort_val = $e.data("sort-value");

                // Store and read from the .data cache for display text only sorts
                // instead of looking through the DOM every time
                if (typeof (sort_val) === "undefined") {
                    var txt = $e.text();
                    $e.data('sort-value', txt);
                    sort_val = txt;
                }
                column.push([sort_val, tr]);
            });

            // Sort by the data-order-by value
            column.sort(function (a, b) {
                return sortMethod(a[0], b[0]);
            });
            if (sort_dir != dir.ASC)
                column.reverse();

            // Replace the content of tbody with the sorted rows. Strangely
            // enough, .append accomplishes this for us.
            trs = $.map(column, function (kv) {
                return kv[1];
            });
            $table.children("tbody").append(trs);

            // Reset siblings
            $table.find("th").data("sort-dir", null).removeClass("desc asc");
            $this_th.data("sort-dir", sort_dir).addClass(sort_dir);

            $table.trigger("aftertablesort", {
                column: th_index,
                direction: sort_dir
            });
            $table.css("display");
        }, 10);

        return $this_th;
    };

    // Call on a sortable td to update its value in the sort. This should be the
    // only mechanism used to update a cell's sort value. If your display value is
    // different from your sort value, use jQuery's .text() or .html() to update
    // the td contents, Assumes sorttable has already been called for the table.
    $.fn.updateSortVal = function (new_sort_val) {
        var $this_td = $(this);
        if ($this_td.is('[data-sort-value]')) {
            // For visual consistency with the .data cache
            $this_td.attr('data-sort-value', new_sort_val);
        }
        $this_td.data("sort-value", new_sort_val);
        return $this_td;
    };

    // ------------------------------------------------------------------
    // Default settings
    // ------------------------------------------------------------------
    $.fn.sorttable.dir = {
        ASC: "asc",
        DESC: "desc"
    };
    $.fn.sorttable.default_sort_fns = {
        "int": function (a, b) {
            return parseInt(a, 10) - parseInt(b, 10);
        },
        "float": function (a, b) {
            return parseFloat(a) - parseFloat(b);
        },
        "string": function (a, b) {
            return a.localeCompare(b);
        },
        "string-ins": function (a, b) {
            a = a.toLocaleLowerCase();
            b = b.toLocaleLowerCase();
            return a.localeCompare(b);
        }
    };
})(jQuery);