/** Core game state + rules: tools, energy, day cycle, growth, money, save. */
import {
  COLS, ROWS, CROPS, cropDef, makeWorld, walkable, tillable,
  type Tile, type CropType,
} from './world.ts';
import { sfx } from './audio.ts';
import { burst } from './particles.ts';

export type Phase = 'title' | 'play' | 'sleep';
export type Facing = 'down' | 'up' | 'left' | 'right';
export type Tool = 'hoe' | 'can' | 'seeds' | 'scythe';

export const TOOLS: Tool[] = ['hoe', 'can', 'seeds', 'scythe'];
export const MAX_ENERGY = 30;
export const DAY_LENGTH = 240; // real seconds per in-game day
const SAVE_KEY = 'hearthroot-hollow-save-v2'; // v2: 22x16 world + new crops
const SPEED = 4.2; // tiles per second

export interface ShippedLine {
  name: string;
  count: number;
  gold: number;
  color: string;
}

export interface GameState {
  phase: Phase;
  tiles: Tile[][];
  day: number;
  time: number; // 0..1 across the day (0 = 6am)
  money: number;
  energy: number;
  inventory: Partial<Record<CropType, number>>;
  player: { x: number; y: number; facing: Facing; moving: boolean };
  tool: Tool;
  seedIndex: number; // which crop the seed pouch sows
  toolAnim: number; // >0 while swing animation plays (seconds left)
  lastShipped: ShippedLine[]; // for the sleep screen
  lastEarned: number;
  tilesDirty: boolean; // static tile layer needs re-render
  hudDirty: boolean;
}

export function newGame(): GameState {
  return {
    phase: 'title',
    tiles: makeWorld(),
    day: 1,
    time: 0,
    money: 0,
    energy: MAX_ENERGY,
    inventory: {},
    player: { x: 8.5, y: 8.5, facing: 'down', moving: false },
    tool: 'hoe',
    seedIndex: 0,
    toolAnim: 0,
    lastShipped: [],
    lastEarned: 0,
    tilesDirty: true,
    hudDirty: true,
  };
}

/** tile the player is standing on */
export function feetTile(g: GameState): { x: number; y: number } {
  return { x: Math.floor(g.player.x), y: Math.floor(g.player.y) };
}

/** tile the current tool would act on — always the tile adjacent to the
 *  player's feet in the facing direction (never the tile underfoot) */
export function targetTile(g: GameState): { x: number; y: number } {
  const f = g.player.facing;
  const dx = f === 'left' ? -1 : f === 'right' ? 1 : 0;
  const dy = f === 'up' ? -1 : f === 'down' ? 1 : 0;
  return { x: Math.floor(g.player.x) + dx, y: Math.floor(g.player.y) + dy };
}

export function tileAt(g: GameState, x: number, y: number): Tile | null {
  // null-safe for anything: fractional coords floor, NaN/Infinity/out-of-range -> null
  if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
  const tx = Math.floor(x);
  const ty = Math.floor(y);
  if (tx < 0 || ty < 0 || tx >= COLS || ty >= ROWS) return null;
  return g.tiles[ty][tx];
}

// ---------------------------------------------------------------------------
// movement
// ---------------------------------------------------------------------------

export function movePlayer(g: GameState, ix: number, iy: number, dt: number): void {
  const p = g.player;
  p.moving = ix !== 0 || iy !== 0;
  if (!p.moving) return;

  // facing follows the dominant axis
  if (Math.abs(ix) >= Math.abs(iy)) p.facing = ix < 0 ? 'left' : 'right';
  else p.facing = iy < 0 ? 'up' : 'down';

  const len = Math.hypot(ix, iy) || 1;
  const step = SPEED * dt;
  const nx = p.x + (ix / len) * step;
  const ny = p.y + (iy / len) * step;

  // axis-separated collision with a small body radius
  const R = 0.28;
  const can = (x: number, y: number) =>
    walkable(g.tiles, Math.floor(x - R), Math.floor(y - R)) &&
    walkable(g.tiles, Math.floor(x + R), Math.floor(y - R)) &&
    walkable(g.tiles, Math.floor(x - R), Math.floor(y + R)) &&
    walkable(g.tiles, Math.floor(x + R), Math.floor(y + R));
  if (can(nx, p.y)) p.x = nx;
  if (can(p.x, ny)) p.y = ny;
}

