"use strict";

var DataTable = function (table, opts) {

    this.options = {};
    this.table = table;
    this.currentPage = 0;
    this.currentStart = 0; // different from currentPage * pageSize because there is a filter
    this.filterIndex = [];

    for (var k in DataTable.defaultOptions) {
        if (DataTable.defaultOptions.hasOwnProperty(k)) {
            if (opts.hasOwnProperty(k)) {
                this.options[k] = opts[k] ;
            }
            else {
                this.options[k] = DataTable.defaultOptions[k] ;
            }
        }
    }

    if (opts.hasOwnProperty('data')) {
        this.options.data = opts.data ;
    }

    /* If nb columns not specified, count the number of column from thead. */
    if (this.options.nbColumns < 0) {
        this.options.nbColumns = this.table.tHead.rows[0].cells.length;
    }

    /* Create the base for pagination. */
    this.pagingDivs = document.querySelectorAll(this.options.pagingDivSelector);
    for (var i = 0; i < this.pagingDivs.length; ++i) {
        var div = this.pagingDivs[i];
        this.addClass(div, 'pagination-datatables');
        this.addClass(div, this.options.pagingDivClass);
        var ul = document.createElement('ul');
        this.addClass(ul, this.options.pagingListClass);
        div.appendChild(ul);
    }
    this.pagingLists = document.querySelectorAll(this.options.pagingDivSelector + ' ul');
    this.counterDivs = document.querySelectorAll(this.options.counterDivSelector);
    this.loadingDiv = document.querySelector(this.options.loadingDivSelector);

    /* DATA ! */

    var dataTable = this;

    if (!this.table.tHead) {
        this.table.tHead = document.createElement('thead');
        this.table.appendChild(this.table.rows[0]);
    }

    /* Compatibility issue (forceStrings => No defined data types). */
    if (this.options.forceStrings) {
        this.options.dataTypes = false ;
    }

    var ths = this.table.tHead.rows[0].cells ;

    if (!this.table.tBodies[0]) {
        this.table.tBodies[0] = document.createElement('tbody');
    }

    if (this.options.data instanceof Array) {
        this.data = this.options.data;
    }
    else if (this.options.data instanceof Object) {
        var ajaxOptions = DataTable.defaultAjaxOptions;
        for (var k in this.options.data) {
            ajaxOptions[k] = this.options.data[k];
        }
        this.options.data = ajaxOptions;
        var fetchData = true;
        if (fetchData) {
            if (this.table.dataset.size !== undefined) {
                this.options.data.size = parseInt(this.table.dataset.size, 10);
            }
            this.data = [];
            if (this.options.data.size !== undefined) {
                this.loadingDiv.innerHTML = '<div class="progress datatable-load-bar">'
                    + '<div class="progress-bar progress-bar-striped active" style="width: 0%;">'
                    + '</div></div>';
                if (this.options.data.allInOne) {
                    this.getAjaxDataAsync(true);
                }
                else {
                    for (var i = 0; i < this.options.data.size;
                         i += this.options.pageSize * this.options.pagingNumberOfPages) {
                        this.getAjaxDataAsync(i);
                    }
                }
            }
            else {
                this.loadingDiv.innerHTML = '<div class="progress datatable-load-bar">'
                    + '<div class="progress-bar progress-bar-striped active" style="width: 0%;">'
                    + '</div></div>';
                this.getAjaxDataAsync(0, true);
            }
        }
    }
    else if (this.table.tBodies[0].rows.length > 0) {
        this.data = [];
        var rows = this.table.tBodies[0].rows;
        var nCols = rows[0].cells.length ;
        for (var i = 0; i < rows.length; ++i) {
            this.data.push ([]) ;
        }
        for (var j = 0 ; j < nCols ; ++j) {
            var dt = function (x) { return x ; } ;
            if (this.options.dataTypes instanceof Array) {
                switch (this.options.dataTypes[j]) {
                case 'int':
                    dt = parseInt ;
                    break ;
                case 'float':
                case 'double':
                    dt = parseFloat ;
                    break ;
                case 'date':
                case 'datetime':
                    dt = function (x) { return new Date(x) ; } ;
                    break ;
                case false:
                case true:
                case 'string':
                case 'str':
                    dt = function (x) { return x ; } ;
                    break ;
                default:
                    dt = this.options.dataTypes[j] ;
                }
            }
            for (var i = 0; i < rows.length; ++i) {
                this.data[i].push(dt(rows[i].cells[j].innerHTML.trim())) ;
            }
        }
        if (this.options.dataTypes === true) {
            for (var c = 0; c < this.data[0].length; ++c) {
                var isNumeric = true;
                for (var i = 0; i < this.data.length; ++i) {
                    if (this.data[i][c] !== ""
                        && !((this.data[i][c] - parseFloat(this.data[i][c]) + 1) >= 0)) {
                        isNumeric = false;
                    }
                }
                if (isNumeric) {
                    for (var i = 0; i < this.data.length; ++i) {
                        if (this.data[i][c] !== "") {
                            this.data[i][c] = parseFloat(this.data[i][c]);
                        }
                    }
                }
            }
        }
    }

    /* Add sorting class to all th and add callback. */
    this.createSort();

    /* Add filter where it's needed. */
    this.createFilter();

    this.triggerSort();
    this.filter();

};

