const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
const WIDTH = canvas.width;
const HEIGHT = canvas.height;
const fpsInput = document.getElementById("fpsInput");
const scoreValue = document.getElementById("scoreValue");
const difficultyValue = document.getElementById("difficultyValue");
const spawnChanceInput = document.getElementById("spawnChanceInput");
const dogSpeedInput = document.getElementById("dogSpeedInput");
const debugStats = document.getElementById("debugStats");
let timeScale = 1;
const MS_PER_FRAME = 1000 / 60;
let targetFrameTime = MS_PER_FRAME;
let lastUpdateTime = 0;

function updateTimeScale() {
  const rawFPS = Math.max(1, Math.min(120, Number(fpsInput?.value) || 60));
  targetFrameTime = 1000 / rawFPS;
  timeScale = rawFPS / 60;
}

fpsInput?.addEventListener("change", updateTimeScale);
fpsInput?.addEventListener("input", updateTimeScale);
spawnChanceInput?.addEventListener("change", updateScoreUI);
spawnChanceInput?.addEventListener("input", updateScoreUI);
dogSpeedInput?.addEventListener("change", updateScoreUI);
dogSpeedInput?.addEventListener("input", updateScoreUI);
updateTimeScale();

// Load sprite sheets and background image
const runSpriteSheet = new Image();
runSpriteSheet.src = "src/run.png";

const run2SpriteSheet = new Image();
run2SpriteSheet.src = "src/run2.png";

const jumpSpriteSheet = new Image();
jumpSpriteSheet.src = "src/jump.png";

const jump2SpriteSheet = new Image();
jump2SpriteSheet.src = "src/jump2.png";

const damageSpriteSheet = new Image();
damageSpriteSheet.src = "src/damage.png";

const damage2SpriteSheet = new Image();
damage2SpriteSheet.src = "src/damage2.png";

const backgroundImg = new Image();
backgroundImg.src = "src/back.png";

function createAudio(src, { loop = false, volume = 1 } = {}) {
  const audio = new Audio(src);
  audio.loop = loop;
  audio.volume = volume;
  audio.preload = "auto";
  return audio;
}

const bgmAudio = createAudio("src/audio/bgm.wav", { loop: true, volume: 0.24 });
const runAudio = createAudio("src/audio/run.wav", { loop: true, volume: 0.34 });
const jumpAudio = createAudio("src/audio/jump.wav", { volume: 0.42 });
const fallAudio = createAudio("src/audio/fall.wav", { volume: 0.52 });
let audioStarted = false;

function playAudio(audio) {
  const playPromise = audio.play();
  if (playPromise) playPromise.catch(() => {});
}

function startAudio() {
  if (audioStarted) return;
  audioStarted = true;
  playAudio(bgmAudio);
}

function playOneShot(audio) {
  if (!audioStarted) return;
  audio.currentTime = 0;
  playAudio(audio);
}

function updateRunAudio() {
  if (!audioStarted) return;
  const shouldRun = !gameOver && playerState === 'running';
  if (shouldRun && runAudio.paused) {
    playAudio(runAudio);
  } else if (!shouldRun && !runAudio.paused) {
    runAudio.pause();
    runAudio.currentTime = 0;
  }
}

// Sprite dimensions (based on run.png: 5 frames horizontal)
const SPRITE_FRAME_COUNT = 5; // Number of frames in run.png
const SPRITE_TOTAL_WIDTH = 1280; // Approximate total width of sprite sheet
const SPRITE_TOTAL_HEIGHT = 400; // Approximate total height
let SPRITE_FRAME_WIDTH = Math.floor(SPRITE_TOTAL_WIDTH / SPRITE_FRAME_COUNT); // Width of each frame
let SPRITE_FRAME_HEIGHT = SPRITE_TOTAL_HEIGHT; // Height of each frame

// Update sprite dimensions when image loads
runSpriteSheet.onload = function() {
  SPRITE_FRAME_WIDTH = Math.floor(runSpriteSheet.width / SPRITE_FRAME_COUNT);
  SPRITE_FRAME_HEIGHT = runSpriteSheet.height;
};

// Player state management
let playerState = 'running'; // 'running', 'jumping', 'damaged'
let playerStateStartedAt = 0;
let damageRecoveryTime = 0; // milliseconds remaining before recovery
const DAMAGE_RECOVERY_DURATION = 1000; // 1 second
const DAMAGE_FIRST_FRAME_DURATION = 180;
let backgroundFrozenUntil = 0; // timestamp when background can resume scrolling

