/*!
 * physics.js — Tilt-maze physics engine
 * -------------------------------------------------------------
 * Gravity-via-tilt, momentum, friction, circle-vs-rectangle collision
 * with sliding + bounce, rotating-bar collision, floor zone effects
 * (ice / sticky / wind / speed pads), hazards, portals and gates.
 *
 * No external dependencies. Attaches everything to a shared LAB
 * namespace so the other game files can use it via <script> tags.
 */
(function (global) {
  'use strict';
  const LAB = (global.LAB = global.LAB || {});

  // ---- Tunable constants -------------------------------------------------
  const GRAVITY = 1500;        // px/s^2 applied at full tilt (magnitude 1.0)
  const RESTITUTION = 0.32;    // bounciness on wall / obstacle impact
  const BASE_FRICTION = 3.2;   // per-second exponential damping (normal floor)
  const ICE_FRICTION = 0.6;    // much less damping -> long slides
  const STICKY_FRICTION = 9.0; // heavy damping -> mud/sticky feel
  const WIND_FORCE = 900;      // px/s^2 constant push inside a wind zone
  const SPEED_PAD_BOOST = 480; // px/s impulse added along current heading
  const MAX_SPEED = 820;       // safety clamp so physics never explodes
  const FIXED_DT = 1 / 120;    // physics sub-step size, decoupled from render
  const PORTAL_COOLDOWN = 0.45;

  function clamp(v, lo, hi) { return v < lo ? lo : (v > hi ? hi : v); }

  // Closest point on an axis-aligned rect to a circle center -> push-out data
  function circleRectPush(cx, cy, r, rect) {
    const closestX = clamp(cx, rect.x, rect.x + rect.w);
    const closestY = clamp(cy, rect.y, rect.y + rect.h);
    const dx = cx - closestX, dy = cy - closestY;
    const d2 = dx * dx + dy * dy;
    if (d2 >= r * r) return null;
    const d = Math.sqrt(d2);
    if (d > 1e-6) return { nx: dx / d, ny: dy / d, depth: r - d };
    // Center is inside the rect (rare, e.g. tunneling) - push out the short way
    const left = cx - rect.x, right = (rect.x + rect.w) - cx;
    const top = cy - rect.y, bottom = (rect.y + rect.h) - cy;
    const m = Math.min(left, right, top, bottom);
    if (m === left) return { nx: -1, ny: 0, depth: r };
    if (m === right) return { nx: 1, ny: 0, depth: r };
    if (m === top) return { nx: 0, ny: -1, depth: r };
    return { nx: 0, ny: 1, depth: r };
  }

  // Resolve a static rectangle: push the ball out, reflect only the normal
  // velocity component (this is what gives real edge-sliding + bounce).
  function resolveStaticRect(ball, rect) {
    const hit = circleRectPush(ball.x, ball.y, ball.radius, rect);
    if (!hit) return false;
    ball.x += hit.nx * hit.depth;
    ball.y += hit.ny * hit.depth;
    const vn = ball.vx * hit.nx + ball.vy * hit.ny;
    if (vn < 0) {
      ball.vx -= (1 + RESTITUTION) * vn * hit.nx;
      ball.vy -= (1 + RESTITUTION) * vn * hit.ny;
      ball.vx *= 0.985; ball.vy *= 0.985; // tiny extra rolling friction on impact
    }
    return Math.abs(vn);
  }

  // Rotating barrier: transform the ball into the bar's local (unrotated)
  // frame, collide as an axis-aligned rect there, then rotate the normal back.
  function resolveRotatingBar(ball, bar) {
    const cos = Math.cos(-bar.angle), sin = Math.sin(-bar.angle);
    const rx = ball.x - bar.cx, ry = ball.y - bar.cy;
    const lx = rx * cos - ry * sin;
    const ly = rx * sin + ry * cos;
    const rect = { x: -bar.length / 2, y: -bar.thickness / 2, w: bar.length, h: bar.thickness };
    const hit = circleRectPush(lx, ly, ball.radius, rect);
    if (!hit) return false;
    const cos2 = Math.cos(bar.angle), sin2 = Math.sin(bar.angle);
    const nx = hit.nx * cos2 - hit.ny * sin2;
    const ny = hit.nx * sin2 + hit.ny * cos2;
    ball.x += nx * hit.depth;
    ball.y += ny * hit.depth;
    const vn = ball.vx * nx + ball.vy * ny;
    if (vn < 0) {
      ball.vx -= (1 + RESTITUTION) * vn * nx;
      ball.vy -= (1 + RESTITUTION) * vn * ny;
    }
    // impart a touch of the bar's own motion so a fast spin feels dangerous
    ball.vx += -ny * bar.tipSpeed * 0.15;
    ball.vy += nx * bar.tipSpeed * 0.15;
    return true;
  }

  // Only test walls near the ball's current cell (3x3 neighbourhood).
  // Keeps collision O(1) regardless of maze size.
  function nearbyWalls(level, ball) {
    const cs = level.cellSize;
    const col = clamp(Math.floor(ball.x / cs), 0, level.cols - 1);
    const row = clamp(Math.floor(ball.y / cs), 0, level.rows - 1);
    const out = [];
    for (let r = row - 1; r <= row + 1; r++) {
      for (let c = col - 1; c <= col + 1; c++) {
        const bucket = level.wallBuckets[r + ',' + c];
        if (bucket) for (let i = 0; i < bucket.length; i++) out.push(bucket[i]);
      }
    }
    return out;
  }

  function zoneAt(level, x, y) {
    const zones = level.zones;
    for (let i = 0; i < zones.length; i++) {
      const z = zones[i];
      if (x >= z.x && x <= z.x + z.w && y >= z.y && y <= z.y + z.h) return z;
    }
    return null;
  }

  // Advance the whole level's simulation by dt seconds. Mutates `ball` and
  // level's dynamic state, and reports what happened this tick in `events`.
  function integrate(ball, level, dt, tiltX, tiltY, elapsed, events) {
    ball.vx += tiltX * GRAVITY * dt;
    ball.vy += tiltY * GRAVITY * dt;

    const zone = zoneAt(level, ball.x, ball.y);
    let friction = BASE_FRICTION;
    if (zone) {
      switch (zone.type) {
        case 'ice': friction = ICE_FRICTION; break;
        case 'sticky': friction = STICKY_FRICTION; break;
        case 'wind':
          ball.vx += zone.dirX * WIND_FORCE * dt;
          ball.vy += zone.dirY * WIND_FORCE * dt;
          break;
        case 'speedpad':
          if (!(ball._padCooldown > 0)) {
            const spd = Math.hypot(ball.vx, ball.vy) || 1;
            ball.vx += (ball.vx / spd) * SPEED_PAD_BOOST;
            ball.vy += (ball.vy / spd) * SPEED_PAD_BOOST;
            ball._padCooldown = 0.5;
            events.pad = true;
          }
          break;
        case 'checkpoint':
          if (!zone.triggered) { zone.triggered = true; events.checkpoint = { x: zone.cx, y: zone.cy }; }
          break;
        case 'portal':
          if (!(ball._portalCooldown > 0)) {
            const pair = level.portalPairs[zone.pairId];
            const dest = pair[0] === zone ? pair[1] : pair[0];
            ball.x = dest.cx; ball.y = dest.cy;
            ball._portalCooldown = PORTAL_COOLDOWN;
            events.teleported = true;
          }
          break;
        case 'switch':
          if (!zone.triggered) {
            zone.triggered = true;
            events.switchHit = zone.gateId;
            if (zone.timed) level.gates[zone.gateId].openUntil = elapsed + zone.timedDuration;
            else level.gates[zone.gateId].open = true;
          }
          break;
        case 'key':
          if (!zone.collected) {
            zone.collected = true;
            events.keyHit = zone.doorId;
            level.gates[zone.doorId].open = true;
          }
          break;
        case 'hole':
        case 'spike':
          events.hazard = zone.type;
          break;
        case 'finish':
          events.finished = true;
          break;
      }
    }
    if (ball._padCooldown > 0) ball._padCooldown -= dt;
    if (ball._portalCooldown > 0) ball._portalCooldown -= dt;

    for (const id in level.gates) {
      const g = level.gates[id];
      if (g.openUntil !== undefined && g.openUntil <= elapsed) { g.open = false; g.openUntil = undefined; }
    }

    const damp = Math.exp(-friction * dt);
    ball.vx *= damp; ball.vy *= damp;

    const speed = Math.hypot(ball.vx, ball.vy);
    if (speed > MAX_SPEED) { const k = MAX_SPEED / speed; ball.vx *= k; ball.vy *= k; }

    ball.x += ball.vx * dt;
    ball.y += ball.vy * dt;

    let maxImpact = 0;
    const walls = nearbyWalls(level, ball);
    for (let i = 0; i < walls.length; i++) {
      const w = walls[i];
      if (w.gateId && level.gates[w.gateId] && level.gates[w.gateId].open) continue;
      const impact = resolveStaticRect(ball, w);
      if (impact) maxImpact = Math.max(maxImpact, impact);
    }
    const b = level.boundary;
    resolveStaticRect(ball, b.top); resolveStaticRect(ball, b.bottom);
    resolveStaticRect(ball, b.left); resolveStaticRect(ball, b.right);

    for (let i = 0; i < level.movingObstacles.length; i++) {
      const ob = level.movingObstacles[i];
      const t = (Math.sin(elapsed * ob.speed + ob.phase) + 1) / 2;
      ob.x = ob.x0 + (ob.x1 - ob.x0) * t;
      ob.y = ob.y0 + (ob.y1 - ob.y0) * t;
      const dx = ball.x - ob.x, dy = ball.y - ob.y;
      const dist = Math.hypot(dx, dy);
      const minDist = ball.radius + ob.radius;
      if (dist < minDist) {
        if (ob.hazard) { events.hazard = 'obstacle'; }
        else {
          const nx = dist > 1e-6 ? dx / dist : 1, ny = dist > 1e-6 ? dy / dist : 0;
          ball.x += nx * (minDist - dist); ball.y += ny * (minDist - dist);
          const vn = ball.vx * nx + ball.vy * ny;
          if (vn < 0) { ball.vx -= (1 + RESTITUTION) * vn * nx; ball.vy -= (1 + RESTITUTION) * vn * ny; }
        }
      }
    }
    for (let i = 0; i < level.rotatingBars.length; i++) {
      const bar = level.rotatingBars[i];
      bar.angle = bar.baseAngle + elapsed * bar.angularSpeed;
      bar.tipSpeed = bar.angularSpeed * (bar.length / 2);
      resolveRotatingBar(ball, bar);
    }

    events.impact = maxImpact;
    events.speed = Math.hypot(ball.vx, ball.vy);
  }

  // Public step(): sub-steps a possibly-large dt into fixed-size ticks for
  // numerical stability, and stops early once the level is won or lost.
  function step(ball, level, dtInput, tiltX, tiltY, elapsed) {
    const steps = Math.max(1, Math.ceil(dtInput / FIXED_DT));
    const subDt = dtInput / steps;
    const events = { finished: false, hazard: null, checkpoint: null, pad: false, teleported: false, switchHit: null, keyHit: null, impact: 0, speed: 0 };
    let t = elapsed;
    for (let i = 0; i < steps; i++) {
      t += subDt;
      integrate(ball, level, subDt, tiltX, tiltY, t, events);
      if (events.finished || events.hazard) break;
    }
    return events;
  }

  LAB.Physics = { FIXED_DT, integrate, step, circleRectPush, clamp };
})(typeof window !== 'undefined' ? window : globalThis);
