import { BurstPoint } from './burstOrigin';
import {
  BurstMotionStyle,
  EmojiBurstProfile,
  getEmojiBurstProfile,
  pickParticleEmoji,
  sampleBurstAngle,
} from './emojiParticleProfiles';

/** Particles spawn over this window. */
export const BURST_SPAWN_MS = 500;
/** Global fade runs from 0.5s → 2s after burst start. */
export const BURST_FADE_START_MS = 500;
export const BURST_FADE_END_MS = 2000;
export const BURST_TOTAL_MS = BURST_FADE_END_MS;

export type BurstParticle = {
  burstId: string;
  burstStartMs: number;
  emoji: string;
  fontSize: number;
  spawnX: number;
  spawnY: number;
  maskRadius: number;
  x: number;
  y: number;
  vx: number;
  vy: number;
  bornAt: number;
  rotation: number;
  spin: number;
  peakScale: number;
  style: BurstMotionStyle;
  gravity: number;
  drag: number;
  phase: number;
  twinkle?: boolean;
  wobble?: boolean;
  orbitRadius: number;
  orbitSpeed: number;
  orbitAngle: number;
  bounceDamping: number;
  bouncesLeft: number;
};

const EMOJI_CACHE_PX = 64;
const EMOJI_CACHE_SCALE = 2;
const MAX_DPR = 2;
const POP_IN_MS = 70;

const emojiCanvasCache = new Map<string, HTMLCanvasElement>();

function easeOutBack(t: number): number {
  const c1 = 1.70158;
  const c3 = c1 + 1;
  return 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2;
}

function easeOutCubic(t: number): number {
  return 1 - (1 - t) ** 3;
}

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

