import * as React from "react";
import * as R from "ramda";
import { FlatListProps, ViewToken } from "react-native";

type OnLoadFinished = (index: number) => () => void;

type SequentialLoaderOutput = {
  isReadyToRender: (index: number) => boolean;
  onLoadFinished: OnLoadFinished;
  onLoadFailed: OnLoadFinished;
  onViewableItemsChanged: FlatListProps<any>["onViewableItemsChanged"];
  isRendered: (index: number) => boolean;
  allLoaded: boolean;
};

type Action = {
  index: number;
};

type ViewTokens = Array<ViewToken>;

type RenderingState = Array<boolean>;

const setTrueByIndex = (index, state) => R.set(R.lensIndex(index), true)(state);

const isInViewport = (index, viewableItems) =>
  R.any(R.propEq("index", index))(viewableItems);

const reducer = (state: RenderingState, action: Action): RenderingState => {
  if (state?.[action.index]) {
    return state;
  }

  return setTrueByIndex(action.index, state);
};

const getIndexOfFirstNotRendered = R.findIndex(R.equals(false));

const isTrue = (value) => value === true;

export const useSequentialLoader = (
  useSequentialLoading: boolean,
  numberOfItems: number
): SequentialLoaderOutput => {
  const [viewableItems, setViewableItems] = React.useState<ViewTokens>([]);

  const [componentsRenderingState, dispatch] = React.useReducer(
    reducer,
    new Array(numberOfItems).fill(false)
  );

  const isRendered = React.useCallback(
    (index: number): boolean => {
      if (!useSequentialLoading || index === 0) {
        return true;
      }

      return componentsRenderingState[index];
    },
    [componentsRenderingState]
  );

  const isReadyToRender = React.useCallback(
    (index: number): boolean => {
      if (!useSequentialLoading || index === 0) {
        return true;
      }

      if (R.isEmpty(viewableItems)) {
        return false;
      }

      const isRendered = componentsRenderingState[index];

      if (isRendered) {
        return true;
      }

      const previousLoaded =
        index === 0 ? true : componentsRenderingState[index - 1];

      const isNthNotRendered = (nth: number) =>
        index ===
        getIndexOfFirstNotRendered(componentsRenderingState) - 1 + nth;

      const isNthComponentOutsideViewport = (nth: number) =>
        index === R.last(viewableItems)?.index + nth;

      const inViewport = isInViewport(index, viewableItems);

      if (isNthNotRendered(1) && previousLoaded) {
        return (
          inViewport ||
          isNthComponentOutsideViewport(1) ||
          isNthComponentOutsideViewport(2) ||
          isNthComponentOutsideViewport(3)
        );
      }

      return inViewport && previousLoaded;
    },
    [viewableItems, componentsRenderingState]
  );

  const onLoadFinished = React.useCallback(
    (index: number) => () => {
      if (useSequentialLoading) {
        dispatch({ index });
      }
    },
    []
  );

  const onViewableItemsChanged = React.useCallback(({ viewableItems }) => {
    if (useSequentialLoading) {
      setViewableItems(viewableItems);
    }
  }, []);

  const allLoaded = React.useMemo((): boolean => {
    if (useSequentialLoading) {
      return R.all(isTrue, componentsRenderingState);
    }

    return true;
  }, [componentsRenderingState]);

  return {
    isReadyToRender,
    onLoadFinished,
    onViewableItemsChanged,
    isRendered,
    allLoaded,
    onLoadFailed: onLoadFinished,
  };
};
