/*
 * designer/boardSelector.js
 *
 * "Target" picker for the in-browser Designer.  Three cascading levels:
 *   1. Family   — Arduino, ESP32, … (one card per top-level group)
 *   2. Chip     — only shown when the family has >1 chip group (so AVR's
 *                 single chip skips straight to the board list)
 *   3. Board    — full per-board list within the chosen (family, chip)
 *
 * Why levels: pfod messages are capped at 1024 bytes and the largest
 * chip group (esp32-s3) has 145 boards, so a single flat list is
 * impossible.  See [[project_pfodWebContext]].
 *
 * Data source: BOARD_HIERARCHY (auto-generated by build-bundle.js after
 * the per-board consts) describes the full {family → chip → boards}
 * tree, with each board carrying { id, name } pre-resolved to its
 * BOARD_DATA_BY_ID entry.  The id IS the URL ?designer=<id> value, so
 * picking a board is a one-liner: setCurrentTargetId(id) +
 * updateURLFromForm() so Exit→Reconnect lands on the same target.
 *
 * Element contract (declared in pfodCommon.html):
 *   #designer-target-display       — span we set to the resolved displayName
 *   #designer-target-btn           — opens the overlay (onclick attr)
 *   #board-selector-overlay        — outer scrim, display:none by default
 *   #board-selector-title          — current level heading
 *   #board-selector-list           — container we fill with .board-family-btn
 *   #board-selector-close          — closes the overlay (onclick attr)
 *   #board-selector-scroll-up      — ▲ chevron above the list
 *   #board-selector-scroll-down    — ▼ chevron below the list
 *
 * Interaction:
 *   ↑ / ↓ arrows — move the keyboard-focused row (visual: .kb-focused)
 *   Enter        — activate the focused row (same as a click)
 *   Escape       — close the overlay without selecting
 *   Mouse wheel  — also moves the focused row (wraps at the ends)
 *   Scroll btns  — single click steps the list by the focused item's
 *                  height; held-down auto-repeats while pressed.
 *
 * (c)2026 Forward Computing and Control Pty. Ltd.
 */

/// Default target id until the user picks something else or URL/storage
/// restores a saved choice.  Must match a key in BOARD_DATA_BY_ID —
/// i.e. the prefixed board filename emitted by build_boards.js.
const _DEFAULT_TARGET_ID = 'avr_uno';

/// Currently-selected target id.  Plain module-scope variable; setters
/// keep the DOM (#designer-target-display) and the URL (?designer=…) in
/// sync.  Initialised to the default — prefillFormFromURL() may override
/// before the user sees anything by calling setCurrentTargetId().
let _currentTargetId = _DEFAULT_TARGET_ID;

/// Keyboard-focus index into the currently-rendered list of buttons.
/// -1 means "nothing focused yet" — first ↓ moves to 0, first ↑ moves
/// to the last item.  Reset to 0 on every list re-render.
let _focusedIndex = -1;

/// Held-down auto-repeat timer for the scroll chevrons.  Cleared on
/// pointerup / pointerleave / overlay close.
let _scrollHoldTimer = null;

/// Navigation breadcrumb — what the picker is currently showing.
///   { level: 'family' }                       → top-level family list
///   { level: 'chip',  family: 'esp32' }       → chip list inside ESP32
///   { level: 'board', family, chip }          → board list inside chip
/// closeBoardSelector() resets this; openBoardSelector() starts fresh.
let _navState = { level: 'family' };

/// Return the currently-selected target id.  Used by pfodCommon.html's
/// updateURLFromForm() to fill the ?designer=<id> param.
function getCurrentTargetId() {
  return _currentTargetId;
}

/// Set the currently-selected target id and refresh the display label.
/// Caller is responsible for calling updateURLFromForm() afterwards if
/// the URL needs to be updated (updateURLFromForm reads the value back
/// via getCurrentTargetId()).  Unknown ids are accepted silently — the
/// display falls back to the id so the user can at least see what's
/// stored.  Returns true if the id resolved to a known board.
function setCurrentTargetId(id) {
  _currentTargetId = id;
  _refreshTargetDisplay();
  return _hasBoardData(id);
}

/// Look up the label for the target display span: "board name - connections".
/// Connection types are derived from the board's actual connections object so
/// the label reflects what this specific chip supports (not the family default).
/// Falls back to the id itself when the registry is absent or the id is unknown.
function _displayNameFor(id) {
  if (typeof BOARD_DATA_BY_ID !== 'undefined' && BOARD_DATA_BY_ID[id]) {
    const d = BOARD_DATA_BY_ID[id];
    const connStr = _boardConnStr(d.connections);
    return connStr ? d.name + ' - ' + connStr : d.name;
  }
  return id;
}

