/*!
 * ui.js — DOM wiring. All HUD, menus and controls are real DOM elements
 * (not drawn on canvas) so they get correct accessibility, crisp text and
 * keyboard focus for free. This file is the entry point: it boots the
 * game once the document is ready.
 */
(function (global) {
  'use strict';
  const LAB = (global.LAB = global.LAB || {});
  function $(sel) { return document.querySelector(sel); }
  function $all(sel) { return Array.prototype.slice.call(document.querySelectorAll(sel)); }

  function init() {
    const canvas = $('#game-canvas');
    const game = new LAB.Game(canvas);
    LAB.gameInstance = game;

    // A persisted 'gyro' setting from a previous session has no live
    // sensor listener attached yet on a fresh load, and iOS requires a
    // fresh tap to re-grant permission anyway - so always start on the
    // joystick and let the player re-enable gyro with one tap. Without
    // this, reloading with gyro previously selected left the joystick
    // hidden AND no sensor listener running - i.e. no working input at all.
    if (game.save.settings.control === 'gyro') {
      game.save.settings.control = 'joystick';
      game.persist();
    }

    applyTheme(game.save.settings.theme);
    LAB.Audio.setVolume(game.save.settings.volume);
    LAB.Audio.setMuted(game.save.settings.muted);
    $('#volume-slider').value = Math.round(game.save.settings.volume * 100);
    $('#sensitivity-slider').value = Math.round(game.save.settings.sensitivity * 100);
    $('#btn-mute').textContent = game.save.settings.muted ? '🔇' : '🔊';

    // Unlock audio on first user gesture (autoplay policy)
    ['pointerdown', 'keydown'].forEach(evt => window.addEventListener(evt, () => LAB.Audio.resumeIfSuspended(), { once: true }));

    const gyro = createGyroController(game);
    wireButtons(game, gyro);
    wireKeyboard(game);
    wireTouch(game);
    wireModals(game);
    wireInterstitialAd(game);
    updateControlUI(game);

    game.onLevelComplete = (stats) => showVictory(game, stats);
    game.onFail = () => flashFail();
    game.onCheckpoint = () => flashCheckpoint();

    game.start();
    requestAnimationFrame(function hudLoop() { updateHud(game); requestAnimationFrame(hudLoop); });
    registerServiceWorker();
  }

  function applyTheme(theme) {
    document.documentElement.setAttribute('data-theme', theme);
    const btn = $('#btn-theme');
    if (btn) btn.textContent = theme === 'dark' ? '☀️' : '🌙';
  }

  function wireButtons(game, gyro) {
    $('#btn-pause').addEventListener('click', () => { game.pause(); $('#pause-modal').hidden = false; });
    $('#btn-resume').addEventListener('click', () => { game.resume(); $('#pause-modal').hidden = true; });
    $('#btn-restart').addEventListener('click', () => { game.restart(); $('#pause-modal').hidden = true; $('#victory-modal').hidden = true; });
    $('#btn-next').addEventListener('click', () => game.next());
    $('#btn-prev').addEventListener('click', () => game.prev());
    $('#btn-next-victory').addEventListener('click', () => { $('#victory-modal').hidden = true; game.next(); });
    $('#btn-retry-victory').addEventListener('click', () => { $('#victory-modal').hidden = true; game.restart(); });

    $('#btn-mute').addEventListener('click', () => {
      game.save.settings.muted = !game.save.settings.muted;
      LAB.Audio.setMuted(game.save.settings.muted);
      $('#btn-mute').textContent = game.save.settings.muted ? '🔇' : '🔊';
      $('#btn-mute').setAttribute('aria-pressed', String(game.save.settings.muted));
      game.persist();
    });
    $('#volume-slider').addEventListener('input', (e) => {
      const v = Number(e.target.value) / 100;
      game.save.settings.volume = v; LAB.Audio.setVolume(v); game.persist();
    });
    $('#sensitivity-slider').addEventListener('input', (e) => {
      game.save.settings.sensitivity = Number(e.target.value) / 100; game.persist();
    });
    $('#btn-theme').addEventListener('click', () => {
      const next = game.save.settings.theme === 'light' ? 'dark' : 'light';
      game.save.settings.theme = next; applyTheme(next); game.bakeWoodTexture(); game.persist();
    });
    $('#btn-level-select').addEventListener('click', () => openLevelSelect(game));
    $('#btn-reset-progress').addEventListener('click', () => {
      if (window.confirm('Reset all progress? This unlocks only Level 1 again and clears your best times. This cannot be undone.')) {
        game.resetProgress();
        $('#pause-modal').hidden = true;
      }
    });

    $all('input[name="control-scheme"]').forEach(r => {
      r.checked = r.value === game.save.settings.control;
      r.addEventListener('change', (e) => {
        if (e.target.value === 'gyro') gyro.enable();
        else gyro.disable(e.target.value);
      });
    });
  }

  function updateControlUI(game) {
    const zone = $('#joystick-zone');
    zone.hidden = game.save.settings.control === 'gyro';
    const gyroRadio = $('#control-gyro');
    if (gyroRadio) gyroRadio.disabled = !window.DeviceOrientationEvent;
    const gyroBtn = $('#btn-gyro');
    if (gyroBtn) gyroBtn.setAttribute('aria-pressed', String(game.save.settings.control === 'gyro'));
  }

  function wireKeyboard(game) {
    const keys = new Set();
    function recompute() {
      const rawX = (keys.has('ArrowRight') || keys.has('KeyD') ? 1 : 0) - (keys.has('ArrowLeft') || keys.has('KeyA') ? 1 : 0);
      const rawY = (keys.has('ArrowDown') || keys.has('KeyS') ? 1 : 0) - (keys.has('ArrowUp') || keys.has('KeyW') ? 1 : 0);
      const len = Math.hypot(rawX, rawY) || 1;
      game.setTilt(rawX / len, rawY / len);
    }
    window.addEventListener('keydown', (e) => {
      if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space'].includes(e.code)) e.preventDefault();
      if (e.code === 'Escape') { $('#btn-pause').click(); return; }
      keys.add(e.code); recompute();
    });
    window.addEventListener('keyup', (e) => { keys.delete(e.code); recompute(); });
    window.addEventListener('blur', () => { keys.clear(); recompute(); });
  }

  function wireTouch(game) {
    const zone = $('#joystick-zone'), stick = $('#joystick-stick'), base = $('#joystick-base');
    let active = false, originX = 0, originY = 0, activeTouchId = null;
    const MAXR = 46;

    // Convert page (clientX/Y) coordinates into coordinates local to the
    // joystick zone, since base/stick are positioned relative to it - using
    // raw page coordinates directly here was the bug that made the stick
    // render away from the actual finger.
    function toLocal(clientX, clientY) {
      const rect = zone.getBoundingClientRect();
      return { x: clientX - rect.left, y: clientY - rect.top };
    }

    function start(clientX, clientY) {
      if (game.save.settings.control === 'gyro') return;
      const p = toLocal(clientX, clientY);
      active = true; originX = p.x; originY = p.y;
      if (game.save.settings.control === 'joystick') {
        base.style.left = p.x + 'px'; base.style.top = p.y + 'px'; base.hidden = false;
        stick.style.left = p.x + 'px'; stick.style.top = p.y + 'px';
        stick.style.transform = 'translate(-50%, -50%)';
      }
    }
    function move(clientX, clientY) {
      if (!active) return;
      const p = toLocal(clientX, clientY);
      let dx = p.x - originX, dy = p.y - originY;
      const dist = Math.hypot(dx, dy);
      if (dist > MAXR) { dx = dx / dist * MAXR; dy = dy / dist * MAXR; }
      game.setTilt(dx / MAXR, dy / MAXR);
      // Keep the -50%/-50% centering AND apply the drag offset - overwriting
      // the transform with just the offset (dropping the centering term)
      // was the other half of the original bug.
      if (game.save.settings.control === 'joystick') {
        stick.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
      }
    }
    function end() {
      active = false; activeTouchId = null; game.setTilt(0, 0);
      base.hidden = true; stick.style.transform = 'translate(-50%, -50%)';
    }
    zone.addEventListener('touchstart', (e) => {
      const t = e.changedTouches[0]; activeTouchId = t.identifier;
      start(t.clientX, t.clientY); e.preventDefault();
    }, { passive: false });
    zone.addEventListener('touchmove', (e) => {
      let t = null;
      for (let i = 0; i < e.changedTouches.length; i++) if (e.changedTouches[i].identifier === activeTouchId) t = e.changedTouches[i];
      if (!t) t = e.changedTouches[0];
      move(t.clientX, t.clientY); e.preventDefault();
    }, { passive: false });
    zone.addEventListener('touchend', end);
    zone.addEventListener('touchcancel', end);
    // Mouse fallback so drag/joystick modes are also testable on desktop
    zone.addEventListener('mousedown', (e) => { start(e.clientX, e.clientY); });
    window.addEventListener('mousemove', (e) => { if (active) move(e.clientX, e.clientY); });
    window.addEventListener('mouseup', () => { if (active) end(); });
  }

  // Single controller for gyroscope input, shared by both the HUD icon
  // button and the Settings radio option - whichever UI you enable it from,
  // it goes through the same permission request + watchdog + cleanup logic.
  function createGyroController(game) {
    const btn = $('#btn-gyro');
    let activeHandler = null;
    let watchdogTimer = null;
    let gotEvent = false;

    function disable(revertTo) {
      if (activeHandler) window.removeEventListener('deviceorientation', activeHandler);
      activeHandler = null;
      if (watchdogTimer) { clearTimeout(watchdogTimer); watchdogTimer = null; }
      game.save.settings.control = revertTo || 'joystick';
      game.persist();
      $all('input[name="control-scheme"]').forEach(r => r.checked = r.value === game.save.settings.control);
      updateControlUI(game);
    }

    function enable() {
      const DOE = window.DeviceOrientationEvent;
      if (!DOE) { window.alert('Gyroscope is not available on this device. Use the joystick or drag controls instead.'); disable('joystick'); return; }

      const start = () => {
        let refBeta = null, refGamma = null;
        gotEvent = false;
        const handler = (e) => {
          // If beta/gamma are both null the browser isn't actually
          // delivering real sensor data (common over an insecure http://
          // or file:// origin) - treat that the same as no data at all.
          if (e.beta === null && e.gamma === null) return;
          gotEvent = true;
          if (refBeta === null) { refBeta = e.beta || 0; refGamma = e.gamma || 0; }
          const x = LAB.Physics.clamp(((e.gamma || 0) - refGamma) / 35, -1, 1);
          const y = LAB.Physics.clamp(((e.beta || 0) - refBeta) / 35, -1, 1);
          game.setTilt(x, y);
        };
        activeHandler = handler;
        window.addEventListener('deviceorientation', handler);
        game.save.settings.control = 'gyro'; game.persist();
        $all('input[name="control-scheme"]').forEach(r => r.checked = r.value === 'gyro');
        updateControlUI(game);

        // Watchdog: if no real sensor event arrives shortly after enabling,
        // this device/browser/origin isn't actually delivering tilt data.
        // Fail loudly and fall back, instead of leaving the ball unresponsive
        // with no explanation.
        watchdogTimer = setTimeout(() => {
          if (!gotEvent) {
            disable('joystick');
            window.alert('No tilt data was received from this device. This usually means the page needs to be loaded over HTTPS (not a local file), or this browser/device does not support motion sensors. Switched back to the joystick.');
          }
        }, 1800);
      };

      if (typeof DOE.requestPermission === 'function') {
        DOE.requestPermission()
          .then(state => { if (state === 'granted') start(); else { window.alert('Gyroscope permission was denied.'); disable('joystick'); } })
          .catch(() => { window.alert('Could not enable the gyroscope.'); disable('joystick'); });
      } else {
        start();
      }
    }

    btn.addEventListener('click', () => {
      if (game.save.settings.control === 'gyro') disable('joystick'); // tap again to turn off
      else enable();
    });

    return { enable, disable };
  }

  function updateHud(game) {
    if (!game.level) return;
    $('#hud-level').textContent = 'Level ' + game.currentId + ' · ' + game.level.tierName;
    const m = Math.floor(game.elapsed / 60), s = Math.floor(game.elapsed % 60);
    $('#hud-timer').textContent = m + ':' + String(s).padStart(2, '0');
    $('#btn-prev').disabled = game.currentId <= 1;
    $('#btn-next').disabled = game.currentId >= game.save.unlockedLevel;
  }

  function showVictory(game, stats) {
    $('#victory-title').textContent = stats.isLast ? 'All 100 levels complete!' : 'Level Complete';
    $('#victory-time').textContent = 'Time ' + stats.time.toFixed(1) + 's';
    $('#victory-stars').textContent = '★'.repeat(stats.stars) + '☆'.repeat(3 - stats.stars);
    $('#btn-next-victory').hidden = stats.isLast;
    $('#victory-modal').hidden = false;
  }
  function flashFail() {
    const el = $('#fail-flash'); el.classList.add('show');
    setTimeout(() => el.classList.remove('show'), 500);
  }
  function flashCheckpoint() {
    const el = $('#checkpoint-toast'); el.classList.add('show');
    setTimeout(() => el.classList.remove('show'), 900);
  }

  function openLevelSelect(game) {
    const grid = $('#level-grid');
    grid.innerHTML = '';
    for (let i = 1; i <= LAB.Levels.TOTAL_LEVELS; i++) {
      const locked = i > game.save.unlockedLevel;
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.className = 'level-tile' + (locked ? ' locked' : '') + (i === game.currentId ? ' current' : '');
      btn.textContent = locked ? '' : String(i);
      btn.disabled = locked;
      btn.setAttribute('aria-label', locked ? ('Level ' + i + ', locked') : ('Play level ' + i + ', ' + LAB.Levels.getMeta(i).tierName));
      btn.addEventListener('click', () => { game.loadLevel(i); $('#level-select-modal').hidden = true; });
      grid.appendChild(btn);
    }
    $('#level-select-modal').hidden = false;
  }

  function wireModals(game) {
    $all('[data-close-modal]').forEach(btn => {
      btn.addEventListener('click', () => {
        const modal = btn.closest('.modal');
        modal.hidden = true;
        if (modal.id === 'pause-modal' && game.state === 'paused') game.resume();
      });
    });
    $all('.modal').forEach(modal => {
      modal.addEventListener('click', (e) => { if (e.target === modal) modal.hidden = true; });
    });
  }

  // The one and only ad placement: a full-screen interstitial shown every
  // 5 minutes of session time. Pauses the game while it's up. Swap the
  // placeholder markup in index.html for your ad network's interstitial
  // call (see README - AdSense itself doesn't have a "every N minutes"
  // trigger built in; that part is this timer).
  function wireInterstitialAd(game) {
    const overlay = $('#interstitial-ad');
    const continueBtn = $('#interstitial-continue');
    if (!overlay || !continueBtn) return;
    const AD_INTERVAL_MS = 5 * 60 * 1000;
    const SKIP_DELAY_MS = 5000;
    let wasPlaying = false;
    let countdownTimer = null;

    function schedule(ms) { setTimeout(tryShow, ms); }

    function tryShow() {
      const modalOpen = $all('.modal').some(m => !m.hidden);
      if (modalOpen) { schedule(15000); return; } // don't interrupt another modal - retry soon
      wasPlaying = game.state === 'playing';
      game.pause();
      overlay.hidden = false;
      continueBtn.disabled = true;
      let remaining = Math.ceil(SKIP_DELAY_MS / 1000);
      continueBtn.textContent = 'Continue in ' + remaining + 's';
      countdownTimer = setInterval(() => {
        remaining -= 1;
        if (remaining <= 0) {
          clearInterval(countdownTimer); countdownTimer = null;
          continueBtn.disabled = false;
          continueBtn.textContent = 'Continue Playing';
        } else {
          continueBtn.textContent = 'Continue in ' + remaining + 's';
        }
      }, 1000);
    }

    function dismiss() {
      overlay.hidden = true;
      if (wasPlaying) game.resume();
      schedule(AD_INTERVAL_MS);
    }

    continueBtn.addEventListener('click', () => { if (!continueBtn.disabled) dismiss(); });
    schedule(AD_INTERVAL_MS); // first one after 5 minutes of play, not immediately on load
  }

  function registerServiceWorker() {
    if ('serviceWorker' in navigator) {
      window.addEventListener('load', () => {
        navigator.serviceWorker.register('./service-worker.js').catch(() => { /* offline support unavailable, game still playable online */ });
      });
    }
  }

  document.addEventListener('DOMContentLoaded', init);
  LAB.UI = { init };
})(typeof window !== 'undefined' ? window : globalThis);
