import { computed, shallowReactive, shallowRef, toValue, watch } from 'vue';
import type { ComputedRef, MaybeRef, Ref, ShallowRef, WritableComputedRef } from 'vue';
import { isObject, noop, omit } from '@robonen/stdlib';
import { defaultWindow } from '@/types';
import type { ConfigurableWindow } from '@/types';
import { unrefElement } from '@/composables/component/unrefElement';
import type { MaybeComputedElementRef } from '@/composables/component/unrefElement';
import { useSupported } from '@/composables/utilities/useSupported';
import { useEventListener } from '@/composables/browser/useEventListener';
import { useRafFn } from '@/composables/animation/useRafFn';
import { tryOnMounted } from '@/composables/lifecycle/tryOnMounted';
import { tryOnScopeDispose } from '@/composables/lifecycle/tryOnScopeDispose';

export interface UseAnimateOptions extends KeyframeAnimationOptions, ConfigurableWindow {
  /**
   * Automatically call `play()` once the target element is resolved.
   *
   * @default true
   */
  immediate?: boolean;

  /**
   * Commit the end styling state of the animation to the element when it finishes.
   * Usually paired with the `fill` option.
   *
   * @default false
   */
  commitStyles?: boolean;

  /**
   * Persist the animation so it is not automatically removed by the browser.
   *
   * @default false
   */
  persist?: boolean;

  /**
   * Called once the underlying `Animation` instance has been created.
   */
  onReady?: (animation: Animation) => void;

  /**
   * Called when an error is thrown while controlling the animation.
   *
   * @default noop
   */
  onError?: (error: unknown) => void;
}

export type UseAnimateKeyframes
  = MaybeRef<Keyframe[] | PropertyIndexedKeyframes | null>;

export interface UseAnimateReturn {
  /**
   * Whether the Web Animations API is supported in the current environment
   */
  isSupported: Readonly<Ref<boolean>>;

  /**
   * The underlying `Animation` instance, or `undefined` before it is created
   */
  animate: ShallowRef<Animation | undefined>;

  /**
   * Start or resume the animation
   */
  play: () => void;

  /**
   * Suspend playback of the animation
   */
  pause: () => void;

  /**
   * Reverse the playback direction of the animation
   */
  reverse: () => void;

  /**
   * Seek the animation to the end of its active duration
   */
  finish: () => void;

  /**
   * Abort the animation, clearing its effects
   */
  cancel: () => void;

  /**
   * Whether the animation is currently waiting for an asynchronous operation
   */
  pending: ComputedRef<boolean>;

  /**
   * The current playback state of the animation
   */
  playState: ComputedRef<AnimationPlayState>;

  /**
   * The current replace state of the animation
   */
  replaceState: ComputedRef<AnimationReplaceState>;

  /**
   * The scheduled time at which the animation should begin (writable)
   */
  startTime: WritableComputedRef<CSSNumberish | number | null>;

  /**
   * The current time value of the animation in milliseconds (writable)
   */
  currentTime: WritableComputedRef<CSSNumberish | null>;

  /**
   * The timeline associated with the animation (writable)
   */
  timeline: WritableComputedRef<AnimationTimeline | null>;

  /**
   * The playback rate of the animation (writable)
   */
  playbackRate: WritableComputedRef<number>;
}

interface AnimateStore {
  startTime: CSSNumberish | number | null;
  currentTime: CSSNumberish | null;
  timeline: AnimationTimeline | null;
  playbackRate: number;
  pending: boolean;
  playState: AnimationPlayState;
  replaceState: AnimationReplaceState;
}

const RESERVED_KEYS = [
  'window',
  'immediate',
  'commitStyles',
  'persist',
  'onReady',
  'onError',
] as const;

/**
 * @name useAnimate
 * @category Animation
 * @description Reactive [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)
 * wrapper for a single element. Exposes imperative controls (`play`, `pause`, `reverse`,
 * `finish`, `cancel`) alongside reactive state (`playState`, `currentTime`, `playbackRate`, ...).
 * The reactive state is synced via `requestAnimationFrame` only while the animation is running,
 * so an idle animation costs nothing. SSR-safe: nothing touches the DOM until the element resolves.
 *
 * @param {MaybeComputedElementRef} target Element to animate (reactive ref, getter, or element)
 * @param {UseAnimateKeyframes} keyframes Keyframes to animate, reactive
 * @param {number | UseAnimateOptions} [options] Duration in ms, or full options object
 * @returns {UseAnimateReturn} Support flag, the `Animation` instance, controls, and reactive state
 *
 * @example
 * const el = useTemplateRef<HTMLElement>('el');
 * const { playState, play, pause } = useAnimate(
 *   el,
 *   [{ transform: 'rotate(0)' }, { transform: 'rotate(360deg)' }],
 *   { duration: 1000, iterations: Infinity },
 * );
 *
 * @example
 * // Shorthand: third argument is the duration in milliseconds
 * useAnimate(el, { opacity: [0, 1] }, 500);
 *
 * @since 0.0.14
 */
