import React, { useCallback, useEffect, useMemo, useRef } from "react";
import { bindActionCreators } from "redux";
import { Provider } from "react-redux";
import { invariant } from "../../invariant";
import createStore from "../../state/create-store";
import createDimensionMarshal from "../../state/dimension-marshal/dimension-marshal";
import canStartDrag from "../../state/can-start-drag";
import scrollWindow from "../window/scroll-window";
import createAutoScroller from "../../state/auto-scroller";
import useStyleMarshal from "../use-style-marshal/use-style-marshal";
import useFocusMarshal from "../use-focus-marshal";
import useRegistry from "../../state/registry/use-registry";
import StoreContext from "../context/store-context";
import {
  collectionStarting,
  flush,
  move,
  publishWhileDragging,
  updateDroppableIsCombineEnabled,
  updateDroppableIsEnabled,
  updateDroppableScroll
} from "../../state/action-creators";
import isMovementAllowed from "../../state/is-movement-allowed";
import useAnnouncer from "../use-announcer";
import useHiddenTextElement from "../use-hidden-text-element";
import AppContext from "../context/app-context";
import useStartupValidation from "./use-startup-validation";
import usePrevious from "../use-previous-ref";
import { warning } from "../../dev-warning";
import useSensorMarshal from "../use-sensor-marshal/use-sensor-marshal";

const createResponders = (props) => ({
  onBeforeCapture: props.onBeforeCapture,
  onBeforeDragStart: props.onBeforeDragStart,
  onDragStart: props.onDragStart,
  onDragEnd: props.onDragEnd,
  onDragUpdate: props.onDragUpdate
});

// flow does not support MutableRefObject
// type LazyStoreRef = MutableRefObject<?Store>;

function getStore(lazyRef) {
  invariant(lazyRef.current, "Could not find store from lazy ref");
  return lazyRef.current;
}

export default function App(props) {
  const { contextId, setCallbacks, sensors, nonce, dragHandleUsageInstructions } = props;
  const lazyStoreRef = useRef(null);
  useStartupValidation();

  // lazy collection of responders using a ref - update on ever render
  const lastPropsRef = usePrevious(props);
  const getResponders = useCallback(() => {
    return createResponders(lastPropsRef.current);
  }, [lastPropsRef]);
  const announce = useAnnouncer(contextId);
  const dragHandleUsageInstructionsId = useHiddenTextElement({
    contextId,
    text: dragHandleUsageInstructions
  });
  const styleMarshal = useStyleMarshal(contextId, nonce);
  const lazyDispatch = useCallback((action) => {
    getStore(lazyStoreRef).dispatch(action);
  }, []);
  const marshalCallbacks = useMemo(
    () =>
      bindActionCreators(
        {
          publishWhileDragging,
          updateDroppableScroll,
          updateDroppableIsEnabled,
          updateDroppableIsCombineEnabled,
          collectionStarting
        },
        // $FlowFixMe - not sure why this is wrong
        lazyDispatch
      ),
    [lazyDispatch]
  );
  const registry = useRegistry();
  const dimensionMarshal = useMemo(() => {
    return createDimensionMarshal(registry, marshalCallbacks);
  }, [registry, marshalCallbacks]);
  const autoScroller = useMemo(
    () =>
      createAutoScroller({
        scrollWindow,
        scrollDroppable: dimensionMarshal.scrollDroppable,
        ...bindActionCreators(
          {
            move
          },
          // $FlowFixMe - not sure why this is wrong
          lazyDispatch
        )
      }),
    [dimensionMarshal.scrollDroppable, lazyDispatch]
  );
  const focusMarshal = useFocusMarshal(contextId);
  const store = useMemo(
    () =>
      createStore({
        announce,
        autoScroller,
        dimensionMarshal,
        focusMarshal,
        getResponders,
        styleMarshal
      }),
    [announce, autoScroller, dimensionMarshal, focusMarshal, getResponders, styleMarshal]
  );

  // Checking for unexpected store changes
  if (import.meta.env.DEV) {
    if (lazyStoreRef.current && lazyStoreRef.current !== store) {
      warning("unexpected store change");
    }
  }

  // assigning lazy store ref
  lazyStoreRef.current = store;
  const tryResetStore = useCallback(() => {
    const current = getStore(lazyStoreRef);
    const state = current.getState();
    if (state.phase !== "IDLE") {
      current.dispatch(flush());
    }
  }, []);
  const isDragging = useCallback(() => {
    const state = getStore(lazyStoreRef).getState();
    return state.isDragging || state.phase === "DROP_ANIMATING";
  }, []);
  const appCallbacks = useMemo(
    () => ({
      isDragging,
      tryAbort: tryResetStore
    }),
    [isDragging, tryResetStore]
  );

  // doing this in render rather than a side effect so any errors on the
  // initial mount are caught
  setCallbacks(appCallbacks);
  const getCanLift = useCallback((id) => canStartDrag(getStore(lazyStoreRef).getState(), id), []);
  const getIsMovementAllowed = useCallback(() => isMovementAllowed(getStore(lazyStoreRef).getState()), []);
  const appContext = useMemo(
    () => ({
      marshal: dimensionMarshal,
      focus: focusMarshal,
      contextId,
      canLift: getCanLift,
      isMovementAllowed: getIsMovementAllowed,
      dragHandleUsageInstructionsId,
      registry
    }),
    [
      contextId,
      dimensionMarshal,
      dragHandleUsageInstructionsId,
      focusMarshal,
      getCanLift,
      getIsMovementAllowed,
      registry
    ]
  );
  useSensorMarshal({
    contextId,
    store,
    registry,
    customSensors: sensors,
    // default to 'true' unless 'false' is explicitly passed
    enableDefaultSensors: props.enableDefaultSensors !== false
  });

  // Clean store when unmounting
  useEffect(() => {
    return tryResetStore;
  }, [tryResetStore]);
  return /*#__PURE__*/ React.createElement(
    AppContext.Provider,
    {
      value: appContext
    },
    /*#__PURE__*/ React.createElement(
      Provider,
      {
        context: StoreContext,
        store: store
      },
      props.children
    )
  );
}