DataTable.prototype = {

    constructor: DataTable,

    /**
     *
     * Add the specified class(es) to the specified DOM Element.
     *
     * @param node The DOM Element
     * @param classes (Array or String)
     *
     **/
    addClass: function (node, classes) {
        if (typeof classes === "string") {
            classes = classes.split(' ');
        }
        classes.forEach(function (c) {
            if (!c) { return; }
            node.classList.add(c);
        });
    },

    /**
     *
     * Remove the specified node from the DOM.
     *
     * @param node The node to removes
     *
     **/
    removeNode: function (node) {
        if (node) {
            node.parentNode.removeChild(node);
        }
    },

    /**
     *
     * Clear size option and set timeout (if specified) for refresh.
     *
     * Note: This function should be call when a ajax loading is finished.
     *
     * @update refreshTimeOut The new timeout
     *
     **/
    setRefreshTimeout: function () {
        if (this.options.data.refresh) {
            clearTimeout(this.refreshTimeOut);
            this.refreshTimeOut = setTimeout((function (datatable) {
                return function () { datatable.getAjaxDataAsync(0, true); };
            })(this), this.options.data.refresh);
        }
    },

    /**
     *
     * Hide the loading divs.
     *
     **/
    hideLoadingDivs: function () {
        this.removeNode(this.loadingDiv);
    },


    /**
     *
     * Update the loading divs with the current % of data load (according
     * to this.options.data.size).
     *
     * Note: Call setRefreshTimeout & hideLoadingDivs if all the data have been loaded.
     *
     **/
    updateLoadingDivs: function () {
        if (this.data.length >= this.options.data.size) {
            this.setRefreshTimeout();
            this.hideLoadingDivs();
        }
        else {
            this.loadingDiv.querySelector('div.progress .progress-bar').style.width =
                parseInt(100 * this.data.length / this.options.data.size, 10) + '%';
        }
    },

    /**
     *
     * Get data according to this.options.data, asynchronously, using recursivity.
     *
     * @param start The first offset to send to the server
     *
     * @update data Concat data received from server to old data
     *
     * Note: Each call increment start by pageSize * pagingNumberOfPages.
     *
     **/
    getAjaxDataAsync: function (start, recursive) {
        if (typeof recursive === "undefined") { recursive = false; }
        if (recursive && typeof this.syncData === "undefined") {
            this.syncData = {
                data: [],
                toAdd: [],
                toUpdate: {},
                toDelete: []
            };
        }
        var xhr = new XMLHttpRequest();
        xhr.timeout = this.options.data.timeout;
        xhr.onreadystatechange = function (datatable, start, recursive) {
            return function () {
                if (this.readyState == 4) {
                    switch (this.status) {
                    case 200:
                        if (recursive) {
                            if (this.response.length > 0) {
                                datatable.syncData.data =
                                    datatable.syncData.data.concat(this.response);
                                datatable.getAjaxDataAsync(start + datatable.options.pageSize * datatable.options.pagingNumberOfPages, true);
                            }
                            else {
                                var syncData = datatable.syncData;
                                delete datatable.syncData;
                                datatable.data = syncData.data;
                                datatable.addRows(syncData.toAdd);
                                syncData.toDelete.forEach(function (e) {
                                    if (e instanceof Function) {
                                        datatable.deleteAll(e);
                                    }
                                    else {
                                        datatable.deleteRow(e);
                                    }
                                });
                                for (var id in syncData.toUpdate) {
                                    datatable.updateRow(id, syncData.toUpdate[id]);
                                }

                                datatable.sort(true);
                                datatable.setRefreshTimeout();
                            }
                        }
                        else {
                            datatable.data = datatable.data.concat(this.response);
                            datatable.updateLoadingDivs();
                            datatable.sort(true);
                        }
                        break;
                    case 404:
                    case 500:
                        console.log("ERROR: " + this.status + " - " + this.statusText);
                        console.log(xhr);
                        break;
                    default:
                        datatable.getAjaxDataAsync(start, recursive);
                        break;
                    }
                }
            }
        } (this, start, recursive);
        var url      = this.options.data.url ;
        var limit    = this.options.pageSize * this.options.pagingNumberOfPages ;
        var formdata = new FormData();
        if (start !== true) {
            if (this.options.data.type.toUpperCase() == 'GET') {
                url += '?start=' + start + '&limit=' + limit ;
            }
            else {
                formdata.append('offset', start);
                formdata.append('limit', limit);
            }
        }
        xhr.open(this.options.data.type, url, true);
        xhr.responseType = 'json';
        xhr.send(formdata);
    },

    /**
     *
     * @return The last page number according to options.pageSize and
     * current number of filtered elements.
     *
     **/
    getLastPageNumber: function () {
        return parseInt(Math.ceil(this.filterIndex.length / this.options.pageSize), 10);
    },

    createPagingLink: function (content, page, disabled) {
        var link = document.createElement('a');

        // Check options...
        if (this.options.pagingLinkClass) {
            link.classList.add(this.options.pagingLinkClass);
        }

        if (this.options.pagingLinkHref) {
            link.href = this.options.pagingLinkHref;
        }

        if (this.options.pagingLinkDisabledTabIndex !== false && disabled) {
            link.tabIndex = this.options.pagingLinkDisabledTabIndex;
        }

        link.dataset.page = page;
        link.innerHTML = content;
        return link;
    },

    /**
     *
     * Update the paging divs.
     *
     **/
    updatePaging: function () {

        /* Be carefull if you change something here, all this part calculate the first
           and last page to display. I choose to center the current page, it's more beautiful... */

        var nbPages = this.options.pagingNumberOfPages;
        var dataTable = this;
        var cp = parseInt(this.currentStart / this.options.pageSize, 10) + 1;
        var lp = this.getLastPageNumber();
        var start;
        var end;
        var first = this.filterIndex.length ? this.currentStart + 1 : 0;
        var last = (this.currentStart + this.options.pageSize) > this.filterIndex.length ?
            this.filterIndex.length : this.currentStart + this.options.pageSize;

        if (cp < nbPages / 2) {
            start = 1;
        }
        else if (cp >= lp - nbPages / 2) {
            start = lp - nbPages + 1;
            if (start < 1) {
                start = 1;
            }
        }
        else {
            start = parseInt(cp - nbPages / 2 + 1, 10);
        }

        if (start + nbPages < lp + 1) {
            end = start + nbPages - 1;
        }
        else {
            end = lp;
        }

        /* Juste iterate over each paging list and append li to ul. */

        for (var i = 0; i < this.pagingLists.length; ++i) {
            var childs = [];
            if (dataTable.options.firstPage) {
                var li = document.createElement('li');
                li.appendChild(dataTable.createPagingLink(
                    dataTable.options.firstPage, "first", cp === 1
                ));
                if (cp === 1) { li.classList.add('active'); }
                childs.push(li);
            }
            if (dataTable.options.prevPage) {
                var li = document.createElement('li');
                li.appendChild(dataTable.createPagingLink(
                    dataTable.options.prevPage, "prev", cp === 1
                ));
                if (cp === 1) { li.classList.add('active'); }
                childs.push(li);
            }
            if (dataTable.options.pagingPages) {
                var _childs = this.options.pagingPages.call(this.table, start,
                                                            end, cp, first, last);
                if (_childs instanceof Array) {
                    childs = childs.concat(_childs);
                }
                else {
                    childs.push(_childs);
                }
            }
            else {
                for (var k = start; k <= end; k++) {
                    var li = document.createElement('li');
                    li.appendChild(dataTable.createPagingLink(
                        k, k, cp === k
                    ));
                    if (k === cp) { li.classList.add('active'); }
                    childs.push(li);
                }
            }
            if (dataTable.options.nextPage) {
                var li = document.createElement('li');
                li.appendChild(dataTable.createPagingLink(
                    dataTable.options.nextPage, "next", cp === lp || lp === 0
                ));
                if (cp === lp || lp === 0) { li.classList.add('active'); }
                childs.push(li);
            }
            if (dataTable.options.lastPage) {
                var li = document.createElement('li');
                li.appendChild(dataTable.createPagingLink(
                    dataTable.options.lastPage, "last", cp === lp || lp === 0
                ));
                if (cp === lp || lp === 0) { li.classList.add('active'); }
                childs.push(li);
            }
            this.pagingLists[i].innerHTML = '';
            childs.forEach(function (e) {
                if (dataTable.options.pagingItemClass) {
                    e.classList.add(dataTable.options.pagingItemClass);
                }
                if (e.childNodes.length > 0) {
                    e.childNodes[0].addEventListener('click', function (event) {
                        event.preventDefault();
                        if (this.parentNode.classList.contains('active') ||
                            typeof this.dataset.page === 'undefined') {
                            return;
                        }
                        switch (this.dataset.page) {
                        case 'first':
                            dataTable.loadPage(1);
                            break;
                        case 'prev':
                            dataTable.loadPage(cp - 1);
                            break;
                        case 'next':
                            dataTable.loadPage(cp + 1);
                            break;
                        case 'last':
                            dataTable.loadPage(lp);
                            break;
                        default:
                            dataTable.loadPage(parseInt(parseInt(this.dataset.page), 10));
                        }
                    }, false);
                }
                this.pagingLists[i].appendChild(e);
            }, this);
        }

    },

    /**
     *
     * Update the counter divs.
     *
     **/
    updateCounter: function () {
        var cp = this.filterIndex.length ?
            parseInt(this.currentStart / this.options.pageSize, 10) + 1 : 0;
        var lp = parseInt(Math.ceil(this.filterIndex.length / this.options.pageSize), 10);
        var first = this.filterIndex.length ? this.currentStart + 1 : 0;
        var last = (this.currentStart + this.options.pageSize) > this.filterIndex.length ?
            this.filterIndex.length : this.currentStart + this.options.pageSize;
        for (var i = 0; i < this.counterDivs.length; ++i) {
            this.counterDivs[i].innerHTML = this.options.counterText.call(this.table, cp, lp,
                                                                          first, last,
                                                                          this.filterIndex.length,
                                                                          this.data.length);
        }
    },

    /**
     *
     * @return The sort function according to options.sort, options.sortKey & options.sortDir.
     *
     * Note: This function could return false if no sort function can be generated.
     *
     **/
    getSortFunction: function () {
        if (this.options.sort === false) {
            return false;
        }
        if (this.options.sort instanceof Function) {
            return this.options.sort;
        }
        if (this.data.length === 0 || !(this.options.sortKey in this.data[0])) {
            return false;
        }
        var key = this.options.sortKey;
        var asc = this.options.sortDir === 'asc';
        if (this.options.sort[key] instanceof Function) {
            return function (s) {
                return function (a, b) {
                    var vala = a[key], valb = b[key];
                    return asc ? s(vala, valb) : -s(vala, valb);
                };
            } (this.options.sort[key]);
        }
        return function (a, b) {
            var vala = a[key], valb = b[key];
            if (vala > valb) { return asc ? 1 : -1; }
            if (vala < valb) { return asc ? -1 : 1; }
            return 0;
        };
    },

    /**
     *
     * Destroy the filters (remove the filter line).
     *
     **/
    destroyFilter: function () {
        this.removeNode(this.table.querySelector('.datatable-filter-line'));
    },

    /**
     *
     * Change the text input filter placeholder according to this.options.filterText.
     *
     **/
    changePlaceHolder: function () {
        var placeholder = this.options.filterText ? this.options.filterText : '';
        var inputTexts = this.table.querySelectorAll('.datatable-filter-line input[type="text"]');
        for (var i = 0; i < inputTexts.length; ++i) {
            inputTexts[i].placeholder = placeholder;
        }
    },

    /**
     *
     * Create a text filter for the specified field.
     *
     * @param field The field corresponding to the filter
     *
     * @update filters Add the new filter to the list of filter (calling addFilter)
     *
     * @return The input filter
     *
     **/
    createTextFilter: function (field) {
        var opt = this.options.filters[field];
        var input = opt instanceof HTMLInputElement ? opt : document.createElement('input');
        input.type = 'text';
        if (this.options.filterText) {
            input.placeholder = this.options.filterText;
        }
        this.addClass(input, 'datatable-filter datatable-input-text');
        input.dataset.filter = field;
        this.filterVals[field] = '';
        var typewatch = (function () {
            var timer = 0;
            return function (callback, ms) {
                clearTimeout(timer);
                timer = setTimeout(callback, ms);
            };
        })();
        input.onkeyup = function (datatable) {
            return function () {
                var val = this.value.toUpperCase();
                var field = this.dataset.filter;
                typewatch(function () {
                    datatable.filterVals[field] = val;
                    datatable.filter();
                }, 300);
            };
        } (this);
        input.onkeydown = input.onkeyup;
        var regexp = opt === 'regexp' || input.dataset.regexp;
        if (opt instanceof Function) {
            this.addFilter(field, opt);
        }
        else if (regexp) {
            this.addFilter(field, function (data, val) {
                return new RegExp(val).test(String(data));
            });
        }
        else {
            this.addFilter(field, function (data, val) {
                return String(data).toUpperCase().indexOf(val) !== -1;
            });
        }
        this.addClass(input, this.options.filterInputClass);
        return input;
    },

    /**
     * Check if the specified value is in the specified array, without strict type checking.
     *
     * @param val The val to search
     * @param arr The array
     *
     * @return true if the value was found in the array
     **/
    _isIn: function (val, arr) {
        var found = false;
        for (var i = 0; i < arr.length && !found; ++i) {
            found = arr[i] == val;
        }
        return found;
    },

    /**
     * Return the index of the specified element in the object.
     *
     * @param v
     * @param a
     *
     * @return The index, or -1
     **/
    _index: function (v, a) {
        if (a === undefined || a === null) {
            return -1;
        }
        var index = -1;
        for (var i = 0; i < a.length && index == -1; ++i) {
            if (a[i] === v) index = i;
        }
        return index;
    },

    /**
     * Return the keys of the specified object.
     *
     * @param obj
     *
     * @return The keys of the specified object.
     **/
    _keys: function (obj) {
        if (obj === undefined || obj === null) {
            return undefined;
        }
        var keys = [];
        for (var k in obj) {
            if (obj.hasOwnProperty(k)) keys.push(k);
        }
        return keys;
    },

    /**
     *
     * Create a select filter for the specified field.
     *
     * @param field The field corresponding to the filter
     *
     * @update filters Add the new filter to the list of filter (calling addFilter)
     *
     * @return The select filter.
     *
     **/
    createSelectFilter: function (field) {
        var opt = this.options.filters[field];
        var values = {}, selected = [], multiple = false,
            empty = true, emptyValue = this.options.filterEmptySelect;
        var tag = false;
        if (opt instanceof HTMLSelectElement) {
            tag = opt;
        }
        else if (opt instanceof Object && 'element' in opt && opt.element) {
            tag = opt.element;
        }
        if (opt instanceof HTMLSelectElement || opt === 'select') {
            values = this.getFilterOptions(field);
        }
        else {
            multiple = ('multiple' in opt) && (opt.multiple === true);
            empty = ('empty' in opt) && opt.empty;
            emptyValue = (('empty' in opt) && (typeof opt.empty === 'string')) ?
                opt.empty : this.options.filterEmptySelect;
            if ('values' in opt) {
                if (opt.values === 'auto') {
                    values = this.getFilterOptions(field);
                }
                else {
                    values = opt.values;
                }
                if ('default' in opt) {
                    selected = opt['default'];
                }
                else if (multiple) {
                    selected = [];
                    for (var k in values) {
                        if (values[k] instanceof Object) {
                            selected = selected.concat(this._keys(values[k]));
                        }
                        else {
                            selected.push(k);
                        }
                    }
                }
                else {
                    selected = [];
                }
                if (!(selected instanceof Array)) {
                    selected = [selected];
                }
            }
            else {
                values = opt;
                selected = multiple ? this._keys(values) : [];
            }
        }
        var select = tag ? tag : document.createElement('select');
        if (multiple) {
            select.multiple = true;
        }
        if (opt['default']) {
            select.dataset['default'] = opt['default'];
        }
        this.addClass(select, 'datatable-filter datatable-select');
        select.dataset.filter = field;
        if (empty) {
            var option = document.createElement('option');
            option.dataset.empty = true;
            option.value = "";
            option.innerHTML = emptyValue;
            select.appendChild(option);
        }
        var allKeys = [];
        for (var key in values) {
            if (values[key] instanceof Object) {
                var optgroup = document.createElement('optgroup');
                optgroup.label = key;
                for (var skey in values[key]) {
                    if (values[key].hasOwnProperty(skey)) {
                        allKeys.push(skey);
                        var option = document.createElement('option');
                        option.value = skey;
                        option.selected = this._isIn(skey, selected);
                        option.innerHTML = values[key][skey];
                        optgroup.appendChild(option);
                    }
                }
                select.appendChild(optgroup);
            }
            else {
                allKeys.push(key);
                var option = document.createElement('option');
                option.value = key;
                option.selected = this._isIn(key, selected);
                option.innerHTML = values[key];
                select.appendChild(option);
            }
        }
        var val = select.value;
        if (multiple) {
            val = [];
            for (var i = 0; i < select.options.length; ++i) {
                if (select.options[i].selected) { val.push(select.options[i].value); }
            }
        }
        this.filterVals[field] = multiple ? val : ((empty && !val) ? allKeys : [val]);
        select.onchange = function (allKeys, multiple, empty, datatable) {
            return function () {
                var val = this.value;
                if (multiple) {
                    val = [];
                    for (var i = 0; i < this.options.length; ++i) {
                        if (this.options[i].selected) { val.push(this.options[i].value); }
                    }
                }
                var field = this.dataset.filter;
                datatable.filterVals[field] = multiple ? val : ((empty && !val) ? allKeys : [val]);
                datatable.filter();
            };
        } (allKeys, multiple, empty, this);
        if (opt instanceof Object && opt.fn instanceof Function) {
            this.addFilter(field, opt.fn);
            select.dataset.filterType = 'function';
        }
        else {
            this.addFilter(field, function (aKeys, datatable) {
                return function (data, val) {
                    if (!val) { return false; }
                    if (val == aKeys && !data) { return true; }
                    return datatable._isIn(data, val);
                };
            } (allKeys, this));
            select.dataset.filterType = 'default';
        }
        this.addClass(select, this.options.filterSelectClass);
        return select;
    },

    /**
     *
     * Create the filter line according to options.filters.
     *
     **/
    createFilter: function () {
        this.filters = [];
        this.filterTags = [];
        this.filterVals = [];
        // Speical options if '*'
        if (this.options.filters === '*') {
            var nThs = this.table.tHead.rows[0].cells.length;
            this.options.filters = [];
            while (nThs--) {
                this.options.filters.push(true);
            }
        }
        if (this.options.filters) {
            var filterLine = false;
            var tr = document.createElement('tr');
            tr.classList.add('datatable-filter-line');
            for (var field in this.options.filters) {
                if (this.options.filters.hasOwnProperty(field)) {
                    var td = document.createElement('td');
                    if (this.options.filters[field] !== false) {
                        var opt = this.options.filters[field];
                        var input = opt === true || opt === 'regexp' || opt === 'input' || opt instanceof Function || opt instanceof HTMLInputElement;
                        var filter = input ? this.createTextFilter(field) : this.createSelectFilter(field);
                        this.filterTags[field] = filter;
                        if (!document.body.contains(filter)) {
                            td.classList.add('datatable-filter-cell');
                            td.appendChild(filter);
                        }
                    }
                    if (!(this.options.filters[field] instanceof Object) || !this.options.filters[field].noColumn) {
                        tr.appendChild(td);
                    }
                }
            }
            if (tr.querySelectorAll('td.datatable-filter-cell').length > 0) {
                this.table.tHead.appendChild(tr);
            }
        }
    },

    /**
     *
     * Filter data and refresh.
     *
     * @param keepCurrentPage true if the current page should not be changed (on refresh
     *      for example), if not specified or false, the current page will be set to 0.
     *
     * @update filterIndex Will contain the new filtered indexes
     * @update currentStart The new starting point
     *
     **/
    filter: function (keepCurrentPage) {
        if (typeof keepCurrentPage === 'undefined') {
            keepCurrentPage = false;
        }
        var oldCurrentStart = this.currentStart;
        this.currentStart = 0;
        this.filterIndex = [];
        for (var i = 0; i < this.data.length; i++) {
            if (this.checkFilter(this.data[i])) { this.filterIndex.push(i); }
        }
        if (keepCurrentPage) {
            this.currentStart = oldCurrentStart;
            while (this.currentStart >= this.filterIndex.length) {
                this.currentStart -= this.options.pageSize;
            }
            if (this.currentStart < 0) {
                this.currentStart = 0;
            }
        }
        if (this.options.filterSelectOptions && this.filterIndex.length > 0) {
            var dtable = this;
            var allKeys = [];
            for (var j = 0; j < this.data[0].length; ++j) {
                allKeys.push({});
            }
            for (var i = 0; i < this.filterIndex.length; ++i) {
                var row = this.data[this.filterIndex[i]];
                for (var j = 0; j < row.length; ++j) {
                    allKeys[j][row[j]] = true;
                }
            }
            for (var k = 0; k < allKeys.length; ++k) {
                var keys = this._keys(allKeys[k]);
                if (this.filterTags[k]
                    && this.filterTags[k] instanceof HTMLSelectElement
                    && this.filterTags[k].dataset.filterType == 'default') {
                    var options = this.filterTags[k].childNodes;
                    for (var i = 0; i < options.length; ++i) {
                        if (!options[i].dataset.empty) {
                            options[i].style.display = dtable._isIn(options[i].value, keys)
                                ? 'block' : 'none';
                        }
                    }
                }
            }
        }
        this.refresh();
    },


    /**
     *
     * Reset all filters.
     *
     **/
    resetFilters: function () {
        var dtable = this;
        this.filterTags.forEach(function (e) {
            var field = e.dataset.filter;
            if (e instanceof HTMLInputElement) {
                e.value = '';
                dtable.filterVals[field] = '';
            }
            else {
                if (e.multiple) {
                    var allKeys = [];
                    for (var i = 0; i < e.childNodes.length; ++i) {
                        e.childNodes[i].selected = true;
                        allKeys.push(e.childNodes[i].value);
                    }
                    dtable.filterVals[field] = allKeys;
                }
                else if (e.dataset['default']
                         && e.querySelector('option[value="' + e.dataset['default'] + '"]').length > 0) {
                    for (var i = 0; i < e.childNodes.length; ++i) {
                        e.childNodes[i].selected = e.childNodes[i].value == e.dataset['default'];
                    }
                    dtable.filterVals[field] = [e.dataset['default']];
                }
                else if (e.childNodes.length > 0) {
                    e.childNodes[0].selected = true;
                    for (var i = 1; i < e.childNodes.length; ++i) {
                        e.childNodes[i].selected = false;
                    }
                    if (e.childNodes[0].dataset.empty) {
                        var allKeys = [];
                        for (var i = 1; i < e.childNodes.length; ++i) {
                            allKeys.push(e.childNodes[i].value);
                        }
                        dtable.filterVals[field] = allKeys;
                    }
                    else {
                        dtable.filterVals[field] = [e.childNodes[0].value];
                    }
                }
            }
        });
        this.filter();
    },

    /**
     * Strip HTML tags for the specified string.
     *
     * @param str The string from which tags must be stripped.
     *
     * @return The string with HTML tags removed.
     *
     **/
    stripTags: function (str) {
        var e = document.createElement('div');
        e.innerHTML = str;
        return e.textContent || e.innerText;
    },

    /**
     *
     * Check if the specified data match the filters according to this.filters
     * and this.filterVals.
     *
     * @param data The data to check
     *
     * @return true if the data match the filters, false otherwise
     *
     **/
    checkFilter: function (data) {
        var ok = true;
        for (var fk in this.filters) {
            var currentData = fk[0] === '_' ? data : data[fk];
            if (typeof currentData === "string") {
                currentData = this.stripTags(currentData);
            }
            if (!this.filters[fk](currentData, this.filterVals[fk])) {
                ok = false;
                break;
            }
        }
        return ok;
    },

    /**
     *
     * Add a new filter.
     *
     * @update filters
     *
     **/
    addFilter: function (field, filter) {
        this.filters[field] = filter;
    },

    /**
     *
     * Get the filter select options for a specified field according
     * to this.data.
     *
     * @return The options found.
     *
     **/
    getFilterOptions: function (field) {
        var options = {}, values = [];
        for (var key in this.data) {
            if (this.data[key][field] !== '') {
                values.push(this.data[key][field]);
            }
        }
        values.sort();
        for (var i in values) {
            if (values.hasOwnProperty(i)) {
                var txt = this.stripTags(values[i]);
                options[txt] = txt;
            }
        }
        return options;
    },

    /**
     *
     * Remove class, data and event on sort headers.
     *
     **/
    destroySort: function () {
        $('thead th').removeClass('sorting sorting-asc sorting-desc')
            .unbind('click.datatable')
            .removeData('sort');
    },

    /**
     *
     * Add class, event & data to headers according to this.options.sort or data-sort attribute
     * of headers.
     *
     * @update options.sort Will be set to true if not already and a data-sort attribute is found.
     *
     **/
    createSort: function () {
        var dataTable = this;
        if (!(this.options.sort instanceof Function)) {

            var countTH = 0;
            var ths = this.table.tHead.rows[0].cells;
            for (var i = 0; i < ths.length; ++i) {

                if (ths[i].dataset.sort) {
                    dataTable.options.sort = true;
                }
                else if (dataTable.options.sort === '*') {
                    ths[i].dataset.sort = countTH;
                }
                else {
                    var key;
                    if (dataTable.options.sort instanceof Array) {
                        key = countTH;
                    }
                    else if (dataTable.options.sort instanceof Object) {
                        key = dataTable._keys(dataTable.options.sort)[countTH];
                    }
                    if (key !== undefined && dataTable.options.sort[key]) {
                        ths[i].dataset.sort = key;
                    }
                }

                if (ths[i].dataset.sort !== undefined) {
                    ths[i].classList.add('sorting');
                }

                countTH++;

                ths[i].addEventListener('click', function () {
                    if (this.dataset.sort) {
                        if (this.classList.contains('sorting-asc')) {
                            dataTable.options.sortDir = 'desc';
                            this.classList.remove('sorting-asc')
                            this.classList.add('sorting-desc');
                        }
                        else if (this.classList.contains('sorting-desc')) {
                            dataTable.options.sortDir = 'asc';
                            this.classList.remove('sorting-desc');
                            this.classList.add('sorting-asc');
                        }
                        else {
                            var oths = this.parentNode.cells;
                            for (var j = 0; j < oths.length; j++) {
                                oths[j].classList.remove('sorting-desc');
                                oths[j].classList.remove('sorting-asc');
                            }
                            dataTable.options.sortDir = 'asc';
                            dataTable.options.sortKey = this.dataset.sort;
                            this.classList.add('sorting-asc');
                        }
                        dataTable.sort();
                        dataTable.refresh();
                    }
                }, false);

            }

        }
    },

    /**
     *
     * Trigger sort event on the table: If options.sort is a function,
     * sort the table, otherwize trigger click on the column specifid by options.sortKey.
     *
     **/
    triggerSort: function () {
        if (this.options.sort instanceof Function) {
            this.sort();
            this.refresh();
        }
        else if (this.options.sortKey !== false) {
            var ths = this.table.tHead.rows[0].cells;
            var th;
            for (var j = 0; j < ths.length; j++) {
                ths[j].classList.remove('sorting-desc');
                ths[j].classList.remove('sorting-asc');
                if (ths[j].dataset.sort === this.options.sortKey) {
                    th = ths[j];
                }
            }
            if (th !== undefined) {
                th.classList.add('sorting-' + this.options.sortDir);
                this.sort();
                this.refresh();
            }
        }
    },

    /**
     *
     * Sort the data.
     *
     * @update data
     *
     **/
    sort: function (keepCurrentPage) {
        var fnSort = this.getSortFunction();
        if (fnSort !== false) {
            this.data.sort(fnSort);
        }
        this.filter(keepCurrentPage);
    },

    /**
     *
     * Try to identify the specified data with the specify identifier according
     * to this.options.identify.
     *
     * @return true if the data match, false otherwize
     *
     **/
    identify: function (id, data) {
        if (this.options.identify === false) {
            return false;
        }
        if (this.options.identify instanceof Function) {
            return this.options.identify(id, data);
        }
        return data[this.options.identify] == id;
    },

    /**
     *
     * Find the index of the first element matching id in the data array.
     *
     * @param The id to find (will be match according to this.options.identify)
     *
     * @return The index of the first element found, or -1 if no element is found
     *
     **/
    indexOf: function (id) {
        var index = -1;
        for (var i = 0; i < this.data.length && index === -1; i++) {
            if (this.identify(id, this.data[i])) {
                index = i;
            }
        }
        return index;
    },

    /**
     *
     * Get an elements from the data array.
     *
     * @param id An identifier for the element (see this.options.identify)
     *
     **/
    row: function (id) {
        if (this.options.identify === true) {
            return this.data[id];
        }
        return this.data[this.indexOf(id)];
    },

    /**
     *
     * Retrieve all data.
     *
     *
     **/
    all: function (filter) {
        if (typeof filter === "undefined"
            || filter === true) {
            return this.data;
        }
        var returnData = [];
        for (var i = 0; i < this.data.length; ++i) {
            if (filter(this.data[i])) {
                returnData.push(this.data[i]);
            }
        }
        return returnData;
    },

    /**
     *
     * Add an element to the data array.
     *
     * @param data The element to add
     *
     * @update data
     *
     **/
    addRow: function (data) {
        this.data.push(data);
        if (typeof this.syncData !== "undefined") {
            this.syncData.toAdd.push(data);
        }
        this.sort();
        this.filter();
        this.currentStart = parseInt(this._index(this._index(data, this.data),
                                                 this.filterIndex)
                                     / this.options.pageSize, 10) * this.options.pageSize;
        this.refresh();
    },

    /**
     *
     * Add elements to the data array.
     *
     * @param data Array of elements to add
     *
     * @update data
     *
     **/
    addRows: function (data) {
        this.data = this.data.concat(data);
        if (typeof this.syncData !== "undefined") {
            this.syncData.toAdd = this.syncData.toAdd.concat(data);
        }
        this.sort();
        this.filter();
        this.currentStart = parseInt(this._index(this._index(data, this.data),
                                                 this.filterIndex)
                                     / this.options.pageSize, 10) * this.options.pageSize;
        this.refresh();
    },

    /**
     *
     * Remove an element from the data array.
     *
     * @param id An identifier for the element (see this.options.identify)
     *
     **/
    deleteRow: function (id) {
        var oldCurrentStart = this.currentStart;
        var index = this.indexOf(id);
        if (index === -1) {
            console.log('No data found with id: ' + id);
            return;
        }
        this.data.splice(index, 1);
        if (typeof this.syncData !== "undefined") {
            this.syncData.toDelete.push(id);
        }
        this.filter();
        if (oldCurrentStart < this.filterIndex.length) {
            this.currentStart = oldCurrentStart;
        }
        else {
            this.currentStart = oldCurrentStart - this.options.pageSize;
            if (this.currentStart < 0) { this.currentStart = 0; }
        }
        this.refresh();
    },

    /**
     *
     * Delete all elements matching the filter arg.
     *
     **/
    deleteAll: function (filter) {
        var oldCurrentStart = this.currentStart
        var newData = [];
        if (typeof this.syncData !== "undefined") {
            this.syncData.toDelete.push(filter);
        }
        for (var i = 0; i < this.data.length; ++i) {
            if (!filter(this.data[i])) {
                newData.push(this.data[i]);
            }
        }
        this.data = newData;
        this.filter();
        if (oldCurrentStart < this.filterIndex.length) {
            this.currentStart = oldCurrentStart;
        }
        else {
            this.currentStart = oldCurrentStart - this.options.pageSize;
            if (this.currentStart < 0) { this.currentStart = 0; }
        }
        this.refresh();
    },

    /**
     *
     * Update an element in the data array. Will add the element if it is not found.
     *
     * @param id An identifier for the element (see this.options.identify)
     * @param data The new data (identifier value will be set to id)
     *
     **/
    updateRow: function (id, data) {
        var index = this.indexOf(id);
        if (typeof this.syncData !== "undefined") {
            this.syncData.toUpdate[id] = data;
        }
        if (index !== -1) {
            if (id in data) {
                delete data[id];
            }
            for (var key in this.data[index]) {
                if (key in data) {
                    this.data[index][key] = data[key];
                }
            }
            this.sort();
            this.filter();
            this.currentStart = parseInt(this._index(this.indexOf(id),
                                                     this.filterIndex)
                                         / this.options.pageSize, 10) * this.options.pageSize;
            this.refresh();
        }
    },

    /**
     *
     * Change the current page and refresh.
     *
     * @param page The number of the page to load
     *
     * @update currentStart
     *
     **/
    loadPage: function (page) {
        var oldPage = this.currentStart / this.options.pageSize;
        if (page < 1) {
            page = 1;
        }
        else if (page > this.getLastPageNumber()) {
            page = this.getLastPageNumber();
        }
        this.currentStart = (page - 1) * this.options.pageSize;
        this.refresh();
        this.options.onChange.call(this.table, oldPage + 1, page);
    },

    /**
     *
     * @return The current page
     *
     **/
    getCurrentPage: function () {
        return this.currentStart / this.options.pageSize + 1;
    },

    /**
     *
     * Refresh the page according to current page (DO NOT SORT).
     * This function call options.lineFormat.
     *
     **/
    refresh: function () {
        this.options.beforeRefresh.call(this.table);
        this.updatePaging();
        this.updateCounter();
        this.table.tBodies[0].innerHTML = "";
        if (this.currentStart >= this.currentDataLength) {
            this.table.tBodies[0].innerHTML = '<tr><td colspan="' + this.options.nbColumns + '">'
                + '<div class="progress progress-striped active">'
                + '<div class="bar" style="width: 100%;"></div>'
                + '</div></div></tr>';
            return;
        }
        for (var i = 0;
             i < this.options.pageSize && i + this.currentStart < this.filterIndex.length;
             i++) {
            var index = this.filterIndex[this.currentStart + i];
            var data  = this.data[index];
            this.table.tBodies[0].appendChild(this.options.lineFormat.call(this.table,
                                                                           index, data));
        }
        this.options.afterRefresh.call(this.table);
    },

    /**
     *
     * Set a option and refresh the table if necessary.
     *
     * @param key The name of the option to change
     * @param val The new option value
     *
     * @update options
     *
     **/
    setOption: function (key, val) {
        if (key in this.options) {
            this.options[key] = val;
            if (key === 'sort') {
                this.destroySort();
                this.createSort();
                this.triggerSort();
            }
            if (key === 'sortKey' || key === 'sortDir') {
                this.sort();
            }
            if (key === 'filters') {
                this.destroyFilter();
                this.createFilter();
            }
            if (key === 'filterText') {
                this.changePlaceHolder();
            }
            this.filter();
        }
    },

    /**
     *
     * Set a list of options and refresh the table if necessary.
     *
     * @param options A list of options to set (plain object)
     *
     * @update options
     *
     **/
    setOptions: function (options) {
        for (var key in options) {
            if (key in this.options) {
                this.options[key] = options[key];
            }
        }
        if ('sort' in options) {
            this.destroySort();
            this.createSort();
            this.triggerSort();
        }
        else if ('sortKey' in options || 'sortDir' in options) {
            this.sort();
        }
        if ('filters' in options) {
            this.destroyFilter();
            this.createFilter();
        }
        if ('filterText' in options) {
            this.changePlaceHolder();
        }
        this.filter();
    },

    /**
     *
     * Remove all the elements added by the datatable.
     *
     **/
    destroy: function () {
        if (this.refreshTimeOut !== undefined) {
            clearTimeout(this.refreshTimeOut);
        }
        this.destroySort();
        for (var i = 0; i < this.pagingDivs.length; ++i) {
            this.pagingDivs[i].classList.remove('pagination-datatable');
            this.pagingDivs[i].classList.remove(this.options.pagingDivClass);
            this.pagingDivs[i].innerHTML = '';
        }
        this.destroyFilter();
        this.table.classList.remove(this.options.tableClass);
        this.removeNode(this.table.tBodies[0]);
        this.table.appendChild(document.createElement('tbody'));
        for (var i = 0; i < this.data.length; i++) {
            var index = this.filterIndex[this.currentStart + i];
            var data  = this.data[index];
            this.table.tBodies[0].appendChild(this.options.lineFormat.call(this.table,
                                                                           index, data));
        }

    }
};

