popup-messages.js

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

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.
 */
/**
 * Look up the `.cell` span at (x, y). Cannot index `boardEl.children`
 * directly — `#board` also contains non-cell children (e.g.
 * `#loading-overlay`), which would throw off a positional index.
 */
function _cellEl(boardEl, x, y) {
  return boardEl.querySelectorAll(".cell")[y * COLUMNS + x];
}

function _syncBoardScale(boardEl) {
  const w = boardEl.getBoundingClientRect().width;
  if (w > 0) {
    document.documentElement.style.setProperty(
      "--board-cell-w",
      `${w / COLUMNS}px`,
    );
  }
}

export function initPopupMessages() {
  const boardEl = document.getElementById("board");
  const popupLayer = document.getElementById("popup-layer");
  if (!boardEl || !popupLayer) return;

  _syncBoardScale(boardEl);
  const ro = new ResizeObserver(() => _syncBoardScale(boardEl));
  ro.observe(boardEl);

  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.
 * Positioning mirrors Java's CellMessageModalPanel.center(): starts right of cell,
 * then clamps to viewport bounds.
 */
function _showModalPopup(boardEl, popupLayer, text, cellX, cellY) {
  const cellEl = _cellEl(boardEl, cellX, cellY);
  if (!cellEl) return;

  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);

  // Measure before positioning
  popup.style.visibility = "hidden";
  popup.style.left = "0";
  popup.style.top = "0";
  popupLayer.appendChild(popup);

  const cellRect = cellEl.getBoundingClientRect();
  const layerRect = popupLayer.getBoundingClientRect();
  const popupRect = popup.getBoundingClientRect();
  const vw = window.innerWidth;
  const vh = window.innerHeight;

  let left = cellRect.right + 6;
  let top = cellRect.top;

  // If too far left, move to left:5 and shift above the cell
  if (left < 5) {
    left = 5;
    top -= popupRect.height;
  }
  // If extends past right edge, shift left
  if (left + popupRect.width > vw) {
    left -= left + popupRect.width - vw + 5;
  }
  // If extends past bottom, shift up
  if (top + popupRect.height > vh) {
    top -= top + popupRect.height - vh + 5;
  }

  popup.style.left = `${left - layerRect.left}px`;
  popup.style.top = `${top - layerRect.top}px`;
  popup.style.visibility = "";

  // 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 near a cell.
 * Uses an 8-position strategy (matching Java's MessageManager) to find the
 * first position that stays in the viewport, avoids other popups, and avoids
 * the player's adjacent cells.
 * 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 = _cellEl(boardEl, cellX, cellY);
  if (!cellEl) return null;

  const popup = document.createElement("div");
  popup.className = "cell-popup";
  if (isItemList) popup.dataset.itemList = "true";
  popup.innerHTML = text;

  // Append hidden so we can measure its rendered size
  popup.style.visibility = "hidden";
  popup.style.left = "0";
  popup.style.top = "0";
  popupLayer.appendChild(popup);

  _positionPopup(popup, cellEl, popupLayer, boardEl, cellX, cellY);
  popup.style.visibility = "";

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

/**
 * Position a popup using the 8-position strategy from Java's MessageManager.
 * Tries positions in the same order as Java, picks the first valid one.
 */
function _positionPopup(popup, cellEl, popupLayer, boardEl, cellX, cellY) {
  const cellRect = cellEl.getBoundingClientRect();
  const layerRect = popupLayer.getBoundingClientRect();
  const popupRect = popup.getBoundingClientRect();
  const vw = window.innerWidth;
  const vh = window.innerHeight;

  const cL = cellRect.left;
  const cT = cellRect.top;
  const cR = cellRect.right;
  const cB = cellRect.bottom;
  const pH = popupRect.height;
  const pW = popupRect.width;

  // 8 positions in the same priority order as Java's POSITIONS array
  const candidates = [
    { left: cR + 6, top: cT }, // RightDown
    { left: cR + 6, top: cB - pH + 3 }, // RightUp
    { left: cL - pW - 1, top: cT }, // LeftDown
    { left: cL - pW - 1, top: cB - pH + 3 }, // LeftUp
    { left: cL + 3, top: cB + 7 }, // BelowRight
    { left: cL - pW - 3, top: cB + 7 }, // BelowLeft
    { left: cL - pW - 3, top: cT - pH - 3 }, // AboveLeft
    { left: cL, top: cT - pH - 3 }, // AboveRight
  ];

  // Existing popup rects for overlap checking
  const peerRects = [
    ...popupLayer.querySelectorAll(".cell-popup, .cell-popup-modal"),
  ]
    .filter((el) => el !== popup)
    .map((el) => el.getBoundingClientRect());

  // Player-adjacent cell rects (only checked when message is not from player's cell)
  const playerCell = game.board?.getCurrentCell();
  const adjacentRects =
    playerCell && (cellX !== playerCell.x || cellY !== playerCell.y)
      ? _getPlayerAdjacentRects(boardEl, playerCell.x, playerCell.y)
      : [];

  let chosenLeft = candidates[0].left;
  let chosenTop = candidates[0].top;
  let found = false;

  for (const { left, top } of candidates) {
    const right = left + pW;
    const bottom = top + pH;

    // Viewport bounds (mirrors Java: left > 5, bottom < window height)
    if (left < 5 || right > vw || top < 0 || bottom > vh) continue;

    const r = { left, top, right, bottom };

    // No overlap with peer popups
    if (peerRects.some((p) => _rectsOverlap(r, p))) continue;

    // No overlap with player's adjacent cells
    if (adjacentRects.some((a) => _rectsOverlap(r, a))) continue;

    chosenLeft = left;
    chosenTop = top;
    found = true;
    break;
  }

  // Fallback: use last candidate; hide any peer popups that overlap us
  if (!found) {
    const last = candidates[candidates.length - 1];
    chosenLeft = last.left;
    chosenTop = last.top;
    const r = {
      left: chosenLeft,
      top: chosenTop,
      right: chosenLeft + pW,
      bottom: chosenTop + pH,
    };
    for (const el of popupLayer.querySelectorAll(
      ".cell-popup, .cell-popup-modal",
    )) {
      if (el !== popup && _rectsOverlap(r, el.getBoundingClientRect())) {
        el.remove();
      }
    }
  }

  popup.style.left = `${chosenLeft - layerRect.left}px`;
  popup.style.top = `${chosenTop - layerRect.top}px`;
}

/** AABB overlap test — mirrors Java's CellMessagePanel.overlaps(). */
function _rectsOverlap(a, b) {
  return !(
    a.bottom < b.top ||
    a.top > b.bottom ||
    a.right < b.left ||
    a.left > b.right
  );
}

/**
 * Returns viewport DOMRects for the 8 cells adjacent to the player.
 * Mirrors Java's current.getAdjacentCells(null) check in positionDoesNotOverlap().
 */
function _getPlayerAdjacentRects(boardEl, px, py) {
  const rects = [];
  for (let dx = -1; dx <= 1; dx++) {
    for (let dy = -1; dy <= 1; dy++) {
      if (dx === 0 && dy === 0) continue;
      const ax = px + dx;
      const ay = py + dy;
      if (ax < 0 || ay < 0) continue;
      const el = _cellEl(boardEl, ax, ay);
      if (el) rects.push(el.getBoundingClientRect());
    }
  }
  return rects;
}

/**
 * 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;

  // 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}`;
}