const canvas = document.getElementById("game");
const frame = document.querySelector(".frame");
const screenCtx = canvas.getContext("2d");
const sceneCanvas = document.createElement("canvas");
const ctx = sceneCanvas.getContext("2d");

const scoreNode = document.getElementById("score");
const bestScoreNode = document.getElementById("best-score");
const leaderboardList = document.getElementById("leaderboard-list");
const overlayLeaderboard = document.getElementById("overlay-leaderboard");
const overlayLeaderboardList = document.getElementById("overlay-leaderboard-list");
const leaderboardForm = document.getElementById("leaderboard-form");
const leaderboardStatus = document.getElementById("leaderboard-status");
const playerNameInput = document.getElementById("player-name");
const submitScoreButton = document.getElementById("submit-score-button");
const skipScoreButton = document.getElementById("skip-score-button");
const audioToggle = document.getElementById("audio-toggle");
const mobileAudioToggle = document.getElementById("mobile-audio-toggle");
const overlay = document.getElementById("overlay");
const overlayTitle = document.getElementById("overlay-title");
const overlayCopy = document.getElementById("overlay-copy");
const menuMusic = document.getElementById("menu-music");
const gameMusic = document.getElementById("game-music");
const logoImage = new Image();

logoImage.src = "Logo-8-bit.png";

const PALETTE = {
  flash: "rgba(250, 250, 250, %alpha%)",
  fieldBase: "#d8d8d8",
  headerBase: "#efefef",
  groundBase: "#a6a6a6",
  panelDark: "#404040",
  ink: "#0e0e0e",
  screenMid: "#cfcfcf",
  cloudLight: "#fafafa",
  fishShadow: "rgba(14, 14, 14, 0.18)",
  particle: "rgba(14, 14, 14, %alpha%)",
  patterns: {
    field: ["#d8d8d8", "#b5b5b5"],
    header: ["#efefef", "#d8d8d8"],
    ground: ["#a6a6a6", "#404040"],
    pipe: ["#404040", "#0e0e0e"],
    cap: ["#bdbdbd", "#d8d8d8"],
    cloud: ["#efefef", "#fafafa"],
  },
};