/**
 * Default option for DataTable.
 *
 */
DataTable.defaultOptions = {

    /**
     * Specify whether the type of the column should be deduced or not. If this option
     * is true, the type is not deduced (mainly here for backward compatibility).
     *
     * @see dataTypes
     */
    forceStrings: false,

    /**
     * Specify the class of the table.
     *
     */
    tableClass: 'datatable',

    /**
     * Specify the selector for the paging div element.
     *
     */
    pagingDivSelector: '.paging',

    /**
     * Specify the class for the paging div element.
     *
     */
    pagingDivClass: 'text-center',

    /**
     * Specify the class for the paging list element.
     *
     */
    pagingListClass: 'pagination',

    /**
     * Specify the class for the paging list item elements.
     *
     */
    pagingItemClass: '',

    /**
     * Specify the class for the paging list link elements.
     *
     */
    pagingLinkClass: '',

    /**
     * Specify the href attribute for the paging list link elements.
     *
     */
    pagingLinkHref: '',

    /**
     * Specify the tabindex attribute for the paging list link elements when
     * disabled.
     *
     */
    pagingLinkDisabledTabIndex: false,

    /**
     * Specify the selector for the counter div element.
     *
     * @see counterText
     */
    counterDivSelector: '.counter',

    /**
     * Specify the selector the loading div element.
     *
     * @see data
     */
    loadingDivSelector: '.loading',

    /**
     * Sepcify the sort options.
     *
     * @type boolean|string|Array|Object
     */
    sort: false,

    /**
     * Specify the default sort key.
     *
     * @type boolean|int|string.
     */
    sortKey: false,

    /**
     * Specify the default sort directions, 'asc' or 'desc'.
     *
     */
    sortDir: 'asc',

    /**
     * Specify the number of columns, a value of -1 (default) specify
     * the the number of columns should be retrieved for the <thead>
     * elements of the table.
     *
     */
    nbColumns: -1,

    /**
     * Specify the number of elements to display per page.
     *
     */
    pageSize: 20,

    /**
     * Specify the number of pages to display in the paging list element.
     *
     */
    pagingNumberOfPages: 9,

    /**
     * Specify the way of identifying items from the data array:
     *
     *   - if this option is false (default), no identification is provided.
     *   - if a Function is specified, the function is used to identify:
     *         function (id, item) -> boolean
     *   - if an int or a string is specified, items are identified by the
     *     value corresponding to the key.
     *
     * @type boolean|int|string|Function.
     *
     */
    identify: false,

    /**
     * Callback function when the table is updated.
     *
     */
    onChange: function (oldPage, newPage) { },

    /**
     * Function used to generate content for the counter div element.
     *
     */
    counterText: function (currentPage, totalPage,
                           firstRow, lastRow,
                           totalRow, totalRowUnfiltered) {
        var counterText = 'Page ' + currentPage + ' on ' + totalPage
            + '. Showing ' + firstRow + ' to ' + lastRow + ' of ' + totalRow + ' entries';
        if (totalRow != totalRowUnfiltered) {
            counterText += ' (filtered from ' + totalRowUnfiltered + ' total entries)';
        }
        counterText += '.';
        return counterText;
    },

    /**
     * Content of the paging item pointing to the first page.
     *
     */
    firstPage: '&lt;&lt;',

    /**
     * Content of the paging item pointing to the previous page.
     *
     */
    prevPage: '&lt;',

    /**
     *
     */
    pagingPages: false,

    /**
     * Content of the paging item pointing to the next page.
     *
     */
    nextPage: '&gt;',

    /**
     * Content of the paging item pointing to the last page.
     *
     */
    lastPage: '&gt;&gt;',

    /**
     * Specify the type of the columns:
     *
     *   - if false, the type is not deduced and values are treated as strings.
     *   - if true, the type is deduced automatically.
     *   - if an Array is specified, the type of each column is retrieve from the
     *     array values, possible values are 'int', 'float' <> 'double', 'date' <> 'datetime',
     *     false <> true <> 'string' <> 'str'. A function can also be specified to convert
     *     the value.
     *
     * @see forceStrings
     *
     */
    dataTypes: true,

    /**
     * Specify the filter options.
     *
     */
    filters: {},

    /**
     * Specify the placeholder for the textual input filters.
     */
    filterText: 'Search... ',

    /**
     * Specify the placeholder for the select input filters.
     */
    filterEmptySelect: '',

    /**
     *
     */
    filterSelectOptions: false,

    /**
     *
     */
    filterInputClass: 'form-control',

    /**
     *
     */
    filterSelectClass: 'form-control',

    /**
     * Callback function before the display is reloaded.
     *
     */
    beforeRefresh: function () { },

    /**
     * Callback function after the display has been reloaded.
     *
     */
    afterRefresh: function () { },

    /**
     * Function used to generate the row of the table.
     *
     */
    lineFormat: function (id, data) {
        var res = document.createElement('tr');
        res.dataset.id = id;
        for (var key in data) {
            if (data.hasOwnProperty(key)) {
                res.innerHTML += '<td>' + data[key] + '</td>';
            }
        }
        return res;
    }
};

DataTable.defaultAjaxOptions = {
    url: null,
    size: null,
    refresh: false,
    allInOne: false,
    timeout: 2000
};
