/**
 * Based on https://github.com/roginfarrer/collapsed/blob/main/packages/react-collapsed/src/index.ts
 * Simplified for our usecase.
 */

import {
  useState,
  useRef,
  useEffect,
  useLayoutEffect as useReactLayoutEffect,
  useCallback,
} from 'react';

import type React from 'react';
import type { RefObject } from 'react';

const useLayoutEffect = typeof window === 'undefined' ? useEffect : useReactLayoutEffect;

export interface UseCollapseProps {
  isExpanded?: boolean;
  collapseElRef: React.RefObject<HTMLElement | null>;
  onTransitionStateChange?: (
    state:
      | 'collapseEnd'
      | 'expandEnd'
      | 'collapseStart'
      | 'expandStart'
      | 'collapsing'
      | 'expanding',
  ) => void;
}

const easing = 'cubic-bezier(0.4, 0, 0.2, 1)';
const collapsedHeight = `0px`;

export function useCollapse({
  isExpanded,
  collapseElRef,
  onTransitionStateChange: configOnTransitionStateChange = () => {},
}: UseCollapseProps) {
  const prevExpanded = useRef(isExpanded);
  const [isAnimating, setIsAnimating] = useState(false);
  const onTransitionStateChange = useEvent(configOnTransitionStateChange);

  // Animation frames
  const frameId = useRef<number | undefined>(undefined);
  const endFrameId = useRef<{ id?: number } | undefined>(undefined);

  useLayoutEffect(() => {
    const collapse = collapseElRef.current;
    if (!collapse) return;

    if (isExpanded === prevExpanded.current) return;
    prevExpanded.current = isExpanded;

    function getDuration(height: number | string) {
      return getAutoHeightDuration(height);
    }

    const getTransitionStyles = (height: number | string) =>
      `height ${getDuration(height)}ms ${easing}`;

    const setTransitionEndTimeout = (duration: number) => {
      function endTransition() {
        if (isExpanded) {
          setStyles(collapse, {
            height: '',
            overflow: '',
            transition: '',
            display: '',
            pointerEvents: 'auto',
          });
          onTransitionStateChange('expandEnd');
        } else {
          setStyles(collapse, { transition: '', pointerEvents: '' });
          onTransitionStateChange('collapseEnd');
        }
        setIsAnimating(false);
      }

      if (endFrameId.current) {
        clearAnimationTimeout(endFrameId.current);
      }
      endFrameId.current = setAnimationTimeout(endTransition, duration);
    };

    setIsAnimating(true);

    if (isExpanded) {
      frameId.current = requestAnimationFrame(() => {
        onTransitionStateChange('expandStart');
        setStyles(collapse, {
          display: 'block',
          overflow: 'hidden',
          pointerEvents: 'none',
          height: collapsedHeight,
        });
        frameId.current = requestAnimationFrame(() => {
          onTransitionStateChange('expanding');
          const height = getElementHeight(collapseElRef);
          setTransitionEndTimeout(getDuration(height));

          if (collapseElRef.current) {
            // Order is important! Setting directly.
            collapseElRef.current.style.transition = getTransitionStyles(height);
            collapseElRef.current.style.height = `${height}px`;
          }
        });
      });
    } else {
      frameId.current = requestAnimationFrame(() => {
        onTransitionStateChange('collapseStart');
        const height = getElementHeight(collapseElRef);
        setTransitionEndTimeout(getDuration(height));
        setStyles(collapse, {
          transition: getTransitionStyles(height),
          height: `${height}px`,
          pointerEvents: 'none',
        });
        frameId.current = requestAnimationFrame(() => {
          onTransitionStateChange('collapsing');
          setStyles(collapse, {
            height: collapsedHeight,
            overflow: 'hidden',
          });
        });
      });
    }

    return () => {
      if (frameId.current) cancelAnimationFrame(frameId.current);
      if (endFrameId.current) clearAnimationTimeout(endFrameId.current);
    };
  }, [isExpanded, collapseElRef, onTransitionStateChange]);

  return {
    isExpanded,
    style:
      !isAnimating && !isExpanded
        ? {
            // collapsed and not animating
            display: 'none',
            height: collapsedHeight,
            overflow: 'hidden',
          }
        : {},
  };
}

// https://github.com/mui-org/material-ui/blob/da362266f7c137bf671d7e8c44c84ad5cfc0e9e2/packages/material-ui/src/styles/transitions.js#L89-L98
export function getAutoHeightDuration(height: number | string): number {
  if (!height || typeof height === 'string') {
    return 0;
  }

  const constant = height / 36;
  return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10);
}

export function getElementHeight(el: RefObject<HTMLElement | null>): number {
  // scrollHeight will give us the height of the element, even if it's not visible.
  // clientHeight, offsetHeight, nor getBoundingClientRect().height will do so
  return el.current?.scrollHeight ?? 0;
}

export function setAnimationTimeout(callback: () => void, timeout: number) {
  const startTime = performance.now();
  const frame: { id?: number } = {};

  function call() {
    frame.id = requestAnimationFrame((now) => {
      if (now - startTime > timeout) {
        callback();
      } else {
        call();
      }
    });
  }

  call();
  return frame;
}

export function clearAnimationTimeout(frame: { id?: number }) {
  if (frame.id) cancelAnimationFrame(frame.id);
}

function setStyles<T extends Partial<CSSStyleDeclaration>>(
  target: HTMLElement | null,
  newStyles: T,
) {
  if (!target) return;
  for (const property in newStyles) {
    const value = newStyles[property];
    if (value) {
      target.style[property] = value;
    } else {
      target.style.removeProperty(property);
    }
  }
}
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export function useEvent<T extends (...args: any[]) => any>(callback?: T): T {
  const ref = useRef<T | undefined>(callback);

  useEffect(() => {
    ref.current = callback;
  });

  return useCallback(((...args: Parameters<T>) => ref.current?.(...args)) as T, []);
}
