// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {

  Array.prototype.forEach = function(callback, thisArg) {

    var T, k;

    if (this === null) {
      throw new TypeError(' this is null or not defined');
    }

    // 1. Let O be the result of calling toObject() passing the
    // |this| value as the argument.
    var O = Object(this);

    // 2. Let lenValue be the result of calling the Get() internal
    // method of O with the argument "length".
    // 3. Let len be toUint32(lenValue).
    var len = O.length >>> 0;

    // 4. If isCallable(callback) is false, throw a TypeError exception.
    // See: http://es5.github.com/#x9.11
    if (typeof callback !== "function") {
      throw new TypeError(callback + ' is not a function');
    }

    // 5. If thisArg was supplied, let T be thisArg; else let
    // T be undefined.
    if (arguments.length > 1) {
      T = thisArg;
    }

    // 6. Let k be 0
    k = 0;

    // 7. Repeat, while k < len
    while (k < len) {

      var kValue;

      // a. Let Pk be ToString(k).
      //    This is implicit for LHS operands of the in operator
      // b. Let kPresent be the result of calling the HasProperty
      //    internal method of O with argument Pk.
      //    This step can be combined with c
      // c. If kPresent is true, then
      if (k in O) {

        // i. Let kValue be the result of calling the Get internal
        // method of O with argument Pk.
        kValue = O[k];

        // ii. Call the Call internal method of callback with T as
        // the this value and argument list containing kValue, k, and O.
        callback.call(T, kValue, k, O);
      }
      // d. Increase k by 1.
      k++;
    }
    // 8. return undefined
  };
}

// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {

  Array.prototype.map = function(callback, thisArg) {

    var T, A, k;

    if (this == null) {
      throw new TypeError(' this is null or not defined');
    }

    // 1. Let O be the result of calling ToObject passing the |this|
    //    value as the argument.
    var O = Object(this);

    // 2. Let lenValue be the result of calling the Get internal
    //    method of O with the argument "length".
    // 3. Let len be ToUint32(lenValue).
    var len = O.length >>> 0;

    // 4. If IsCallable(callback) is false, throw a TypeError exception.
    // See: http://es5.github.com/#x9.11
    if (typeof callback !== 'function') {
      throw new TypeError(callback + ' is not a function');
    }

    // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
    if (arguments.length > 1) {
      T = thisArg;
    }

    // 6. Let A be a new array created as if by the expression new Array(len)
    //    where Array is the standard built-in constructor with that name and
    //    len is the value of len.
    A = new Array(len);

    // 7. Let k be 0
    k = 0;

    // 8. Repeat, while k < len
    while (k < len) {

      var kValue, mappedValue;

      // a. Let Pk be ToString(k).
      //   This is implicit for LHS operands of the in operator
      // b. Let kPresent be the result of calling the HasProperty internal
      //    method of O with argument Pk.
      //   This step can be combined with c
      // c. If kPresent is true, then
      if (k in O) {

        // i. Let kValue be the result of calling the Get internal
        //    method of O with argument Pk.
        kValue = O[k];

        // ii. Let mappedValue be the result of calling the Call internal
        //     method of callback with T as the this value and argument
        //     list containing kValue, k, and O.
        mappedValue = callback.call(T, kValue, k, O);

        // iii. Call the DefineOwnProperty internal method of A with arguments
        // Pk, Property Descriptor
        // { Value: mappedValue,
        //   Writable: true,
        //   Enumerable: true,
        //   Configurable: true },
        // and false.

        // In browsers that support Object.defineProperty, use the following:
        // Object.defineProperty(A, k, {
        //   value: mappedValue,
        //   writable: true,
        //   enumerable: true,
        //   configurable: true
        // });

        // For best browser support, use the following:
        A[k] = mappedValue;
      }
      // d. Increase k by 1.
      k++;
    }

    // 9. return A
    return A;
  };
}

var JsonData = function (data) {
  this.length = data.length;

  this.slice = function(idxStart, idxEnd) {
    return data.slice(idxStart, idxEnd);
  };

  this.getRow = function(idxRow) {
    return data[idxRow];
  };
};