/// Build "Serial, BLE, TCP/IP, HTTP" from a board connections object,
/// using only the keys actually present.
const _CONN_ORDER  = ['serial', 'ble', 'tcp', 'http'];
const _CONN_LABELS = { serial: 'Serial', ble: 'BLE', tcp: 'TCP/IP', http: 'HTTP' };
function _boardConnStr(conns) {
  if (!conns) return '';
  return _CONN_ORDER.filter(c => conns[c] !== undefined).map(c => _CONN_LABELS[c]).join(', ');
}

function _hasBoardData(id) {
  return typeof BOARD_DATA_BY_ID !== 'undefined' && !!BOARD_DATA_BY_ID[id];
}

function _refreshTargetDisplay() {
  const span = document.getElementById('designer-target-display');
  if (span) span.innerHTML = _displayNameFor(_currentTargetId).replace(/\n/g, '<br>');
}

// ───────────────────────── Hierarchy access ──────────────────────────────

/// True when BOARD_HIERARCHY exists and has at least one family.  Used
/// to guard the picker — without data there's nothing to show.
function _hierarchyAvailable() {
  return typeof BOARD_HIERARCHY !== 'undefined'
      && BOARD_HIERARCHY
      && Object.keys(BOARD_HIERARCHY).length > 0;
}

/// Return [{id, name}] for the top-level family selection screen,
/// sorted by each family's sortOrder (from its family.json — lower
/// sorts first), then alphabetically by display name among ties.
function _familyItems() {
  if (!_hierarchyAvailable()) return [];
  return Object.entries(BOARD_HIERARCHY)
    .map(([id, fam]) => ({ id, name: fam.name, sortOrder: fam.sortOrder }))
    .sort((a, b) => (a.sortOrder - b.sortOrder) || a.name.localeCompare(b.name));
}

/// Return [{id, name}] for the chip selection screen within a family,
/// sorted alphabetically.  Empty when the family is unknown.
function _chipItems(familyId) {
  const fam = _hierarchyAvailable() ? BOARD_HIERARCHY[familyId] : null;
  if (!fam) return [];
  return Object.entries(fam.chips)
    .map(([id, chip]) => ({ id, name: chip.name }))
    .sort((a, b) => a.name.localeCompare(b.name));
}

/// Return [{id, name}] for the board selection screen within a
/// (family, chip).  Already alphabetically sorted by build-bundle.js.
function _boardItems(familyId, chipId) {
  const fam = _hierarchyAvailable() ? BOARD_HIERARCHY[familyId] : null;
  if (!fam) return [];
  const chip = fam.chips[chipId];
  return chip ? chip.boards : [];
}

/// Return the single {id, name} board entry for a family that has
/// exactly one board total across all of its chip groups, or null
/// otherwise.  Used to skip straight past the chip/board picker
/// screens when there's nothing to actually choose between (e.g. the
/// single-board "Minimal C Code" family).
function _onlyBoardInFamily(familyId) {
  const fam = _hierarchyAvailable() ? BOARD_HIERARCHY[familyId] : null;
  if (!fam) return null;
  let only = null;
  for (const chip of Object.values(fam.chips)) {
    for (const board of chip.boards) {
      if (only) return null;   // more than one board found
      only = board;
    }
  }
  return only;
}

// ───────────────────────── Picker render ─────────────────────────────────

/// Render the current navigation level into #board-selector-list.
/// Single dispatch point for openBoardSelector() and the back/forward
/// transitions — recomputes title, items, keyboard focus, and scroll
/// chevron visibility in one pass so a level change can't leave any of
/// those out of sync.
function _renderCurrentLevel() {
  const title = document.getElementById('board-selector-title');
  const list  = document.getElementById('board-selector-list');
  if (!title || !list) return;

  let items, headingText;
  switch (_navState.level) {
    case 'family':
      items = _familyItems();
      headingText = 'Select Target';
      break;
    case 'chip':
      items = _chipItems(_navState.family);
      headingText = (BOARD_HIERARCHY[_navState.family] || {}).name + ' — pick chip';
      break;
    case 'board':
      items = _boardItems(_navState.family, _navState.chip);
      const chipName = (BOARD_HIERARCHY[_navState.family]
                        && BOARD_HIERARCHY[_navState.family].chips[_navState.chip]
                        || {}).name || _navState.chip;
      headingText = chipName + ' — pick board';
      break;
    default:
      items = [];
      headingText = 'Select Target';
  }

  title.textContent = headingText;
  list.innerHTML = '';

  // Back row for non-family levels.  Same .board-family-btn styling so
  // it picks up the existing :hover / kb-focused treatment; dataset
  // marker so the click handler can route to _navigateBack.
  if (_navState.level !== 'family') {
    const back = document.createElement('button');
    back.type        = 'button';
    back.className   = 'board-family-btn board-back-btn';
    back.textContent = '← Back';
    back.dataset.role = 'back';
    back.addEventListener('click', _navigateBack);
    list.appendChild(back);
  }

  for (const it of items) {
    const btn = document.createElement('button');
    btn.type        = 'button';
    btn.className   = 'board-family-btn';
    btn.innerHTML = it.name.replace(/\n/g, '<br>');
    btn.dataset.id  = it.id;
    btn.addEventListener('click', () => _onSelectItem(it));
    list.appendChild(btn);
  }

  // After a level change, focus the first row (the Back button when
  // present, else the first selectable item).  Gives every drill in/out
  // a deterministic keyboard starting point.
  _focusedIndex = 0;
  _applyFocus();
  _refreshScrollChevrons();
}

