/**
 * MoreminiMore - Animation Library
 * Reusable scroll/load animations. Lightweight, no dependencies.
 *
 * Usage in any .astro file:
 *   <script>import { initAnimations } from '../lib/animations';
 *   document.addEventListener('DOMContentLoaded', initAnimations);</script>
 *
 * Or for per-page targeted init:
 *   <script>import { revealOnScroll, counterUp } from '../lib/animations';
 *   revealOnScroll('.reveal');</script>
 */

/* ------------------------------------------------------------------ */
/*  TYPES                                                              */
/* ------------------------------------------------------------------ */

interface RevealOptions {
  threshold?: number;
  rootMargin?: string;
  once?: boolean;
  delayMs?: number;
}

interface CounterOptions {
  duration?: number;
  easing?: (t: number) => number;
}

/* ------------------------------------------------------------------ */
/*  ENTRANCE ANIMATIONS                                                */
/* ------------------------------------------------------------------ */

/**
 * Add .reveal class to any element you want to fade-in-up on scroll.
 * Default: trigger at 15% visibility, slide up 30px, 700ms.
 */
export function revealOnScroll(
  selector: string | string[] = '.reveal',
  options: RevealOptions = {}
): void {
  if (typeof window === 'undefined') return;
  const els = collectElements(selector);
  if (els.length === 0) return;

  const opts: Required<RevealOptions> = {
    threshold: options.threshold ?? 0,
    rootMargin: options.rootMargin ?? '0px 0px 100px 0px',
    once: options.once ?? true,
    delayMs: options.delayMs ?? 0,
  };

  // SSR-safe fallback: if IntersectionObserver is unavailable, show everything.
  if (typeof IntersectionObserver === 'undefined') {
    els.forEach((el) => el.classList.add('revealed'));
    return;
  }

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const el = entry.target as HTMLElement;
          setTimeout(() => el.classList.add('revealed'), opts.delayMs);
          if (opts.once) observer.unobserve(el);
        } else if (!opts.once) {
          entry.target.classList.remove('revealed');
        }
      });
    },
    { threshold: opts.threshold, rootMargin: opts.rootMargin }
  );

  const vh = window.innerHeight || document.documentElement.clientHeight;
  els.forEach((el) => {
    // FALLBACK: if already in view on load, apply immediately.
    const r = el.getBoundingClientRect();
    if (r.top < vh && r.bottom > 0) {
      setTimeout(() => el.classList.add('revealed'), opts.delayMs);
      if (opts.once) return; // don't observe
    }
    observer.observe(el);
  });
}

/**
 * Stagger-children reveal — children of the matched container fade in sequentially.
 * Reads --stagger CSS var (in ms) from each child, or uses 100ms default.
 */
export function staggerReveal(
  containerSelector: string,
  childSelector: string = ':scope > *',
  baseDelayMs: number = 0
): void {
  if (typeof window === 'undefined') return;
  const containers = collectElements(containerSelector);
  if (containers.length === 0) return;

  // Helper: apply reveal to one container's children.
  const apply = (container: HTMLElement) => {
    const children = container.querySelectorAll<HTMLElement>(childSelector);
    children.forEach((child, i) => {
      const stagger = Number(child.dataset.stagger) || i * 100;
      setTimeout(() => child.classList.add('revealed'), baseDelayMs + stagger);
    });
  };

  if (typeof IntersectionObserver === 'undefined') {
    containers.forEach(apply);
    return;
  }

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          apply(entry.target as HTMLElement);
          observer.unobserve(entry.target);
        }
      });
    },
    { threshold: 0, rootMargin: '0px 0px 100px 0px' }
  );

  containers.forEach((c) => {
    // FALLBACK: if the container is already in view on load, apply immediately.
    // IO can miss elements that are in viewport at registration time on some browsers.
    const r = c.getBoundingClientRect();
    const vh = window.innerHeight || document.documentElement.clientHeight;
    if (r.top < vh && r.bottom > 0) {
      apply(c);
      return; // do not observe — already revealed
    }
    observer.observe(c);
  });
}

/* ------------------------------------------------------------------ */
/*  KINETIC TYPOGRAPHY                                                 */
/* ------------------------------------------------------------------ */

/**
 * Reveal each word of the matched headline with a staggered translate-up.
 * Wraps each word in a .word-wrapper if not already, then animates.
 */
