/* eslint-disable jsx-a11y/no-noninteractive-tabindex */
import { Children, cloneElement, useCallback, useEffect, useRef, useState } from 'react';
import isEqual from 'lodash/isEqual';
import isNil from 'lodash/isNil';
import isNull from 'lodash/isNull';

import usePrevious from './usePrevValue';
import { withDeprecated } from '../Util/deprecate';

const Events = {
  TOUCHMOVE: 'touchmove',
  KEYDOWN: 'keydown',
} as const;

const DEFAULT_ANIMATION_TIMER = 500;
const DEFAULT_ANIMATION = 'ease-in-out';
const DEFAULT_CONTAINER_HEIGHT = '100vh';
const DEFAULT_CONTAINER_WIDTH = '100vw';
const DEFAULT_COMPONENT_INDEX = 0;
const DEFAULT_COMPONENTS_TO_RENDER_LENGTH = 0;

const ANIMATION_TIMER_BUFFER = 100;
const KEY_UP = 38;
const KEY_DOWN = 40;
const DISABLED_CLASS_NAME = 'rps-scroll--disabled';

let previousTouchMove: number | null = null;
let isScrolling = false;
let isMounted = false;
let isBodyScrollEnabled = true;
let isTransitionAfterComponentsToRenderChanged = false;
const containers: boolean[] = [];

export interface PageScrollerProps {
  animationTimer?: number;
  blockScrollDown?: false | true;
  blockScrollUp?: false | true;
  children?: React.ReactElement<any> | React.ReactElement<any>[];
  height?: number | string;
  width?: number | string;
  customPageNumber?: number;
  onScrollUnavailable?: () => void;
  onChange?: (index: number) => void;
  renderAllPagesOnFirstRender?: false | true;
  transitionTimingFunction?: string;
}