const BEST_SCORE_KEY = "flappy-fish-best-score";
const AUDIO_MUTED_KEY = "flappy-fish-audio-muted";
const LOCAL_LEADERBOARD_KEY = "flappy-fish-local-leaderboard";
const ANALYTICS_VISITOR_KEY = "flappy-fish-visitor-id";
const LEADERBOARD_DISPLAY_LIMIT = 5;
const LEADERBOARD_STORAGE_LIMIT = 100;
const GAME_CONFIG = {
  referenceSize: 240,
  flapVelocity: -220,
  gravity: 760,
  pipeSpeed: 92,
  spawnInterval: 1.36,
  pipeWidth: 28,
  minGap: 60,
  maxGap: 72,
  pipeCap: 10,
};
const PATTERN_CACHE = new Map();
const GAME_OVER_OVERLAY_DELAY = 0.75;
const DOUBLE_TAP_ZOOM_WINDOW_MS = 320;
const DESKTOP_BREAKPOINT = 921;
const IS_IOS =
  /iPad|iPhone|iPod/.test(navigator.userAgent) ||
  (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
const state = {
  width: 0,
  height: 0,
  unit: 1,
  lastTime: 0,
  phase: "idle",
  fish: {
    x: 0,
    y: 0,
    velocity: 0,
    radius: 28,
    bob: 0,
  },
  pipes: [],
  particles: [],
  score: 0,
  bestScore: Number(localStorage.getItem(BEST_SCORE_KEY) || 0),
  leaderboard: [],
  spawnTimer: 0,
  flash: 0,
  hasSubmittedScore: false,
  lastSubmittedName: "",
  leaderboardAvailable: true,
  audioReady: false,
  audioContext: null,
  isMuted: localStorage.getItem(AUDIO_MUTED_KEY) === "true",
  gameOverOverlayTimer: 0,
  pendingGameOver: null,
};

const analyticsState = {
  visitorId: "",
  sessionId: "",
  startedAtMs: 0,
  ended: false,
};
let lastTouchEndTime = 0;
let lastTouchStartTime = 0;
let lastActionButtonTime = 0;
const activeTouchDrags = new Set();

bestScoreNode.textContent = String(state.bestScore);
updateAudioToggle();
applyAudioSettings();

function syncDitherPalette() {
  const useDesktopDither = window.matchMedia(`(min-width: ${DESKTOP_BREAKPOINT}px)`).matches;
  const nextFieldAccent = useDesktopDither ? "#bcbcbc" : "#b5b5b5";
  const nextHeaderAccent = useDesktopDither ? "#dbdbdb" : "#d8d8d8";

  if (PALETTE.patterns.field[1] === nextFieldAccent && PALETTE.patterns.header[1] === nextHeaderAccent) {
    return;
  }

  PALETTE.patterns.field[1] = nextFieldAccent;
  PALETTE.patterns.header[1] = nextHeaderAccent;
  PATTERN_CACHE.clear();
}

function resize() {
  syncDitherPalette();

  const dpr = Math.max(1, window.devicePixelRatio || 1);
  const rect = canvas.getBoundingClientRect();
  const logicalWidth = Math.max(160, Math.round(rect.width));
  const logicalHeight = Math.max(160, Math.round(rect.height));

  canvas.width = Math.round(logicalWidth * dpr);
  canvas.height = Math.round(logicalHeight * dpr);
  screenCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
  screenCtx.imageSmoothingEnabled = false;
  ctx.imageSmoothingEnabled = false;
  sceneCanvas.width = logicalWidth;
  sceneCanvas.height = logicalHeight;

  state.width = logicalWidth;
  state.height = logicalHeight;
  state.unit = Math.min(state.width, state.height) / GAME_CONFIG.referenceSize;
  resetFishPosition();
}

function resetFishPosition() {
  state.fish.x = scaleValue(58);
  state.fish.y = state.height * 0.5;
  state.fish.velocity = 0;
  state.fish.bob = 0;
  state.fish.radius = scaleValue(10);
}

function clearRound() {
  state.score = 0;
  state.spawnTimer = 0;
  state.flash = 0;
  state.pipes = [];
  state.particles = [];
  state.hasSubmittedScore = false;
  state.gameOverOverlayTimer = 0;
  state.pendingGameOver = null;
  scoreNode.textContent = "0";
  resetFishPosition();
  hideLeaderboardForm();
  setLeaderboardStatus("");
}

function startGame() {
  primeAudio();
  clearRound();
  state.phase = "playing";
  overlay.classList.add("hidden");
  hideOverlayLeaderboard();
  gameMusic.pause();
  gameMusic.currentTime = 0;
  syncMusic("game");
  flap();
}

function endGame() {
  state.phase = "over";
  state.flash = 1;
  state.gameOverOverlayTimer = GAME_OVER_OVERLAY_DELAY;
  spawnBurst(state.fish.x, state.fish.y);
  playSfx("crash");

  if (state.score > state.bestScore) {
    state.bestScore = state.score;
    localStorage.setItem(BEST_SCORE_KEY, String(state.bestScore));
    bestScoreNode.textContent = String(state.bestScore);
  }

  const canSubmit = canSubmitLeaderboardScore(state.score);
  state.pendingGameOver = { canSubmit };
  overlay.classList.add("hidden");
  hideLeaderboardForm();

  syncMusic("menu");
}

function showGameOverOverlay() {
  if (!state.pendingGameOver) {
    return;
  }

  const { canSubmit } = state.pendingGameOver;
  overlayTitle.textContent = "Wipeout";
  overlayCopy.textContent = canSubmit
    ? `You cleared ${state.score} column${state.score === 1 ? "" : "s"}. Add your name, or tap skip to restart.`
    : `You cleared ${state.score} column${state.score === 1 ? "" : "s"}. Tap anywhere to go again.`;
  overlay.classList.remove("hidden");

  if (canSubmit) {
    showLeaderboardForm();
  } else {
    hideLeaderboardForm();
  }
  showOverlayLeaderboard();

  state.pendingGameOver = null;
}

function showStartOverlay() {
  overlayTitle.textContent = "Flappy Fish";
  overlayCopy.textContent = "Tap, click, or press space to swim through the columns.";
  hideLeaderboardForm();
  hideOverlayLeaderboard();
  setLeaderboardStatus("");
  overlay.classList.remove("hidden");
  syncMusic("menu");
}

function flap() {
  state.fish.velocity = scaleValue(GAME_CONFIG.flapVelocity);
  spawnBurst(state.fish.x - 10, state.fish.y + 12, 5);
  playSfx("flap");
}

function spawnPipe() {
  const pipeWidth = scaleValue(GAME_CONFIG.pipeWidth);
  const gap = random(scaleValue(GAME_CONFIG.minGap), scaleValue(GAME_CONFIG.maxGap));
  const edgePadding = scaleValue(34);
  const gapY = random(edgePadding + gap * 0.5, state.height - edgePadding - gap * 0.5);

  state.pipes.push({
    x: state.width + pipeWidth + scaleValue(30),
    width: pipeWidth,
    gap,
    gapY,
    scored: false,
  });
}

function spawnBurst(x, y, count = 9) {
  for (let i = 0; i < count; i += 1) {
    state.particles.push({
      kind: "burst",
      x,
      y,
      vx: random(-120, 120),
      vy: random(-120, 80),
      life: random(0.25, 0.6),
      age: 0,
      size: random(4, 12),
    });
  }
}

function spawnClickText(x, y) {
  state.particles.push({
    kind: "text",
    text: "CLICK!",
    x: clamp(x - 10, 12, state.width - 40),
    y: clamp(y - 6, 14, state.height - 16),
    vx: random(-8, 8),
    vy: -34,
    life: 0.42,
    age: 0,
    size: 0,
  });
}

function update(dt) {
  state.flash = Math.max(0, state.flash - dt * 2.8);
  updateParticles(dt);

  if (state.phase !== "playing") {
    if (state.phase === "over" && state.gameOverOverlayTimer > 0) {
      state.gameOverOverlayTimer = Math.max(0, state.gameOverOverlayTimer - dt);
      if (state.gameOverOverlayTimer === 0) {
        showGameOverOverlay();
      }
    }
    state.fish.bob += dt * 3.4;
    return;
  }

  const gravity = GAME_CONFIG.gravity;
  const speed = GAME_CONFIG.pipeSpeed;
  const spawnInterval = GAME_CONFIG.spawnInterval;
  const scaledGravity = scaleValue(gravity);
  const scaledSpeed = scaleValue(speed);

  state.spawnTimer += dt;
  if (state.spawnTimer >= spawnInterval) {
    state.spawnTimer = 0;
    spawnPipe();
  }

  state.fish.velocity += scaledGravity * dt;
  state.fish.y += state.fish.velocity * dt;
  state.fish.bob += dt * 8;

  if (state.fish.y < state.fish.radius || state.fish.y > state.height - state.fish.radius) {
    endGame();
    return;
  }

  for (const pipe of state.pipes) {
    pipe.x -= scaledSpeed * dt;

    if (!pipe.scored && pipe.x + pipe.width < state.fish.x) {
      pipe.scored = true;
      state.score += 1;
      scoreNode.textContent = String(state.score);
      spawnBurst(state.fish.x, state.fish.y, 6);
      playSfx("score");
    }

    if (collidesWithPipe(pipe)) {
      endGame();
      return;
    }
  }

  state.pipes = state.pipes.filter((pipe) => pipe.x + pipe.width > -scaleValue(140));
}

function updateParticles(dt) {
  for (const particle of state.particles) {
    particle.age += dt;
    particle.x += particle.vx * dt;
    particle.y += particle.vy * dt;
    if (particle.kind === "text") {
      particle.vy += 28 * dt;
      continue;
    }

    particle.vy += 260 * dt;
  }

  state.particles = state.particles.filter((particle) => particle.age < particle.life);
}

function collidesWithPipe(pipe) {
  const fishLeft = state.fish.x - state.fish.radius * 0.85;
  const fishRight = state.fish.x + state.fish.radius * 0.9;
  const fishTop = state.fish.y - state.fish.radius * 0.75;
  const fishBottom = state.fish.y + state.fish.radius * 0.75;

  const inHorizontalRange = fishRight > pipe.x && fishLeft < pipe.x + pipe.width;
  if (!inHorizontalRange) {
    return false;
  }

  const gapTop = pipe.gapY - pipe.gap * 0.5;
  const gapBottom = pipe.gapY + pipe.gap * 0.5;
  return fishTop < gapTop || fishBottom > gapBottom;
}

function render() {
  ctx.clearRect(0, 0, state.width, state.height);
  drawBackdrop();
  drawPipes();
  drawParticles();
  drawFish();

  if (state.flash > 0) {
    ctx.fillStyle = PALETTE.flash.replace("%alpha%", String(state.flash * 0.45));
    ctx.fillRect(0, 0, state.width, state.height);
  }

  screenCtx.clearRect(0, 0, state.width, state.height);
  screenCtx.drawImage(sceneCanvas, 0, 0, state.width, state.height);
}

function drawBackdrop() {
  const headerHeight = scalePixel(18);
  const groundHeight = scalePixel(22);
  const floorDashWidth = scalePixel(12);
  const floorDashStep = scalePixel(24);
  const topDashStep = scalePixel(16);
  const topDashWidth = scalePixel(8);
  const topDashOffset = scalePixel(18);

  ctx.fillStyle = PALETTE.fieldBase;
  ctx.fillRect(0, 0, state.width, state.height);

  ctx.fillStyle = PALETTE.headerBase;
  ctx.fillRect(0, 0, state.width, headerHeight);

  ctx.fillStyle = PALETTE.groundBase;
  ctx.fillRect(0, state.height - groundHeight, state.width, groundHeight);

  ctx.fillStyle = PALETTE.panelDark;
  for (let x = 0; x < state.width; x += floorDashStep) {
    ctx.fillRect(x, state.height - scalePixel(6), floorDashWidth, scalePixel(2));
  }

  ctx.fillStyle = PALETTE.panelDark;
  for (let x = 0; x < state.width; x += topDashStep) {
    const bob = ((x / 16) % 2) * 2;
    ctx.fillRect(x, topDashOffset + scalePixel(bob), topDashWidth, scalePixel(2));
  }

  for (let i = 0; i < 5; i += 1) {
    const drift = Math.floor((state.lastTime * 0.012 * state.unit + i * scalePixel(17)) % (state.width + scalePixel(32))) - scalePixel(16);
    const y = scalePixel(34) + i * scalePixel(22);
    drawCloud(drift, y);
  }

  ctx.fillStyle = PALETTE.panelDark;
  for (let x = scalePixel(8); x < state.width; x += scalePixel(20)) {
    const height = scalePixel(8 + ((Math.floor(x / Math.max(1, scalePixel(20)))) % 3) * 4);
    ctx.fillRect(x, state.height - groundHeight - height, scalePixel(2), height);
    ctx.fillRect(x + scalePixel(2), state.height - scalePixel(18) - height, scalePixel(2), Math.max(scalePixel(2), height - scalePixel(2)));
  }

}

function drawPipes() {
  for (const pipe of state.pipes) {
    const gapTop = pipe.gapY - pipe.gap * 0.5;
    const gapBottom = pipe.gapY + pipe.gap * 0.5;

    drawPipeSegment(pipe.x, 0, pipe.width, gapTop, true);
    drawPipeSegment(pipe.x, gapBottom, pipe.width, state.height - gapBottom, false);
  }
}

function drawPipeSegment(x, y, width, height, topPipe) {
  if (height <= 0) {
    return;
  }

  const pipeX = snap(x);
  const pipeY = snap(y);
  const pipeWidth = snap(width);
  const pipeHeight = snap(height);
  const capHeight = scalePixel(GAME_CONFIG.pipeCap);
  const capY = topPipe ? pipeY + pipeHeight - capHeight : pipeY;

  ctx.fillStyle = PALETTE.panelDark;
  ctx.fillRect(pipeX, pipeY, pipeWidth, pipeHeight);
  fillPatternRect(pipeX + scalePixel(2), pipeY + scalePixel(2), pipeWidth - scalePixel(4), pipeHeight - scalePixel(4), getPattern("pipe"));

  ctx.fillStyle = PALETTE.ink;
  ctx.fillRect(pipeX - scalePixel(4), capY, pipeWidth + scalePixel(8), capHeight);

  ctx.fillStyle = PALETTE.groundBase;
  ctx.fillRect(pipeX - scalePixel(2), capY + scalePixel(2), pipeWidth + scalePixel(4), Math.max(scalePixel(2), capHeight - scalePixel(4)));
  fillPatternRect(pipeX - scalePixel(2), capY + scalePixel(2), pipeWidth + scalePixel(4), Math.max(scalePixel(2), capHeight - scalePixel(4)), getPattern("cap"));

  ctx.fillStyle = PALETTE.fieldBase;
  ctx.fillRect(pipeX + scalePixel(5), pipeY + scalePixel(4), scalePixel(2), pipeHeight - scalePixel(8));
  ctx.fillRect(pipeX + pipeWidth - scalePixel(7), pipeY + scalePixel(4), scalePixel(2), pipeHeight - scalePixel(8));
}

function drawFish() {
  const idleOffset = state.phase === "playing" ? 0 : Math.round(Math.sin(state.fish.bob) * 2);
  const angle = clamp(state.fish.velocity / 280, -0.45, 0.6);

  ctx.save();
  ctx.translate(snap(state.fish.x), snap(state.fish.y + idleOffset));
  ctx.rotate(angle);

  drawLogoFish();

  ctx.restore();
}

function drawLogoFish() {
  if (!logoImage.complete || !logoImage.naturalWidth || !logoImage.naturalHeight) {
    return;
  }

  const targetHeight = scalePixel(28);
  const targetWidth = Math.round(targetHeight * (logoImage.naturalWidth / logoImage.naturalHeight));
  ctx.imageSmoothingEnabled = false;
  ctx.drawImage(logoImage, -scalePixel(18), -scalePixel(14), targetWidth, targetHeight);
}

function drawCloud(x, y) {
  ctx.fillStyle = PALETTE.headerBase;
  ctx.fillRect(x, y, 4, 4);
  ctx.fillRect(x + 6, y - 3, 3, 3);
  ctx.fillRect(x + 10, y + 2, 2, 2);
}

function drawParticles() {
  for (const particle of state.particles) {
    const alpha = 1 - particle.age / particle.life;
    if (particle.kind === "text") {
      ctx.save();
      ctx.globalAlpha = alpha;
      ctx.fillStyle = PALETTE.ink;
      ctx.font = '6px "Press Start 2P"';
      ctx.textBaseline = "top";
      ctx.fillText(particle.text, snap(particle.x, 1), snap(particle.y, 1));
      ctx.restore();
      continue;
    }

    ctx.fillStyle = PALETTE.particle.replace("%alpha%", String(alpha));
    ctx.fillRect(snap(particle.x), snap(particle.y), Math.max(2, snap(particle.size, 2)), Math.max(2, snap(particle.size, 2)));
  }
}

function getPointerScenePosition(event) {
  const rect = canvas.getBoundingClientRect();
  const scaleX = state.width / rect.width;
  const scaleY = state.height / rect.height;

  return {
    x: (event.clientX - rect.left) * scaleX,
    y: (event.clientY - rect.top) * scaleY,
  };
}

function loop(timestamp) {
  if (!state.lastTime) {
    state.lastTime = timestamp;
  }

  const frame = Math.min(0.033, (timestamp - state.lastTime) / 1000);
  state.lastTime = timestamp;
  update(frame);
  render();
  requestAnimationFrame(loop);
}

function random(min, max) {
  return Math.random() * (max - min) + min;
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(max, value));
}