var ArrayData = function (data) {
  var keys = Object.keys(data);
  this.length = Object.keys(data[keys[0]]).length;

  this.slice = function(idxStart, idxEnd) {
    var slice = [];
    for (var idx = idxStart; idx < idxEnd; idx++)
      slice.push(this.getRow(idx));

    return slice;
  };

  this.getRow = function(idxRow) {
    var row = {};
    for (idxCol = 0; idxCol < keys.length; idxCol++) {
      row[keys[idxCol]] = data[keys[idxCol]][idxRow];
    }

    return row;
  };
};

var PagedTable = function (pagedTable) {
  var me = this;

  var source = function(pagedTable) {
    var sourceElems = [].slice.call(pagedTable.children).filter(function(e) {
      return e.hasAttribute("data-pagedtable-source");
    });

    if (sourceElems === null || sourceElems.length !== 1) {
      throw("A single data-pagedtable-source was not found");
    }

    return JSON.parse(sourceElems[0].innerHTML);
  }(pagedTable);

  var options = function(source) {
    var options = typeof(source.options) !== "undefined" &&
      source.options !== null ? source.options : {};

    var columns = typeof(options.columns) !== "undefined" ? options.columns : {};
    var rows = typeof(options.rows) !== "undefined" ? options.rows : {};

    var positiveIntOrNull = function(value) {
      return parseInt(value) >= 0 ? parseInt(value) : null;
    };

    return {
      pages: positiveIntOrNull(options.pages),
      rows: {
        min: positiveIntOrNull(rows.min),
        max: positiveIntOrNull(rows.max),
        total: positiveIntOrNull(rows.total)
      },
      columns: {
        min: positiveIntOrNull(columns.min),
        max: positiveIntOrNull(columns.max),
        total: positiveIntOrNull(columns.total)
      }
    };
  }(source);

  var Measurer = function() {

    // set some default initial values that will get adjusted in runtime
    me.measures = {
      padding: 12,
      character: 8,
      height: 15,
      defaults: true
    };

    me.calculate = function(measuresCell) {
      if (!me.measures.defaults)
        return;

      var measuresCellStyle = window.getComputedStyle(measuresCell, null);

      var newPadding = parsePadding(measuresCellStyle.paddingLeft) +
            parsePadding(measuresCellStyle.paddingRight);

      var sampleString = "ABCDEFGHIJ0123456789";
      var newCharacter = Math.ceil(measuresCell.clientWidth / sampleString.length);

      if (newPadding <= 0 || newCharacter <= 0)
        return;

      me.measures.padding = newPadding;
      me.measures.character = newCharacter;
      me.measures.height = measuresCell.clientHeight;
      me.measures.defaults = false;
    };

    return me;
  };

  var Page = function(data, options) {
    var me = this;

    var defaults = {
      max: 7,
      rows: 10
    };

    var totalPages = function() {
      return Math.ceil(data.length / me.rows);
    };

    me.number = 0;
    me.max = options.pages !== null ? options.pages : defaults.max;
    me.visible = me.max;
    me.rows = options.rows.min !== null ? options.rows.min : defaults.rows;
    me.total = totalPages();

    me.setRows = function(newRows) {
      me.rows = newRows;
      me.total = totalPages();
    };

    me.setPageNumber = function(newPageNumber) {
      if (newPageNumber < 0) newPageNumber = 0;
      if (newPageNumber >= me.total) newPageNumber = me.total - 1;

      me.number = newPageNumber;
    };

    me.setVisiblePages = function(visiblePages) {
      me.visible = Math.min(me.max, visiblePages);
      me.setPageNumber(me.number);
    };

    me.getVisiblePageRange = function() {
      var start = me.number - Math.max(Math.floor((me.visible - 1) / 2), 0);
      var end = me.number + Math.floor(me.visible / 2) + 1;
      var pageCount = me.total;

      if (start < 0) {
        var diffToStart = 0 - start;
        start += diffToStart;
        end += diffToStart;
      }

      if (end > pageCount) {
        var diffToEnd = end - pageCount;
        start -= diffToEnd;
        end -= diffToEnd;
      }

      start = start < 0 ? 0 : start;
      end = end >= pageCount ? pageCount : end;

      var first = false;
      var last = false;

      if (start > 0 && me.visible > 1) {
        start = start + 1;
        first = true;
      }

      if (end < pageCount && me.visible > 2) {
        end = end - 1;
        last = true;
      }

      return {
        first: first,
        start: start,
        end: end,
        last: last
      };
    };

    me.getRowStart = function() {
      var rowStart = page.number * page.rows;
      if (rowStart < 0)
        rowStart = 0;

      return rowStart;
    };

    me.getRowEnd = function() {
      var rowStart = me.getRowStart();
      return Math.min(rowStart + me.rows, data.length);
    };

    me.getPaddingRows = function() {
      var rowStart = me.getRowStart();
      var rowEnd = me.getRowEnd();
      return data.length > me.rows ? me.rows - (rowEnd - rowStart) : 0;
    };
  };

  var Columns = function(data, columns, options) {
    var me = this;

    me.defaults = {
      min: 5
    };

    me.number = 0;
    me.visible = 0;
    me.total = columns.length;
    me.subset = [];
    me.padding = 0;
    me.min = options.columns.min !== null ? options.columns.min : me.defaults.min;
    me.max = options.columns.max !== null ? options.columns.max : null;
    me.widths = {};

    var widthsLookAhead = Math.max(100, options.rows.min);
    var paddingColChars = 10;

    me.emptyNames = function() {
      columns.forEach(function(column) {
        if (columns.label !== null && columns.label !== "")
          return false;
      });

      return true;
    };

    var parsePadding = function(value) {
      return parseInt(value) >= 0 ? parseInt(value) : 0;
    };

    me.calculateWidths = function(measures) {
      columns.forEach(function(column) {
        var maxChars = Math.max(
          column.label.toString().length,
          column.type.toString().length
        );

        for (var idxRow = 0; idxRow < Math.min(widthsLookAhead, data.length); idxRow++) {
          var entryValue = data.getRow(idxRow)[column.name.toString()];
          maxChars = typeof(entryValue) === "undefined" ? 0 : Math.max(maxChars, entryValue.toString().length);
        }

        var maxWidth = pagedTable.clientWidth - 2 * columnNavigationWidthPX - 2 * measures.padding;
        me.widths[column.name] = {
          // width in characters
          chars: maxChars,
          // width for the inner html columns
          inner: Math.min(maxWidth, maxChars * measures.character),
          // width adding outer styles like padding
          outer: Math.min(maxWidth, maxChars * measures.character) + measures.padding
        };
      });
    };

    me.getWidth = function() {
      var widthOuter = 0;
      for (var idxCol = 0; idxCol < me.subset.length; idxCol++) {
        var columnName = me.subset[idxCol].name;
        widthOuter = widthOuter + me.widths[columnName].outer;
      }

      widthOuter = widthOuter + me.padding * paddingColChars * measurer.measures.character;

      if (me.hasMoreLeftColumns()) {
        widthOuter = widthOuter + columnNavigationWidthPX + measurer.measures.padding;
      }

      if (me.hasMoreRightColumns()) {
        widthOuter = widthOuter + columnNavigationWidthPX + measurer.measures.padding;
      }

      return widthOuter;
    };

    me.updateSlice = function() {
      if (me.number + me.visible >= me.total)
        me.number = me.total - me.visible;

      if (me.number < 0) me.number = 0;

      me.subset = columns.slice(me.number, Math.min(me.number + me.visible, me.total));

      me.subset = me.subset.map(function(column) {
        Object.keys(column).forEach(function(colKey) {
          column[colKey] = column[colKey] === null ? "" : column[colKey].toString();
        });

        column.width = null;
        return column;
      });
    };

    me.setVisibleColumns = function(columnNumber, newVisibleColumns, paddingCount) {
      me.number = columnNumber;
      me.visible = newVisibleColumns;
      me.padding = paddingCount;

      me.updateSlice();
    };

    me.incColumnNumber = function(increment) {
      me.number = me.number + increment;
    };

    me.setColumnNumber = function(newNumber) {
      me.number = newNumber;
    };

    me.setPaddingCount = function(newPadding) {
      me.padding = newPadding;
    };

    me.getPaddingCount = function() {
      return me.padding;
    };

    me.hasMoreLeftColumns = function() {
      return me.number > 0;
    };

    me.hasMoreRightColumns = function() {
      return me.number + me.visible < me.total;
    };

    me.updateSlice(0);
    return me;
  };

  var data = null;
  if (source.array) { data = new ArrayData(source.array); } else { data = new JsonData(source.data)};
  var page = new Page(data, options);
  var measurer = new Measurer(data, options);
  var columns = new Columns(data, source.columns, options);

  var table = null;
  var tableDiv = null;
  var header = null;
  var footer = null;
  var tbody = null;

  // Caches pagedTable.clientWidth, specially for webkit
  var cachedPagedTableClientWidth = null;

  var onChangeCallbacks = [];

  var clearSelection = function() {
    if(document.selection && document.selection.empty) {
      document.selection.empty();
    } else if(window.getSelection) {
      var sel = window.getSelection();
      sel.removeAllRanges();
    }
  };

  var columnNavigationWidthPX = 5;

  var renderColumnNavigation = function(increment, backwards) {
    var arrow = document.createElement("div");
    arrow.setAttribute("style",
      "border-top: " + columnNavigationWidthPX + "px solid transparent;" +
      "border-bottom: " + columnNavigationWidthPX + "px solid transparent;" +
      "border-" + (backwards ? "right" : "left") + ": " + columnNavigationWidthPX + "px solid;");

    var header = document.createElement("th");
    header.appendChild(arrow);
    header.setAttribute("style",
      "cursor: pointer;" +
      "vertical-align: middle;" +
      "min-width: " + columnNavigationWidthPX + "px;" +
      "width: " + columnNavigationWidthPX + "px;");

    header.onclick = function() {
      columns.incColumnNumber(backwards ? -1 : increment);

      me.animateColumns(backwards);
      renderFooter();

      clearSelection();
      triggerOnChange();
    };

    return header;
  };

  var maxColumnWidth = function(width) {
    var padding = 80;
    var columnMax = Math.max(cachedPagedTableClientWidth - padding, 0);

    return parseInt(width) > 0 ?
      Math.min(columnMax, parseInt(width)) + "px" :
      columnMax + "px";
  };

  var clearHeader = function() {
    var thead = pagedTable.querySelectorAll("thead")[0];
    thead.innerHTML = "";
  };

  var renderHeader = function(clear) {
    cachedPagedTableClientWidth = pagedTable.clientWidth;

    var fragment = document.createDocumentFragment();

    header = document.createElement("tr");
    fragment.appendChild(header);

    if (columns.number > 0)
      header.appendChild(renderColumnNavigation(-columns.visible, true));

    columns.subset = columns.subset.map(function(columnData) {
      var column = document.createElement("th");
      column.setAttribute("align", columnData.align);
      column.style.textAlign = columnData.align;

      column.style.maxWidth = maxColumnWidth(null);
      if (columnData.width) {
        column.style.minWidth =
          column.style.maxWidth = maxColumnWidth(columnData.width);
      }

      var columnName = document.createElement("div");
      columnName.setAttribute("class", "pagedtable-header-name");
      if (columnData.label === "") {
        columnName.innerHTML = "&nbsp;";
      }
      else {
        columnName.appendChild(document.createTextNode(columnData.label));
      }
      column.appendChild(columnName);

      var columnType = document.createElement("div");
      columnType.setAttribute("class", "pagedtable-header-type");
      if (columnData.type === "") {
        columnType.innerHTML = "&nbsp;";
      }
      else {
        columnType.appendChild(document.createTextNode("<" + columnData.type + ">"));
      }
      column.appendChild(columnType);

      header.appendChild(column);

      columnData.element = column;

      return columnData;
    });

    for (var idx = 0; idx < columns.getPaddingCount(); idx++) {
      var paddingCol = document.createElement("th");
      paddingCol.setAttribute("class", "pagedtable-padding-col");
      header.appendChild(paddingCol);
    }

    if (columns.number + columns.visible < columns.total)
      header.appendChild(renderColumnNavigation(columns.visible, false));

    if (typeof(clear) == "undefined" || clear) clearHeader();
    var thead = pagedTable.querySelectorAll("thead")[0];
    thead.appendChild(fragment);
  };

  me.animateColumns = function(backwards) {
    var thead = pagedTable.querySelectorAll("thead")[0];

    var headerOld = thead.querySelectorAll("tr")[0];
    var tbodyOld = table.querySelectorAll("tbody")[0];

    me.fitColumns(backwards);

    renderHeader(false);

    header.style.opacity = "0";
    header.style.transform = backwards ? "translateX(-30px)" : "translateX(30px)";
    header.style.transition = "transform 200ms linear, opacity 200ms";
    header.style.transitionDelay = "0";

    renderBody(false);

    if (headerOld) {
      headerOld.style.position = "absolute";
      headerOld.style.transform = "translateX(0px)";
      headerOld.style.opacity = "1";
      headerOld.style.transition = "transform 100ms linear, opacity 100ms";
      headerOld.setAttribute("class", "pagedtable-remove-head");
      if (headerOld.style.transitionEnd) {
        headerOld.addEventListener("transitionend", function() {
          var headerOldByClass = thead.querySelector(".pagedtable-remove-head");
          if (headerOldByClass) thead.removeChild(headerOldByClass);
        });
      }
      else {
        thead.removeChild(headerOld);
      }
    }

    if (tbodyOld) table.removeChild(tbodyOld);

    tbody.style.opacity = "0";
    tbody.style.transition = "transform 200ms linear, opacity 200ms";
    tbody.style.transitionDelay = "0ms";

    // force relayout
    window.getComputedStyle(header).opacity;
    window.getComputedStyle(tbody).opacity;

    if (headerOld) {
      headerOld.style.transform = backwards ? "translateX(20px)" : "translateX(-30px)";
      headerOld.style.opacity = "0";
    }

    header.style.transform = "translateX(0px)";
    header.style.opacity = "1";

    tbody.style.opacity = "1";
  }

  me.onChange = function(callback) {
    onChangeCallbacks.push(callback);
  };

  var triggerOnChange = function() {
    onChangeCallbacks.forEach(function(onChange) {
      onChange();
    });
  };

  var clearBody = function() {
    if (tbody) {
      table.removeChild(tbody);
      tbody = null;
    }
  };

  var renderBody = function(clear) {
    cachedPagedTableClientWidth = pagedTable.clientWidth

    var fragment = document.createDocumentFragment();

    var pageData = data.slice(page.getRowStart(), page.getRowEnd());

    pageData.forEach(function(dataRow, idxRow) {
      var htmlRow = document.createElement("tr");
      htmlRow.setAttribute("class", (idxRow % 2 !==0) ? "even" : "odd");

      if (columns.hasMoreLeftColumns())
        htmlRow.appendChild(document.createElement("td"));

      columns.subset.forEach(function(columnData) {
        var cellName = columnData.name;
        var dataCell = dataRow[cellName];
        var htmlCell = document.createElement("td");

        if (dataCell === "NA") htmlCell.setAttribute("class", "pagedtable-na-cell");
        if (dataCell === "__NA__" || typeof(dataCell) === "undefined") dataCell = "NA";

        var cellText = document.createTextNode(dataCell);
        htmlCell.appendChild(cellText);
        if (dataCell.length > 50) {
          htmlCell.setAttribute("title", dataCell);
        }
        htmlCell.setAttribute("align", columnData.align);
        htmlCell.style.textAlign = columnData.align;
        htmlCell.style.maxWidth = maxColumnWidth(null);
        if (columnData.width) {
          htmlCell.style.minWidth = htmlCell.style.maxWidth = maxColumnWidth(columnData.width);
        }
        htmlRow.appendChild(htmlCell);
      });

      for (var idx = 0; idx < columns.getPaddingCount(); idx++) {
        var paddingCol = document.createElement("td");
        paddingCol.setAttribute("class", "pagedtable-padding-col");
        htmlRow.appendChild(paddingCol);
      }

      if (columns.hasMoreRightColumns())
        htmlRow.appendChild(document.createElement("td"));

      fragment.appendChild(htmlRow);
    });

    for (var idxPadding = 0; idxPadding < page.getPaddingRows(); idxPadding++) {
      var paddingRow = document.createElement("tr");

      var paddingCellRow = document.createElement("td");
      paddingCellRow.innerHTML = "&nbsp;";
      paddingCellRow.setAttribute("colspan", "100%");
      paddingRow.appendChild(paddingCellRow);

      fragment.appendChild(paddingRow);
    }

    if (typeof(clear) == "undefined" || clear) clearBody();
    tbody = document.createElement("tbody");
    tbody.appendChild(fragment);

    table.appendChild(tbody);
  };

  var getLabelInfo = function() {
    var pageStart = page.getRowStart();
    var pageEnd = page.getRowEnd();
    var totalRows = data.length;

    var totalRowsLabel = options.rows.total ? options.rows.total : totalRows;
    var totalRowsLabelFormat = totalRowsLabel.toString().replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');

    var infoText = (pageStart + 1) + "-" + pageEnd + " of " + totalRowsLabelFormat + " rows";
    if (totalRows < page.rows) {
      infoText = totalRowsLabel + " row" + (totalRows != 1 ? "s" : "");
    }
    if (columns.total > columns.visible) {
      var totalColumnsLabel = options.columns.total ? options.columns.total : columns.total;

      infoText = infoText + " | " + (columns.number + 1) + "-" +
        (Math.min(columns.number + columns.visible, columns.total)) +
        " of " + totalColumnsLabel + " columns";
    }

    return infoText;
  };

  var clearFooter = function() {
    footer = pagedTable.querySelectorAll("div.pagedtable-footer")[0];
    footer.innerHTML = "";

    return footer;
  };

  var createPageLink = function(idxPage) {
    var pageLink = document.createElement("a");
    pageLinkClass = idxPage === page.number ? "pagedtable-index pagedtable-index-current" : "pagedtable-index";
    pageLink.setAttribute("class", pageLinkClass);
    pageLink.setAttribute("data-page-index", idxPage);
    pageLink.onclick = function() {
      page.setPageNumber(parseInt(this.getAttribute("data-page-index")));
      renderBody();
      renderFooter();

      triggerOnChange();
    };

    pageLink.appendChild(document.createTextNode(idxPage + 1));

    return pageLink;
  }

  var renderFooter = function() {
    footer = clearFooter();

    var next = document.createElement("a");
    next.appendChild(document.createTextNode("Next"));
    next.onclick = function() {
      page.setPageNumber(page.number + 1);
      renderBody();
      renderFooter();

      triggerOnChange();
    };
    if (data.length > page.rows) footer.appendChild(next);

    var pageNumbers = document.createElement("div");
    pageNumbers.setAttribute("class", "pagedtable-indexes");

    var pageRange = page.getVisiblePageRange();

    if (pageRange.first) {
      var pageLink = createPageLink(0);
      pageNumbers.appendChild(pageLink);

      var pageSeparator = document.createElement("div");
      pageSeparator.setAttribute("class", "pagedtable-index-separator-left");
      pageSeparator.appendChild(document.createTextNode("..."))
      pageNumbers.appendChild(pageSeparator);
    }

    for (var idxPage = pageRange.start; idxPage < pageRange.end; idxPage++) {
      var pageLink = createPageLink(idxPage);

      pageNumbers.appendChild(pageLink);
    }

    if (pageRange.last) {
      var pageSeparator = document.createElement("div");
      pageSeparator.setAttribute("class", "pagedtable-index-separator-right");
      pageSeparator.appendChild(document.createTextNode("..."))
      pageNumbers.appendChild(pageSeparator);

      var pageLink = createPageLink(page.total - 1);
      pageNumbers.appendChild(pageLink);
    }

    if (data.length > page.rows) footer.appendChild(pageNumbers);

    var previous = document.createElement("a");
    previous.appendChild(document.createTextNode("Previous"));
    previous.onclick = function() {
      page.setPageNumber(page.number - 1);
      renderBody();
      renderFooter();

      triggerOnChange();
    };
    if (data.length > page.rows) footer.appendChild(previous);

    var infoLabel = document.createElement("div");
    infoLabel.setAttribute("class", "pagedtable-info");
    infoLabel.setAttribute("title", getLabelInfo());
    infoLabel.appendChild(document.createTextNode(getLabelInfo()));
    footer.appendChild(infoLabel);

    var enabledClass = "pagedtable-index-nav";
    var disabledClass = "pagedtable-index-nav pagedtable-index-nav-disabled";
    previous.setAttribute("class", page.number <= 0 ? disabledClass : enabledClass);
    next.setAttribute("class", (page.number + 1) * page.rows >= data.length ? disabledClass : enabledClass);
  };

  var measuresCell = null;

  var renderMeasures = function() {
    var measuresTable = document.createElement("table");
    measuresTable.style.visibility = "hidden";
    measuresTable.style.position = "absolute";
    measuresTable.style.whiteSpace = "nowrap";
    measuresTable.style.height = "auto";
    measuresTable.style.width = "auto";

    var measuresRow = document.createElement("tr");
    measuresTable.appendChild(measuresRow);

    measuresCell = document.createElement("td");
    var sampleString = "ABCDEFGHIJ0123456789";
    measuresCell.appendChild(document.createTextNode(sampleString));

    measuresRow.appendChild(measuresCell);

    tableDiv.appendChild(measuresTable);
  }

  me.init = function() {
    tableDiv = document.createElement("div");
    pagedTable.appendChild(tableDiv);
    var pagedTableClass = data.length > 0 ?
      "pagedtable pagedtable-not-empty" :
      "pagedtable pagedtable-empty";

    if (columns.total == 0 || (columns.emptyNames() && data.length == 0)) {
      pagedTableClass = pagedTableClass + " pagedtable-empty-columns";
    }

    tableDiv.setAttribute("class", pagedTableClass);

    renderMeasures();
    measurer.calculate(measuresCell);
    columns.calculateWidths(measurer.measures);

    table = document.createElement("table");
    table.setAttribute("cellspacing", "0");
    table.setAttribute("class", "table table-condensed");
    tableDiv.appendChild(table);

    table.appendChild(document.createElement("thead"));

    var footerDiv = document.createElement("div");
    footerDiv.setAttribute("class", "pagedtable-footer");
    tableDiv.appendChild(footerDiv);

    // if the host has not yet provided horizontal space, render hidden
    if (tableDiv.clientWidth <= 0) {
      tableDiv.style.opacity = "0";
    }

    me.render();

    // retry seizing columns later if the host has not provided space
    function retryFit() {
      if (tableDiv.clientWidth <= 0) {
        setTimeout(retryFit, 100);
      } else {
        me.render();
        triggerOnChange();
      }
    }
    if (tableDiv.clientWidth <= 0) {
      retryFit();
    }
  };

  var registerWidths = function() {
    columns.subset = columns.subset.map(function(column) {
      column.width = columns.widths[column.name].inner;
      return column;
    });
  };

  var parsePadding = function(value) {
    return parseInt(value) >= 0 ? parseInt(value) : 0;
  };

  me.fixedHeight = function() {
    return options.rows.max != null;
  }

  me.fitRows = function() {
    if (me.fixedHeight())
      return;

    measurer.calculate(measuresCell);

    var rows = options.rows.min !== null ? options.rows.min : 0;
    var headerHeight = header !== null && header.offsetHeight > 0 ? header.offsetHeight : 0;
    var footerHeight = footer !== null && footer.offsetHeight > 0 ? footer.offsetHeight : 0;

    if (pagedTable.offsetHeight > 0) {
      var availableHeight = pagedTable.offsetHeight - headerHeight - footerHeight;
      rows = Math.floor((availableHeight) / measurer.measures.height);
    }

    rows = options.rows.min !== null ? Math.max(options.rows.min, rows) : rows;

    page.setRows(rows);
  }

  // The goal of this function is to add as many columns as possible
  // starting from left-to-right, when the right most limit is reached
  // it tries to add columns from the left as well.
  //
  // When startBackwards is true columns are added from right-to-left
  me.fitColumns = function(startBackwards) {
    measurer.calculate(measuresCell);
    columns.calculateWidths(measurer.measures);

    if (tableDiv.clientWidth > 0) {
      tableDiv.style.opacity = 1;
    }

    var visibleColumns = tableDiv.clientWidth <= 0 ? Math.max(columns.min, 1) : 1;
    var columnNumber = columns.number;
    var paddingCount = 0;

    // track a list of added columns as we build the visible ones to allow us
    // to remove columns when they don't fit anymore.
    var columnHistory = [];

    var lastTableHeight = 0;
    var backwards = startBackwards;

    var tableDivStyle = window.getComputedStyle(tableDiv, null);
    var tableDivPadding = parsePadding(tableDivStyle.paddingLeft) +
      parsePadding(tableDivStyle.paddingRight);

    var addPaddingCol = false;
    var currentWidth = 0;

    while (true) {
      columns.setVisibleColumns(columnNumber, visibleColumns, paddingCount);
      currentWidth = columns.getWidth();

      if (tableDiv.clientWidth - tableDivPadding < currentWidth) {
        break;
      }

      columnHistory.push({
        columnNumber: columnNumber,
        visibleColumns: visibleColumns,
        paddingCount: paddingCount
      });

      if (columnHistory.length > 100) {
        console.error("More than 100 tries to fit columns, aborting");
        break;
      }

      if (columns.max !== null &&
        columns.visible + columns.getPaddingCount() >= columns.max) {
        break;
      }

      // if we run out of right-columns
      if (!backwards && columnNumber + columns.visible >= columns.total) {
        // if we started adding right-columns, try adding left-columns
        if (!startBackwards && columnNumber > 0) {
          backwards = true;
        }
        else if (columns.min === null || visibleColumns + columns.getPaddingCount() >= columns.min) {
          break;
        }
        else {
          paddingCount = paddingCount + 1;
        }
      }

      // if we run out of left-columns
      if (backwards && columnNumber == 0) {
        // if we started adding left-columns, try adding right-columns
        if (startBackwards && columnNumber + columns.visible < columns.total) {
          backwards = false;
        }
        else if (columns.min === null || visibleColumns + columns.getPaddingCount() >= columns.min) {
          break;
        }
        else {
          paddingCount = paddingCount + 1;
        }
      }

      // when moving backwards try fitting left columns first
      if (backwards && columnNumber > 0) {
        columnNumber = columnNumber - 1;
      }

      if (columnNumber + visibleColumns < columns.total) {
        visibleColumns = visibleColumns + 1;
      }
    }

    var lastRenderableColumn = {
        columnNumber: columnNumber,
        visibleColumns: visibleColumns,
        paddingCount: paddingCount
    };

    if (columnHistory.length > 0) {
      lastRenderableColumn = columnHistory[columnHistory.length - 1];
    }

    columns.setVisibleColumns(
      lastRenderableColumn.columnNumber,
      lastRenderableColumn.visibleColumns,
      lastRenderableColumn.paddingCount);

    if (pagedTable.offsetWidth > 0) {
      page.setVisiblePages(Math.max(Math.ceil(1.0 * (pagedTable.offsetWidth - 250) / 40), 2));
    }

    registerWidths();
  };

  me.fit = function(startBackwards) {
    me.fitRows();
    me.fitColumns(startBackwards);
  }

  me.render = function() {
    me.fitColumns(false);

    // render header/footer to measure height accurately
    renderHeader();
    renderFooter();

    me.fitRows();
    renderBody();

    // re-render footer to match new rows
    renderFooter();
  }

  var resizeLastWidth = -1;
  var resizeLastHeight = -1;
  var resizeNewWidth = -1;
  var resizeNewHeight = -1;
  var resizePending = false;

  me.resize = function(newWidth, newHeight) {

    function resizeDelayed() {
      resizePending = false;

      if (
        (resizeNewWidth !== resizeLastWidth) ||
        (!me.fixedHeight() && resizeNewHeight !== resizeLastHeight)
      ) {
        resizeLastWidth = resizeNewWidth;
        resizeLastHeight = resizeNewHeight;

        setTimeout(resizeDelayed, 200);
        resizePending = true;
      } else {
        me.render();
        triggerOnChange();

        resizeLastWidth = -1;
        resizeLastHeight = -1;
      }
    }

    resizeNewWidth = newWidth;
    resizeNewHeight = newHeight;

    if (!resizePending) resizeDelayed();
  };
};

var PagedTableDoc;
(function (PagedTableDoc) {
  var allPagedTables = [];

  PagedTableDoc.initAll = function() {
    allPagedTables = [];

    var pagedTables = [].slice.call(document.querySelectorAll('[data-pagedtable="false"],[data-pagedtable=""]'));
    pagedTables.forEach(function(pagedTable, idx) {
      pagedTable.setAttribute("data-pagedtable", "true");
      pagedTable.setAttribute("pagedtable-page", 0);
      pagedTable.setAttribute("class", "pagedtable-wrapper");

      var pagedTableInstance = new PagedTable(pagedTable);
      pagedTableInstance.init();

      allPagedTables.push(pagedTableInstance);
    });
  };

  PagedTableDoc.resizeAll = function() {
    allPagedTables.forEach(function(pagedTable) {
      pagedTable.render();
    });
  };

  window.addEventListener("resize", PagedTableDoc.resizeAll);

  return PagedTableDoc;
})(PagedTableDoc || (PagedTableDoc = {}));

window.onload = function() {
  PagedTableDoc.initAll();
};
