import * as R from "ramda";
import * as React from "react";
import { View, ScrollView, FlatListProps, ScrollViewProps } from "react-native";
import { FocusableChild } from "./FocusableChild";
import { Focusable } from "@applicaster/zapp-react-native-ui-components/Components/Focusable";
import { ChildrenFocusDeactivatorView } from "../ChildrenFocusDeactivatorView";
import { useSequentialLoader } from "@applicaster/zapp-react-native-utils/reactHooks/flatList";

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

export type Props = ScrollViewProps & {
  id?: number | string;
  horizontal?: boolean;
  loop?: boolean;
  numColumns?: number;
  data: ZappEntry[] | any[];
  useSequentialLoading?: boolean;
  onListElementFocus?: (any, RenderItemProps, Direction) => void;
  onListElementBlur?: (any, RenderItemProps, Direction) => void;
  onListElementPress?: (any, RenderItemProps) => void;
  onListElementPressOut?: (any, RenderItemProps) => void;
  onListElementLongPress?: (any, 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: (info: {
    item: ZappUIComponent;
    index: number;
    focused: boolean;
    parentFocus: ParentFocus;
  }) => React.ReactElement;
} & ParentFocus;

type State = boolean[];
type Action = { index?: number; isFocusable: boolean; type?: string };

const focusableStateReducer = (
  state: State,
  { index = null, isFocusable, type = null }: Action
) => {
  if (state[index] === isFocusable) {
    return state;
  }

  if (type === "push") {
    return [...state, isFocusable];
  }

  return R.set(R.lensIndex(index), isFocusable)(state);
};

const extendScrollViewRef = (_ref, childCompsMeasurementsMap) => {
  const scrollTo = ({ y }) => {
    _ref.getScrollResponder().setState({ y });
    _ref.scrollTo({ y });
  };

  _ref.scrollToOffset = (params) => {
    const { offset } = params;

    const state = _ref.getScrollResponder()?.state;

    scrollTo({ y: state?.y ?? 0 + offset - 200 });
  };

  _ref.scrollToIndex = (params) => {
    const { index, viewOffset } = params;

    const itemByIndex = R.find(R.propEq("index", index))(
      R.values(childCompsMeasurementsMap)
    );

    if (itemByIndex) {
      scrollTo({ y: itemByIndex.layout.y - viewOffset });
    }
  };
};

function FocusableScrollViewComponent(props: Props, ref) {
  const {
    horizontal,
    onListElementFocus,
    onListElementBlur,
    onListElementPress,
    onListElementPressOut,
    onListElementLongPress,
    nextFocusDown,
    nextFocusRight,
    nextFocusLeft,
    nextFocusUp,
    focusableItemProps,
    focused,
    loop = false, // only for horizontal layout
    initialScrollIndex = 0,
    withStateMemory = true,
    initialFocusDirection,
    useSequentialLoading = false,
    onAllComponentsLoaded,
  } = props;

  const data = props.data || [];

  const {
    isRendered,
    isReadyToRender,
    onLoadFinished,
    onLoadFailed,
    allLoaded,
  } = useSequentialLoader(useSequentialLoading, data.length);

  const [focusableState, dispatchFocusableState] = React.useReducer<
    React.Reducer<State, Action>
  >(focusableStateReducer, data.map(isFocusable));

  const setIsFocusable = React.useCallback(
    (index) => (isFocusable) => {
      dispatchFocusableState({ index, isFocusable });
    },
    []
  );

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

      return entry ? `${props.id}--${entry?.id}` : undefined;
    },
    [props.id, data.length]
  );

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

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

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

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

  React.useEffect(() => {
    if (allLoaded) {
      onAllComponentsLoaded?.();
    }
  }, [allLoaded, onAllComponentsLoaded]);

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

      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 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 shouldDisableFocus = (index: number) => !focusableState?.[index];

  /**
   * Checks if the amount of focusable items have changed and dispatches push event for each missing item
   * Updates the focusableState to properly reflect the number of focusable items
   * Prior to this focusableState array was missing items on refresh
   */
  const checkIfFocusableStateNeedsUpdate = (): void => {
    if (data.length !== focusableState.length) {
      const TYPE = "push";
      const diff = data.length - focusableState.length;

      for (let i = diff; i <= data.length; i++) {
        dispatchFocusableState({
          isFocusable: isFocusable(props?.data?.[i]),
          type: TYPE,
        });
      }
    }
  };

  React.useEffect(() => {
    checkIfFocusableStateNeedsUpdate();
  }, [data.length]);

  const childCompsMeasurementsMap = React.useRef({});

  const renderItem = React.useCallback(
    (item, index) => {
      const renderArgs = { item, index };
      const id = `${props.id}--${renderArgs?.item?.id}`;
      const nextFocus = getNextFocus(renderArgs?.index);

      const onItemLayout = (event) => {
        childCompsMeasurementsMap.current[item.id] = {
          index,
          layout: event.nativeEvent.layout,
        };
      };

      return (
        <View key={id} onLayout={onItemLayout}>
          <Focusable
            {...nextFocus}
            onFocus={(element, direction) =>
              onFocus?.(element, renderArgs, direction)
            }
            onBlur={(element, direction) =>
              onListElementBlur?.(element, renderArgs, direction)
            }
            onPress={(element) => {
              onListElementPress?.(element, renderArgs);
            }}
            onLongPress={(element) => {
              onListElementLongPress?.(element, renderArgs);
            }}
            onPressOut={(element) => {
              onListElementPressOut?.(element, renderArgs);
            }}
            id={id}
            disableFocus={shouldDisableFocus(renderArgs?.index)}
            blockFocus={!isRendered(renderArgs?.index)}
            {...focusableItemProps}
          >
            {(focused, parentFocus, parentRef) => (
              <FocusableChild
                {...renderArgs}
                focused={focused}
                parentFocus={parentFocus}
                parentRef={parentRef}
                renderItem={props.renderItem}
                extraChildrenData={props.extraChildrenData}
                useSequentialLoading={useSequentialLoading}
                isReadyToRender={isReadyToRender}
                onLoadFinished={onLoadFinished(renderArgs.index)}
                onLoadFailed={onLoadFailed(renderArgs.index)}
                setIsFocusable={setIsFocusable(renderArgs.index)}
              />
            )}
          </Focusable>
        </View>
      );
    },
    [
      getNextFocus,
      props.extraChildrenData,
      onFocus,
      props.renderItem,
      isReadyToRender,
      isRendered,
      setIsFocusable,
      focusableState,
      data.length,
      onListElementPress,
      onListElementLongPress,
      onListElementPressOut,
    ]
  );

  const scrollViewProps = React.useMemo(
    () =>
      R.omit([
        "onListElementFocus",
        "onListElementPress",
        "onListElementPressOut",
        "onListElementLongPress",
        "onListElementBlur",
        "nextFocusDown",
        "nextFocusRight",
        "nextFocusLeft",
        "nextFocusUp",
        "focusableItemProps",
        "renderItem",
        "extraChildrenData",
        "extraData",
        "keyExtractor",
        "onAllComponentsLoaded",
        "focused",
        "id",
        "initialFocusDirection",
        "withStateMemory",
        "useSequentialLoading",
        ...(props.omitPropsPropagation || []),
      ])(props),
    [props]
  );

  const setScrollViewRef = (_ref) => {
    if (_ref) {
      extendScrollViewRef(_ref, childCompsMeasurementsMap.current);

      ref.current = _ref;
    }
  };

  return (
    // @ts-ignore
    <ChildrenFocusDeactivatorView flex={1}>
      <ScrollView
        focusable={false}
        ref={setScrollViewRef}
        {...scrollViewProps}
        scrollEnabled={false}
      >
        {scrollViewProps?.data?.map(renderItem)}
      </ScrollView>
    </ChildrenFocusDeactivatorView>
  );
}

export const FocusableScrollView = React.forwardRef(
  FocusableScrollViewComponent
);