const PLAYER_X = 180;
const GROUND_Y = 560;
const PLAYER_WIDTH = 120;  // 80 * 1.5
const PLAYER_HEIGHT = 270; // 180 * 1.5
const GRAVITY = 0.9;
const JUMP_VELOCITY = -18;
const JUMP_RELEASE_VELOCITY = -7;
const SCROLL_SPEED = 5;
const DAMAGE_MAX = 2;
const INVINCIBLE_TIME = 1400;
const SPAWN_CHECK_INTERVAL = 2000;
const DIFFICULTY_INTERVAL = 5000;
const SPAWN_CHANCE_STEP = 0.08;
const MAX_SPAWN_CHANCE = 3;
const DOG_SPEED_STEP = 0.75;

let gameOver = false;
let scrollOffset = 0;
let dogSpawnTimer = 0;
let scheduledDogSpawns = [];
let dogs = [];
let damageState = 0;
let invincibleUntil = 0;
let shakeTime = 0;
let score = 0;
let difficultyLevel = 1;
let difficultyTimer = 0;
let jumpButtonHeld = false;

const player = {
  x: PLAYER_X,
  y: GROUND_Y - PLAYER_HEIGHT,
  width: PLAYER_WIDTH,
  height: PLAYER_HEIGHT,
  vy: 0,
  jumping: false,
  damage: 0,
};

const background = {
  skyOffset: 0,
  houseOffset: 0,
  roadOffset: 0,
};

function resetGame() {
  gameOver = false;
  scrollOffset = 0;
  dogSpawnTimer = 0;
  scheduledDogSpawns = [];
  difficultyTimer = 0;
  difficultyLevel = 1;
  score = 0;
  dogs = [];
  damageState = 0;
  invincibleUntil = 0;
  shakeTime = 0;
  playerState = 'running';
  playerStateStartedAt = performance.now();
  damageRecoveryTime = 0;
  backgroundFrozenUntil = 0;
  player.y = GROUND_Y - PLAYER_HEIGHT;
  player.vy = 0;
  player.jumping = false;
  jumpButtonHeld = false;
  updateDamageIndicators();
  updateScoreUI();
}

function getBaseSpawnChance() {
  const raw = Number(spawnChanceInput?.value);
  return Math.max(0, Math.min(MAX_SPAWN_CHANCE, (Number.isFinite(raw) ? raw : 55) / 100));
}

function getBaseDogSpeed() {
  const raw = Number(dogSpeedInput?.value);
  return Math.max(1, Math.min(30, Number.isFinite(raw) ? raw : 8));
}

function getCurrentSpawnChance() {
  return Math.min(MAX_SPAWN_CHANCE, getBaseSpawnChance() + (difficultyLevel - 1) * SPAWN_CHANCE_STEP);
}

function getCurrentDogSpeed() {
  return getBaseDogSpeed() + (difficultyLevel - 1) * DOG_SPEED_STEP;
}

function updateScoreUI() {
  if (scoreValue) scoreValue.textContent = String(score);
  if (difficultyValue) difficultyValue.textContent = String(difficultyLevel);
  if (debugStats) {
    debugStats.textContent = `実効 ${Math.round(getCurrentSpawnChance() * 100)}% / ${getCurrentDogSpeed().toFixed(1)}`;
  }
}

function updateDamageIndicators() {
  const container = document.getElementById("damageIndicators");
  container.innerHTML = "";
  for (let i = 0; i < DAMAGE_MAX; i += 1) {
    const dot = document.createElement("div");
    dot.className = "damage-dot";
    if (i < damageState) dot.classList.add("lost");
    container.appendChild(dot);
  }
}

function spawnDog() {
  const height = 80;
  const dog = {
    x: WIDTH + 120,
    y: GROUND_Y - height + 10,
    width: 120,
    height,
    frame: 0,
    frameTimer: 0,
    scored: false,
  };
  dogs.push(dog);
}

function scheduleDogSpawnsForWindow() {
  const spawnChance = getCurrentSpawnChance();
  const guaranteedSpawns = Math.floor(spawnChance);
  const extraChance = spawnChance - guaranteedSpawns;
  let spawnCount = guaranteedSpawns;

  if (Math.random() < extraChance) {
    spawnCount += 1;
  }

  spawnCount = Math.min(3, spawnCount);
  for (let i = 0; i < spawnCount; i += 1) {
    scheduledDogSpawns.push(i === 0 ? 0 : Math.random() * SPAWN_CHECK_INTERVAL);
  }
  scheduledDogSpawns.sort((a, b) => a - b);
}