function snap(value, step = 2) {
  return Math.round(value / step) * step;
}

function scaleValue(value) {
  return value * state.unit;
}

function scalePixel(value) {
  return Math.max(1, Math.round(scaleValue(value)));
}

function getPattern(name) {
  if (PATTERN_CACHE.has(name)) {
    return PATTERN_CACHE.get(name);
  }

  const patternCanvas = document.createElement("canvas");
  patternCanvas.width = 12;
  patternCanvas.height = 12;
  const patternCtx = patternCanvas.getContext("2d");

  const [base, accent] = PALETTE.patterns[name];

  patternCtx.fillStyle = base;
  patternCtx.fillRect(0, 0, 12, 12);
  patternCtx.fillStyle = accent;
  patternCtx.fillRect(0, 0, 3, 3);
  patternCtx.fillRect(6, 0, 3, 3);
  patternCtx.fillRect(3, 3, 3, 3);
  patternCtx.fillRect(9, 3, 3, 3);
  patternCtx.fillRect(0, 6, 3, 3);
  patternCtx.fillRect(6, 6, 3, 3);
  patternCtx.fillRect(3, 9, 3, 3);
  patternCtx.fillRect(9, 9, 3, 3);

  const pattern = ctx.createPattern(patternCanvas, "repeat");
  PATTERN_CACHE.set(name, pattern);
  return pattern;
}

