// 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 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 metadata =
         typeof source.metadata !== "undefined" && source.metadata !== null ? source.metadata : {};

      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),
         },
         metadata: metadata,
      };
   })(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++) {
               maxChars = Math.max(maxChars, data[idxRow][column.name.toString()].length);
            }

            me.widths[column.name] = {
               // width in characters
               chars: maxChars,
               // width for the inner html columns
               inner: maxChars * measures.character,
               // width adding outer styles like padding
               outer: 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 = 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 metaheader = 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 renderMetadata = function(){
      // show metadata on top of header as long as we have > 1 entry (the first one is always just
      // dimensions, which we are already displaying)
      if (Object.keys(options.metadata).length < 1) 
         return;

      // ignore if we've already rendered
      if (pagedTable.querySelector(".pagedtable-metacell") !== null)
         return;

      // create a column that spans the entire header space
      metaheader = document.createElement("div");
      var metacell = document.createElement("div");
      metacell.className = "pagedtable-metacell";
      metaheader.appendChild(metacell);

      // create a small table cell inside that column to host the metadata
      for (var metafield in options.metadata) {
         if (options.metadata.hasOwnProperty(metafield)) {
            var metadiv = document.createElement("div");

            var field = document.createElement("span");
            field.innerText = metafield + ":";
            field.className = "pagedtable-metafield";
            metadiv.appendChild(field);

            var val = document.createElement("span");
            val.innerText = options.metadata[metafield];
            val.className = "pagedtable-metaval";
            metadiv.appendChild(val);

            metacell.appendChild(metadiv);
         }
      }

      // insert the metadata above the table
      var table = pagedTable.querySelectorAll("table")[0];
      table.parentElement.insertBefore(metaheader, table);
   };

   var setColumnWidth = function (column, columnData) {
      if (columnData.width) {
         var width = maxColumnWidth(columnData.width);
         column.style.minWidth = column.style.maxWidth = width;
         if (columnData.name === "_rn_") {
            var widthPx = parseInt(width, 10);
            if (widthPx > 160)
              width = "160px";
            column.style.width = width;
         }
      }
   }

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

      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);
         setColumnWidth(column, columnData);

         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__") dataCell = "NA";

            var cellText = document.createTextNode(dataCell);
            htmlCell.appendChild(cellText);
            if (dataCell.length > 50 || cellName === "_rn_") {
               htmlCell.setAttribute("title", dataCell);
            }
            htmlCell.setAttribute("align", columnData.align);
            htmlCell.style.textAlign = columnData.align;
            htmlCell.style.maxWidth = maxColumnWidth(null);
            setColumnWidth(htmlCell, columnData);
            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
      var retries = 100;
      function retryFit() {
         retries = retries - 1;
         if (retries > 0) {
            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 metaHeight =
         metaheader !== null && metaheader.offsetHeight > 0 ? metaheader.offsetHeight : 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 - metaHeight - 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
      renderMetadata();
      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"]'));
      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();
};
