ui/popup-messages.js

import { events } from "../core/events.js";
import { COLUMNS } from "../core/board.js";
import { game, STATE } from "../core/game.js";
import { HIDES_ITEMS, PLAYER } from "../core/flags.js";

const POPUP_DURATION_MS = 2000;

/**
 * Cell popup messages — mirrors the Java "popup menu" feature.
 *
 * When a non-modal message fires, a small label appears above the relevant
 * cell on the board and fades out after 4 seconds.
 */
export function initPopupMessages() {
  const boardEl = document.getElementById("board");
  const popupLayer = document.getElementById("popup-layer");
  if (!boardEl || !popupLayer) return;

  events.onMessage((cell, text) => {
    if (!text || !cell) return;
    _showFadePopup(boardEl, popupLayer, text, cell.x, cell.y);
  });

  events.onHandleInventoryMessaging(() => {
    const cell = game.board?.getCurrentCell();
    if (!cell) return;
    const text = _buildItemListText(cell);
    // Only remove item-list popups, not terrain/trigger message popups
    for (const el of popupLayer.querySelectorAll(
      ".cell-popup[data-item-list]",
    )) {
      el.remove();
    }
    if (text) {
      _showFadePopup(boardEl, popupLayer, text, cell.x, cell.y, {
        isItemList: true,
      });
    }
  });

  events.onModalMessage((msg) => {
    // Win dialogs (.html) are handled by dialogs.js — skip those here
    if (!msg || msg.endsWith(".html")) return;
    const cell = game.board?.getCurrentCell();
    if (!cell) return;
    _showModalPopup(boardEl, popupLayer, msg, cell.x, cell.y);
  });
}

/**
 * Show a modal (blocking) positional popup. Freezes the game until ESC/Enter.
 */
function _showModalPopup(boardEl, popupLayer, text, cellX, cellY) {
  const cellEl = boardEl.children[cellY * COLUMNS + cellX];
  if (!cellEl) return;

  const cellRect = cellEl.getBoundingClientRect();
  const layerRect = popupLayer.getBoundingClientRect();

  const popup = document.createElement("div");
  popup.className = "cell-popup-modal";
  popup.innerHTML = text;

  const hint = document.createElement("span");
  hint.className = "popup-dismiss-hint";
  hint.textContent = "ESC/ENTER to continue…";
  popup.appendChild(hint);

  popup.style.left = `${cellRect.right - layerRect.left}px`;
  popup.style.top = `${cellRect.top - layerRect.top + cellRect.height / 2}px`;

  popupLayer.appendChild(popup);

  // Freeze the game while the popup is shown
  const prevState = game.state;
  game.state = STATE.ANIMATING;

  function dismiss(e) {
    if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      e.stopPropagation();
      popup.remove();
      game.state = prevState;
      document.removeEventListener("keydown", dismiss, { capture: true });
    }
  }
  document.addEventListener("keydown", dismiss, { capture: true });
}

/**
 * Show a fade-out popup anchored to the right of a cell.
 * Returns the popup element so callers can remove it early if needed.
 * @param {HTMLElement} boardEl
 * @param {HTMLElement} popupLayer
 * @param {string} text
 * @param {number} cellX
 * @param {number} cellY
 * @param {{isItemList: boolean}} [options]
 * @returns {HTMLElement|null}
 */
function _showFadePopup(
  boardEl,
  popupLayer,
  text,
  cellX,
  cellY,
  { isItemList = false } = {},
) {
  const cellEl = boardEl.children[cellY * COLUMNS + cellX];
  if (!cellEl) return null;

  const cellRect = cellEl.getBoundingClientRect();
  const layerRect = popupLayer.getBoundingClientRect();

  const popup = document.createElement("div");
  popup.className = "cell-popup";
  if (isItemList) popup.dataset.itemList = "true";
  popup.innerHTML = text;
  popup.style.left = `${cellRect.right - layerRect.left}px`;
  popup.style.top = `${cellRect.bottom - layerRect.top + cellRect.height}px`;

  popupLayer.appendChild(popup);
  setTimeout(() => popup.remove(), POPUP_DURATION_MS);
  return popup;
}

/**
 * Build the HTML text for the item-list popup, mirroring Java's
 * MessageManager.displayItemsAtCurrentCell().
 * Returns null if the cell has no items.
 * @param {Cell} cell
 * @returns {string|null}
 */