function getEmojiCanvas(emoji: string): HTMLCanvasElement {
  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);
  const cacheKey = `${emoji}:${dpr}`;
  const cached = emojiCanvasCache.get(cacheKey);
  if (cached) return cached;

  const fontSize = Math.ceil(EMOJI_CACHE_PX * dpr);
  const size = Math.ceil(fontSize * EMOJI_CACHE_SCALE);
  const canvas = document.createElement('canvas');
  canvas.width = size;
  canvas.height = size;

  const context = canvas.getContext('2d');
  if (context) {
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif`;
    context.fillText(emoji, size / 2, size / 2);
  }

  emojiCanvasCache.set(cacheKey, canvas);
  return canvas;
}

function emergeOpacity(
  x: number,
  y: number,
  spawnX: number,
  spawnY: number,
  maskRadius: number
): number {
  const dist = Math.hypot(x - spawnX, y - spawnY);
  const inner = maskRadius * 0.55;
  const outer = maskRadius * 1.05;
  if (dist <= inner) return 0;
  if (dist >= outer) return 1;
  return (dist - inner) / (outer - inner);
}

function spawnParticle(
  particles: BurstParticle[],
  burstId: string,
  burstStartMs: number,
  point: BurstPoint,
  primaryEmoji: string,
  profile: EmojiBurstProfile,
  bornAt: number,
  options?: { hero?: boolean }
) {
  const hero = options?.hero ?? false;
  const angle = sampleBurstAngle(profile);
  const speed = hero
    ? lerp(profile.speedMin * 0.65, profile.speedMax * 0.55)
    : lerp(profile.speedMin, profile.speedMax);
  const launchUp = lerp(profile.launchUpMin, profile.launchUpMax);
  const maskRadius = point.maskRadius ?? 28;
  const jitter = hero ? 3 : 12;
  const emoji = pickParticleEmoji(profile, primaryEmoji);

  let vx = Math.cos(angle) * speed + (Math.random() - 0.5) * 60;
  let vy = Math.sin(angle) * speed - launchUp;

  if (profile.style === 'spiral') {
    vx *= 0.35;
    vy *= 0.35;
  }

  if (profile.style === 'sparkle') {
    vx *= 0.75;
    vy *= 0.75;
  }

  particles.push({
    burstId,
    burstStartMs,
    emoji,
    fontSize: hero
      ? lerp(profile.heroFontSizeMin, profile.heroFontSizeMax)
      : lerp(profile.fontSizeMin, profile.fontSizeMax),
    spawnX: point.x,
    spawnY: point.y,
    maskRadius,
    x: point.x + (Math.random() - 0.5) * jitter,
    y: point.y + (Math.random() - 0.5) * jitter,
    vx,
    vy,
    bornAt,
    rotation: (Math.random() - 0.5) * 40,
    spin: lerp(profile.spinMin, profile.spinMax),
    peakScale: hero ? 1.1 + Math.random() * 0.2 : 0.9 + Math.random() * 0.4,
    style: profile.style,
    gravity: profile.gravity,
    drag: profile.drag,
    phase: Math.random() * Math.PI * 2,
    twinkle: profile.twinkle,
    wobble: profile.wobble,
    orbitRadius: hero ? 8 : 14 + Math.random() * 28,
    orbitSpeed: (Math.random() < 0.5 ? -1 : 1) * (1.8 + Math.random() * 2.4),
    orbitAngle: Math.random() * Math.PI * 2,
    bounceDamping: 0.42 + Math.random() * 0.18,
    bouncesLeft: profile.style === 'bounce' ? 2 : 0,
  });
}

/** Fountain from behind the emoji for 0.5s, fade 0.5s→2s. */
export function spawnEmojiBurst(
  particles: BurstParticle[],
  burstId: string,
  point: BurstPoint,
  emoji: string,
  now = performance.now()
) {
  const profile = getEmojiBurstProfile(emoji);
  const burstStartMs = now;

  spawnParticle(particles, burstId, burstStartMs, point, emoji, profile, burstStartMs, {
    hero: true,
  });

  const particleCount = profile.particleCount;
  for (let i = 0; i < particleCount; i += 1) {
    const slot = i / particleCount;
    const bornAt =
      burstStartMs + slot * BURST_SPAWN_MS + Math.random() * (BURST_SPAWN_MS / particleCount);
    spawnParticle(particles, burstId, burstStartMs, point, emoji, profile, bornAt);
  }
}

function stepSpiralParticle(particle: BurstParticle, dtSeconds: number) {
  particle.orbitRadius += 42 * dtSeconds;
  particle.orbitAngle += particle.orbitSpeed * dtSeconds;
  particle.x = particle.spawnX + Math.cos(particle.orbitAngle) * particle.orbitRadius;
  particle.y =
    particle.spawnY + Math.sin(particle.orbitAngle) * particle.orbitRadius * 0.65 + particle.vy * dtSeconds * 8;
  particle.vy += particle.gravity * dtSeconds * 0.35;
  particle.rotation += particle.spin * dtSeconds;
}

function stepBounceParticle(particle: BurstParticle, dtSeconds: number, dragFactor: number) {
  particle.vy += particle.gravity * dtSeconds;
  particle.vx *= dragFactor;
  particle.vy *= dragFactor;
  particle.x += particle.vx * dtSeconds;
  particle.y += particle.vy * dtSeconds;
  particle.rotation += particle.spin * dtSeconds;

  if (particle.bouncesLeft > 0 && particle.vy > 0 && particle.y >= particle.spawnY + 6) {
    particle.y = particle.spawnY + 6;
    particle.vy = -Math.abs(particle.vy) * particle.bounceDamping;
    particle.vx *= 0.82;
    particle.bouncesLeft -= 1;
    particle.spin += (Math.random() - 0.5) * 120;
  }
}

function stepDefaultParticle(particle: BurstParticle, dtSeconds: number, dragFactor: number) {
  particle.vy += particle.gravity * dtSeconds;
  particle.vx *= dragFactor;
  particle.vy *= dragFactor;
  particle.x += particle.vx * dtSeconds;
  particle.y += particle.vy * dtSeconds;
  particle.rotation += particle.spin * dtSeconds;
}

export function stepBurstParticles(
  particles: BurstParticle[],
  now: number,
  dtSeconds: number
): void {
  for (let i = particles.length - 1; i >= 0; i -= 1) {
    const particle = particles[i];
    const burstElapsed = now - particle.burstStartMs;

    if (burstElapsed > BURST_TOTAL_MS) {
      particles[i] = particles[particles.length - 1];
      particles.pop();
      continue;
    }

    const ageMs = now - particle.bornAt;
    if (ageMs < 0) continue;

    const dragFactor = particle.drag ** (dtSeconds * 60);

    switch (particle.style) {
      case 'spiral':
        stepSpiralParticle(particle, dtSeconds);
        break;
      case 'bounce':
        stepBounceParticle(particle, dtSeconds, dragFactor);
        break;
      default:
        stepDefaultParticle(particle, dtSeconds, dragFactor);
        break;
    }

    particle.phase += dtSeconds * (particle.twinkle ? 9 : 5);
  }
}

type DrawState = {
  x: number;
  y: number;
  scale: number;
  opacity: number;
  rotation: number;
};

function particleDrawState(particle: BurstParticle, now: number): DrawState | null {
  const burstElapsed = now - particle.burstStartMs;
  if (burstElapsed > BURST_TOTAL_MS) return null;

  const ageMs = now - particle.bornAt;
  if (ageMs < 0) return null;

  let scale = particle.peakScale;
  let opacity = emergeOpacity(
    particle.x,
    particle.y,
    particle.spawnX,
    particle.spawnY,
    particle.maskRadius
  );

  if (opacity <= 0) return null;

  if (ageMs < POP_IN_MS) {
    const pop = easeOutBack(ageMs / POP_IN_MS);
    scale = 0.1 + pop * particle.peakScale;
    opacity *= pop;
  }

  if (particle.twinkle) {
    opacity *= 0.55 + 0.45 * Math.sin(particle.phase * 1.6);
  }

  if (particle.wobble) {
    scale *= 1 + 0.12 * Math.sin(particle.phase * 2.2);
  }

  if (particle.style === 'sparkle' && burstElapsed < BURST_FADE_START_MS) {
    scale *= 0.85 + 0.3 * Math.sin(particle.phase * 3);
  }

  if (burstElapsed >= BURST_FADE_START_MS) {
    const fadeT = (burstElapsed - BURST_FADE_START_MS) / (BURST_FADE_END_MS - BURST_FADE_START_MS);
    opacity *= 1 - easeOutCubic(Math.min(1, fadeT));
    scale *= 1 - easeOutCubic(Math.min(1, fadeT)) * 0.2;
  }

  if (opacity <= 0.02) return null;

  return {
    x: particle.x,
    y: particle.y,
    scale,
    opacity,
    rotation: particle.rotation,
  };
}

export function drawBurstParticles(
  context: CanvasRenderingContext2D,
  particles: BurstParticle[],
  dpr: number,
  now: number
) {
  for (const particle of particles) {
    const state = particleDrawState(particle, now);
    if (!state) continue;

    context.globalAlpha = state.opacity;

    const emojiCanvas = getEmojiCanvas(particle.emoji);
    const drawSize = particle.fontSize * state.scale * EMOJI_CACHE_SCALE;
    const halfSize = drawSize / 2;
    const radians = (state.rotation * Math.PI) / 180;
    const cos = Math.cos(radians) * dpr;
    const sin = Math.sin(radians) * dpr;

    context.setTransform(cos, sin, -sin, cos, state.x * dpr, state.y * dpr);
    context.drawImage(emojiCanvas, -halfSize, -halfSize, drawSize, drawSize);
  }

  context.globalAlpha = 1;
  context.setTransform(dpr, 0, 0, dpr, 0, 0);
}

export function hasBurstParticles(particles: BurstParticle[], burstId: string): boolean {
  return particles.some((particle) => particle.burstId === burstId);
}
