/* ============================================================
   game.js - Snoopy Tennis game engine: state machine, ball
   physics (authentic step-by-step LCD motion), scoring, speed
   ramp, Lucy interceptions and chance-time bonus.
   ============================================================ */
(function (ST) {
  "use strict";

  var Geo, Sound, Renderer, Input;
  var canvas, ctx;
  var running = false, lastTs = 0;

  /* ---- tunables ---- */
  var SWING_MS = 230;         // whiffed swing lock time
  var SWING_HIT_MS = 130;     // swing anim on a successful hit (movement still allowed)
  var FACE_LINGER = 1.35;     // how long (× step) the ball waits at Snoopy's face
  // Speeds, probabilities and timings live in ST.Gameplay (config.js).
  function GP() { return ST.Gameplay; }

  var state = null;

  function freshState(type) {
    return {
      mode: "playing",
      type: type || "A",
      score: 0,
      misses: 0,
      snoopy: { level: 1, swing: 0, missLock: false, sleeping: false, sleepTimer: 0 },
      charlie: { frame: 0, timer: 0, pow: 0 },
      lucy: { visible: false, frame: 0, timer: 0, active: 0 },
      balls: [],
      serveTimer: 900,
      bonus: false,
      bonusTimer: 0,
      slowUntil: 0,             // temporary slowdown timestamp (ms elapsed)
      elapsed: 0,
      clock: { h: 12, m: 0, t: 0 },
      lastMilestone: 0
    };
  }

  /* ---- geometry helpers ---- */
  function makeArc(x0, y0, x1, y1, peak, n) {
    var pts = [];
    for (var i = 0; i < n; i++) {
      var t = n === 1 ? 0 : i / (n - 1);
      pts.push({
        x: x0 + (x1 - x0) * t,
        y: y0 + (y1 - y0) * t - peak * Math.sin(Math.PI * t)
      });
    }
    return pts;
  }

  /* ---- speed model ---- */
  function stepInterval() {
    var g = GP();
    var base = g.normalStep * (state.type === "B" ? g.gameBFactor : 1);
    var v = base - state.score * 0.05;
    if (state.elapsed < state.slowUntil) v += 45;   // brief slowdown after each 100
    return Math.max(58, v);
  }
  function serveInterval() {
    var base = 2100 * (state.type === "B" ? GP().gameBFactor : 1);
    var v = base - state.score * 0.7;
    if (state.elapsed < state.slowUntil) v += 500;
    return Math.max(650, v);
  }
  function maxBalls() {
    if (state.type === "B") return state.score > 300 ? 3 : 2;
    return state.score > 500 ? 2 : 1;
  }

  /* ---- ball spawning ---- */
  function countIncoming() {
    var c = 0;
    for (var i = 0; i < state.balls.length; i++) if (state.balls[i].kind === "in") c++;
    return c;
  }

  // isServe = true for a fresh serve (shows POW!), false for a rally return.
  function spawnServe(isServe) {
    var level = Math.floor(Math.random() * 3);
    var s = GP().serveStart;
    var path = makeArc(s.x, s.y, Geo.hitX(level), Geo.faceY(level), 72, 9);
    state.balls.push({
      kind: "in", via: "charlie", level: level, path: path, idx: 0,
      stepT: stepInterval(), step: stepInterval(), arrived: false, faceT: 0
    });
    state.charlie.frame = 1; state.charlie.timer = 260;
    if (isServe) state.charlie.pow = GP().powMs;    // POW! only on a serve
    state.serveTimer = serveInterval();
    Sound.serve();
  }

  // Snoopy's return: either intercepted by Lucy, or all the way to Charlie.
  function spawnReturnVisual(level, toLucy) {
    var path, dest;
    if (toLucy) {
      path = makeArc(Geo.hitX(level), Geo.faceY(level), Geo.lucyHitPoint.x, Geo.lucyHitPoint.y, 46, 7);
      dest = "lucy";
      state.lucy.visible = true; state.lucy.active++;
    } else {
      path = makeArc(Geo.hitX(level), Geo.faceY(level), Geo.charlie.x, Geo.charlieRacketY, 40, 8);
      dest = "charlie";
    }
    state.balls.push({
      kind: "out", dest: dest, level: level, path: path, idx: 0,
      stepT: stepInterval() * 0.75, step: stepInterval() * 0.75, arrived: false
    });
  }

  function spawnLucySmash() {
    var level = Math.floor(Math.random() * 3);      // Lucy can aim at any level
    var spd = stepInterval() * GP().lucyFactor;     // faster than a normal ball
    var path = makeArc(Geo.lucyHitPoint.x, Geo.lucyHitPoint.y, Geo.hitX(level), Geo.faceY(level), 54, 7);
    state.balls.push({
      kind: "in", via: "lucy", level: level, path: path, idx: 0,
      stepT: spd, step: spd, arrived: false, faceT: 0
    });
    state.lucy.frame = 1; state.lucy.timer = 240;
    Sound.lucy();
  }

  // Ball returned by Snoopy has reached Charlie: he returns it, or whiffs.
  function charlieReceives() {
    if (Math.random() < GP().pCharlieMiss) {
      state.charlie.frame = 0;                       // Charlie misses - court clears
      state.serveTimer = 600;                        // brief pause, then a new serve
    } else {
      spawnServe(false);                             // Charlie returns it (no POW!)
    }
  }

  /* ---- scoring & milestones ---- */
  function addScore(pts) {
    var prev = state.score;
    state.score += pts;
    if (state.score > 9999) state.score -= 10000;   // authentic rollover
    Sound.point();
    checkMilestones(prev, state.score);
  }

  function checkMilestones(prev, now) {
    if (now < prev) return;                          // rolled over; skip
    for (var base = 0; base <= now; base += 1000) {
      [base + 200, base + 500].forEach(function (m) {
        if (m > prev && m <= now && m > state.lastMilestone) {
          state.lastMilestone = m;
          var hadNoMiss = state.misses === 0;
          state.misses = 0;                          // clear misses
          if (hadNoMiss) {
            state.bonus = true;
            state.bonusTimer = 30000;                // chance time (~30s)
            Sound.bonus();
            console.log("[game] CHANCE TIME! milestone " + m);
          }
        }
      });
    }
  }

  function registerMiss() {
    state.misses++;
    Sound.miss();
    console.log("[game] miss " + state.misses + "/3");
    if (state.misses >= 3) { gameOver(); return; }
    startSleep();
  }

  // Snoopy naps on the doghouse; the court is cleared and play resumes
  // after the configured sleep time.
  function startSleep() {
    state.snoopy.sleeping = true;
    state.snoopy.sleepTimer = GP().sleepMs;
    state.snoopy.swing = 0; state.snoopy.missLock = false;
    state.balls.length = 0;
    state.lucy.visible = false; state.lucy.active = 0; state.lucy.frame = 0;
    state.charlie.frame = 0; state.charlie.pow = 0;
  }

  function gameOver() {
    state.mode = "over";
    Sound.gameover();
    console.log("[game] GAME OVER - score " + state.score);
    updateOverlay();
  }

  /* ---- input actions ---- */
  function doMove(dir) {
    if (state.mode !== "playing" || state.snoopy.sleeping) return;
    if (state.snoopy.missLock && state.snoopy.swing > 0) return;   // locked mid-whiff
    var nl = state.snoopy.level + (dir === "up" ? -1 : 1);
    state.snoopy.level = Math.max(0, Math.min(2, nl));
  }

  function doHit() {
    if (state.mode !== "playing" || state.snoopy.sleeping) return;
    // find the closest hittable ball at Snoopy's level
    var target = null;
    for (var i = 0; i < state.balls.length; i++) {
      var b = state.balls[i];
      if (b.kind !== "in") continue;
      var atFace = b.arrived || b.idx >= b.path.length - 2;
      if (atFace && b.level === state.snoopy.level) { target = b; break; }
    }
    if (target) {
      var pts = target.via === "lucy" ? (state.bonus ? 6 : 3) : (state.bonus ? 5 : 2);
      addScore(pts);
      Sound.hit();
      removeBall(target);
      state.snoopy.swing = SWING_HIT_MS; state.snoopy.missLock = false;
      // Snoopy's return heads to Charlie, unless Lucy intercepts it.
      var toLucy = Math.random() < GP().pLucy;
      spawnReturnVisual(state.snoopy.level, toLucy);
    } else {
      // whiff: locked until racket returns
      state.snoopy.swing = SWING_MS; state.snoopy.missLock = true;
      Sound.serve();  // soft swish
    }
  }

  function removeBall(b) {
    var i = state.balls.indexOf(b);
    if (i >= 0) state.balls.splice(i, 1);
  }

  // A missed ball slips quickly off the right edge before Snoopy naps.
  function startSlip(b) {
    b.kind = "slip";
    b.arrived = false;
    b.idx = 0;
    b.path = makeArc(b.x, b.y, 660, b.y + 34, 8, 6);   // quick exit to the right
    b.step = 26; b.stepT = 26;
  }

  /* ---- per-frame update ---- */
  function advanceBall(b, dt) {
    if (b.arrived) {
      if (b.kind === "in") {
        b.faceT -= dt;
        if (b.faceT <= 0) { startSlip(b); }            // missed: slip off-screen
      }
      return;
    }
    b.stepT -= dt;
    if (b.stepT > 0) return;
    b.stepT += b.step;
    b.idx++;
    var last = b.path.length - 1;
    if (b.idx >= last) {
      b.idx = last;
      b.arrived = true;
      if (b.kind === "in") {
        b.faceT = b.step * FACE_LINGER;
      } else if (b.kind === "slip") {
        // the missed ball has left the screen
        removeBall(b);
        registerMiss();
      } else {
        // "out" ball reached its destination
        removeBall(b);
        if (b.dest === "lucy") {
          spawnLucySmash();
          state.lucy.active--;
        } else if (b.dest === "charlie") {
          charlieReceives();
        }
      }
    }
  }

  function ballXY(b) {
    var p = b.path[Math.min(b.idx, b.path.length - 1)];
    b.x = p.x; b.y = p.y;
  }

  function update(dt) {
    state.elapsed += dt;

    if (state.mode === "clock") { updateClock(dt); return; }
    if (state.mode !== "playing") return;

    // Snoopy asleep on the doghouse: freeze play until he wakes
    if (state.snoopy.sleeping) {
      state.snoopy.sleepTimer -= dt;
      if (state.snoopy.sleepTimer <= 0) {
        state.snoopy.sleeping = false;
        state.serveTimer = 500;               // resume shortly after waking
        console.log("[game] Snoopy wakes up");
      }
      return;
    }

    // bonus timer
    if (state.bonus) {
      state.bonusTimer -= dt;
      if (state.bonusTimer <= 0) { state.bonus = false; console.log("[game] chance time over"); }
    }

    // POW! text fade
    if (state.charlie.pow > 0) state.charlie.pow -= dt;

    // swing / animation timers
    if (state.snoopy.swing > 0) {
      state.snoopy.swing -= dt;
      if (state.snoopy.swing <= 0) { state.snoopy.swing = 0; state.snoopy.missLock = false; }
    }
    if (state.charlie.timer > 0) { state.charlie.timer -= dt; if (state.charlie.timer <= 0) state.charlie.frame = 0; }
    if (state.lucy.timer > 0)   { state.lucy.timer -= dt;   if (state.lucy.timer <= 0) state.lucy.frame = 0; }
    if (state.lucy.active <= 0 && !hasLucyBall()) state.lucy.visible = false;

    // serve cadence - only start a fresh rally when the court is empty.
    // (Rally continuations come from Charlie/Lucy, never from here, so
    // Charlie can't "return and serve" at the same time.)
    state.serveTimer -= dt;
    if (state.serveTimer <= 0 && state.balls.length === 0) {
      spawnServe(true);
    }

    // brief slowdown when passing each 100 boundary
    var band = Math.floor(state.score / 100);
    if (band !== state._band) {
      if (state._band !== undefined) state.slowUntil = state.elapsed + 4500;
      state._band = band;
    }

    // advance balls (iterate over a copy since we may splice)
    var snapshot = state.balls.slice();
    for (var i = 0; i < snapshot.length; i++) advanceBall(snapshot[i], dt);
    for (var j = 0; j < state.balls.length; j++) ballXY(state.balls[j]);
  }

  function hasLucyBall() {
    for (var i = 0; i < state.balls.length; i++) {
      var b = state.balls[i];
      if (b.via === "lucy") return true;
      if (b.dest === "lucy") return true;
    }
    return false;
  }

  function hasOutBall() {
    for (var i = 0; i < state.balls.length; i++) {
      if (state.balls[i].kind === "out") return true;
    }
    return false;
  }

  function updateClock(dt) {
    state.clock.t += dt;
    if (state.clock.t >= 60000) {           // advance a minute
      state.clock.t -= 60000;
      state.clock.m++;
      if (state.clock.m >= 60) { state.clock.m = 0; state.clock.h = state.clock.h % 12 + 1; }
    }
  }

  /* ---- main loop ---- */
  function loop(ts) {
    if (!running) return;
    var dt = Math.min(50, ts - lastTs);     // clamp big gaps (tab switches)
    lastTs = ts;
    try {
      update(dt);
      Renderer.drawScene(ctx, state);
      if (state.mode === "over") drawGameOver();
    } catch (e) {
      console.error("[game] loop error:", e);
    }
    requestAnimationFrame(loop);
  }

  function drawGameOver() {
    ctx.save();
    ctx.fillStyle = "rgba(60,64,44,0.28)";
    ctx.fillRect(0, 0, Geo.W, Geo.H);
    ctx.fillStyle = ST.Sprites.SEG;
    ctx.font = "bold 34px Arial"; ctx.textAlign = "center";
    ctx.fillText("GAME OVER", Geo.W / 2, Geo.H / 2 - 6);
    ctx.font = "bold 16px Arial";
    ctx.fillText("Press GAME A or GAME B", Geo.W / 2, Geo.H / 2 + 26);
    ctx.textAlign = "left";
    ctx.restore();
  }

  /* ---- overlay text (for accessibility / status) ---- */
  function updateOverlay() {
    var el = document.getElementById("lcd-overlay");
    if (el) el.setAttribute("data-status", state.mode + " score:" + state.score);
  }

  /* ---- public control ---- */
  function start(type) {
    Sound.unlock();
    state = freshState(type);
    state.mode = "playing";
    Sound.start();
    console.log("[game] start Game " + type);
    updateOverlay();
  }

  function toClock() {
    state = freshState("A");
    state.mode = "clock";
  }

  ST.Game = {
    init: function (canvasEl) {
      canvas = canvasEl;
      ctx = canvas.getContext("2d");
      Geo = ST.Geo; Sound = ST.Sound; Renderer = ST.Renderer; Input = ST.Input;
      if (!ctx) { console.error("[game] Canvas 2D context unavailable."); return; }
      toClock();

      Input.on("up", function () { doMove("up"); });
      Input.on("down", function () { doMove("down"); });
      Input.on("hit", function () { doHit(); });
      Input.on("gameA", function () { start("A"); });
      Input.on("gameB", function () { start("B"); });

      running = true;
      lastTs = performance.now();
      requestAnimationFrame(loop);
    },
    getState: function () { return state; },
    start: start
  };
})(window.ST = window.ST || {});
