import React from 'react';
import type { Animated } from 'react-native';
import FAB from '../FAB';
import type { ActionGroupHandles, ActionGroupProps } from '../FAB/ActionGroup';
import ActionGroup from '../FAB/ActionGroup';
import type { FABHandles, FABProps } from '../FAB/FAB';

const LAST_BREAKPOINT = 100;
const MIDDLE_BREAKPOINT = 250;
const MAX_ANIMATABLE_SCROLL_DISTANCE = 400;
const REF_ACTIONS_BY_COMPONENT = {
  FAB: {
    show: 'show',
    hide: 'hide',
    collapse: 'collapse',
  },
  ActionGroup: {
    show: 'showFAB',
    hide: 'hideFAB',
    collapse: 'collapseFAB',
  },
} as const;

interface AnimatedFABProps {
  fabProps: FABProps | ActionGroupProps;
  contentOffsetY: Animated.Value;
  contentHeight: Animated.Value;
  layoutHeight: Animated.Value;
}

const AnimatedFAB = ({
  fabProps,
  contentOffsetY,
  contentHeight,
  layoutHeight,
}: AnimatedFABProps) => {
  const component = 'items' in fabProps ? 'ActionGroup' : 'FAB';
  const ref = React.useRef<FABHandles & ActionGroupHandles>(null);
  const currentContentHeight = React.useRef(0);
  const currentLayoutHeight = React.useRef(0);

  /** fabState is used to avoid calling duplicated animations. */
  const fabState = React.useRef<'show' | 'hide' | 'collapse'>('show');

  /** remainingScrollOffset determines whether to animate the FAB. */
  const remainingScrollOffset = React.useRef(MAX_ANIMATABLE_SCROLL_DISTANCE);

  /** currentScrollDirection is used to determine the scroll direction. */
  const currentScrollDirection = React.useRef<'up' | 'down'>('down');

  /** lastScrollY is the scrollY from the preview scroll event. */
  const lastScrollY = React.useRef(0);

  const animateFab = React.useCallback(
    (newState: 'show' | 'hide' | 'collapse') => {
      if (fabState.current !== newState) {
        if (newState === 'show') {
          ref.current?.[REF_ACTIONS_BY_COMPONENT[component].show]();
        } else if (newState === 'hide') {
          ref.current?.[REF_ACTIONS_BY_COMPONENT[component].hide]();
        } else {
          ref.current?.[REF_ACTIONS_BY_COMPONENT[component].collapse]();
        }
        fabState.current = newState;
      }
    },
    [component]
  );

  React.useEffect(() => {
    contentHeight.addListener(({ value }) => {
      if (value > 0 && value !== currentContentHeight.current) {
        currentContentHeight.current = value;
      }
    });

    layoutHeight.addListener(({ value }) => {
      if (value > 0 && value !== currentLayoutHeight.current) {
        currentLayoutHeight.current = value;
      }
    });

    // Listen to ScrollView's contentOffsetY value
    contentOffsetY.addListener(({ value }) => {
      if (
        value < 0 ||
        // Prevent calling the function if the scroll is not significant
        (value > 0 && Math.abs(value - lastScrollY.current) < 5)
      ) {
        return;
      }

      // Scroll up to top, bouncing included.
      if (value === 0 && lastScrollY.current !== 0) {
        animateFab('show');
      }

      const newScrollDirection = value >= lastScrollY.current ? 'down' : 'up';

      if (newScrollDirection !== currentScrollDirection.current) {
        // If scroll direction changes, reset all values
        currentScrollDirection.current = newScrollDirection;
        remainingScrollOffset.current = MAX_ANIMATABLE_SCROLL_DISTANCE;
      }

      const hasReachedBottom =
        value + currentLayoutHeight.current >= currentContentHeight.current;

      // Scroll down to bottom, bouncing included.
      if (hasReachedBottom) {
        animateFab('hide');
        return;
      }

      if (remainingScrollOffset.current) {
        const offsetDiff = Math.round(
          Math.max(Math.abs(value - lastScrollY.current), 0)
        );

        const newRemainingScrollOffset = Math.max(
          remainingScrollOffset.current - offsetDiff,
          0
        );

        if (newRemainingScrollOffset <= LAST_BREAKPOINT) {
          animateFab(
            currentScrollDirection.current === 'down' ? 'hide' : 'show'
          );
        } else if (newRemainingScrollOffset <= MIDDLE_BREAKPOINT) {
          animateFab('collapse');
        }

        remainingScrollOffset.current = newRemainingScrollOffset;
      }

      lastScrollY.current = value;
    });

    return () => {
      contentOffsetY.removeAllListeners();
      contentHeight.removeAllListeners();
      layoutHeight.removeAllListeners();
    };
  }, [contentHeight, contentOffsetY, layoutHeight]);

  return component === 'FAB' ? (
    <FAB ref={ref} {...(fabProps as FABProps)} />
  ) : (
    <ActionGroup ref={ref} {...(fabProps as ActionGroupHandles)} />
  );
};

export default AnimatedFAB;
