import { useCallback, useMemo, useRef } from "react";
import rafSchedule from "raf-schd";
import memoizeOne from "memoize-one";
import { invariant } from "../../invariant";
import checkForNestedScrollContainers from "./check-for-nested-scroll-container";
import * as dataAttr from "../data-attributes";
import { origin } from "../../state/position";
import getScroll from "./get-scroll";
import getEnv from "./get-env";
import getDimension from "./get-dimension";
import AppContext from "../context/app-context";
import { warning } from "../../dev-warning";
import getListenerOptions from "./get-listener-options";
import useRequiredContext from "../use-required-context";
import usePreviousRef from "../use-previous-ref";
import useLayoutEffect from "../use-isomorphic-layout-effect";
import useUniqueId from "../use-unique-id";

const getClosestScrollableFromDrag = (dragging) => (dragging && dragging.env.closestScrollable) || null;
export default function useDroppablePublisher(args) {
  const whileDraggingRef = useRef(null);
  const appContext = useRequiredContext(AppContext);
  const uniqueId = useUniqueId("droppable");
  const { registry, marshal } = appContext;
  const previousRef = usePreviousRef(args);
  const descriptor = useMemo(
    () => ({
      id: args.droppableId,
      type: args.type,
      mode: args.mode
    }),
    [args.droppableId, args.mode, args.type]
  );
  const publishedDescriptorRef = useRef(descriptor);
  const memoizedUpdateScroll = useMemo(
    () =>
      memoizeOne((x, y) => {
        invariant(whileDraggingRef.current, "Can only update scroll when dragging");
        const scroll = {
          x,
          y
        };
        marshal.updateDroppableScroll(descriptor.id, scroll);
      }),
    [descriptor.id, marshal]
  );
  const getClosestScroll = useCallback(() => {
    const dragging = whileDraggingRef.current;
    if (!dragging || !dragging.env.closestScrollable) {
      return origin;
    }
    return getScroll(dragging.env.closestScrollable);
  }, []);
  const updateScroll = useCallback(() => {
    // reading scroll value when called so value will be the latest
    const scroll = getClosestScroll();
    memoizedUpdateScroll(scroll.x, scroll.y);
  }, [getClosestScroll, memoizedUpdateScroll]);
  const scheduleScrollUpdate = useMemo(() => rafSchedule(updateScroll), [updateScroll]);
  const onClosestScroll = useCallback(() => {
    const dragging = whileDraggingRef.current;
    const closest = getClosestScrollableFromDrag(dragging);
    invariant(dragging && closest, "Could not find scroll options while scrolling");
    const options = dragging.scrollOptions;
    if (options.shouldPublishImmediately) {
      updateScroll();
      return;
    }
    scheduleScrollUpdate();
  }, [scheduleScrollUpdate, updateScroll]);
  const getDimensionAndWatchScroll = useCallback(
    (windowScroll, options) => {
      invariant(!whileDraggingRef.current, "Cannot collect a droppable while a drag is occurring");
      const previous = previousRef.current;
      const ref = previous.getDroppableRef();
      invariant(ref, "Cannot collect without a droppable ref");
      const env = getEnv(ref);
      const dragging = {
        ref,
        descriptor,
        env,
        scrollOptions: options
      };
      // side effect
      whileDraggingRef.current = dragging;
      const dimension = getDimension({
        ref,
        descriptor,
        env,
        windowScroll,
        direction: previous.direction,
        isDropDisabled: previous.isDropDisabled,
        isCombineEnabled: previous.isCombineEnabled,
        shouldClipSubject: !previous.ignoreContainerClipping
      });
      const scrollable = env.closestScrollable;
      if (scrollable) {
        scrollable.setAttribute(dataAttr.scrollContainer.contextId, appContext.contextId);

        // bind scroll listener
        scrollable.addEventListener("scroll", onClosestScroll, getListenerOptions(dragging.scrollOptions));
        // print a debug warning if using an unsupported nested scroll container setup
        if (import.meta.env.DEV) {
          checkForNestedScrollContainers(scrollable);
        }
      }
      return dimension;
    },
    [appContext.contextId, descriptor, onClosestScroll, previousRef]
  );
  const getScrollWhileDragging = useCallback(() => {
    const dragging = whileDraggingRef.current;
    const closest = getClosestScrollableFromDrag(dragging);
    invariant(dragging && closest, "Can only recollect Droppable client for Droppables that have a scroll container");
    return getScroll(closest);
  }, []);
  const dragStopped = useCallback(() => {
    const dragging = whileDraggingRef.current;
    invariant(dragging, "Cannot stop drag when no active drag");
    const closest = getClosestScrollableFromDrag(dragging);

    // goodbye old friend
    whileDraggingRef.current = null;
    if (!closest) {
      return;
    }

    // unwatch scroll
    scheduleScrollUpdate.cancel();
    closest.removeAttribute(dataAttr.scrollContainer.contextId);
    closest.removeEventListener("scroll", onClosestScroll, getListenerOptions(dragging.scrollOptions));
  }, [onClosestScroll, scheduleScrollUpdate]);
  const scroll = useCallback((change) => {
    // arrange
    const dragging = whileDraggingRef.current;
    invariant(dragging, "Cannot scroll when there is no drag");
    const closest = getClosestScrollableFromDrag(dragging);
    invariant(closest, "Cannot scroll a droppable with no closest scrollable");

    // act
    closest.scrollTop += change.y;
    closest.scrollLeft += change.x;
  }, []);
  const callbacks = useMemo(() => {
    return {
      getDimensionAndWatchScroll,
      getScrollWhileDragging,
      dragStopped,
      scroll
    };
  }, [dragStopped, getDimensionAndWatchScroll, getScrollWhileDragging, scroll]);
  const entry = useMemo(
    () => ({
      uniqueId,
      descriptor,
      callbacks
    }),
    [callbacks, descriptor, uniqueId]
  );

  // Register with the marshal and let it know of:
  // - any descriptor changes
  // - when it unmounts
  useLayoutEffect(() => {
    publishedDescriptorRef.current = entry.descriptor;
    registry.droppable.register(entry);
    return () => {
      if (whileDraggingRef.current) {
        warning("Unsupported: changing the droppableId or type of a Droppable during a drag");
        dragStopped();
      }
      registry.droppable.unregister(entry);
    };
    // eslint-disable-next-line react-compiler/react-compiler
  }, [callbacks, descriptor, dragStopped, entry, marshal, registry.droppable]);

  // update is enabled with the marshal
  // only need to update when there is a drag
  useLayoutEffect(() => {
    if (!whileDraggingRef.current) {
      return;
    }
    marshal.updateDroppableIsEnabled(publishedDescriptorRef.current.id, !args.isDropDisabled);
  }, [args.isDropDisabled, marshal]);

  // update is combine enabled with the marshal
  // only need to update when there is a drag
  useLayoutEffect(() => {
    if (!whileDraggingRef.current) {
      return;
    }
    marshal.updateDroppableIsCombineEnabled(publishedDescriptorRef.current.id, args.isCombineEnabled);
  }, [args.isCombineEnabled, marshal]);
}