export function useAnimate(
  target: MaybeComputedElementRef,
  keyframes: UseAnimateKeyframes,
  options?: number | UseAnimateOptions,
): UseAnimateReturn {
  let config: UseAnimateOptions;
  let animateOptions: number | KeyframeAnimationOptions | undefined;

  if (isObject(options)) {
    config = options;
    animateOptions = omit(options, RESERVED_KEYS as unknown as Array<keyof UseAnimateOptions>);
  }
  else {
    config = { duration: options };
    animateOptions = options;
  }

  const {
    immediate = true,
    commitStyles = false,
    persist = false,
    playbackRate: initialPlaybackRate = 1,
    onReady,
    onError = noop,
  } = config;

  // Honor an explicit `window: undefined` (SSR / opt-out) rather than letting a
  // default parameter silently restore `defaultWindow`.
  const window = 'window' in config ? config.window : defaultWindow;

  const isSupported = useSupported(() =>
    Boolean(window) && typeof HTMLElement !== 'undefined' && 'animate' in HTMLElement.prototype);

  const animate = shallowRef<Animation | undefined>(undefined);

  const store = shallowReactive<AnimateStore>({
    startTime: null,
    currentTime: null,
    timeline: null,
    playbackRate: initialPlaybackRate,
    pending: false,
    playState: immediate ? 'idle' : 'paused',
    replaceState: 'active',
  });

  const pending = computed(() => store.pending);
  const playState = computed(() => store.playState);
  const replaceState = computed(() => store.replaceState);

  const startTime = computed<CSSNumberish | number | null>({
    get: () => store.startTime,
    set(value) {
      store.startTime = value;
      if (animate.value)
        animate.value.startTime = value;
    },
  });

  const currentTime = computed<CSSNumberish | null>({
    get: () => store.currentTime,
    set(value) {
      store.currentTime = value;
      if (animate.value) {
        animate.value.currentTime = value;
        syncResume();
      }
    },
  });

  const timeline = computed<AnimationTimeline | null>({
    get: () => store.timeline,
    set(value) {
      store.timeline = value;
      if (animate.value)
        animate.value.timeline = value;
    },
  });

  const playbackRate = computed<number>({
    get: () => store.playbackRate,
    set(value) {
      store.playbackRate = value;
      if (animate.value)
        animate.value.playbackRate = value;
    },
  });

  function update(init?: boolean): void {
    const el = unrefElement(target);
    if (!isSupported.value || !el)
      return;

    if (!animate.value)
      animate.value = (el as HTMLElement).animate(toValue(keyframes), animateOptions);

    if (persist)
      animate.value.persist();

    if (initialPlaybackRate !== 1)
      animate.value.playbackRate = initialPlaybackRate;

    if (init && !immediate)
      animate.value.pause();
    else
      syncResume();

    onReady?.(animate.value);
  }

  function play(): void {
    if (!animate.value) {
      update();
      return;
    }

    try {
      animate.value.play();
      syncResume();
    }
    catch (error) {
      syncPause();
      onError(error);
    }
  }

  function pause(): void {
    try {
      animate.value?.pause();
      syncPause();
    }
    catch (error) {
      onError(error);
    }
  }

  function reverse(): void {
    if (!animate.value)
      update();

    try {
      animate.value?.reverse();
      syncResume();
    }
    catch (error) {
      syncPause();
      onError(error);
    }
  }

  function finish(): void {
    try {
      animate.value?.finish();
      syncPause();
    }
    catch (error) {
      onError(error);
    }
  }

  function cancel(): void {
    try {
      animate.value?.cancel();
      syncPause();
    }
    catch (error) {
      onError(error);
    }
  }

  // Sync the reactive store from the live Animation on every frame. The loop is
  // paused by default and only resumed while the animation is actually playing,
  // so an idle (or finished) animation incurs zero per-frame cost.
  const { resume: resumeRaf, pause: pauseRaf } = useRafFn(() => {
    const a = animate.value;
    if (!a)
      return;

    store.pending = a.pending;
    store.playState = a.playState;
    store.replaceState = a.replaceState;
    store.startTime = a.startTime;
    store.currentTime = a.currentTime;
    store.timeline = a.timeline;
    store.playbackRate = a.playbackRate;
  }, { immediate: false, window });

  function syncResume(): void {
    if (isSupported.value)
      resumeRaf();
  }

  function syncPause(): void {
    // Defer the stop by one frame so the final state (e.g. 'finished') is captured
    // before the loop halts.
    if (isSupported.value && window)
      window.requestAnimationFrame(pauseRaf);
  }

  watch(() => unrefElement(target), (el) => {
    if (el)
      update(true);
    else
      animate.value = undefined;
  });

  watch(() => keyframes, (value) => {
    if (!animate.value)
      return;

    update();

    const el = unrefElement(target);
    if (el && typeof KeyframeEffect !== 'undefined')
      animate.value.effect = new KeyframeEffect(el as HTMLElement, toValue(value), animateOptions);
  }, { deep: true });

  const listenerOptions = { passive: true };
  useEventListener(animate, ['cancel', 'finish', 'remove'], syncPause, listenerOptions);
  useEventListener(animate, 'finish', () => {
    if (commitStyles)
      animate.value?.commitStyles();
  }, listenerOptions);

  tryOnMounted(() => update(true), { sync: false });

  tryOnScopeDispose(cancel);

  return {
    isSupported,
    animate,

    play,
    pause,
    reverse,
    finish,
    cancel,

    pending,
    playState,
    replaceState,
    startTime,
    currentTime,
    timeline,
    playbackRate,
  };
}