function updateScheduledDogSpawns(deltaTime) {
  if (scheduledDogSpawns.length === 0) return;
  const remaining = [];

  scheduledDogSpawns.forEach((timeUntilSpawn) => {
    const nextTime = timeUntilSpawn - deltaTime;
    if (nextTime <= 0) {
      spawnDog();
    } else {
      remaining.push(nextTime);
    }
  });

  scheduledDogSpawns = remaining;
}

function getPlayerSprites() {
  if (damageState >= 1) {
    return {
      run: run2SpriteSheet,
      jump: jump2SpriteSheet,
      damage: damage2SpriteSheet,
    };
  }

  return {
    run: runSpriteSheet,
    jump: jumpSpriteSheet,
    damage: damageSpriteSheet,
  };
}

function updatePlayer(deltaTime) {
  if (gameOver) return;
  const frameDelta = deltaTime / MS_PER_FRAME;
  
  // Update damage recovery timer
  if (damageRecoveryTime > 0) {
    damageRecoveryTime -= deltaTime;
    if (damageRecoveryTime <= 0) {
      damageRecoveryTime = 0;
      playerState = 'running'; // Auto-resume running
      playerStateStartedAt = performance.now();
    }
  }
  
  // Only update physics when not in damaged state
  if (playerState === 'damaged') {
    return; // Skip physics during damage animation
  }
  
  player.vy += GRAVITY * frameDelta;
  player.y += player.vy * frameDelta;
  if (player.y >= GROUND_Y - player.height) {
    player.y = GROUND_Y - player.height;
    player.vy = 0;
    player.jumping = false;
    if (playerState === 'jumping') {
      playerState = 'running';
      playerStateStartedAt = performance.now();
    }
  }
}

function updateDogs(deltaTime) {
  if (gameOver) return;
  dogSpawnTimer += deltaTime;
  difficultyTimer += deltaTime;
  updateScheduledDogSpawns(deltaTime);

  while (difficultyTimer >= DIFFICULTY_INTERVAL) {
    difficultyTimer -= DIFFICULTY_INTERVAL;
    difficultyLevel += 1;
    updateScoreUI();
  }

  while (dogSpawnTimer >= SPAWN_CHECK_INTERVAL) {
    dogSpawnTimer -= SPAWN_CHECK_INTERVAL;
    scheduleDogSpawnsForWindow();
  }

  const dogSpeed = getCurrentDogSpeed();
  dogs.forEach((dog) => {
    dog.x -= dogSpeed * (deltaTime / MS_PER_FRAME);
    dog.frameTimer += deltaTime;
    if (dog.frameTimer > 120) {
      dog.frameTimer = 0;
      dog.frame = (dog.frame + 1) % 4;
    }
  });

  const remainingDogs = [];
  dogs.forEach((dog) => {
    if (dog.x + dog.width > -60) {
      remainingDogs.push(dog);
    } else if (!dog.scored) {
      score += 1;
      dog.scored = true;
      updateScoreUI();
    }
  });
  dogs = remainingDogs;
}

function checkCollisions() {
  if (gameOver) return;
  const now = performance.now();
  if (now < invincibleUntil) return;

  const playerHitboxWidth = player.width / 3;
  const playerHitboxHeight = player.height * 2 / 3;
  const playerBox = {
    x: player.x + (player.width - playerHitboxWidth) / 2,
    y: player.y + (player.height - playerHitboxHeight) / 2,
    width: playerHitboxWidth,
    height: playerHitboxHeight,
  };

  for (const dog of dogs) {
    const dogBox = {
      x: dog.x + 12,
      y: dog.y + 12,
      width: dog.width - 25,
      height: dog.height - 18,
    };

    if (
      playerBox.x < dogBox.x + dogBox.width &&
      playerBox.x + playerBox.width > dogBox.x &&
      playerBox.y < dogBox.y + dogBox.height &&
      playerBox.y + playerBox.height > dogBox.y
    ) {
      handleHit();
      dog.scored = true;
      dog.x = -200;
      break;
    }
  }
}

