/*
  utils.js — Save Pet
  Math helpers, a deterministic RNG (used to generate the 100
  levels + daily challenge), a safe localStorage save-game
  wrapper, a tiny pub/sub event bus, and small DOM helpers.
*/
window.SP = window.SP || {};

SP.Utils = (function () {
  "use strict";

  function lerp(a, b, t) { return a + (b - a) * t; }
  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function dist(x1, y1, x2, y2) { return Math.hypot(x2 - x1, y2 - y1); }

  function distToSegment(px, py, ax, ay, bx, by) {
    const abx = bx - ax, aby = by - ay;
    const apx = px - ax, apy = py - ay;
    const abLenSq = abx * abx + aby * aby;
    let t = abLenSq > 0 ? (apx * abx + apy * aby) / abLenSq : 0;
    t = clamp(t, 0, 1);
    const cx = ax + abx * t, cy = ay + aby * t;
    return Math.hypot(px - cx, py - cy);
  }

  /* Deterministic PRNG (mulberry32). Same seed -> same sequence,
     which is how every one of the 100 levels stays identical
     across plays/devices instead of being randomised each time. */
  function mulberry32(seed) {
    let a = seed >>> 0;
    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 rngRange(rng, min, max) { return min + rng() * (max - min); }
  function rngInt(rng, min, max) { return Math.floor(rngRange(rng, min, max + 1)); }
  function rngPick(rng, arr) { return arr[Math.floor(rng() * arr.length)]; }
  function rngChance(rng, p) { return rng() < p; }

  function formatTime(seconds) {
    seconds = Math.max(0, Math.ceil(seconds));
    const m = Math.floor(seconds / 60);
    const s = seconds % 60;
    return m + ":" + String(s).padStart(2, "0");
  }

  function todayKey(d) {
    d = d || new Date();
    return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
  }

  function dateSeed(dateStr) {
    let h = 0;
    for (let i = 0; i < dateStr.length; i++) {
      h = (Math.imul(31, h) + dateStr.charCodeAt(i)) | 0;
    }
    return h >>> 0;
  }

  function daysBetween(a, b) {
    const MS = 24 * 60 * 60 * 1000;
    const da = new Date(a + "T00:00:00");
    const db = new Date(b + "T00:00:00");
    return Math.round((db - da) / MS);
  }

  /* ---------------- DOM helpers ---------------- */
  function $(sel, root) { return (root || document).querySelector(sel); }
  function $all(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
  function on(el, ev, fn, opts) { if (el) el.addEventListener(ev, fn, opts || false); }
  function ce(tag, cls, html) {
    const el = document.createElement(tag);
    if (cls) el.className = cls;
    if (html !== undefined) el.innerHTML = html;
    return el;
  }

  /* ---------------- Haptics ---------------- */
  function vibrate(pattern) {
    try {
      const save = SP.Storage && SP.Storage.data;
      if (save && save.settings && !save.settings.hapticsOn) return;
      if (navigator.vibrate) navigator.vibrate(pattern);
    } catch (e) { /* unsupported — safe no-op */ }
  }

  /* ---------------- Tiny event bus ---------------- */
  const listeners = {};
  function on_(evt, fn) {
    (listeners[evt] = listeners[evt] || []).push(fn);
    return fn;
  }
  function off_(evt, fn) {
    if (!listeners[evt]) return;
    listeners[evt] = listeners[evt].filter(function (f) { return f !== fn; });
  }
  function emit(evt, payload) {
    (listeners[evt] || []).slice().forEach(function (fn) {
      try { fn(payload); } catch (e) { console.error("[SavePet] listener error for", evt, e); }
    });
  }

  return {
    lerp: lerp, clamp: clamp, dist: dist, distToSegment: distToSegment,
    mulberry32: mulberry32, rngRange: rngRange, rngInt: rngInt, rngPick: rngPick, rngChance: rngChance,
    formatTime: formatTime, todayKey: todayKey, dateSeed: dateSeed, daysBetween: daysBetween,
    $: $, $all: $all, on: on, ce: ce,
    vibrate: vibrate,
    events: { on: on_, off: off_, emit: emit }
  };
})();

/* ---------------- Save-game storage ---------------- */
SP.Storage = (function () {
  "use strict";
  const KEY = (SP.Config && SP.Config.STORAGE_KEY) || "savepet_save_v1";
  let memoryFallback = null;
  let storageAvailable = true;

  try {
    const t = "__sp_test__";
    window.localStorage.setItem(t, "1");
    window.localStorage.removeItem(t);
  } catch (e) {
    storageAvailable = false;
  }

  function defaultSave() {
    return {
      version: 1,
      coins: SP.Config.STARTING_COINS,
      levels: {},
      unlockedLevel: 1,
      achievements: {},
      stats: {
        totalBeesPopped: 0,
        totalCoinsEarnedLifetime: SP.Config.STARTING_COINS,
        levelsCompletedWithoutHint: 0
      },
      endless: { bestTimeSeconds: 0, bestBeesPopped: 0 },
      random: { bestTimeSeconds: 0 },
      daily: { lastCompletedDate: null, streakCount: 0, totalCompleted: 0, lastLoginBonusDate: null, loginStreak: 0 },
      settings: { musicOn: true, sfxOn: true, hapticsOn: true, musicVolume: 0.6, sfxVolume: 0.85, reduceEffects: false }
    };
  }

  function deepMerge(base, extra) {
    if (!extra || typeof extra !== "object") return base;
    Object.keys(base).forEach(function (k) {
      if (extra[k] === undefined) return;
      if (typeof base[k] === "object" && base[k] !== null && !Array.isArray(base[k])) {
        base[k] = deepMerge(base[k], extra[k]);
      } else {
        base[k] = extra[k];
      }
    });
    return base;
  }

  function load() {
    let parsed = null;
    try {
      const raw = storageAvailable ? window.localStorage.getItem(KEY) : memoryFallback;
      if (raw) parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
    } catch (e) { parsed = null; }
    const merged = deepMerge(defaultSave(), parsed);
    this.data = merged;
    return merged;
  }

  function save() {
    try {
      const json = JSON.stringify(this.data);
      if (storageAvailable) window.localStorage.setItem(KEY, json);
      else memoryFallback = json;
    } catch (e) {
      console.warn("[SavePet] Could not persist save game", e);
    }
    SP.Utils.events.emit("saveChanged", this.data);
  }

  function reset() {
    this.data = defaultSave();
    this.save();
  }

  return { data: null, load: load, save: save, reset: reset, isAvailable: function () { return storageAvailable; } };
})();