function fillPatternRect(x, y, width, height, pattern) {
  if (width <= 0 || height <= 0) {
    return;
  }

  ctx.save();
  ctx.fillStyle = pattern;
  ctx.fillRect(x, y, width, height);
  ctx.restore();
}

function applyAudioSettings() {
  menuMusic.muted = state.isMuted;
  gameMusic.muted = state.isMuted;
  menuMusic.volume = 0.38;
  gameMusic.volume = 0.28;
}

function updateAudioToggle() {
  if (audioToggle) {
    audioToggle.textContent = state.isMuted ? "Audio Off" : "Audio On";
    audioToggle.dataset.muted = String(state.isMuted);
    audioToggle.setAttribute("aria-pressed", String(state.isMuted));
  }

  if (mobileAudioToggle) {
    mobileAudioToggle.dataset.muted = String(state.isMuted);
    mobileAudioToggle.setAttribute("aria-pressed", String(state.isMuted));
    mobileAudioToggle.setAttribute("aria-label", state.isMuted ? "Unmute audio" : "Mute audio");
  }
}

function createId(prefix) {
  const random = Math.random().toString(36).slice(2, 10);
  const time = Date.now().toString(36);
  return `${prefix}-${time}-${random}`;
}

function getAnalyticsEndpoint() {
  const pageUrl = new URL(window.location.href);
  const directoryUrl = new URL("./", pageUrl);
  return new URL("analytics.php", directoryUrl).toString();
}

