import * as R from "ramda";
import * as React from "react";
import {
  FlatList,
  FlatListProps,
  ListRenderItemInfo as IListRenderItemInfo,
} from "react-native";
import { toFiniteNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";

import { ChildrenFocusDeactivatorView } from "../ChildrenFocusDeactivatorView";

import { useInitialFocus } from "@applicaster/zapp-react-native-utils/focusManager";

import { noop } from "@applicaster/zapp-react-native-utils/functionUtils";

import { useParentFocus, useCheckItemIdsForUnique } from "./hooks";
import { FocusableListItemWrapper } from "./FocusableListItemWrapper";
import { FocusableScrollView } from "../FocusableScrollView";

const mapIndexed = R.addIndex(R.map);

export type IListRenderItem<ItemT> = (
  info: {
    focused: boolean;
    onLoadFinished: () => void;
    onLoadFailed: () => void;
  } & IListRenderItemInfo<ItemT>
) => React.ReactElement | null;

export const getFocusableId = (parentId, index) =>
  `${parentId}___index:${index}`;

type Item = ZappEntry | ZappUIComponent | any;

export type Props<ItemT> = FlatListProps<ItemT> & {
  id?: number | string;
  horizontal?: boolean;
  loop?: boolean;
  numColumns?: number;
  data: ZappEntry[] | ZappUIComponent[] | any[];
  onListElementFocus?: (
    element: FocusManager.FocusableRef,
    renderItemProps: { item: Item; index: number },
    options: FocusManager.Android.CallbackOptions,
    context: FocusManager.FocusContext
  ) => void;
  onListElementBlur?: (element, renderItemProps, direction) => void;
  onListElementPress?: (element, renderItemProps) => void;
  onListElementPressOut?: (element, renderItemProps) => void;
  onListElementLongPress?: (element, renderItemProps) => void;
  focusableItemProps?: any;
  focused?: boolean;
  initialScrollIndex?: number;
  withStateMemory?: boolean;
  extraChildrenData?: any;
  loadNextData?: () => void;
  initialFocusDirection?: FocusManager.Android.FocusNavigationDirections;
  onAllComponentsLoaded?: () => void;
  omitPropsPropagation?: Array<keyof FlatListProps<any>>;
  renderItem: IListRenderItem<ItemT> | null | undefined;
  disableNextFocusDown?: boolean;
  disableNextFocusUp?: boolean;
  disableNextFocusLeft?: boolean;
  disableNextFocusRight?: boolean;
  useScrollView?: boolean;
} & ParentFocus;

function FocusableListComponent<ItemT>(props: Props<ItemT>, ref) {
  const {
    horizontal,
    onListElementFocus,
    onListElementBlur,
    onListElementPress,
    onListElementPressOut,
    onListElementLongPress,

    disableNextFocusDown,
    disableNextFocusRight,
    disableNextFocusLeft,
    disableNextFocusUp,

    nextFocusDown: parentNextFocusDown,
    nextFocusRight: parentNextFocusRight,
    nextFocusLeft: parentNextFocusLeft,
    nextFocusUp: parentNextFocusUp,

    numColumns,

    focused: parentFocused,

    focusableItemProps,
    loop = false, // only for horizontal layout
    initialScrollIndex = 0,
    withStateMemory = true,
    loadNextData = noop,
    initialFocusDirection = "down",
    data = [],
    // eslint-disable-next-line unused-imports/no-unused-vars
    omitPropsPropagation = [],
    useScrollView = false,
    onScrollToIndexFailed = noop,
  } = props;

  useCheckItemIdsForUnique({ componentId: props.id, items: data });

  const { focused, nextFocusUp, nextFocusRight, nextFocusDown, nextFocusLeft } =
    useParentFocus({
      parentFocused,
      disableNextFocusUp,
      disableNextFocusRight,
      disableNextFocusDown,
      disableNextFocusLeft,
      parentNextFocusUp,
      parentNextFocusRight,
      parentNextFocusDown,
      parentNextFocusLeft,
    });

  const getFocusableEntryId = React.useCallback(
    (_entry: ZappEntry | null, index?: number) => {
      const entry = R.isNil(index) ? _entry : data[index as number];

      return entry ? getFocusableId(props.id, index) : undefined;
    },
    [props.id, data.length]
  );

  const initialFocusOptions = React.useMemo(
    () =>
      withStateMemory
        ? {
            withStateMemory,
            refsList: mapIndexed(getFocusableEntryId)(data),
            initialFocusDirection,
            initialScrollIndex,
          }
        : { initialFocusDirection },
    [getFocusableEntryId, withStateMemory, initialFocusDirection, data.length]
  );

  const updateFocusedIndex = useInitialFocus(
    focused,
    getFocusableEntryId(data[initialScrollIndex], initialScrollIndex),
    initialFocusOptions
  );

  const onFocus = React.useCallback(
    (element, renderArgs, options, context) => {
      const { index } = renderArgs;

      updateFocusedIndex?.(index);
      onListElementFocus?.(element, renderArgs, options, context);
    },
    [onListElementFocus, updateFocusedIndex]
  );

  const getNextFocus = React.useCallback(
    (index) => {
      const itemCount = R.length(data);

      const getFocusableEntryIdForIndex = (entryIndex) =>
        getFocusableEntryId(null, entryIndex);

      const getHorizontalFocus = () => ({
        nextFocusLeft:
          getFocusableEntryIdForIndex(index - 1) ||
          (loop ? getFocusableEntryIdForIndex(itemCount - 1) : nextFocusLeft),
        nextFocusRight:
          getFocusableEntryIdForIndex(index + 1) ||
          (loop ? getFocusableEntryIdForIndex(0) : nextFocusRight),
        nextFocusUp,
        nextFocusDown,
      });

      const getGridFocus = () => {
        const _numColumns = toFiniteNumberWithDefault(1, numColumns);
        const numberOfRows = Math.ceil(itemCount / _numColumns);
        const currentRowIndex = Math.floor(index / _numColumns);
        const isFirstRow = currentRowIndex === 0;
        const isLastRow = currentRowIndex + 1 === numberOfRows;
        const isLeftEdge = index % _numColumns === 0;

        const isRightEdge =
          (index + 1) % _numColumns === 0 || itemCount === index + 1;

        return {
          nextFocusUp: isFirstRow
            ? nextFocusUp
            : getFocusableEntryIdForIndex(index - _numColumns),
          nextFocusDown: isLastRow
            ? nextFocusDown
            : getFocusableEntryIdForIndex(index + _numColumns),
          nextFocusLeft: isLeftEdge
            ? nextFocusLeft
            : getFocusableEntryIdForIndex(index - 1),
          nextFocusRight: isRightEdge
            ? nextFocusRight
            : getFocusableEntryIdForIndex(index + 1),
        };
      };

      const getVerticalFocus = () => ({
        nextFocusLeft,
        nextFocusRight,
        nextFocusUp: getFocusableEntryIdForIndex(index - 1) || nextFocusUp,
        nextFocusDown: getFocusableEntryIdForIndex(index + 1) || nextFocusDown,
      });

      if (horizontal) {
        return getHorizontalFocus();
      } else if (numColumns) {
        return getGridFocus();
      } else {
        return getVerticalFocus();
      }
    },
    [
      data.length,
      getFocusableEntryId,
      nextFocusDown,
      nextFocusUp,
      nextFocusLeft,
      nextFocusRight,
      loop,
      horizontal,
    ]
  );

  const renderItem = React.useCallback(
    ({ item, index }) => {
      const id = getFocusableId(props.id, index);

      return (
        <FocusableListItemWrapper
          {...{
            onFocus,
            onListElementBlur,
            onListElementPress,
            onListElementLongPress,
            onListElementPressOut,
          }}
          renderItem={props.renderItem}
          focusableItemProps={focusableItemProps}
          getNextFocus={getNextFocus}
          id={id}
          item={item}
          index={index}
          extraData={props.extraData}
          extraChildrenData={props.extraChildrenData}
        />
      );
    },
    [
      getNextFocus,
      props.extraChildrenData,
      onFocus,
      props.renderItem,
      data.length,
      onListElementBlur,
      onListElementPress,
      onListElementLongPress,
      onListElementPressOut,
    ]
  );

  const getFlatListProps = React.useCallback(
    R.omit(
      [
        "onListElementFocus",
        "onListElementPress",
        "onListElementPressOut",
        "onListElementLongPress",
        "onListElementBlur",
        "nextFocusDown",
        "nextFocusRight",
        "nextFocusLeft",
        "nextFocusUp",
        "focusableItemProps",
        "renderItem",
        "extraChildrenData",
        "onAllComponentsLoaded",
        "focused",
        "id",
        "initialFocusDirection",
        "withStateMemory",
        "useSequentialLoading",
        "useScrollView",
        "onScrollToIndexFailed",
        ...omitPropsPropagation,
      ],
      R.__
    ),
    [omitPropsPropagation]
  );

  const onEndReached = React.useCallback(
    ({ distanceFromEnd }) => {
      if (distanceFromEnd > 1000) loadNextData();
    },
    [loadNextData]
  );

  const initialNumToRender = toFiniteNumberWithDefault(
    10,
    props.initialNumToRender
  );

  return (
    // @ts-ignore
    <ChildrenFocusDeactivatorView flex={1}>
      {useScrollView ? (
        <FocusableScrollView
          ref={ref}
          {...getFlatListProps(props)}
          onEndReached={onEndReached}
          initialNumToRender={initialNumToRender}
          onScrollToIndexFailed={onScrollToIndexFailed}
          renderItem={renderItem}
          focused={focused}
          data={data}
        />
      ) : (
        <FlatList
          testID="focusable-list-flat-list"
          focusable={false}
          scrollEnabled={false}
          ref={ref}
          {...getFlatListProps(props)}
          renderItem={renderItem}
          onEndReached={onEndReached}
          initialNumToRender={initialNumToRender}
          onScrollToIndexFailed={onScrollToIndexFailed}
        />
      )}
    </ChildrenFocusDeactivatorView>
  );
}

export const FocusableList = React.forwardRef(FocusableListComponent);
