import { connect } from 'react-redux';
import memoizeOne from 'memoize-one';
import { FunctionComponent } from 'react';
import { invariant } from '../../invariant';
import type {
  State,
  DroppableId,
  DraggableId,
  CompletedDrag,
  DraggableDimension,
  DimensionMap,
  TypeId,
  Critical,
  DraggableRubric,
  DraggableDescriptor,
} from '../../types';
import type {
  MapProps,
  InternalOwnProps,
  DroppableProps,
  DefaultProps,
  Selector,
  DispatchProps,
  DroppableStateSnapshot,
  UseClone,
  DraggableChildrenFn,
} from './droppable-types';
import Droppable from './droppable';
import isStrictEqual from '../is-strict-equal';
import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over';
import { updateViewportMaxScroll as updateViewportMaxScrollAction } from '../../state/action-creators';
import isDragging from '../../state/is-dragging';
import StoreContext from '../context/store-context';
import whatIsDraggedOverFromResult from '../../state/droppable/what-is-dragged-over-from-result';

function getBody(): HTMLElement {
  invariant(document.body, 'document.body is not ready');
  return document.body;
}

const defaultProps: DefaultProps = {
  mode: 'standard',
  type: 'DEFAULT',
  direction: 'vertical',
  isDropDisabled: false,
  isCombineEnabled: false,
  ignoreContainerClipping: false,
  renderClone: null,
  getContainerForClone: getBody,
};

const attachDefaultPropsToOwnProps = (ownProps: InternalOwnProps) => {
  // We need to assign default props manually because upcoming React version will stop supporting
  // defaultProps on functional components.
  // see: https://github.com/facebook/react/pull/25699
  let mergedProps = { ...ownProps };
  let defaultPropKey: keyof typeof defaultProps;
  for (defaultPropKey in defaultProps) {
    if (ownProps[defaultPropKey] === undefined) {
      mergedProps = {
        ...mergedProps,
        [defaultPropKey]: defaultProps[defaultPropKey],
      };
    }
  }

  return mergedProps;
};

const isMatchingType = (type: TypeId, critical: Critical): boolean =>
  type === critical.droppable.type;

const getDraggable = (
  critical: Critical,
  dimensions: DimensionMap,
): DraggableDimension => dimensions.draggables[critical.draggable.id];