// ---------------------------------------------------------------------------
// tool actions
// ---------------------------------------------------------------------------

export function useTool(g: GameState): void {
  const { x, y } = targetTile(g);
  useToolAt(g, x, y);
}

/** click-to-act: run the current tool on (x,y) if it's the player's own tile
 *  or any of the 8 surrounding ones; turns the player to face it */
export function useToolOnTile(g: GameState, x: number, y: number): void {
  const feet = feetTile(g);
  const dx = x - feet.x;
  const dy = y - feet.y;
  if (Math.abs(dx) > 1 || Math.abs(dy) > 1) return; // out of reach
  if (dx !== 0 || dy !== 0) {
    g.player.facing =
      Math.abs(dx) >= Math.abs(dy) ? (dx < 0 ? 'left' : 'right') : (dy < 0 ? 'up' : 'down');
  }
  useToolAt(g, x, y);
}

function useToolAt(g: GameState, x: number, y: number): void {
  if (g.phase !== 'play') return;
  if (g.toolAnim > 0.12) return; // still mid-swing
  if (g.energy <= 0) {
    sfx.deny();
    window.dispatchEvent(new CustomEvent('toast', { detail: 'Too sleepy to work… time for bed 💤' }));
    return;
  }
  const t = tileAt(g, x, y);
  if (!t) return;

  let acted = false;
  const px = x + 0.5;
  const py = y + 0.5;

  switch (g.tool) {
    case 'hoe':
      if (tillable(t)) {
        t.kind = 'soil';
        t.watered = false;
        sfx.till();
        burst(px, py, '#8a6240', 10);
        acted = true;
      }
      break;
    case 'can':
      if (t.kind === 'soil' && !t.watered) {
        t.watered = true;
        sfx.water();
        burst(px, py, '#6db5e8', 12);
        acted = true;
      }
      break;
    case 'seeds':
      if (t.kind === 'soil' && !t.crop) {
        t.crop = { type: CROPS[g.seedIndex].id, stage: 0, progress: 0 };
        sfx.plant();
        burst(px, py, '#9ad06b', 8);
        acted = true;
      }
      break;
    case 'scythe':
      if (t.crop && t.crop.stage === 2) {
        const def = cropDef(t.crop.type);
        g.inventory[def.id] = (g.inventory[def.id] ?? 0) + 1;
        t.crop = null;
        sfx.harvest();
        burst(px, py, def.color, 18);
        acted = true;
      }
      break;
  }

  g.toolAnim = 0.28; // swing animation even on a whiff — feels better
  if (acted) {
    g.energy -= 1;
    g.tilesDirty = true;
    g.hudDirty = true;
    save(g);
  } else {
    sfx.whiff();
  }
}

export function setTool(g: GameState, tool: Tool): void {
  if (tool === 'seeds' && g.tool === 'seeds') {
    g.seedIndex = (g.seedIndex + 1) % CROPS.length; // re-select cycles seed type
  }
  g.tool = tool;
  g.hudDirty = true;
  sfx.tick();
  save(g);
}

// ---------------------------------------------------------------------------
// day cycle
// ---------------------------------------------------------------------------

export function tickClock(g: GameState, dt: number): void {
  if (g.phase !== 'play') return;
  g.time += dt / DAY_LENGTH;
  if (g.time >= 1) sleep(g); // you stayed up way too late — auto collapse
}