function handleHit() {
  if (gameOver) return;
  const now = performance.now();
  playOneShot(fallAudio);
  damageState += 1;
  invincibleUntil = now + INVINCIBLE_TIME;
  shakeTime = 240;
  updateDamageIndicators();

  // Enter damaged state, freeze background for 1 second
  playerState = 'damaged';
  playerStateStartedAt = now;
  damageRecoveryTime = DAMAGE_RECOVERY_DURATION;
  backgroundFrozenUntil = now + DAMAGE_RECOVERY_DURATION;

  if (damageState >= DAMAGE_MAX) {
    damageState = DAMAGE_MAX;
    endGame();
  }
}

function endGame() {
  gameOver = true;
}

function updateBackground(deltaTime) {
  if (gameOver) return;
  const now = performance.now();
  if (now > backgroundFrozenUntil) {
    // Scroll background image continuously (left to right)
    background.roadOffset += SCROLL_SPEED * (deltaTime / MS_PER_FRAME);
    // Reset offset to avoid large numbers
    if (Math.abs(background.roadOffset) > 10000) {
      background.roadOffset = 0;
    }
  }
}

function drawBackground() {
  ctx.save();
  
  // Draw background with seamless scrolling
  if (backgroundImg.complete) {
    const bgWidth = backgroundImg.width;
    const bgHeight = backgroundImg.height;
    
    // Calculate how many complete cycles we need
    const cycleWidth = bgWidth * 2; // original + flipped
    const scrollMod = ((background.roadOffset % cycleWidth) + cycleWidth) % cycleWidth;
    
    // Draw enough tiles to cover the screen with scroll offset
    let drawX = -scrollMod;
    let cycleIndex = 0;
    
    while (drawX < WIDTH) {
      if (cycleIndex % 2 === 0) {
        // Draw original
        ctx.drawImage(backgroundImg, 0, 0, bgWidth, bgHeight, drawX, 0, bgWidth, bgHeight);
      } else {
        // Draw horizontally flipped
        ctx.save();
        ctx.translate(drawX + bgWidth, 0);
        ctx.scale(-1, 1);
        ctx.drawImage(backgroundImg, 0, 0, bgWidth, bgHeight, 0, 0, bgWidth, bgHeight);
        ctx.restore();
      }
      drawX += bgWidth;
      cycleIndex++;
    }
  } else {
    // Fallback if image not loaded
    ctx.fillStyle = "#87ceeb";
    ctx.fillRect(0, 0, WIDTH, HEIGHT);
  }
  
  ctx.restore();
}

function drawPlayer() {
  ctx.save();
  const x = player.x;
  const y = player.y;
  const active = performance.now() < invincibleUntil;
  if (active) {
    const flicker = Math.floor(performance.now() / 80) % 2;
    ctx.globalAlpha = flicker ? 0.55 : 1;
  }

  const sprites = getPlayerSprites();
  let spriteSheet = runSpriteSheet;
  let frameIndex = 0;
  let maxFrames = SPRITE_FRAME_COUNT;
  let spriteWidth = SPRITE_FRAME_WIDTH;
  let spriteHeight = SPRITE_FRAME_HEIGHT;
  let drawY = y;
  
  // Select sprite sheet and frame based on player state
  if (playerState === 'running' && sprites.run.complete) {
    frameIndex = Math.floor((performance.now() / 80) % SPRITE_FRAME_COUNT);
    spriteSheet = sprites.run;
    maxFrames = SPRITE_FRAME_COUNT;
    spriteWidth = Math.floor(spriteSheet.width / SPRITE_FRAME_COUNT);
    spriteHeight = spriteSheet.height;
  } else if (playerState === 'jumping' && sprites.jump.complete) {
    // Jump has 2 poses: rising and falling. Pick by velocity so it does not loop midair.
    frameIndex = player.vy < 0 ? 0 : 1;
    spriteSheet = sprites.jump;
    maxFrames = 2;
    spriteWidth = Math.floor(spriteSheet.width / 2);
    spriteHeight = spriteSheet.height;
  } else if (playerState === 'damaged' && sprites.damage.complete) {
    // Damage is a one-shot 2-frame animation, then it stays on the fallen frame.
    const damageElapsed = performance.now() - playerStateStartedAt;
    frameIndex = damageElapsed < DAMAGE_FIRST_FRAME_DURATION ? 0 : 1;
    spriteSheet = sprites.damage;
    maxFrames = 2;
    spriteWidth = Math.floor(spriteSheet.width / 2);
    spriteHeight = spriteSheet.height;
    drawY = GROUND_Y - PLAYER_HEIGHT; // Position on ground
  }
  
  if (spriteSheet.complete) {
    const sourceX = frameIndex * spriteWidth;
    const sourceY = 0;
    
    ctx.drawImage(
      spriteSheet,
      sourceX, sourceY, spriteWidth, spriteHeight,
      x, drawY, PLAYER_WIDTH, PLAYER_HEIGHT
    );
  } else {
    // Fallback placeholder
    ctx.fillStyle = "#ff6b9d";
    ctx.fillRect(x, drawY, PLAYER_WIDTH, PLAYER_HEIGHT);
  }

  ctx.restore();
}