/// Commit a board id as the new target: update state, write the URL,
/// close the overlay.  Shared by the board-level click and the family-
/// level single-board auto-select short-circuit below.
function _commitBoardSelection(id) {
  setCurrentTargetId(id);
  // updateURLFromForm() lives in pfodCommon.html's inline script and
  // is a global function — call it so the new ?designer=<id> lands
  // in the address bar immediately, surviving any subsequent reload.
  if (typeof updateURLFromForm === 'function') updateURLFromForm();
  closeBoardSelector();
}

/// Click/Enter handler for one row of the picker.  Routes by current
/// level: family → commit directly when the family has only one board
/// total (nothing to choose between), else drill into chip (or skip
/// straight to board when the family has only one chip); chip → drill
/// into board; board → commit the selection.
function _onSelectItem(item) {
  if (_navState.level === 'family') {
    const onlyBoard = _onlyBoardInFamily(item.id);
    if (onlyBoard) {
      _commitBoardSelection(onlyBoard.id);
      return;
    }
    const chips = _chipItems(item.id);
    if (chips.length === 1) {
      _navState = { level: 'board', family: item.id, chip: chips[0].id };
    } else {
      _navState = { level: 'chip', family: item.id };
    }
    _renderCurrentLevel();
    return;
  }
  if (_navState.level === 'chip') {
    _navState = { level: 'board', family: _navState.family, chip: item.id };
    _renderCurrentLevel();
    return;
  }
  if (_navState.level === 'board') {
    _commitBoardSelection(item.id);
    return;
  }
}

/// Step the breadcrumb back one level.  Always lands the user on a
/// valid screen (board → chip, chip → family).  Family level has no
/// back — the Back row isn't rendered there.
function _navigateBack() {
  if (_navState.level === 'board') {
    // If we entered via the auto-skip path (family had 1 chip), the
    // chip list would be a single row — go straight back to family
    // so the user has somewhere meaningful to navigate.
    const chipCount = _chipItems(_navState.family).length;
    if (chipCount <= 1) {
      _navState = { level: 'family' };
    } else {
      _navState = { level: 'chip', family: _navState.family };
    }
    _renderCurrentLevel();
    return;
  }
  if (_navState.level === 'chip') {
    _navState = { level: 'family' };
    _renderCurrentLevel();
    return;
  }
}

// ───────────────────────── Overlay open/close ────────────────────────────

/// Show the overlay starting from the top-level family screen.  Wired to
/// the "Change Target" button's onclick in pfodCommon.html.
function openBoardSelector() {
  _navState = { level: 'family' };
  _renderCurrentLevel();
  document.getElementById('board-selector-overlay').style.display = 'flex';
}

/// Hide the overlay without making a selection.  Wired to the "×"
/// close button in pfodCommon.html.
function closeBoardSelector() {
  document.getElementById('board-selector-overlay').style.display = 'none';
  _navState = { level: 'family' };
  _focusedIndex = -1;
  _stopScrollHold();
}

// ───────────────────────── Keyboard + scroll wiring ──────────────────────

function _currentItems() {
  const list = document.getElementById('board-selector-list');
  if (!list) return [];
  return Array.from(list.querySelectorAll('.board-family-btn'));
}

function _applyFocus() {
  const items = _currentItems();
  items.forEach((el, i) => el.classList.toggle('kb-focused', i === _focusedIndex));
  const target = items[_focusedIndex];
  if (target) target.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}

function _refreshScrollChevrons() {
  const list = document.getElementById('board-selector-list');
  const up   = document.getElementById('board-selector-scroll-up');
  const down = document.getElementById('board-selector-scroll-down');
  if (!list || !up || !down) return;
  const overflows = list.scrollHeight > list.clientHeight + 1;
  up.style.display   = overflows ? 'block' : 'none';
  down.style.display = overflows ? 'block' : 'none';
}