function sendAnalytics(payload, useBeacon = false) {
  const body = JSON.stringify(payload);
  const endpoint = getAnalyticsEndpoint();

  if (useBeacon && navigator.sendBeacon) {
    const blob = new Blob([body], { type: "application/json" });
    navigator.sendBeacon(endpoint, blob);
    return;
  }

  fetch(endpoint, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body,
    keepalive: true,
  }).catch(() => {});
}

function startAnalyticsSession() {
  let visitorId = localStorage.getItem(ANALYTICS_VISITOR_KEY);
  if (!visitorId) {
    visitorId = createId("v");
    localStorage.setItem(ANALYTICS_VISITOR_KEY, visitorId);
  }

  analyticsState.visitorId = visitorId;
  analyticsState.sessionId = createId("s");
  analyticsState.startedAtMs = Date.now();
  analyticsState.ended = false;

  sendAnalytics({
    type: "visit_start",
    visitorId: analyticsState.visitorId,
    sessionId: analyticsState.sessionId,
    path: window.location.pathname,
  });
}

function endAnalyticsSession(useBeacon = false) {
  if (analyticsState.ended || !analyticsState.sessionId) {
    return;
  }

  analyticsState.ended = true;
  const elapsedSec = Math.max(0, Math.round((Date.now() - analyticsState.startedAtMs) / 1000));

  sendAnalytics({
    type: "visit_end",
    visitorId: analyticsState.visitorId,
    sessionId: analyticsState.sessionId,
    path: window.location.pathname,
    durationSec: elapsedSec,
  }, useBeacon);
}

function getAudioContext() {
  if (state.audioContext) {
    return state.audioContext;
  }

  const AudioContextClass = window.AudioContext || window.webkitAudioContext;
  if (!AudioContextClass) {
    return null;
  }

  state.audioContext = new AudioContextClass();
  return state.audioContext;
}

async function primeAudio() {
  const audioContext = getAudioContext();
  if (state.audioReady) {
    if (audioContext?.state === "suspended") {
      await audioContext.resume();
    }
    return;
  }

  state.audioReady = true;
  try {
    if (audioContext?.state === "suspended") {
      await audioContext.resume();
    }

    await Promise.all([
      menuMusic.play().then(() => menuMusic.pause()),
      gameMusic.play().then(() => gameMusic.pause()),
    ]);
  } catch {
    state.audioReady = false;
  } finally {
    menuMusic.currentTime = 0;
    gameMusic.currentTime = 0;
    syncMusic(state.phase === "playing" ? "game" : "menu");
  }
}

function playSfx(kind) {
  if (state.isMuted) {
    return;
  }

  const audioContext = getAudioContext();
  if (!audioContext || audioContext.state !== "running") {
    return;
  }

  const now = audioContext.currentTime;
  const output = audioContext.createGain();
  output.connect(audioContext.destination);
  output.gain.setValueAtTime(0.0001, now);
  output.gain.exponentialRampToValueAtTime(0.16, now + 0.01);
  output.gain.exponentialRampToValueAtTime(0.0001, now + 0.22);

  const tone = audioContext.createOscillator();
  tone.connect(output);

  if (kind === "flap") {
    tone.type = "triangle";
    tone.frequency.setValueAtTime(520, now);
    tone.frequency.exponentialRampToValueAtTime(260, now + 0.12);
    output.gain.exponentialRampToValueAtTime(0.0001, now + 0.14);
    tone.start(now);
    tone.stop(now + 0.15);
    return;
  }

  if (kind === "score") {
    const harmony = audioContext.createOscillator();
    harmony.type = "triangle";
    harmony.frequency.setValueAtTime(740, now);
    harmony.frequency.exponentialRampToValueAtTime(980, now + 0.18);
    harmony.connect(output);

    tone.type = "square";
    tone.frequency.setValueAtTime(620, now);
    tone.frequency.exponentialRampToValueAtTime(880, now + 0.18);
    output.gain.exponentialRampToValueAtTime(0.0001, now + 0.24);

    tone.start(now);
    harmony.start(now + 0.015);
    tone.stop(now + 0.24);
    harmony.stop(now + 0.21);
    return;
  }

  tone.type = "sawtooth";
  tone.frequency.setValueAtTime(220, now);
  tone.frequency.exponentialRampToValueAtTime(58, now + 0.28);
  output.gain.exponentialRampToValueAtTime(0.0001, now + 0.3);
  tone.start(now);
  tone.stop(now + 0.3);
}

function pauseTrack(track) {
  track.pause();
}

async function playTrack(track) {
  if (state.isMuted) {
    return;
  }

  try {
    await track.play();
  } catch {
    state.audioReady = false;
  }
}