export function kineticHeadline(selector: string = '.kinetic-title'): void {
  if (typeof window === 'undefined') return;
  const headings = collectElements(selector);
  if (headings.length === 0) return;

  headings.forEach((heading) => {
    // Skip if already processed
    if (heading.dataset.kineticReady === 'true') return;
    heading.dataset.kineticReady = 'true';

    // Skip if text is empty / pure whitespace
    if (!heading.textContent?.trim()) return;

    const text = heading.textContent;
    const words = text.split(/(\s+)/); // keep whitespace
    heading.innerHTML = '';

    let wordIndex = 0;
    words.forEach((segment) => {
      if (/^\s+$/.test(segment)) {
        heading.appendChild(document.createTextNode(segment));
      } else {
        const wrapper = document.createElement('span');
        wrapper.className = 'word-wrapper';
        const inner = document.createElement('span');
        inner.className = 'word';
        inner.textContent = segment;
        inner.style.setProperty('--delay', `${0.3 + wordIndex * 0.08}s`);
        wrapper.appendChild(inner);
        heading.appendChild(wrapper);
        wordIndex++;
      }
    });
  });
}

/* ------------------------------------------------------------------ */
/*  COUNTER ANIMATION                                                  */
/* ------------------------------------------------------------------ */

/**
 * Animate any element with a number to count up from 0 (or data-from) to the
 * number in its text. Triggered when the element scrolls into view.
 *
 * Element: <span class="counter" data-from="0" data-suffix="+">50+</span>
 */
export function counterUp(
  selector: string = '.counter',
  options: CounterOptions = {}
): void {
  if (typeof window === 'undefined') return;
  const els = collectElements(selector);
  if (els.length === 0) return;

  const duration = options.duration ?? 1600;
  const ease = options.easing ?? easeOutCubic;

  if (typeof IntersectionObserver === 'undefined') {
    els.forEach((el) => animateCounter(el as HTMLElement, duration, ease));
    return;
  }

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          animateCounter(entry.target as HTMLElement, duration, ease);
          observer.unobserve(entry.target);
        }
      });
    },
    { threshold: 0, rootMargin: '0px 0px 100px 0px' }
  );

  els.forEach((el) => {
    // FALLBACK: if already in view on load, animate immediately.
    const r = el.getBoundingClientRect();
    const vh = window.innerHeight || document.documentElement.clientHeight;
    if (r.top < vh && r.bottom > 0) {
      animateCounter(el as HTMLElement, duration, ease);
      return;
    }
    observer.observe(el);
  });
}

function animateCounter(
  el: HTMLElement,
  duration: number,
  ease: (t: number) => number
): void {
  const raw = el.textContent?.trim() ?? '0';
  const fromStr = el.dataset.from;
  const from = fromStr ? parseFloat(fromStr) : 0;
  // Extract leading number, ignore trailing chars (e.g. "50+", "5+", "5 ปี", "24/7")
  const match = raw.match(/^([\d,.]+)(.*)$/);
  if (!match) return;
  const targetStr = match[1].replace(/,/g, '');
  const target = parseFloat(targetStr);
  const suffix = match[2] || '';
  const prefix = el.dataset.prefix || '';
  if (Number.isNaN(target)) return;

  const start = performance.now();
  const tick = (now: number) => {
    const elapsed = now - start;
    const progress = Math.min(elapsed / duration, 1);
    const value = from + (target - from) * ease(progress);
    // Preserve any decimal formatting from original
    const decimals = (targetStr.split('.')[1] || '').length;
    el.textContent = `${prefix}${formatNumber(value, decimals)}${suffix}`;
    if (progress < 1) requestAnimationFrame(tick);
  };
  requestAnimationFrame(tick);
}

function formatNumber(n: number, decimals: number): string {
  return n.toFixed(decimals);
}

function easeOutCubic(t: number): number {
  return 1 - Math.pow(1 - t, 3);
}

/* ------------------------------------------------------------------ */
/*  MAGNETIC BUTTON                                                    */
/* ------------------------------------------------------------------ */

/**
 * Subtle hover-tracking effect for CTAs. Btn slightly tracks cursor position.
 * Add `data-magnetic` attribute to any button.
 */