function _scrollListBy(direction) {
  const list = document.getElementById('board-selector-list');
  if (!list) return;
  const first = list.querySelector('.board-family-btn');
  const step  = first ? Math.max(20, first.offsetHeight) : 32;
  list.scrollTop += direction * step;
}

function _startScrollHold(direction) {
  _stopScrollHold();
  _scrollListBy(direction);
  _scrollHoldTimer = setInterval(() => _scrollListBy(direction), 120);
}

function _stopScrollHold() {
  if (_scrollHoldTimer !== null) {
    clearInterval(_scrollHoldTimer);
    _scrollHoldTimer = null;
  }
}

function _onOverlayKeyDown(ev) {
  const overlay = document.getElementById('board-selector-overlay');
  if (!overlay || overlay.style.display === 'none') return;
  const items = _currentItems();
  if (items.length === 0) return;
  // Helper: claim the key event so it can't bubble to other listeners.
  // The connection-prompt has its own Enter handler that fires
  // connectWithPrompt() — without stopPropagation here, pressing Enter
  // on a family/chip/board row would both navigate the picker AND
  // trigger Connect.  preventDefault alone only blocks the default
  // browser action, not other JS listeners further along the bubble.
  const claim = () => { ev.preventDefault(); ev.stopPropagation(); };
  switch (ev.key) {
    case 'ArrowDown':
      claim();
      _focusedIndex = (_focusedIndex + 1) % items.length;
      _applyFocus();
      break;
    case 'ArrowUp':
      claim();
      _focusedIndex = (_focusedIndex - 1 + items.length) % items.length;
      _applyFocus();
      break;
    case 'Enter':
      claim();
      if (_focusedIndex >= 0 && items[_focusedIndex]) items[_focusedIndex].click();
      break;
    case 'Escape':
      claim();
      // Escape inside chip/board mirrors Back; at the top level closes
      // the picker.  Matches the principle "Escape always backs off
      // one level of UI".
      if (_navState.level === 'family') closeBoardSelector();
      else _navigateBack();
      break;
    case 'Backspace':
      // Backspace as an alternate back key — common in similar pickers.
      // No effect at top level so the user can't accidentally close.
      if (_navState.level !== 'family') {
        claim();
        _navigateBack();
      }
      break;
  }
}

function _onOverlayWheel(ev) {
  const overlay = document.getElementById('board-selector-overlay');
  if (!overlay || overlay.style.display === 'none') return;
  const items = _currentItems();
  if (items.length === 0) return;
  ev.preventDefault();
  if (ev.deltaY > 0) {
    _focusedIndex = (_focusedIndex + 1) % items.length;
  } else if (ev.deltaY < 0) {
    _focusedIndex = (_focusedIndex - 1 + items.length) % items.length;
  } else {
    return;
  }
  _applyFocus();
}

function _wireBoardSelectorControls() {
  const up   = document.getElementById('board-selector-scroll-up');
  const down = document.getElementById('board-selector-scroll-down');
  if (up) {
    up.addEventListener('pointerdown',  () => _startScrollHold(-1));
    up.addEventListener('pointerup',    _stopScrollHold);
    up.addEventListener('pointerleave', _stopScrollHold);
    up.addEventListener('pointercancel', _stopScrollHold);
  }
  if (down) {
    down.addEventListener('pointerdown',  () => _startScrollHold(+1));
    down.addEventListener('pointerup',    _stopScrollHold);
    down.addEventListener('pointerleave', _stopScrollHold);
    down.addEventListener('pointercancel', _stopScrollHold);
  }
  // CAPTURE phase (third arg true) — without it, an Enter keypress
  // while the picker is open would first bubble through #connection-prompt
  // (whose own keydown listener at pfodCommon.html:749 fires
  // connectWithPrompt() on Enter), THEN reach this document-level
  // handler.  That would auto-Connect every time the user pressed
  // Enter to pick a board, dropping them straight into the Designer
  // menu without ever clicking the Connect button.  Capturing at
  // document means we run BEFORE any bubble-phase listener anywhere
  // in the tree; claim() stops further propagation in both phases.
  document.addEventListener('keydown', _onOverlayKeyDown, true);
  const overlay = document.getElementById('board-selector-overlay');
  if (overlay) overlay.addEventListener('wheel', _onOverlayWheel, { passive: false });
}

// Refresh the display once on script load so the placeholder "…" in the
// HTML is replaced with the value resolved from BOARD_DATA_BY_ID.
// prefillFormFromURL() may call setCurrentTargetId() shortly after to
// apply a URL override.
_refreshTargetDisplay();
_wireBoardSelectorControls();