function drawDogs() {
  dogs.forEach((dog) => {
    ctx.save();
    ctx.fillStyle = "#8b5a2b";
    ctx.fillRect(dog.x + 10, dog.y + 20, 100, 32);
    ctx.fillRect(dog.x + 18, dog.y, 44, 34);
    ctx.fillRect(dog.x + 74, dog.y + 18, 26, 28);
    ctx.beginPath();
    ctx.moveTo(dog.x + 86, dog.y + 10);
    ctx.lineTo(dog.x + 104, dog.y + 2);
    ctx.lineTo(dog.x + 108, dog.y + 18);
    ctx.fill();
    ctx.fillStyle = "#000000";
    ctx.beginPath();
    ctx.arc(dog.x + 34, dog.y + 18, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.restore();
  });
}

function drawEffects() {
  if (shakeTime > 0) {
    const shakeAmount = 6;
    const dx = (Math.random() - 0.5) * shakeAmount;
    const dy = (Math.random() - 0.5) * shakeAmount;
    ctx.translate(dx, dy);
  }
}

function drawGameOver() {
  if (!gameOver) return;
  ctx.save();
  ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
  ctx.fillRect(0, 0, WIDTH, HEIGHT);
  ctx.fillStyle = "#ffffff";
  ctx.font = "bold 80px Arial";
  ctx.textAlign = "center";
  ctx.fillText("GAME OVER", WIDTH / 2, HEIGHT / 2 - 10);
  ctx.font = "24px Arial";
  ctx.fillText("RESTARTボタンを押してやり直そう", WIDTH / 2, HEIGHT / 2 + 40);
  ctx.restore();
}

function draw() {
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  ctx.save();
  drawEffects();
  drawBackground();
  drawPlayer();
  drawDogs();
  ctx.restore();
  drawGameOver();
}

function update(deltaTime) {
  if (shakeTime > 0) {
    shakeTime = Math.max(0, shakeTime - deltaTime);
  }

  updateBackground(deltaTime);
  updatePlayer(deltaTime);
  updateDogs(deltaTime);
  checkCollisions();
  updateRunAudio();
}

function gameLoop(timestamp) {
  if (!lastUpdateTime) lastUpdateTime = timestamp;
  const deltaTime = Math.min(timestamp - lastUpdateTime, 100);
  if (deltaTime >= targetFrameTime) {
    lastUpdateTime = timestamp;
    update(deltaTime);
    draw();
  }
  requestAnimationFrame(gameLoop);
}

function pressJump() {
  if (gameOver) return;
  startAudio();
  jumpButtonHeld = true;
  if (playerState === 'damaged') return; // Cannot jump while damaged
  if (!player.jumping) {
    playOneShot(jumpAudio);
    player.vy = JUMP_VELOCITY;
    player.jumping = true;
    playerState = 'jumping';
  }
}

function releaseJump() {
  jumpButtonHeld = false;
  if (player.jumping && player.vy < JUMP_RELEASE_VELOCITY) {
    player.vy = JUMP_RELEASE_VELOCITY;
  }
}

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

window.addEventListener("keyup", (event) => {
  if (event.code === "Space") {
    event.preventDefault();
    releaseJump();
  }
});

canvas.addEventListener("pointerdown", (event) => {
  canvas.setPointerCapture?.(event.pointerId);
  pressJump();
});

canvas.addEventListener("pointerup", releaseJump);
canvas.addEventListener("pointercancel", releaseJump);
canvas.addEventListener("pointerleave", releaseJump);

document.getElementById("restartButton").addEventListener("click", () => {
  startAudio();
  resetGame();
});

resetGame();
requestAnimationFrame(gameLoop);
