import { cubicOut } from 'svelte/easing';
import { fade, slide, type TransitionConfig } from 'svelte/transition';

const entering = new Set<string>();
const leaving = new Set<string>();
let busyUntil = 0;

export function motionDuration(ms: number): number {
  return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : ms;
}

export function motionActive(): boolean {
  return performance.now() < busyUntil;
}

function noteBusy(ms: number): void {
  busyUntil = Math.max(busyUntil, performance.now() + ms + 60);
}

export function setRowMotion(enter: Iterable<string>, leave: Iterable<string>): void {
  entering.clear();
  leaving.clear();
  for (const key of enter) entering.add(key);
  for (const key of leave) leaving.add(key);
}

function rowKey(node: Element): string {
  const { item, row } = (node as HTMLElement).dataset;
  return item ?? row ?? '';
}

/** Sub-pixel sizes from many rows easing out together made the pinned bottom flip between roundings. */
function wholePixels(css: string): string {
  return css.replace(/(-?\d*\.?\d+)px/g, (_, n: string) => `${Math.round(Number(n))}px`);
}

function reveal(node: Element, ms: number, axis: 'x' | 'y' = 'y'): TransitionConfig {
  const duration = motionDuration(ms);
  if (duration === 0) return { duration: 0 };
  noteBusy(duration);
  const base = slide(node, { duration, easing: cubicOut, axis });
  return { duration, easing: cubicOut, css: (t, u) => `${wholePixels(base.css?.(t, u) ?? '')};opacity: ${t};` };
}

export function rowIn(node: Element): TransitionConfig {
  return entering.has(rowKey(node)) ? reveal(node, 200) : { duration: 0 };
}

export function rowOut(node: Element): TransitionConfig {
  return leaving.has(rowKey(node)) ? reveal(node, 160) : { duration: 0 };
}

export function expand(node: Element, params: { axis?: 'x' | 'y' } = {}): TransitionConfig {
  return reveal(node, 170, params.axis);
}

export function dissolve(node: Element): TransitionConfig {
  const duration = motionDuration(150);
  if (duration > 0) noteBusy(duration);
  return fade(node, { duration });
}

const settling = new WeakSet<HTMLElement>();

export function settleHeight(node: HTMLElement, from: number): void {
  const duration = motionDuration(180);
  const to = node.offsetHeight;
  if (duration === 0 || Math.abs(to - from) < 2 || settling.has(node) || node.getAnimations({ subtree: true }).length > 0) return;
  noteBusy(duration);
  settling.add(node);
  const start = performance.now();
  node.style.overflow = 'hidden';
  const step = (now: number) => {
    const t = Math.min(1, (now - start) / duration);
    if (t < 1) {
      node.style.height = `${Math.round(from + (to - from) * cubicOut(t))}px`;
      requestAnimationFrame(step);
    } else {
      node.style.height = '';
      node.style.overflow = '';
      settling.delete(node);
    }
  };
  node.style.height = `${Math.round(from)}px`;
  requestAnimationFrame(step);
}