function syncMusic(mode) {
  if (!state.audioReady || state.isMuted) {
    pauseTrack(menuMusic);
    pauseTrack(gameMusic);
    return;
  }

  if (mode === "game") {
    pauseTrack(menuMusic);
    if (gameMusic.paused) {
      playTrack(gameMusic);
    }
    return;
  }

  pauseTrack(gameMusic);
  if (menuMusic.paused) {
    playTrack(menuMusic);
  }
}

async function toggleAudio() {
  state.isMuted = !state.isMuted;
  localStorage.setItem(AUDIO_MUTED_KEY, String(state.isMuted));
  updateAudioToggle();
  applyAudioSettings();

  if (!state.isMuted) {
    await primeAudio();
    syncMusic(state.phase === "playing" ? "game" : "menu");
    return;
  }

  syncMusic("menu");
}

function handlePrimaryInput() {
  primeAudio();

  if (state.phase === "idle") {
    startGame();
    return;
  }

  if (state.phase === "playing") {
    flap();
    return;
  }

  if (state.phase === "over" && state.gameOverOverlayTimer > 0) {
    return;
  }

  startGame();
}

function isEditableTarget(target) {
  if (!(target instanceof Element)) {
    return false;
  }

  return target.closest("input, textarea, select, [contenteditable=''], [contenteditable='true']") !== null;
}

function isInteractiveTarget(target) {
  if (!(target instanceof Element)) {
    return false;
  }

  return target.closest("button, a, input, textarea, select, label, [role='button']") !== null;
}

function isOverlayTarget(target) {
  if (!(target instanceof Element) || !overlay) {
    return false;
  }

  return overlay.contains(target);
}

function consumeTouchDragState(event) {
  let hasDrag = false;

  for (let i = 0; i < event.changedTouches.length; i += 1) {
    const touch = event.changedTouches.item(i);
    if (!touch) {
      continue;
    }
    if (activeTouchDrags.has(touch.identifier)) {
      hasDrag = true;
    }
    activeTouchDrags.delete(touch.identifier);
  }

  return hasDrag;
}

function trackTouchMove(event) {
  if (!event.isTrusted) {
    return;
  }

  for (let i = 0; i < event.changedTouches.length; i += 1) {
    const touch = event.changedTouches.item(i);
    if (!touch) {
      continue;
    }
    activeTouchDrags.add(touch.identifier);
  }
}

function clearTouchState(event) {
  for (let i = 0; i < event.changedTouches.length; i += 1) {
    const touch = event.changedTouches.item(i);
    if (!touch) {
      continue;
    }
    activeTouchDrags.delete(touch.identifier);
  }
}

function isGameTouchTarget(target) {
  if (!(target instanceof Element)) {
    return false;
  }

  return target === canvas;
}

function preventDoubleTapZoom(event) {
  if (!IS_IOS || !event.isTrusted) {
    return;
  }

  if (isOverlayTarget(event.target)) {
    lastTouchEndTime = 0;
    return;
  }

  const isDrag = consumeTouchDragState(event);
  if (isEditableTarget(event.target) || isInteractiveTarget(event.target)) {
    lastTouchEndTime = 0;
    return;
  }

  if (isDrag || event.changedTouches.length !== 1) {
    lastTouchEndTime = 0;
    return;
  }

  const now = performance.now();
  if (now - lastTouchEndTime <= DOUBLE_TAP_ZOOM_WINDOW_MS) {
    event.preventDefault();
  }
  lastTouchEndTime = now;
}

function preventDoubleTapZoomOnTouchStart(event) {
  if (!IS_IOS || !event.isTrusted) {
    return;
  }

  if (isOverlayTarget(event.target)) {
    lastTouchStartTime = 0;
    return;
  }

  if (isEditableTarget(event.target) || isInteractiveTarget(event.target) || event.touches.length !== 1) {
    lastTouchStartTime = 0;
    return;
  }

  const now = performance.now();
  if (now - lastTouchStartTime <= DOUBLE_TAP_ZOOM_WINDOW_MS) {
    event.preventDefault();
  }
  lastTouchStartTime = now;
}

function preventIosGestureOnGame(event) {
  if (!IS_IOS || !event.isTrusted || !isGameTouchTarget(event.target)) {
    return;
  }

  if (isEditableTarget(event.target)) {
    return;
  }

  event.preventDefault();
}

function renderLeaderboard() {
  renderLeaderboardList(leaderboardList);
  renderLeaderboardList(overlayLeaderboardList);
}