// Returning a function to ensure each
// Droppable gets its own selector
export const makeMapStateToProps = (): Selector => {
  const idleWithAnimation: MapProps = {
    placeholder: null,
    shouldAnimatePlaceholder: true,
    snapshot: {
      isDraggingOver: false,
      draggingOverWith: null,
      draggingFromThisWith: null,
      isUsingPlaceholder: false,
    },
    useClone: null,
  };

  const idleWithoutAnimation = {
    ...idleWithAnimation,
    shouldAnimatePlaceholder: false,
  };

  const getDraggableRubric = memoizeOne(
    (descriptor: DraggableDescriptor): DraggableRubric => ({
      draggableId: descriptor.id,
      type: descriptor.type,

      source: {
        index: descriptor.index,
        droppableId: descriptor.droppableId,
      },
    }),
  );

  const getMapProps = memoizeOne(
    (
      id: DroppableId,
      isEnabled: boolean,
      isDraggingOverForConsumer: boolean,
      isDraggingOverForImpact: boolean,
      dragging: DraggableDimension,
      // snapshot: StateSnapshot,
      renderClone?: DraggableChildrenFn | null,
    ): MapProps => {
      const draggableId: DraggableId = dragging.descriptor.id;
      const isHome: boolean = dragging.descriptor.droppableId === id;

      if (isHome) {
        const useClone: UseClone | null = renderClone
          ? {
              render: renderClone,
              dragging: getDraggableRubric(dragging.descriptor),
            }
          : null;

        const snapshot: DroppableStateSnapshot = {
          isDraggingOver: isDraggingOverForConsumer,
          draggingOverWith: isDraggingOverForConsumer ? draggableId : null,
          draggingFromThisWith: draggableId,
          isUsingPlaceholder: true,
        };

        return {
          placeholder: dragging.placeholder,
          shouldAnimatePlaceholder: false,
          snapshot,
          useClone,
        };
      }

      if (!isEnabled) {
        return idleWithoutAnimation;
      }

      // not over foreign list - return idle
      if (!isDraggingOverForImpact) {
        return idleWithAnimation;
      }

      const snapshot: DroppableStateSnapshot = {
        isDraggingOver: isDraggingOverForConsumer,
        draggingOverWith: draggableId,
        draggingFromThisWith: null,
        isUsingPlaceholder: true,
      };

      return {
        placeholder: dragging.placeholder,
        // Animating placeholder in foreign list
        shouldAnimatePlaceholder: true,
        snapshot,
        useClone: null,
      };
    },
  );

  const selector = (state: State, ownProps: InternalOwnProps): MapProps => {
    // not checking if item is disabled as we need the home list to display a placeholder

    const ownPropsWithDefaultProps = attachDefaultPropsToOwnProps(ownProps);
    const id: DroppableId = ownPropsWithDefaultProps.droppableId;
    const type: TypeId = ownPropsWithDefaultProps.type;
    const isEnabled = !ownPropsWithDefaultProps.isDropDisabled;
    const renderClone: DraggableChildrenFn | null =
      ownPropsWithDefaultProps.renderClone;

    if (isDragging(state)) {
      const critical: Critical = state.critical;
      if (!isMatchingType(type, critical)) {
        return idleWithoutAnimation;
      }

      const dragging: DraggableDimension = getDraggable(
        critical,
        state.dimensions,
      );
      const isDraggingOver: boolean = whatIsDraggedOver(state.impact) === id;

      return getMapProps(
        id,
        isEnabled,
        isDraggingOver,
        isDraggingOver,
        dragging,
        renderClone,
      );
    }

    if (state.phase === 'DROP_ANIMATING') {
      const completed: CompletedDrag = state.completed;
      if (!isMatchingType(type, completed.critical)) {
        return idleWithoutAnimation;
      }

      const dragging: DraggableDimension = getDraggable(
        completed.critical,
        state.dimensions,
      );

      // Snapshot based on result and not impact
      // The result might be null (cancel) but the impact is populated
      // to move everything back
      return getMapProps(
        id,
        isEnabled,
        whatIsDraggedOverFromResult(completed.result) === id,
        whatIsDraggedOver(completed.impact) === id,
        dragging,
        renderClone,
      );
    }

    if (state.phase === 'IDLE' && state.completed && !state.shouldFlush) {
      const completed: CompletedDrag = state.completed;
      if (!isMatchingType(type, completed.critical)) {
        return idleWithoutAnimation;
      }

      // Looking at impact as this controls the placeholder
      const wasOver: boolean = whatIsDraggedOver(completed.impact) === id;
      const wasCombining = Boolean(
        completed.impact.at && completed.impact.at.type === 'COMBINE',
      );
      const isHome: boolean = completed.critical.droppable.id === id;

      if (wasOver) {
        // if reordering we need to cut an animation immediately
        // if merging: animate placeholder closed after drop
        return wasCombining ? idleWithAnimation : idleWithoutAnimation;
      }

      // we need to animate the home placeholder closed if it is not
      // being dropped into
      if (isHome) {
        return idleWithAnimation;
      }

      return idleWithoutAnimation;
    }

    // default: including when flushed
    return idleWithoutAnimation;
  };

  return selector;
};

const mapDispatchToProps: DispatchProps = {
  updateViewportMaxScroll: updateViewportMaxScrollAction,
};

// Abstract class allows to specify props and defaults to component.
// All other ways give any or do not let add default props.
// eslint-disable-next-line
/*::
class DroppableType extends Component<OwnProps> {
  static defaultProps = defaultProps;
}
*/

// Leaning heavily on the default shallow equality checking
// that `connect` provides.
// It avoids needing to do it own within `Droppable`
const ConnectedDroppable = connect(
  // returning a function so each component can do its own memoization
  makeMapStateToProps,
  // no dispatch props for droppable
  mapDispatchToProps,
  // We need to assign default props manually because upcoming React version will stop supporting
  // defaultProps on functional components.
  // see: https://github.com/facebook/react/pull/25699
  (
    stateProps: MapProps,
    dispatchProps: DispatchProps,
    ownProps: InternalOwnProps,
  ) => {
    return {
      ...attachDefaultPropsToOwnProps(ownProps),
      ...stateProps,
      ...dispatchProps,
    };
  },
  {
    // Ensuring our context does not clash with consumers
    context: StoreContext as any,

    // Default value: shallowEqual
    // Switching to a strictEqual as we return a memoized object on changes
    areStatePropsEqual: isStrictEqual,
  },
  // FIXME: Typings are really complexe
)(Droppable) as unknown as FunctionComponent<DroppableProps>;

export default ConnectedDroppable;
