var SortableTables; // Public to maintain any legacy stuff relying on it

document.addEventListener("DOMContentLoaded", function() {

    // Publicly accessible
    SortableTables = {
        tables: {},
        init: function () {
            const sortableTables = document.querySelectorAll("table.sitebuilder_sortable");
            var i;

            for (i = 0; i < sortableTables.length; i += 1) {
                if (!sortableTables[i].id) {
                    sortableTables[i].id = 'sortableTable_' + Math.floor(Math.random() * 100000);
                }
                SortableTables.tables[sortableTables[i].id] = new SortableTable(sortableTables[i]);
            }
        }
    }

    const DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/;
    const FILESIZE_RE = /^\(?([\d\.]+)\s([KM]B)\)?$/;
    const POUND_RE = new RegExp('(\u00A3|&pound;)', 'g');

    // *******************************************
    // * Sorting functions for the table to call *
    // *******************************************

    const sorters = {}

    /**
     * Sort the table based on text values, ignoring case.
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @returns {number}
     */
    sorters.caseInsensitive = function (a, b) {
        const sortColumn = getSortColumn(a);
        const aa = getInnerText(a.cells[sortColumn]).toLowerCase();
        const bb = getInnerText(b.cells[sortColumn]).toLowerCase();

        if (aa === bb) {
            return 0;
        }
        if (aa < bb) {
            return -1;
        }
        return 1;
    }

    /**
     * Sort the table by numerical values. A timeless classic.
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @returns {number}
     */
    sorters.numeric = function (a, b) {
        const sortColumn = getSortColumn(a);
        return numericSort(
            getInnerText(a.cells[sortColumn]),
            getInnerText(b.cells[sortColumn])
        );
    }

    /**
     * Sort the table based on parsed value in British Pound Sterling
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @returns {number}
     */
    sorters.pounds = function (a, b) {
        const sortColumn = getSortColumn(a);
        return numericSort(
            getInnerText(a.cells[sortColumn]).replace(POUND_RE, '').replace(',', ''),
            getInnerText(b.cells[sortColumn]).replace(POUND_RE, '').replace(',', '')
        );
    }

    /**
     * Sort table based on dates parsed in dd mm yyyy format
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @returns {number}
     */
    sorters.ddmm = function (a, b) {
        return sort_date(a, b, false);
    }

    /**
     * Sort table based on dates parsed in mm dd yyyy format
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @returns {number}
     */
    sorters.mmdd = function (a, b) {
        return sort_date(a, b, true);
    }

    /**
     * Sort based on parsed filesize
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @returns {number}
     */
    sorters.filesize = function (a, b) {
        const sortColumn = getSortColumn(a);
        const aa = getInnerText(a.cells[sortColumn]);
        const bb = getInnerText(b.cells[sortColumn]);

        return numericSort(parseFileSize(aa), parseFileSize(bb));
    }

    // ********************
    // * Helper functions *
    // ********************

    /**
     * Sort table based on parsed date
     * @param {HTMLElement} a
     * @param {HTMLElement} b
     * @param {boolean} parseAsMMDD
     * @returns {number}
     */
    function sort_date (a, b, parseAsMMDD) {
        const sortColumn = getSortColumn(a);
        const aa = getInnerText(a.cells[sortColumn]);
        const bb = getInnerText(b.cells[sortColumn]);
        const dateA = parseDate(aa, parseAsMMDD);
        const dateB = parseDate(bb, parseAsMMDD);

        if (!dateA.parsedSuccessfully || !dateB.parsedSuccessfully) {
            return -1;
        } else if (dateA.fullDate === dateB.fullDate) {
            return 0
        } else if (dateA.fullDate < dateB.fullDate) {
            return -1;
        }

        return 1;
    }

    /**
     * Get the current sort column value for the table the supplied element is in
     * @param {HTMLElement} el
     * @returns {number}
     */
    function getSortColumn(el) {
        return getParent(el, "table").dataset.sortColumn;
    }

    /**
     * Numeric sort function that treats any non-numerical value as 0
     * @param {*} a
     * @param {*} b
     * @returns {number}
     */
    function numericSort (a, b) {
        var aa = parseFloat(a);
        aa = aa !== aa ? 0 : aa; // Reliable NaN check

        var bb = parseFloat(b);
        bb = bb !== bb ? 0 : bb;

        return aa - bb;
    }

    /**
     * Returns neatly trimmed inner text of an element
     * @param {HTMLElement} el
     * @returns {string}
     */
    function getInnerText (el) {
        return el.innerText.trim();
    }

    /**
     * Get first element in the supplied element's parent nodes that matches the supplied tag
     * @param {HTMLElement} el
     * @param {string} pTagName
     * @returns {null|HTMLElement}
     */
    function getParent (el, pTagName) {
        if (el === null) {
            return null;
        }
        else if (el.nodeType === Node.ELEMENT_NODE && el.tagName.toLowerCase() === pTagName.toLowerCase()) {	// Gecko bug, supposed to be uppercase
            return el;
        } else {
            return getParent(el.parentNode, pTagName);
        }
    }

    /**
     * Attempt to parse string as date. Return an object with the results.
     * @param {string} input
     * @param {boolean} parseAsMMDD
     * @returns {{parsedSuccessfully: boolean}|{fullDate: *, month: (string|*), year: *, parsedSuccessfully: boolean, day: (string|*)}}
     */
    function parseDate (input, parseAsMMDD) {
        const match = input.match(DATE_RE);

        if (match === null) {
            return { parsedSuccessfully: false };
        }

        const day = parseAsMMDD ? match[2] : match[1];
        const month = parseAsMMDD ? match[1] : match[2];
        const year = match[3];

        return {
            parsedSuccessfully: true,
            year: year,
            day: day.length === 1 ? '0' + day : day,
            month: month.length === 1 ? '0' + month : month,
            fullDate: year + month + day
        }
    }

    /**
     * Convert a filesize string into a number
     * @param {string} input
     * @returns {number}
     */
    function parseFileSize (input) {
        const match = input.match(FILESIZE_RE);

        if (match) {
            if (match[2].toUpperCase() === 'MB') {
                return match[1] * 1024 * 1024;
            } else {
                return match[1] * 1024;
            }
        } else {
            return 0;
        }
    }

    // *****************************************
    // * SortableTable constructor and methods *
    // *****************************************

    /**
     * @param {HTMLTableElement} table
     * @constructor
     */
    function SortableTable (table) {
        var i;
        this.table = table;
        const ST = this;
        if (!table.rows || table.rows.length === 0) {
            return
        }
        this.firstRow = table.rows[0];
        this.columnTitles = [];

        // We have a first row: assume it's the header, and make its contents clickable links
        for (i = 0; i < this.firstRow.cells.length; i += 1) {
            const cell = this.firstRow.cells[i];

            if (cell.classList.contains("sortable")) {
                const text = getInnerText(cell);
                const title = cell.title ? cell.title : text;
                const link = document.createElement('a');

                this.columnTitles.push(text);

                link.href = 'javascript:void(0);';
                link.className = 'sortheader';
                link.dataset.column = i;
                link.addEventListener("click", function(e) {
                    e.preventDefault();
                    ST.sort(link);
                });
                link.innerHTML = text +
                    '<span class="sr-only">Select to sort ' +
                    ' <span class="sort-direction-text">(ascending)</span>' +
                    '</span>';
                link.dataset.sortType = this.determineSorterForColumn(i);
                link.setAttribute("role", "button");

                cell.title = 'Select to sort by ' + title.toLowerCase();
                cell.innerHTML = '';
                cell.appendChild(link);

                this.ariaAnnouncer = document.createElement("div");
                this.ariaAnnouncer.className = "sr-only";
                this.ariaAnnouncer.setAttribute("aria-live", "polite");
                document.body.appendChild(this.ariaAnnouncer);
            }
        }
    }

    /**
     * Sort the table based on the sort link that has been clicked
     * @param {HTMLElement} link
     */
    SortableTable.prototype.sort = function (link) {
        // If the table is devoid of content, there's nothing to sort
        if (this.table.rows.length <= 1) {
            return;
        }

        const directionTextSpan = link.querySelector("span.sort-direction-text");
        const columnNumber = link.dataset.column;
        const sortFunction = sorters[link.dataset.sortType];
        // IE hax :sadface:
        const rowsToSort = Array.prototype.slice.call(this.table.querySelectorAll("tr"));
        const columnTitle = this.columnTitles[columnNumber];
        var i;

        rowsToSort.shift(); // Don't care about the first row so chuck it.

        this.table.dataset.sortColumn = columnNumber;
        rowsToSort.sort(sortFunction);

        if (link.dataset.sortDirection === 'down') {
            rowsToSort.reverse();
            link.dataset.sortDirection = 'up';
            link.className = 'sortheader sortup';
            directionTextSpan.innerText = "(ascending)";
            this.ariaAnnouncer.innerText = "Sorted by " + columnTitle + " (descending)";
        } else {
            link.dataset.sortDirection = 'down';
            link.className = 'sortheader sortdown';
            directionTextSpan.innerText = "(descending)";
            this.ariaAnnouncer.innerText = "Sorted by " + columnTitle + " (ascending)";
        }

        // Move the sorted rows into their new positions, or put them at the bottom if they have the class .sortbottom
        const rowsToAddToBottom = [];
        for (i = 0; i < rowsToSort.length; i += 1) {
            if (!rowsToSort[i].classList.contains('sortbottom')) {
                this.table.tBodies[0].appendChild(rowsToSort[i]);
            } else {
                rowsToAddToBottom.push(rowsToSort[i])
            }
        }

        for (i = 0; i < rowsToAddToBottom.length; i += 1) {
            this.table.tBodies[0].appendChild(rowsToAddToBottom[i]);
        }

        // Unset any other link classes
        const links = getParent(link, "tr").getElementsByTagName("a");
        for (i = 0; i < links.length; i += 1) {
            if (links[i] !== link) {
                links[i].className = 'sortheader';
            }
        }
    };

    /**
     * Figure out which of our SORTER functions the column should use based on the first cell
     * in that column that contains readable content
     * @param {number} columnNumber
     * @returns {string}
     */
    SortableTable.prototype.determineSorterForColumn = function (columnNumber) {
        const sampleContent = getInnerText(this.firstCellWithContentInColumn(columnNumber));

        // Default to text-based sorting
        var sortFunctionName = "caseInsensitive";

        // Might be numbers...
        if (sampleContent.match(/^[\d\.]+$/)) {
            sortFunctionName = "numeric";
        }

        // Might be money...
        if (sampleContent.match(POUND_RE)) {
            sortFunctionName = "pounds";
        }

        // Might be a date...
        const possibleDate = parseDate(sampleContent, false);

        if (possibleDate.parsedSuccessfully) {
            if (possibleDate.day > 12) { // definitely dd/mm
                sortFunctionName = "ddmm";
            } else if (possibleDate.month > 12) { // definitely mm/dd
                sortFunctionName = "mmdd";
            } else { // looks like a date, but we can't tell which, so assume dd/mm
                sortFunctionName = "ddmm";
            }
        }

        // Might be file sizes
        if (this.firstRow.cells[columnNumber].classList.contains("sortable_filesize")) {
            sortFunctionName = "filesize";
        }

        return sortFunctionName;
    }

    /**
     * Finds first cell in column that has some readable text content (excluding first row of table)
     * @param {number} columnNumber
     * @returns {HTMLElement|undefined}
     */
    SortableTable.prototype.firstCellWithContentInColumn = function (columnNumber) {
        var i;
        for (i = 1; i < this.table.rows.length; i += 1) {
            const cell = this.table.rows[i].cells[columnNumber]
            if (getInnerText(cell)) {
                return cell;
            }
        }
    };

    SortableTables.init();
    window.SortableTables = SortableTables;
});
