import { RefObject } from 'react';
import {
  useEffect, useRef, useCallback, useState,
} from '../lib/teact/teact';

import { throttle, debounce } from '../util/schedulers';
import useHeavyAnimationCheck from './useHeavyAnimationCheck';

type TargetCallback = (entry: IntersectionObserverEntry) => void;
type RootCallback = (entries: IntersectionObserverEntry[]) => void;
type ObserveCleanup = NoneToVoidFunction;
export type ObserveFn = (target: HTMLElement, targetCallback?: TargetCallback) => ObserveCleanup;

interface IntersectionController {
  observer: IntersectionObserver;
  callbacks: Map<HTMLElement, TargetCallback>;
}

interface Response {
  observe: ObserveFn;
  freeze: NoneToVoidFunction;
  unfreeze: NoneToVoidFunction;
}

export function useIntersectionObserver({
  rootRef,
  throttleMs,
  debounceMs,
  shouldSkipFirst,
  margin,
  threshold,
  isDisabled,
}: {
  rootRef: RefObject<HTMLDivElement>;
  throttleMs?: number;
  debounceMs?: number;
  shouldSkipFirst?: boolean;
  margin?: number;
  threshold?: number | number[];
  isDisabled?: boolean;
}, rootCallback?: RootCallback): Response {
  const controllerRef = useRef<IntersectionController>();
  const rootCallbackRef = useRef<RootCallback>();
  const freezeFlagsRef = useRef(0);
  const onUnfreezeRef = useRef<NoneToVoidFunction>();

  rootCallbackRef.current = rootCallback;

  const freeze = useCallback(() => {
    freezeFlagsRef.current++;
  }, []);

  const unfreeze = useCallback(() => {
    if (!freezeFlagsRef.current) {
      return;
    }

    freezeFlagsRef.current--;

    if (!freezeFlagsRef.current && onUnfreezeRef.current) {
      onUnfreezeRef.current();
      onUnfreezeRef.current = undefined;
    }
  }, []);

  useHeavyAnimationCheck(freeze, unfreeze);

  useEffect(() => {
    if (isDisabled) {
      return undefined;
    }

    return () => {
      if (controllerRef.current) {
        controllerRef.current.observer.disconnect();
        controllerRef.current.callbacks.clear();
        controllerRef.current = undefined;
      }
    };
  }, [isDisabled]);

  function initController() {
    const callbacks = new Map();
    const entriesAccumulator = new Map<Element, IntersectionObserverEntry>();
    const observerCallbackSync = () => {
      const entries = Array.from(entriesAccumulator.values());

      entries.forEach((entry: IntersectionObserverEntry) => {
        const callback = callbacks.get(entry.target);
        if (callback) {
          callback!(entry, entries);
        }
      });

      if (rootCallbackRef.current) {
        rootCallbackRef.current(entries);
      }

      entriesAccumulator.clear();
    };
    const scheduler = throttleMs ? throttle : debounceMs ? debounce : undefined;
    const observerCallback = scheduler
      ? scheduler(observerCallbackSync, (throttleMs || debounceMs)!, !shouldSkipFirst)
      : observerCallbackSync;
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          entriesAccumulator.set(entry.target, entry);
        });

        if (freezeFlagsRef.current) {
          onUnfreezeRef.current = () => {
            observerCallback();
          };
        } else {
          observerCallback();
        }
      },
      {
        root: rootRef.current,
        rootMargin: margin ? `${margin}px` : undefined,
        threshold,
      },
    );

    controllerRef.current = { observer, callbacks };
  }

  const observe = useCallback((target, targetCallback) => {
    if (!controllerRef.current) {
      initController();
    }

    const controller = controllerRef.current!;
    controller.observer.observe(target);

    if (targetCallback) {
      controller.callbacks.set(target, targetCallback);
    }

    return () => {
      if (targetCallback) {
        controller.callbacks.delete(target);
      }

      controller.observer.unobserve(target);
    };
    // Arguments should never change
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isDisabled]);

  return { observe, freeze, unfreeze };
}

export function useOnIntersect(
  targetRef: RefObject<HTMLDivElement>, observe?: ObserveFn, callback?: TargetCallback,
) {
  useEffect(() => {
    return observe ? observe(targetRef.current!, callback) : undefined;
    // Arguments should never change
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
}

export function useIsIntersecting(
  targetRef: RefObject<HTMLDivElement>, observe?: ObserveFn, callback?: TargetCallback,
) {
  const [isIntersecting, setIsIntersecting] = useState(!observe);

  useOnIntersect(targetRef, observe, (entry) => {
    setIsIntersecting(entry.isIntersecting);

    if (callback) {
      callback(entry);
    }
  });

  return isIntersecting;
}