function renderLeaderboardList(target) {
  if (!target) {
    return;
  }

  target.replaceChildren();

  if (!state.leaderboard.length) {
    const item = document.createElement("li");
    item.className = "empty";
    item.textContent = state.leaderboardAvailable ? "No runs saved yet." : "Leaderboard offline.";
    target.appendChild(item);
    return;
  }

  let startIndex = 0;
  const recentIndex = state.leaderboard.findIndex((entry) => {
    const entryName = String(entry.name || "").trim().toLowerCase();
    return entryName !== "" && entryName === state.lastSubmittedName;
  });

  if (recentIndex >= LEADERBOARD_DISPLAY_LIMIT) {
    const halfWindow = Math.floor(LEADERBOARD_DISPLAY_LIMIT / 2);
    const maxStart = Math.max(0, state.leaderboard.length - LEADERBOARD_DISPLAY_LIMIT);
    startIndex = Math.min(Math.max(0, recentIndex - halfWindow), maxStart);
  }

  const visibleEntries = state.leaderboard.slice(startIndex, startIndex + LEADERBOARD_DISPLAY_LIMIT);
  for (const [index, entry] of visibleEntries.entries()) {
    const item = document.createElement("li");
    const rank = document.createElement("span");
    const nameWrap = document.createElement("span");
    const name = document.createElement("span");
    const score = document.createElement("span");

    rank.className = "entry-rank";
    nameWrap.className = "entry-name-wrap";
    name.className = "entry-name";
    score.className = "entry-score";
    rank.textContent = `#${startIndex + index + 1}`;
    name.textContent = entry.name;
    score.textContent = String(entry.score);

    nameWrap.append(name);
    if (startIndex + index === 0) {
      item.classList.add("top-entry");
      const crown = document.createElement("span");
      crown.className = "entry-crown";
      crown.setAttribute("aria-label", "First place");
      crown.textContent = "👑";
      nameWrap.append(crown);
    }

    const globalIndex = startIndex + index;
    const isRecent = globalIndex === recentIndex && recentIndex >= 0;
    if (isRecent) {
      item.classList.add("recent-entry");
    }

    item.append(rank, nameWrap, score);
    target.appendChild(item);
  }
}

function showOverlayLeaderboard() {
  const isMobile = window.matchMedia("(max-width: 620px)").matches;
  if (!isMobile) {
    hideOverlayLeaderboard();
    return;
  }

  overlayLeaderboard.classList.remove("hidden");
}

function hideOverlayLeaderboard() {
  overlayLeaderboard.classList.add("hidden");
}

function handleActionButtonPress(event) {
  if (event) {
    event.preventDefault();
    event.stopPropagation();
  }

  const now = performance.now();
  if (now - lastActionButtonTime < 220) {
    return;
  }
  lastActionButtonTime = now;
  startGame();
}

function handleOverlayPress(event) {
  if (!event.isTrusted || isInteractiveTarget(event.target)) {
    return;
  }

  if (!leaderboardForm.classList.contains("hidden")) {
    return;
  }

  handlePrimaryInput();
}

function canSubmitLeaderboardScore(score) {
  if (state.hasSubmittedScore || score <= 0) {
    return false;
  }
  return true;
}

function showLeaderboardForm() {
  leaderboardForm.classList.remove("hidden");
  playerNameInput.value = "";
  setLeaderboardStatus("");
}

function hideLeaderboardForm() {
  leaderboardForm.classList.add("hidden");
}

function setLeaderboardStatus(message, tone = "") {
  leaderboardStatus.textContent = message;
  leaderboardStatus.className = "form-status";
  if (tone) {
    leaderboardStatus.classList.add(tone);
  }
}

function sanitizeLocalName(value) {
  return String(value || "")
    .replace(/[^a-z0-9 _-]/gi, "")
    .trim()
    .slice(0, 12);
}

function sanitizeLocalScore(value) {
  const score = Number.parseInt(value, 10);
  if (!Number.isFinite(score) || score < 0) {
    return 0;
  }

  return score;
}

function normalizeLeaderboardEntries(entries) {
  const deduped = new Map();

  for (const entry of entries) {
    const sanitized = {
      name: sanitizeLocalName(entry.name),
      score: sanitizeLocalScore(entry.score),
      createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date().toISOString(),
    };

    if (!sanitized.name || sanitized.score <= 0) {
      continue;
    }

    const key = sanitized.name.toLowerCase();
    const existing = deduped.get(key);
    if (!existing || sanitized.score > existing.score) {
      deduped.set(key, sanitized);
      continue;
    }

    if (sanitized.score === existing.score) {
      const existingTime = new Date(existing.createdAt).getTime();
      const nextTime = new Date(sanitized.createdAt).getTime();
      if (nextTime < existingTime) {
        deduped.set(key, sanitized);
      }
    }
  }

  return Array.from(deduped.values())
    .sort((a, b) => {
      if (b.score !== a.score) {
        return b.score - a.score;
      }

      return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
    })
    .slice(0, LEADERBOARD_STORAGE_LIMIT);
}

function readLocalLeaderboard() {
  try {
    const raw = localStorage.getItem(LOCAL_LEADERBOARD_KEY);
    const parsed = JSON.parse(raw || "[]");
    return Array.isArray(parsed) ? normalizeLeaderboardEntries(parsed) : [];
  } catch {
    return [];
  }
}

function writeLocalLeaderboard(entries) {
  const normalized = normalizeLeaderboardEntries(entries);
  localStorage.setItem(LOCAL_LEADERBOARD_KEY, JSON.stringify(normalized));
  return normalized;
}

function getLeaderboardEndpoints() {
  const pageUrl = new URL(window.location.href);
  const directoryUrl = new URL("./", pageUrl);
  return [
    new URL("leaderboard.php", directoryUrl).toString(),
    new URL("api/leaderboard", directoryUrl).toString(),
    "/api/leaderboard",
  ];
}

async function fetchLeaderboardApi(path, options = {}) {
  const response = await fetch(path, options);
  let payload = {};

  try {
    payload = await response.json();
  } catch {
    payload = {};
  }

  if (!response.ok) {
    throw new Error(payload.error || "Leaderboard request failed.");
  }

  return payload;
}

async function loadRemoteLeaderboard() {
  for (const endpoint of getLeaderboardEndpoints()) {
    try {
      const payload = await fetchLeaderboardApi(endpoint, { cache: "no-store" });
      return {
        entries: Array.isArray(payload.entries) ? payload.entries : [],
        endpoint,
      };
    } catch {
      // Try the next endpoint.
    }
  }

  throw new Error("No remote leaderboard endpoint available.");
}