function _buildItemListText(cell) {
  if (cell.isBagEmpty) return null;

  if (cell.terrain?.is(HIDES_ITEMS)) {
    return "Some items are hidden here...";
  }

  // Group items by name (matches Java Bag entry list order)
  const groups = [];
  const groupMap = new Map();
  for (const item of cell.items) {
    if (groupMap.has(item.name)) {
      groupMap.get(item.name).count++;
    } else {
      const entry = { item, count: 1 };
      groupMap.set(item.name, entry);
      groups.push(entry);
    }
  }

  const header =
    groups.length > 10
      ? "Use 'r' to rotate items in list"
      : "Use 'p' or # to pick up item";

  const len = Math.min(groups.length, 10);
  const lines = [header];
  for (let i = 0; i < len; i++) {
    const j = i < 9 ? i + 1 : 0; // 1–9 then 0 for the 10th
    const { item, count } = groups[i];
    const sym = item.symbol;
    const fg = sym?.color?.hex ?? "inherit";
    const bg = sym?.background?.hex ?? "transparent";
    const countStr = count > 1 ? ` (x${count})` : "";
    lines.push(
      `${j}. <span style="color:${fg};background:${bg}">${sym?.entity ?? "?"}</span> ${item.name}${countStr}`,
    );
  }
  return lines.join("<br>");
}

// ── Cell hover info popup ─────────────────────────────────────────────────────

/**
 * Show a brief legend popup when the player hovers over a cell.
 * Mirrors the Java CellInfoPanel behaviour: 600 ms delay to show,
 * auto-hides after 2100 ms or on mouseout/mousedown.
 */
export function initCellHoverPopup() {
  const boardEl = document.getElementById("board");
  const popupLayer = document.getElementById("popup-layer");
  if (!boardEl || !popupLayer) return;

  let showTimer = null;
  let hideTimer = null;
  let lastCell = null;
  let popup = null;

  function hidePopup() {
    if (popup) {
      popup.remove();
      popup = null;
    }
  }

  function showPopup(cell) {
    hidePopup();
    const text = _buildCellHoverText(cell);
    if (!text) return;
    popup = _showFadePopup(boardEl, popupLayer, text, cell.x, cell.y);
  }

  boardEl.addEventListener("mouseover", (e) => {
    if (game.state !== STATE.PLAYING) return;
    if (document.querySelector("dialog[open]")) return;

    const span =
      e.target.closest?.(".cell") ??
      (e.target.classList.contains("cell") ? e.target : null);
    if (!span) return;

    const x = parseInt(span.dataset.x, 10);
    const y = parseInt(span.dataset.y, 10);
    if (isNaN(x) || isNaN(y)) return;

    const cell = game.board?.cells?.[x]?.[y];
    if (!cell) return;

    // Same cell — don't reset the timer (handles animated-cell mouseover storms)
    if (cell === lastCell) return;

    clearTimeout(showTimer);
    clearTimeout(hideTimer);
    hidePopup();
    lastCell = cell;

    showTimer = setTimeout(() => {
      showPopup(cell);
      hideTimer = setTimeout(() => {
        hidePopup();
        lastCell = null;
      }, 2100);
    }, 600);
  });

  boardEl.addEventListener("mouseout", (e) => {
    // Only clear when the mouse leaves the board entirely
    if (!boardEl.contains(e.relatedTarget)) {
      clearTimeout(showTimer);
      clearTimeout(hideTimer);
      hidePopup();
      lastCell = null;
    }
  });

  boardEl.addEventListener("mousedown", () => {
    clearTimeout(showTimer);
    clearTimeout(hideTimer);
    hidePopup();
    lastCell = null;
  });
}

/**
 * Build the HTML legend for a hovered cell: terrain, then agent (if any),
 * then the topmost item (unless the terrain has HIDES_ITEMS).
 * Mirrors Java's CellInfoPanel.renderCellInfo().
 * @param {Cell} cell
 * @returns {string|null}
 */
function _buildCellHoverText(cell) {
  const terrain = cell.getApparentTerrain?.() ?? cell.terrain;
  if (!terrain) return null;

  const outside = game.board?.outside ?? false;
  const lines = [_renderPieceLabel(terrain, outside)];

  if (cell.agent && !cell.agent.is(PLAYER)) {
    lines.push(_renderPieceLabel(cell.agent, outside));
  }

  if (!cell.isBagEmpty && terrain.not(HIDES_ITEMS)) {
    const topItem = cell.items[cell.items.length - 1];
    const count = cell.items.filter((i) => i.name === topItem.name).length;
    const countStr = count > 1 ? ` (x${count})` : "";
    lines.push(_renderPieceLabel(topItem, outside) + countStr);
  }

  return lines.join("<br>");
}

/**
 * Render a colored symbol + name label for one piece.
 * @param {Piece} piece
 * @param {boolean} outside
 * @returns {string}
 */
function _renderPieceLabel(piece, outside) {
  const sym = piece.symbol;
  const fg = sym?.getColor?.(outside)?.hex ?? sym?.color?.hex ?? "inherit";
  const bg =
    sym?.getBackground?.(outside)?.hex ?? sym?.background?.hex ?? "transparent";
  return `<span style="color:${fg};background:${bg}">${sym?.entity ?? "?"}</span> ${piece.name}`;
}