// Memory Match board engine — fully deterministic ("code disposes"): a given seed
// always yields the same board, so the daily is shared across devices with no
// server round-trip. Faces are stored as KEYS (icon name or color key), never raw
// indices, so saved progress survives layout tweaks (Codex).
import { ICON_KEYS } from './icons.js';
import { COLORS, deltaE } from './palette.js';

// Tiers — Gentle/Standard match pairs; Expert matches THREE of a kind and uses a
// bigger grid so it reads as a real step up (Codex's sizing).
//   gentle  4×3 = 12 cards = 6 pairs
//   standard 4×4 = 16 cards = 8 pairs
//   expert  6×4 = 24 cards = 8 triples
export const TIERS = {
  gentle: { label: 'Gentle', cols: 4, rows: 3, faces: 6, matchN: 2 },
  standard: { label: 'Standard', cols: 4, rows: 4, faces: 8, matchN: 2 },
  expert: { label: 'Expert', cols: 6, rows: 4, faces: 8, matchN: 3 },
};
export const TIER_KEYS = Object.keys(TIERS);
export const FORMAT_KEYS = ['icons', 'colors'];

// Minimum perceptual gap (ΔE) we try to keep between a board's colors so two
// near-identical shades never appear together; relaxed only if a board can't fill.
const MIN_DELTA_E = 26;

function hashStr(s) {
  let h = 2166136261 >>> 0;
  for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
  return h >>> 0;
}
function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
function shuffle(arr, rng) {
  const a = arr.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(rng() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

// Pick `n` colors, seed-varied but spaced: walk a seeded shuffle keeping only
// colors at least `threshold` ΔE from those already chosen; relax if we can't fill.
function pickColors(n, rng) {
  const order = shuffle(COLORS, rng);
  for (let threshold = MIN_DELTA_E; threshold >= 0; threshold -= 3) {
    const chosen = [];
    for (const c of order) {
      if (chosen.every((p) => deltaE(p.lab, c.lab) >= threshold)) chosen.push(c);
      if (chosen.length === n) break;
    }
    if (chosen.length === n) return chosen.map((c) => c.key);
  }
  return order.slice(0, n).map((c) => c.key);   // pool too small (shouldn't happen)
}

function pickFaces(format, n, rng) {
  if (format === 'colors') return pickColors(n, rng);
  return shuffle(ICON_KEYS, rng).slice(0, n);
}

// Build a board. `seed` is the day's date (daily) or a random token (free play).
export function buildBoard({ format = 'icons', tier = 'standard', seed = '' } = {}) {
  const t = TIERS[tier] || TIERS.standard;
  const fmt = FORMAT_KEYS.includes(format) ? format : 'icons';
  const rng = mulberry32(hashStr(`match:v1:${fmt}:${tier}:${seed}`));
  const faces = pickFaces(fmt, t.faces, rng);
  const multiset = [];
  for (const key of faces) for (let c = 0; c < t.matchN; c++) multiset.push(key);
  const cards = shuffle(multiset, rng).map((key, id) => ({ id, key }));
  return { cards, faces, matchN: t.matchN, cols: t.cols, rows: t.rows, format: fmt, tier, label: t.label };
}

// A short token for a fresh free-play board (deterministic given the inputs so a
// reload resumes the same board); callers persist it.
export function freeSeed(n) {
  return 'f' + (n >>> 0).toString(36);
}