export function magneticButtons(selector: string = '[data-magnetic]'): void {
  if (typeof window === 'undefined') return;
  const btns = collectElements(selector);
  if (btns.length === 0) return;

  btns.forEach((btn) => {
    if ((btn as HTMLElement).dataset.magneticReady === 'true') return;
    (btn as HTMLElement).dataset.magneticReady = 'true';

    const strength = 0.25;
    btn.addEventListener('mousemove', (e: MouseEvent) => {
      const rect = btn.getBoundingClientRect();
      const x = e.clientX - rect.left - rect.width / 2;
      const y = e.clientY - rect.top - rect.height / 2;
      (btn as HTMLElement).style.transform = `translate(${x * strength}px, ${y * strength}px)`;
    });
    btn.addEventListener('mouseleave', () => {
      (btn as HTMLElement).style.transform = '';
    });
  });
}

/* ------------------------------------------------------------------ */
/*  PARALLAX BACKGROUND                                                 */
/* ------------------------------------------------------------------ */

/**
 * Apply a subtle parallax translateY to elements based on scroll position.
 * Add `data-parallax="0.3"` (0-1) to set speed (default 0.3).
 * Uses requestAnimationFrame + transform for 60fps.
 */
export function parallaxBackgrounds(selector: string = '[data-parallax]'): void {
  if (typeof window === 'undefined') return;
  const els = Array.from(document.querySelectorAll<HTMLElement>(selector));
  if (els.length === 0) return;

  // Use rAF for smooth 60fps
  let ticking = false;
  const update = () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const scrollY = window.scrollY;
      els.forEach((el) => {
        const rect = el.getBoundingClientRect();
        const speed = parseFloat(el.dataset.parallax || '0.3');
        // Only update if visible
        if (rect.bottom > -200 && rect.top < window.innerHeight + 200) {
          const offset = (rect.top - window.innerHeight) * speed;
          el.style.transform = `translate3d(0, ${offset}px, 0)`;
        }
      });
      ticking = false;
    });
  };
  window.addEventListener('scroll', update, { passive: true });
  update(); // initial
}

/* ------------------------------------------------------------------ */
/*  PARALLAX SCROLL PROGRESS                                           */
/* ------------------------------------------------------------------ */

/**
 * Update CSS var --scroll-progress (0-100) on the document.
 * Pair with a CSS rule like:
 *   .scroll-indicator { height: calc(var(--scroll-progress) * 1px); }
 */
export function scrollProgress(selector: string = '.scroll-indicator'): void {
  if (typeof window === 'undefined') return;
  const els = collectElements(selector);
  if (els.length === 0) return;
  const update = () => {
    const docHeight = document.documentElement.scrollHeight - window.innerHeight;
    const progress = docHeight > 0 ? (window.scrollY / docHeight) * 100 : 0;
    els.forEach((el) => {
      el.style.setProperty('--scroll-progress', `${Math.min(progress, 100)}%`);
    });
  };
  window.addEventListener('scroll', update, { passive: true });
  update();
}

/* ------------------------------------------------------------------ */
/*  ENTRY POINT — call from page script                                */
/* ------------------------------------------------------------------ */

/**
 * Initialize all animations on the current page. Pass an options object
 * to opt-out of any animation.
 */
export interface AnimationOptions {
  reveal?: boolean;
  stagger?: boolean;
  kinetic?: boolean;
  counters?: boolean;
  magnetic?: boolean;
  parallax?: boolean;
  scrollProgress?: boolean;
}

export function initAnimations(
  options: AnimationOptions = {}
): void {
  const opts: Required<AnimationOptions> = {
    reveal: true,
    stagger: true,
    kinetic: true,
    counters: true,
    magnetic: true,
    parallax: true,
    scrollProgress: true,
    ...options,
  };

  // Wait for next tick so the DOM is fully painted
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => initAnimations(options));
    return;
  }

  if (opts.reveal) revealOnScroll();
  if (opts.stagger) staggerReveal('.stagger-children');
  if (opts.kinetic) kineticHeadline();
  if (opts.counters) counterUp();
  if (opts.magnetic) magneticButtons();
  if (opts.parallax) parallaxBackgrounds();
  if (opts.scrollProgress) scrollProgress();
}

/* ------------------------------------------------------------------ */
/*  HELPERS                                                            */
/* ------------------------------------------------------------------ */

function collectElements(selector: string | string[]): HTMLElement[] {
  const sels = Array.isArray(selector) ? selector : [selector];
  const out: HTMLElement[] = [];
  sels.forEach((s) => {
    document.querySelectorAll<HTMLElement>(s).forEach((el) => out.push(el));
  });
  return out;
}
