import React, { useEffect, useMemo, useRef } from 'react';

import { clamp } from '../../helpers';

import useEventCallback from '../../hooks/useEventCallback';

export type InteractionProps = {
  left: number;
  top: number;
};

const isTouch = (event: MouseEvent | TouchEvent): event is TouchEvent => 'touches' in event;

const getTouchPoint = (touches: TouchList, touchId: null | number): Touch => {
  for (let i = 0; i < touches.length; i += 1) {
    if (touches[i].identifier === touchId) return touches[i];
  }
  return touches[0];
};

const getParentWindow = (node?: HTMLDivElement | null): Window => (node && node.ownerDocument.defaultView) || window;

const getRelativePosition = (
  node: HTMLDivElement,
  event: MouseEvent | TouchEvent,
  touchId: null | number,
): InteractionProps => {
  const rect = node.getBoundingClientRect();

  const pointer = isTouch(event) ? getTouchPoint(event.touches, touchId) : (event as MouseEvent);

  return {
    left: clamp((pointer.pageX - (rect.left + getParentWindow(node).pageXOffset)) / rect.width),
    top: clamp((pointer.pageY - (rect.top + getParentWindow(node).pageYOffset)) / rect.height),
  };
};

// Browsers introduced an intervention, making touch events passive by default.
// This workaround removes `preventDefault` call from the touch handlers.
// https://github.com/facebook/react/issues/19651
const preventDefaultMove = (event: MouseEvent | TouchEvent): void => {
  if (!isTouch(event)) {
    event.preventDefault();
  }
};

const isInvalid = (event: MouseEvent | TouchEvent, hasTouch: boolean): boolean => hasTouch && !isTouch(event);

type Props = {
  onMove: (interaction: InteractionProps) => void;
  onKey: (offset: InteractionProps) => void;
  className?: string;
  style?: object;
  children: React.ReactNode;
};

function Interactive({ onMove, onKey, ...rest }: Props) {
  const container = useRef<HTMLDivElement>(null);
  const onMoveCallback = useEventCallback<InteractionProps>(onMove);
  const onKeyCallback = useEventCallback<InteractionProps>(onKey);
  const touchId = useRef<null | number>(null);
  const hasTouch = useRef(false);

  const [handleMoveStart$, handleKeyDown$, toggleDocumentEvents$] = useMemo(() => {
    // eslint-disable-next-line no-use-before-define
    const handleMoveEnd = () => toggleDocumentEvents(false);
    const handleMove = (event: MouseEvent | TouchEvent) => {
      preventDefaultMove(event);

      const isDown = isTouch(event) ? event.touches.length > 0 : event.buttons > 0;

      if (isDown && container.current) {
        onMoveCallback(getRelativePosition(container.current, event, touchId.current));
      } else {
        // eslint-disable-next-line no-use-before-define
        toggleDocumentEvents(false);
      }
    };

    function toggleDocumentEvents(state?: boolean) {
      const touch = hasTouch.current;
      const el = container.current;
      const parentWindow = getParentWindow(el);

      const toggleEvent = state ? parentWindow.addEventListener : parentWindow.removeEventListener;
      toggleEvent(touch ? 'touchmove' : 'mousemove', handleMove);
      toggleEvent(touch ? 'touchend' : 'mouseup', handleMoveEnd);
    }

    const handleMoveStart = ({ nativeEvent }: React.MouseEvent | React.TouchEvent) => {
      const el = container.current;
      if (!el) return;

      // Prevent text selection
      preventDefaultMove(nativeEvent);

      if (isInvalid(nativeEvent, hasTouch.current) || !el) return;

      if (isTouch(nativeEvent)) {
        hasTouch.current = true;
        const changedTouches = nativeEvent.changedTouches || [];
        if (changedTouches.length) touchId.current = changedTouches[0].identifier;
      }

      el.focus();
      onMoveCallback(getRelativePosition(el, nativeEvent, touchId.current));
      toggleDocumentEvents(true);
    };

    const handleKeyDown = (event: React.KeyboardEvent) => {
      if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)) {
        return;
      }

      event.preventDefault();

      let left = 0;
      let top = 0;

      if (event.key === 'ArrowRight') {
        left = 0.05;
      } else if (event.key === 'ArrowLeft') {
        left = -0.05;
      }

      if (event.key === 'ArrowDown') {
        top = 0.05;
      } else if (event.key === 'ArrowUp') {
        top = -0.05;
      }

      onKeyCallback({
        left,
        top,
      });
    };

    return [handleMoveStart, handleKeyDown, toggleDocumentEvents];
  }, [onKeyCallback, onMoveCallback]);

  useEffect(() => toggleDocumentEvents$, [toggleDocumentEvents$]);

  return (
    <div
      {...rest}
      onTouchStart={handleMoveStart$}
      onMouseDown={handleMoveStart$}
      ref={container}
      onKeyDown={handleKeyDown$}
      tabIndex={0}
      role="slider"
    />
  );
}

export default React.memo(Interactive);