async function saveRemoteLeaderboard(name, score) {
  for (const endpoint of getLeaderboardEndpoints()) {
    try {
      const payload = await fetchLeaderboardApi(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ name, score }),
      });

      return {
        entries: Array.isArray(payload.entries) ? payload.entries : [],
        endpoint,
      };
    } catch {
      // Try the next endpoint.
    }
  }

  throw new Error("No remote leaderboard endpoint available.");
}

async function loadLeaderboard() {
  try {
    const result = await loadRemoteLeaderboard();
    state.leaderboard = result.entries;
    state.leaderboardAvailable = true;
  } catch (error) {
    state.leaderboard = readLocalLeaderboard();
    state.leaderboardAvailable = false;
  }

  renderLeaderboard();
}

async function submitLeaderboardScore(event) {
  event.preventDefault();

  if (!canSubmitLeaderboardScore(state.score)) {
    setLeaderboardStatus("Already saved this run.", "error");
    return;
  }

  const name = playerNameInput.value.trim();
  if (!name) {
    setLeaderboardStatus("Enter a name first.", "error");
    playerNameInput.focus();
    return;
  }

  submitScoreButton.disabled = true;
  setLeaderboardStatus("Saving run...");

  try {
    let savedEntries;

    if (state.leaderboardAvailable) {
      const result = await saveRemoteLeaderboard(name, state.score);
      savedEntries = result.entries;
    } else {
      savedEntries = writeLocalLeaderboard([
        ...readLocalLeaderboard(),
        {
          name,
          score: state.score,
          createdAt: new Date().toISOString(),
        },
      ]);
    }

    state.leaderboard = savedEntries;
    state.leaderboardAvailable = true;
    state.hasSubmittedScore = true;
    state.lastSubmittedName = sanitizeLocalName(name).toLowerCase();
    renderLeaderboard();
    hideLeaderboardForm();
    setLeaderboardStatus("Score saved.", "success");
    overlayCopy.textContent = `You cleared ${state.score} column${state.score === 1 ? "" : "s"}. Score saved, hit restart and go again.`;
  } catch (error) {
    const savedEntries = writeLocalLeaderboard([
      ...readLocalLeaderboard(),
      {
        name,
        score: state.score,
        createdAt: new Date().toISOString(),
      },
    ]);

    state.leaderboard = savedEntries;
    state.leaderboardAvailable = false;
    state.hasSubmittedScore = true;
    state.lastSubmittedName = sanitizeLocalName(name).toLowerCase();
    renderLeaderboard();
    hideLeaderboardForm();
    setLeaderboardStatus("Saved on this device.", "success");
    overlayCopy.textContent = `You cleared ${state.score} column${state.score === 1 ? "" : "s"}. Saved locally, hit restart and go again.`;
  } finally {
    submitScoreButton.disabled = false;
    playerNameInput.value = "";
  }
}

window.addEventListener("resize", resize);

window.addEventListener("keydown", (event) => {
  if (event.code !== "Space") {
    return;
  }

  if (event.repeat) {
    event.preventDefault();
    return;
  }

  const activeTag = document.activeElement?.tagName;
  if (activeTag === "INPUT" || activeTag === "TEXTAREA") {
    return;
  }

  event.preventDefault();
  handlePrimaryInput();
});

canvas.addEventListener("pointerdown", (event) => {
  const point = getPointerScenePosition(event);
  spawnClickText(point.x, point.y);
  handlePrimaryInput();
});

canvas.addEventListener("touchstart", (event) => {
  if (!event.isTrusted || event.touches.length !== 1) {
    return;
  }
  event.preventDefault();
  const touch = event.changedTouches.item(0);
  if (!touch) {
    return;
  }
  const point = getPointerScenePosition(touch);
  spawnClickText(point.x, point.y);
  handlePrimaryInput();
}, { passive: false });

document.addEventListener("touchmove", trackTouchMove, { passive: true });
document.addEventListener("touchstart", preventDoubleTapZoomOnTouchStart, { passive: false, capture: true });
document.addEventListener("touchend", preventDoubleTapZoom, { passive: false, capture: true });
document.addEventListener("touchcancel", clearTouchState, { passive: true });
document.addEventListener("touchmove", preventIosGestureOnGame, { passive: false, capture: true });
document.addEventListener("gesturestart", preventIosGestureOnGame, { passive: false, capture: true });
document.addEventListener("gesturechange", preventIosGestureOnGame, { passive: false, capture: true });
document.addEventListener("gestureend", preventIosGestureOnGame, { passive: false, capture: true });
frame?.addEventListener("dblclick", (event) => event.preventDefault(), { passive: false });

overlay.addEventListener("pointerup", handleOverlayPress);
overlay.addEventListener("touchend", handleOverlayPress, { passive: true });

leaderboardForm.addEventListener("submit", submitLeaderboardScore);
if (skipScoreButton) {
  skipScoreButton.addEventListener("click", handleActionButtonPress);
  skipScoreButton.addEventListener("touchend", handleActionButtonPress, { passive: false });
}
if (audioToggle) {
  audioToggle.addEventListener("click", toggleAudio);
}
if (mobileAudioToggle) {
  mobileAudioToggle.addEventListener("click", toggleAudio);
}

resize();
clearRound();
showStartOverlay();
renderLeaderboard();
loadLeaderboard();
requestAnimationFrame(loop);
startAnalyticsSession();

window.addEventListener("pagehide", () => {
  endAnalyticsSession(true);
});

document.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    endAnalyticsSession(true);
  }
});