export function sleep(g: GameState): void {
  if (g.phase !== 'play') return;

  // ship the harvest
  const shipped: ShippedLine[] = [];
  let earned = 0;
  for (const def of CROPS) {
    const n = g.inventory[def.id] ?? 0;
    if (n > 0) {
      shipped.push({ name: def.name, count: n, gold: n * def.sell, color: def.color });
      earned += n * def.sell;
    }
  }
  g.money += earned;
  g.inventory = {};
  g.lastShipped = shipped;
  g.lastEarned = earned;

  // grow watered crops, dry all soil
  for (const row of g.tiles) {
    for (const t of row) {
      if (t.crop && t.watered) {
        t.crop.progress += 1;
        const [s1, s2] = cropDef(t.crop.type).stageDays;
        t.crop.stage = t.crop.progress >= s1 + s2 ? 2 : t.crop.progress >= s1 ? 1 : 0;
      }
      t.watered = false;
    }
  }

  g.day += 1;
  g.time = 0;
  g.energy = MAX_ENERGY;
  g.phase = 'sleep';
  g.tilesDirty = true;
  g.hudDirty = true;
  sfx.sleep();
  save(g);
}

export function wake(g: GameState): void {
  if (g.phase !== 'sleep') return;
  g.phase = 'play';
  g.hudDirty = true;
  sfx.morning();
  save(g);
}

// ---------------------------------------------------------------------------
// save / load
// ---------------------------------------------------------------------------

interface SaveData {
  v: 1;
  day: number;
  time: number;
  money: number;
  energy: number;
  inventory: Partial<Record<CropType, number>>;
  player: { x: number; y: number; facing: Facing };
  tool: Tool;
  seedIndex: number;
  tiles: Array<{ k: Tile['kind']; w: 0 | 1; c: { t: CropType; s: 0 | 1 | 2; p: number } | null }>;
}

export function save(g: GameState): void {
  if (g.phase === 'title') return;
  try {
    const data: SaveData = {
      v: 1,
      day: g.day,
      time: g.time,
      money: g.money,
      energy: g.energy,
      inventory: g.inventory,
      player: { x: g.player.x, y: g.player.y, facing: g.player.facing },
      tool: g.tool,
      seedIndex: g.seedIndex,
      tiles: g.tiles.flat().map((t) => ({
        k: t.kind,
        w: t.watered ? 1 : 0,
        c: t.crop ? { t: t.crop.type, s: t.crop.stage, p: t.crop.progress } : null,
      })),
    };
    localStorage.setItem(SAVE_KEY, JSON.stringify(data));
  } catch {
    /* storage may be unavailable — play on without saving */
  }
}

export function hasSave(): boolean {
  try {
    return localStorage.getItem(SAVE_KEY) !== null;
  } catch {
    return false;
  }
}

export function clearSave(): void {
  try {
    localStorage.removeItem(SAVE_KEY);
  } catch {
    /* ignore */
  }
}

export function loadGame(): GameState | null {
  try {
    const raw = localStorage.getItem(SAVE_KEY);
    if (!raw) return null;
    const data = JSON.parse(raw) as SaveData;
    if (data.v !== 1 || !Array.isArray(data.tiles) || data.tiles.length !== COLS * ROWS) return null;
    const g = newGame();
    g.day = data.day;
    g.time = data.time;
    g.money = data.money;
    g.energy = data.energy;
    g.inventory = data.inventory ?? {};
    g.player.x = data.player.x;
    g.player.y = data.player.y;
    g.player.facing = data.player.facing;
    g.tool = data.tool;
    g.seedIndex = data.seedIndex ?? 0;
    data.tiles.forEach((st, i) => {
      const t = g.tiles[Math.floor(i / COLS)][i % COLS];
      t.kind = st.k;
      t.watered = st.w === 1;
      t.crop = st.c ? { type: st.c.t, stage: st.c.s, progress: st.c.p } : null;
    });
    g.phase = 'play';
    return g;
  } catch {
    return null;
  }
}
