import { defineTrait } from '../define.js';

/**
 * 2D arrow-key navigation for grids — calendars, menubars, color pickers,
 * APG `grid` pattern. Sibling 1D trait `keyboard-nav` only fires intent
 * events; this trait owns row+col state and physically moves the active
 * cell.
 *
 *   <div role="grid" data-grid-columns="7">
 *     <button-ui>1</button-ui>
 *     <button-ui>2</button-ui>
 *     ...
 *   </div>
 *
 *   arrowGridNav().connect(grid);
 *   grid.addEventListener('grid-activate', (e) => select(e.detail.row, e.detail.col));
 *
 * Two structural modes via `data-grid-mode`:
 *   "row-flat"   (default) — children are grid items; row/col are inferred
 *                            from index ÷ data-grid-columns.
 *   "row-nested" — children are row containers; each row's children are cells.
 */
export const arrowGridNav = defineTrait({
  name: 'arrow-grid-nav',
  category: 'keyboard-navigation',
  description: '2D arrow-key navigation for grids, calendars, menubars (APG grid pattern)',
  attributes: ['data-arrow-grid-nav-active', 'data-grid-active-row', 'data-grid-active-col'],
  events: ['grid-activate', 'grid-edge', 'grid-cell-change'],
  config: ['data-grid-columns', 'data-grid-mode'],
  setup({ host }) {
    let activeRow = 0;
    let activeCol = 0;
    let activeCell = null;
    let observer = null;

    function getMode() {
      return host.getAttribute('data-grid-mode') === 'row-nested' ? 'row-nested' : 'row-flat';
    }

    function getColumns() {
      const raw = parseInt(host.getAttribute('data-grid-columns'), 10);
      return Number.isFinite(raw) && raw > 0 ? raw : 1;
    }

    /**
     * Returns a 2D matrix of cells: cells[row][col].
     * Both modes return the same shape so navigation logic is uniform.
     */
    function discoverCells() {
      const mode = getMode();
      if (mode === 'row-nested') {
        // Each direct child is a row container; its children are cells.
        return [...host.children].map((rowEl) =>
          [...rowEl.children].filter((c) => !c.hasAttribute('disabled')),
        );
      }
      // row-flat: split a flat child list into rows by column count.
      const cols = getColumns();
      const flat = [...host.children].filter((c) => !c.hasAttribute('disabled'));
      const rows = [];
      for (let i = 0; i < flat.length; i += cols) {
        rows.push(flat.slice(i, i + cols));
      }
      return rows;
    }

    function clearActiveCell() {
      if (activeCell) {
        activeCell.removeAttribute('data-grid-active');
        activeCell.setAttribute('tabindex', '-1');
        activeCell = null;
      }
    }

    function paint(cells, prevRow, prevCol) {
      const safeRow = Math.max(0, Math.min(activeRow, cells.length - 1));
      const row = cells[safeRow] || [];
      const safeCol = Math.max(0, Math.min(activeCol, row.length - 1));
      activeRow = safeRow;
      activeCol = safeCol;

      clearActiveCell();

      const cell = row[safeCol];
      if (cell) {
        activeCell = cell;
        cell.setAttribute('data-grid-active', '');
        cell.setAttribute('tabindex', '0');
      }

      host.setAttribute('data-grid-active-row', String(activeRow));
      host.setAttribute('data-grid-active-col', String(activeCol));

      const changed = prevRow !== activeRow || prevCol !== activeCol;
      if (changed) {
        host.dispatchEvent(new CustomEvent('grid-cell-change', {
          bubbles: true,
          detail: { row: activeRow, col: activeCol, element: cell || null },
        }));
      }
    }

    function paintInitial() {
      const cells = discoverCells();
      // Mark every cell as roving-tabindex -1 so Tab lands once on the host.
      for (const row of cells) {
        for (const cell of row) {
          if (!cell.hasAttribute('tabindex')) cell.setAttribute('tabindex', '-1');
        }
      }
      paint(cells, -1, -1);
    }

    function emitEdge(edge) {
      host.dispatchEvent(new CustomEvent('grid-edge', {
        bubbles: true,
        detail: { edge, row: activeRow, col: activeCol },
      }));
    }

    function onKeyDown(e) {
      // Skip if the event is from a typing surface inside a cell.
      const tag = (e.target?.tagName || '').toLowerCase();
      if (tag === 'input' || tag === 'textarea' || e.target?.isContentEditable) return;

      const cells = discoverCells();
      if (cells.length === 0) return;

      const prevRow = activeRow;
      const prevCol = activeCol;
      const lastRow = cells.length - 1;
      const currentRow = cells[activeRow] || [];
      const lastCol = currentRow.length - 1;

      let handled = true;

      switch (e.key) {
        case 'ArrowRight':
          if (activeCol < lastCol) activeCol++;
          else { emitEdge('right'); handled = true; }
          break;
        case 'ArrowLeft':
          if (activeCol > 0) activeCol--;
          else { emitEdge('left'); handled = true; }
          break;
        case 'ArrowDown': {
          if (activeRow < lastRow) {
            activeRow++;
            const newRow = cells[activeRow] || [];
            // Clamp col to new row's length (rows can vary in row-nested mode).
            if (activeCol > newRow.length - 1) activeCol = Math.max(0, newRow.length - 1);
          } else {
            emitEdge('bottom');
          }
          break;
        }
        case 'ArrowUp': {
          if (activeRow > 0) {
            activeRow--;
            const newRow = cells[activeRow] || [];
            if (activeCol > newRow.length - 1) activeCol = Math.max(0, newRow.length - 1);
          } else {
            emitEdge('top');
          }
          break;
        }
        case 'Home':
          if (e.ctrlKey || e.metaKey) {
            activeRow = 0;
            activeCol = 0;
          } else {
            activeCol = 0;
          }
          break;
        case 'End':
          if (e.ctrlKey || e.metaKey) {
            activeRow = lastRow;
            const lastRowCells = cells[lastRow] || [];
            activeCol = Math.max(0, lastRowCells.length - 1);
          } else {
            activeCol = lastCol;
          }
          break;
        case 'Enter':
        case ' ': {
          // Activate without moving; paint not needed.
          const cell = currentRow[activeCol];
          host.dispatchEvent(new CustomEvent('grid-activate', {
            bubbles: true,
            detail: { row: activeRow, col: activeCol, element: cell || null },
          }));
          e.preventDefault();
          return;
        }
        default:
          handled = false;
      }

      if (!handled) return;

      e.preventDefault();
      paint(cells, prevRow, prevCol);
      // Move physical focus to the new cell so screen readers + visible focus follow.
      activeCell?.focus({ preventScroll: false });
    }

    function onMutate() {
      // Children added / removed — re-paint with clamped indices.
      const cells = discoverCells();
      if (cells.length === 0) return;
      paint(cells, -1, -1);
    }

    host.setAttribute('data-arrow-grid-nav-active', '');
    if (!host.hasAttribute('tabindex')) host.setAttribute('tabindex', '0');
    host.addEventListener('keydown', onKeyDown);

    paintInitial();

    if (typeof MutationObserver !== 'undefined') {
      observer = new MutationObserver(onMutate);
      observer.observe(host, { childList: true, subtree: getMode() === 'row-nested' });
    }

    return () => {
      host.removeEventListener('keydown', onKeyDown);
      if (observer) { observer.disconnect(); observer = null; }
      clearActiveCell();
      host.removeAttribute('data-arrow-grid-nav-active');
      host.removeAttribute('data-grid-active-row');
      host.removeAttribute('data-grid-active-col');
    };
  },
});
