/* Minimal virtual-scroll table */

(function (global) {
  "use strict";

  function qolTable(selector, options) {
    options = options || {};

    var container = typeof selector === "string" ? document.querySelector(selector) : selector;
    if (!container) return;

    var columns   = options.columns || [];
    var data      = (options.data || []).slice();
    var rowHeight = options.rowHeight || 30;                 // px per row
    var bufferPx  = options.virtualDomBuffer != null ? options.virtualDomBuffer : 200;
    var placeholderText = options.placeholder || "No data";

    container.classList.add("qol");
    if (options.height) container.style.height = options.height;
    container.innerHTML = "";

    var gridTemplate = "repeat(" + columns.length + ", minmax(120px, 1fr))";

    // ---- header row ------------------------------------------------
    var headerRow = document.createElement("div");
    headerRow.className = "qol-header-row";
    headerRow.style.gridTemplateColumns = gridTemplate;

    // null dir = unsorted (original data order)
    var sortState = { field: null, dir: null };

    columns.forEach(function (col) {
      var headerCell = document.createElement("div");
      headerCell.className = "qol-col";
      headerCell.setAttribute("qol-field", col.field);
      headerCell.style.textAlign = col.hozAlign || "left";

      var titleSpan = document.createElement("span");
      titleSpan.textContent = col.title != null ? col.title : col.field;
      headerCell.appendChild(titleSpan);

      var arrow = document.createElement("span");
      arrow.className = "qol-sort-arrow";
      headerCell.appendChild(arrow);

      headerCell.addEventListener("click", function () {
        if (sortState.field === col.field) {
          sortState.dir = sortState.dir === "asc" ? "desc" : (sortState.dir === "desc" ? null : "asc");
        } else {
          sortState.field = col.field;
          sortState.dir = "asc";
        }
        applySort();
        updateSortArrows();
        container.scrollTop = 0;
        renderVisibleRows(true);
      });

      headerRow.appendChild(headerCell);
    });

    function updateSortArrows() {
      Array.prototype.forEach.call(headerRow.children, function (cell) {
        var arrowEl = cell.querySelector(".qol-sort-arrow");
        var field = cell.getAttribute("qol-field");
        if (field === sortState.field && sortState.dir) {
          arrowEl.textContent = sortState.dir === "asc" ? " \u25B2" : " \u25BC";
        } else {
          arrowEl.textContent = "";
        }
      });
    }

    container.appendChild(headerRow);

    // ---- empty data guard -------------------------------------------
    if (data.length === 0) {
      var placeholderEl = document.createElement("div");
      placeholderEl.className = "qol-placeholder";
      placeholderEl.textContent = placeholderText;
      container.appendChild(placeholderEl);
      return;
    }

    // ---- sizer: gives the scrollbar the correct total height --------
    var sizer = document.createElement("div");
    sizer.className = "qol-sizer";
    sizer.style.height = (data.length * rowHeight) + "px";
    container.appendChild(sizer);

    var sortedData = data;
    var rowPool = new Map(); // rowIndex -> currently-rendered row element

    function applySort() {
      if (!sortState.field || !sortState.dir) {
        sortedData = data;
        return;
      }
      var field = sortState.field;
      var dir = sortState.dir === "asc" ? 1 : -1;

      sortedData = data.slice().sort(function (a, b) {
        var av = a[field], bv = b[field];
        if (av == null && bv == null) return 0;
        if (av == null) return 1;   // nulls last, regardless of direction
        if (bv == null) return -1;
        if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
        return String(av).localeCompare(String(bv)) * dir;
      });
    }

    function buildRow(rowIndex) {
      var rowData = sortedData[rowIndex];
      var rowEl = document.createElement("div");
      rowEl.className = "qol-row" + (rowIndex % 2 === 0 ? " qol-row-even" : "");
      rowEl.style.top = (rowIndex * rowHeight) + "px";
      rowEl.style.height = rowHeight + "px";
      rowEl.style.gridTemplateColumns = gridTemplate;

      columns.forEach(function (col) {
        var cellEl = document.createElement("div");
        cellEl.className = "qol-cell";
        cellEl.style.textAlign = col.hozAlign || "left";
        var value = rowData[col.field];
        cellEl.textContent = value == null ? "" : value;
        rowEl.appendChild(cellEl);
      });

      return rowEl;
    }

    // Renders only the rows within [viewport - buffer, viewport + buffer].
    // This is what keeps the DOM row count bounded (roughly a few dozen to
    // a couple hundred rows, depending on viewport height and buffer size)
    // no matter how many total rows are in the data set.
    function renderVisibleRows(forceRebuild) {
      var scrollTop = container.scrollTop;
      var viewportHeight = container.clientHeight;
      var bufferRows = Math.ceil(bufferPx / rowHeight);

      var firstVisible = Math.floor(scrollTop / rowHeight);
      var lastVisible = Math.ceil((scrollTop + viewportHeight) / rowHeight);

      var start = Math.max(0, firstVisible - bufferRows);
      var end = Math.min(sortedData.length, lastVisible + bufferRows);

      if (forceRebuild) {
        // data order changed (e.g. a new sort) - drop everything rendered
        // so stale rows/positions don't linger
        rowPool.forEach(function (el) { el.remove(); });
        rowPool.clear();
        sizer.style.height = (sortedData.length * rowHeight) + "px";
      }

      // drop rows that scrolled out of the current window
      rowPool.forEach(function (el, idx) {
        if (idx < start || idx >= end) {
          el.remove();
          rowPool.delete(idx);
        }
      });

      // add rows that newly entered the window
      for (var i = start; i < end; i++) {
        if (!rowPool.has(i)) {
          var rowEl = buildRow(i);
          sizer.appendChild(rowEl);
          rowPool.set(i, rowEl);
        }
      }
    }

    var scrollScheduled = false;
    container.addEventListener("scroll", function () {
      if (scrollScheduled) return;
      scrollScheduled = true;
      requestAnimationFrame(function () {
        renderVisibleRows(false);
        scrollScheduled = false;
      });
    });

    window.addEventListener("resize", function () {
      renderVisibleRows(false);
    });

    applySort();
    updateSortArrows();
    renderVisibleRows(true);
  }

  global.qolTable = qolTable;
})(window);
