import { useState, useRef, useCallback, useEffect } from 'react';
import { fromEvent, Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';

import { getAbsoluteCoordinate } from '../helpers/position';

const defaultTime = 35;

type CursorPosition = {
  x: number | null;
  y: number | null;
  clientX: number | null;
  clientY: number | null;
  screenX: number | null;
  screenY: number | null;
};

type UseCursorPositionType = (
  throttleTime?: number
) => [
  CursorPosition,
  (element: HTMLElement | SVGSVGElement | SVGCircleElement | null) => void
];

const initialState = {
  x: null,
  y: null,
  clientX: null,
  clientY: null,
  screenX: null,
  screenY: null,
};

const useCursorPosition: UseCursorPositionType = (time = defaultTime) => {
  const [state, setState] = useState<CursorPosition>(initialState);
  const [element, setElement] = useState<
    HTMLElement | SVGSVGElement | SVGCircleElement | null
  >(null);
  const entered = useRef(false);
  const isDown = useRef(false);

  const onMouseMoveEvent = useCallback(
    (e: MouseEvent | Event): void => {
      if (element) {
        const { clientX, clientY, screenX, screenY } = e as MouseEvent;
        const coordinate = getAbsoluteCoordinate(element, e as MouseEvent);

        setState({
          x: coordinate.x,
          y: coordinate.y,
          clientX,
          clientY,
          screenX,
          screenY,
        });
      }
    },
    [element]
  );

  const onTouchMoveEvent = useCallback(
    (e: TouchEvent | Event): void => {
      if (element) {
        const {
          clientX,
          clientY,
          screenX,
          screenY,
        } = (e as TouchEvent).touches[0];
        const coordinate = getAbsoluteCoordinate(element, e as TouchEvent);

        setState({
          x: coordinate.x,
          y: coordinate.y,
          clientX,
          clientY,
          screenX,
          screenY,
        });
      }
    },
    [element]
  );

  useEffect(() => {
    let mouseSubscription: Subscription | null = null;
    let touchSubscription: Subscription | null = null;

    const onEnter = (): void => {
      entered.current = true;
    };

    const onLeave = (): void => {
      entered.current = false;
      setState(initialState);
    };

    const onDown = (e: any): void => {
      entered.current = true;
      isDown.current = true;
      onMouseMoveEvent(e);
    };

    const onTouch = (e: any): void => {
      entered.current = true;
      isDown.current = true;
      onTouchMoveEvent(e);
    };

    const onUp = (): void => {
      entered.current = false;
      isDown.current = false;
      setState(initialState);
      if (mouseSubscription) mouseSubscription.unsubscribe();
      if (touchSubscription) touchSubscription.unsubscribe();
    };

    if (element) {
      mouseSubscription = fromEvent(element, 'mousemove')
        .pipe(throttleTime(time))
        .subscribe(onMouseMoveEvent);
      touchSubscription = fromEvent(element, 'touchmove')
        .pipe(throttleTime(time))
        .subscribe(onTouchMoveEvent);

      const addEvent = element.addEventListener.bind(element);
      addEvent('mouseenter', onEnter);
      addEvent('mouseleave', onLeave);
      addEvent('mousedown', onDown);
      addEvent('touchstart', onTouch);
      addEvent('mouseup', onUp);
      addEvent('touchend', onUp);
    }

    return (): void => {
      if (element) {
        const removeEvent = element.removeEventListener.bind(element);
        removeEvent('mouseenter', onEnter);
        removeEvent('mouseleave', onLeave);
        removeEvent('mousedown', onDown);
        removeEvent('touchstart', onTouch);
        removeEvent('mouseup', onUp);
        removeEvent('touchend', onUp);
      }
    };
  }, [element]);

  return [state, setElement];
};

export default useCursorPosition;
