import {
  createContext,
  useState,
  useEffect,
  useCallback,
  useContext,
  useMemo,
  useRef,
  useLayoutEffect,
} from 'react';

interface AnchorContextValue {
  observer: IntersectionObserver | null;
  observe: (target: Element, index: number) => void;
  unobserve: (target: Element) => void;
  active: string | null;
  setActive: React.Dispatch<React.SetStateAction<string | null>>;
  scrollTo: (id: string) => void;
}

export const AnchorContext = createContext<AnchorContextValue | null>(null);

export const useAnchorContext = () => {
  const context = useContext(AnchorContext);
  if (context === null) {
    throw new Error(
      'useNotifications must be used within a NotificationsProvider'
    );
  }
  return context;
};

const IO_OPTIONS: IntersectionObserverInit = {
  threshold: [0, 0.98],
};

interface AnchorProviderProps {
  initialId?: string;
  onChange?: (id: string) => void;
  style?: React.CSSProperties;
  children?: React.ReactNode;
}

export const AnchorProvider = (props: AnchorProviderProps) => {
  const { initialId, onChange } = props;
  const [activeId, setActiveId] = useState<string | null>(null);

  const initialIdRef = useRef(initialId);
  const anchors = useRef<string[]>([]);
  const anchor = useRef<string | null>(null);

  useEffect(() => {
    let id;
    if (initialIdRef.current) {
      id = initialIdRef.current.replace('#', '');
    } else if (anchors.current.length) {
      id = anchors.current[0];
    }

    if (id) {
      anchor.current = id;
      setActiveId(id);

      const el = document.getElementById(id);
      if (el) el.scrollIntoView();
    }
  }, []);

  const changeSection = useCallback(
    (id: string) => {
      anchor.current = id;
      setActiveId(id);
      if (typeof onChange === 'function') {
        onChange(id);
      }
    },
    [onChange]
  );

  const observerInit = useRef(false);

  function getNextAnchor(events: string[], index: 1 | -1) {
    const lastId = events.pop();
    if (lastId) {
      const currentIndex = anchors.current.indexOf(anchor.current as string);
      const lastIndex = anchors.current.indexOf(lastId);
      if (lastIndex >= currentIndex) {
        return anchors.current[lastIndex + index];
      }
    }
  }

  const callback: IntersectionObserverCallback = useCallback(
    function callback(entries) {
      if (observerInit.current === false) {
        observerInit.current = true;
      } else {
        const events: Record<string, string[]> = {
          topLeave: [],
          bottomLeave: [],
          topEnter: [],
          bottomEnter: [],
          topAppear: [],
          bottomAppear: [],
        };

        for (const entry of entries) {
          const {
            target,
            isIntersecting,
            intersectionRatio,
            rootBounds,
            boundingClientRect,
          } = entry;
          const { id: targetId } = target;

          if (!rootBounds) return;

          const rootThreshold = rootBounds.height / 2 + rootBounds.y;
          const entryPos =
            (boundingClientRect.bottom + boundingClientRect.y) / 2;
          const direction = entryPos < rootThreshold ? 'top' : 'bottom';

          if (isIntersecting) {
            if (direction === 'top') {
              if (intersectionRatio >= 0.98) {
                events.topAppear.push(targetId);
              } else {
                events.topEnter.push(targetId);
              }
            } else {
              if (intersectionRatio >= 0.98) {
                events.bottomAppear.push(targetId);
              } else {
                events.bottomEnter.push(targetId);
              }
            }
          } else {
            if (direction === 'top') {
              events.topLeave.push(targetId);
            } else {
              events.bottomLeave.push(targetId);
            }
          }
        }

        let nextId;
        if (anchor.current) {
          if (~events.topLeave.indexOf(anchor.current)) {
            nextId = getNextAnchor(events.topLeave, 1);
          } else if (~events.bottomLeave.indexOf(anchor.current)) {
            nextId = getNextAnchor(events.bottomLeave, -1);
          }

          if (events.topAppear.length) {
            const [topId] = events.topAppear;
            nextId = topId;
          } else if (events.bottomAppear.length) {
            const [bottomId] = events.bottomAppear;
            const entryIndex = anchors.current.indexOf(bottomId);
            const lastItem = entryIndex === anchors.current.length - 1;
            if (lastItem) {
              nextId = bottomId;
            }
          }
        } else {
          nextId = events.topEnter[0] || events.bottomEnter[0];
        }

        if (nextId) changeSection(nextId);
      }
    },
    [changeSection]
  );

  const rootRef = useRef<HTMLDivElement>(null);
  const observerRef = useRef<IntersectionObserver | null>(null);

  useLayoutEffect(() => {
    const observer = new IntersectionObserver(callback, {
      ...IO_OPTIONS,
      root: rootRef.current,
    });
    observerRef.current = observer;
    return () => {
      observer.disconnect();
      observerRef.current = null;
    };
  }, [callback]);

  const observe = useCallback(function (target: Element, index: number) {
    if (observerRef.current) {
      observerRef.current.observe(target);
      anchors.current[index] = target.id;
    }
    // anchors.current[index] = target.id;
  }, []);

  const unobserve = useCallback(function (target: Element) {
    if (observerRef.current) {
      observerRef.current.unobserve(target);
    }
    const i = anchors.current.indexOf(target.id);
    if (i !== -1) {
      anchor.current = null;
      anchors.current.splice(i, 1);
    }
  }, []);

  const value = useMemo(
    () => ({
      observer: observerRef.current,
      observe,
      unobserve,
      active: activeId,
      setActive: setActiveId,
      scrollTo: changeSection,
    }),
    [observe, unobserve, activeId, changeSection]
  );

  return (
    <AnchorContext.Provider value={value}>
      <div style={props.style} ref={rootRef}>
        {props.children}
      </div>
    </AnchorContext.Provider>
  );
};