function PageScroller({
  animationTimer = DEFAULT_ANIMATION_TIMER,
  blockScrollDown = false,
  blockScrollUp = false,
  children: _children = undefined,
  height = DEFAULT_CONTAINER_HEIGHT,
  width = DEFAULT_CONTAINER_WIDTH,
  customPageNumber = undefined,
  onScrollUnavailable = undefined,
  onChange = undefined,
  renderAllPagesOnFirstRender = false,
  transitionTimingFunction = DEFAULT_ANIMATION,
}: PageScrollerProps) {
  const [componentIndex, setComponentIndex] = useState(DEFAULT_COMPONENT_INDEX);
  const [componentsToRenderLength, setComponentsToRenderLength] = useState(DEFAULT_COMPONENTS_TO_RENDER_LENGTH);
  const prevComponentIndex = usePrevious(componentIndex);
  const pageContainer = useRef<HTMLDivElement>(null);
  const children = Children.toArray(_children) as React.ReactElement<any>[];

  const addNextComponent = useCallback(
    (componentsToRenderOnMountLength?: number) => {
      let tempComponentsToRenderLength = 0;

      if (!isNil(componentsToRenderOnMountLength)) {
        tempComponentsToRenderLength = componentsToRenderOnMountLength;
      }

      tempComponentsToRenderLength = Math.max(tempComponentsToRenderLength, componentsToRenderLength);

      if (tempComponentsToRenderLength <= componentIndex + 1) {
        if (!isNil(children[componentIndex + 1])) {
          tempComponentsToRenderLength++;
        }
      }

      setComponentsToRenderLength(tempComponentsToRenderLength);
    },
    [children, componentIndex, componentsToRenderLength]
  );

  const checkRenderOnMount = useCallback(() => {
    if (renderAllPagesOnFirstRender) {
      setComponentsToRenderLength(Children.count(children));
    } else if (!isNil(children[DEFAULT_COMPONENT_INDEX + 1])) {
      addNextComponent(DEFAULT_COMPONENTS_TO_RENDER_LENGTH + 1);
    }
  }, [addNextComponent, children, renderAllPagesOnFirstRender]);

  const disableScroll = useCallback(() => {
    if (isBodyScrollEnabled) {
      isBodyScrollEnabled = false;
      window.scrollTo({
        left: 0,
        top: 0,
        behavior: 'smooth',
      });
      document.body.classList.add(DISABLED_CLASS_NAME);
      document.documentElement.classList.add(DISABLED_CLASS_NAME);
    }
  }, []);

  const enableDocumentScroll = useCallback(() => {
    if (!isBodyScrollEnabled) {
      isBodyScrollEnabled = true;
      document.body.classList.remove(DISABLED_CLASS_NAME);
      document.documentElement.classList.remove(DISABLED_CLASS_NAME);
    }
  }, []);

  const setRenderComponents = useCallback(() => {
    const newComponentsToRender = [];

    let i = 0;

    while (i < componentsToRenderLength && !isNil(children[i])) {
      containers[i] = true;
      newComponentsToRender.push(
        <div key={i} style={{ height: '100%', width: '100%' }}>
          {cloneElement(children[i], { ...children[i].props })}
        </div>
      );
      i++;
    }

    return newComponentsToRender;
  }, [children, componentsToRenderLength]);

  const scrollWindowDown = useCallback(() => {
    if (!isScrolling && !blockScrollDown) {
      if (!isNil(containers[componentIndex + 1])) {
        disableScroll();
        isScrolling = true;
        pageContainer.current!.style.transform = `translate3d(0, ${(componentIndex + 1) * -100}%, 0)`; // prettier-ignore

        setTimeout(() => {
          if (isMounted) {
            setComponentIndex((prevState) => prevState + 1);
          }
        }, animationTimer + ANIMATION_TIMER_BUFFER);
      } else {
        enableDocumentScroll();
        if (onScrollUnavailable) {
          onScrollUnavailable();
        }
      }
    }
  }, [animationTimer, blockScrollDown, componentIndex, disableScroll, enableDocumentScroll, onScrollUnavailable]);

  const scrollWindowUp = useCallback(() => {
    if (!isScrolling && !blockScrollUp) {
      if (!isNil(containers[componentIndex - 1])) {
        disableScroll();
        isScrolling = true;
        pageContainer.current!.style.transform = `translate3d(0, ${(componentIndex - 1) * -100}%, 0)`; // prettier-ignore

        setTimeout(() => {
          if (isMounted) {
            setComponentIndex((prevState) => prevState - 1);
          }
        }, animationTimer + ANIMATION_TIMER_BUFFER);
      } else {
        enableDocumentScroll();
        if (onScrollUnavailable) {
          onScrollUnavailable();
        }
      }
    }
  }, [animationTimer, blockScrollUp, componentIndex, disableScroll, enableDocumentScroll, onScrollUnavailable]);

  const touchMove = useCallback(
    (event: TouchEvent) => {
      if (!isNull(previousTouchMove)) {
        if (event.touches[0].clientY > previousTouchMove) {
          scrollWindowUp();
        } else {
          scrollWindowDown();
        }
      } else {
        previousTouchMove = event.touches[0].clientY;
      }
    },
    [scrollWindowDown, scrollWindowUp]
  );

  const wheelScroll = useCallback(
    (event: React.WheelEvent<HTMLDivElement>) => {
      if (event.deltaY < 0) {
        scrollWindowUp();
      } else {
        scrollWindowDown();
      }
    },
    [scrollWindowDown, scrollWindowUp]
  );

  const keyPress = useCallback(
    (event: KeyboardEvent) => {
      if (isEqual(event.keyCode, KEY_UP)) {
        scrollWindowUp();
      }
      if (isEqual(event.keyCode, KEY_DOWN)) {
        scrollWindowDown();
      }
    },
    [scrollWindowDown, scrollWindowUp]
  );

  useEffect(() => {
    pageContainer.current?.addEventListener(Events.TOUCHMOVE, touchMove);
    pageContainer.current?.addEventListener(Events.KEYDOWN, keyPress);
    return () => {
      if (pageContainer.current?.removeEventListener) {
        pageContainer.current.removeEventListener(Events.TOUCHMOVE, touchMove);
        // eslint-disable-next-line react-hooks/exhaustive-deps
        pageContainer.current.removeEventListener(Events.KEYDOWN, keyPress);
      }
    };
  }, [touchMove, keyPress]);

  useEffect(() => {
    isMounted = true;

    checkRenderOnMount();
    return () => {
      isMounted = false;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    isScrolling = false;
    previousTouchMove = null;
    if (prevComponentIndex !== undefined && componentIndex > prevComponentIndex) {
      addNextComponent();
    }
  }, [addNextComponent, componentIndex, prevComponentIndex]);

  useEffect(() => {
    if (onChange) {
      onChange(componentIndex);
    }
  }, [onChange, componentIndex]);

  useEffect(() => {
    if (!isNil(customPageNumber) && !isEqual(customPageNumber, componentIndex)) {
      let newComponentsToRenderLength = componentsToRenderLength;

      if (!isEqual(componentIndex, customPageNumber)) {
        if (!isNil(containers[customPageNumber]) && !isScrolling) {
          isScrolling = true;
          pageContainer.current!.style.transform = `translate3d(0, ${customPageNumber * -100}%, 0)`;

          if (isNil(containers[customPageNumber]) && !isNil(children[customPageNumber])) {
            newComponentsToRenderLength++;
          }

          setTimeout(() => {
            setComponentIndex(customPageNumber);
            setComponentsToRenderLength(newComponentsToRenderLength);
          }, animationTimer + ANIMATION_TIMER_BUFFER);
        } else if (!isScrolling && !isNil(children[customPageNumber])) {
          for (let i = componentsToRenderLength; i <= customPageNumber; i++) {
            newComponentsToRenderLength++;
          }

          if (!isNil(children[customPageNumber])) {
            newComponentsToRenderLength++;
          }

          isScrolling = true;
          isTransitionAfterComponentsToRenderChanged = true;
          setComponentsToRenderLength(newComponentsToRenderLength);
        }
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [customPageNumber]);

  useEffect(() => {
    if (isTransitionAfterComponentsToRenderChanged) {
      isTransitionAfterComponentsToRenderChanged = false;

      pageContainer.current!.style.transform = `translate3d(0, ${customPageNumber! * -100}%, 0)`;

      setTimeout(() => {
        setComponentIndex(customPageNumber!);
      }, animationTimer + ANIMATION_TIMER_BUFFER);
    }
  }, [animationTimer, componentsToRenderLength, customPageNumber]);

  return (
    <div style={{ height, width, overflow: 'hidden' }}>
      <div
        ref={pageContainer}
        onWheel={wheelScroll}
        style={{
          height: '100%',
          width: '100%',
          transition: `transform ${animationTimer}ms ${transitionTimingFunction}`,
          outline: 'none',
        }}
        tabIndex={0}>
        {setRenderComponents()}
      </div>
    </div>
  );
}

export default withDeprecated(PageScroller, { name: 'PageScroller' });
