import memoizeOne from "memoize-one";
import { connect } from "react-redux";
import Draggable from "./draggable";
import { negate, origin } from "../../state/position";
import isStrictEqual from "../is-strict-equal";
import * as animation from "../../animation";
import { dropAnimationFinished as dropAnimationFinishedAction } from "../../state/action-creators";
import whatIsDraggedOver from "../../state/droppable/what-is-dragged-over";
import StoreContext from "../context/store-context";
import whatIsDraggedOverFromResult from "../../state/droppable/what-is-dragged-over-from-result";
import { tryGetCombine } from "../../state/get-impact-location";

const getCombineWithFromResult = (result) => {
  return result.combine ? result.combine.draggableId : null;
};
const getCombineWithFromImpact = (impact) => {
  return impact.at && impact.at.type === "COMBINE" ? impact.at.combine.draggableId : null;
};

function getDraggableSelector() {
  const memoizedOffset = memoizeOne((x, y) => ({
    x,
    y
  }));
  const getMemoizedSnapshot = memoizeOne((mode, isClone, draggingOver, combineWith, dropping) => ({
    isDragging: true,
    isClone,
    isDropAnimating: Boolean(dropping),
    dropAnimation: dropping,
    mode,
    draggingOver,
    combineWith,
    combineTargetFor: null
  }));
  const getMemoizedProps = memoizeOne(
    (offset, mode, dimension, isClone, draggingOver, combineWith, forceShouldAnimate) => ({
      mapped: {
        type: "DRAGGING",
        dropping: null,
        draggingOver,
        combineWith,
        mode,
        offset,
        dimension,
        forceShouldAnimate,
        snapshot: getMemoizedSnapshot(mode, isClone, draggingOver, combineWith, null)
      }
    })
  );
  const selector = (state, ownProps) => {
    // Dragging
    if (state.isDragging) {
      // not the dragging item
      if (state.critical.draggable.id !== ownProps.draggableId) {
        return null;
      }
      const offset = state.current.client.offset;
      const dimension = state.dimensions.draggables[ownProps.draggableId];
      // const shouldAnimateDragMovement: boolean = state.shouldAnimate;
      const draggingOver = whatIsDraggedOver(state.impact);
      const combineWith = getCombineWithFromImpact(state.impact);
      const forceShouldAnimate = state.forceShouldAnimate;
      return getMemoizedProps(
        memoizedOffset(offset.x, offset.y),
        state.movementMode,
        dimension,
        ownProps.isClone,
        draggingOver,
        combineWith,
        forceShouldAnimate
      );
    }

    // Dropping
    if (state.phase === "DROP_ANIMATING") {
      const completed = state.completed;
      if (completed.result.draggableId !== ownProps.draggableId) {
        return null;
      }
      const isClone = ownProps.isClone;
      const dimension = state.dimensions.draggables[ownProps.draggableId];
      const result = completed.result;
      const mode = result.mode;
      // these need to be pulled from the result as they can be different to the final impact
      const draggingOver = whatIsDraggedOverFromResult(result);
      const combineWith = getCombineWithFromResult(result);
      const duration = state.dropDuration;

      // not memoized as it is the only execution
      const dropping = {
        duration,
        curve: animation.curves.drop,
        moveTo: state.newHomeClientOffset,
        opacity: combineWith ? animation.combine.opacity.drop : null,
        scale: combineWith ? animation.combine.scale.drop : null
      };
      return {
        mapped: {
          type: "DRAGGING",
          offset: state.newHomeClientOffset,
          dimension,
          dropping,
          draggingOver,
          combineWith,
          mode,
          forceShouldAnimate: null,
          snapshot: getMemoizedSnapshot(mode, isClone, draggingOver, combineWith, dropping)
        }
      };
    }
    return null;
  };
  return selector;
}

function getSecondarySnapshot(combineTargetFor) {
  return {
    isDragging: false,
    isDropAnimating: false,
    isClone: false,
    dropAnimation: null,
    mode: null,
    draggingOver: null,
    combineTargetFor,
    combineWith: null
  };
}

const atRest = {
  mapped: {
    type: "SECONDARY",
    offset: origin,
    combineTargetFor: null,
    shouldAnimateDisplacement: true,
    snapshot: getSecondarySnapshot(null)
  }
};

function getSecondarySelector() {
  const memoizedOffset = memoizeOne((x, y) => ({
    x,
    y
  }));
  const getMemoizedSnapshot = memoizeOne(getSecondarySnapshot);
  const getMemoizedProps = memoizeOne((offset, combineTargetFor = null, shouldAnimateDisplacement) => ({
    mapped: {
      type: "SECONDARY",
      offset,
      combineTargetFor,
      shouldAnimateDisplacement,
      snapshot: getMemoizedSnapshot(combineTargetFor)
    }
  }));

  // Is we are the combine target for something then we need to publish that
  // otherwise we will return null to get the default props
  const getFallback = (combineTargetFor) => {
    return combineTargetFor ? getMemoizedProps(origin, combineTargetFor, true) : null;
  };
  const getProps = (ownId, draggingId, impact, afterCritical, overrideDisplacement) => {
    const visualDisplacement = impact.displaced.visible[ownId];
    const isAfterCriticalInVirtualList = Boolean(afterCritical.inVirtualList && afterCritical.effected[ownId]);
    const combine = tryGetCombine(impact);
    const combineTargetFor = combine && combine.draggableId === ownId ? draggingId : null;
    if (!visualDisplacement) {
      if (!isAfterCriticalInVirtualList) {
        return getFallback(combineTargetFor);
      }

      // After critical but not visibly displaced in a virtual list
      // This can occur if:
      // 1. the item is not visible (displaced.invisible)
      // 2. We have moved out of the home list.

      // Don't need to do anything - item is invisible
      if (impact.displaced.invisible[ownId]) {
        return null;
      }

      // We are no longer over the home list.
      // We need to move backwards to close the gap that the dragging item has left
      const change = negate(afterCritical.displacedBy.point);
      const offset = memoizedOffset(change.x, change.y);
      return getMemoizedProps(offset, combineTargetFor, true);
    }
    if (isAfterCriticalInVirtualList) {
      // In a virtual list the removal of a dragging item does
      // not cause the list to collapse. So when something is 'displaced'
      // we can just leave it in the original spot.
      return getFallback(combineTargetFor);
    }
    const displaceBy = overrideDisplacement || impact.displacedBy.point;
    const offset = memoizedOffset(displaceBy.x, displaceBy.y);
    return getMemoizedProps(offset, combineTargetFor, visualDisplacement.shouldAnimate);
  };
  const getDisplacementOverride = (state, ownProps) => {
    if (!state.impact || !state.onLiftImpact || !state.dimensions) return null;
    let impact = state.onLiftImpact;
    if (impact.displaced.all.length < state.impact.displaced.all.length) impact = state.impact;
    const dims = state.dimensions.draggables;
    const displaced = impact.displaced.all;
    const index = displaced.indexOf(ownProps.draggableId);
    if (index > -1 && displaced.length > index + 1 && impact.displaced.visible[ownProps.draggableId]) {
      const ownDimensions = dims[ownProps.draggableId].client.borderBox;
      if (index > 0) {
        const prevDraggableId = displaced[index - 1];
        const prevDraggableDimensions = dims[prevDraggableId].client.borderBox;
        if (prevDraggableDimensions.y !== ownDimensions.y) {
          if (!impact.displaced.visible[ownProps.draggableId].shouldAnimate) {
            return {
              x: ownDimensions.x - prevDraggableDimensions.x,
              y: ownDimensions.y - prevDraggableDimensions.y
            };
          }
        }
      }
      const nextDraggableId = displaced[index + 1];
      const nextDraggableDimensions = dims[nextDraggableId].client.borderBox;
      if (nextDraggableDimensions.y !== ownDimensions.y) {
        if (!impact.displaced.visible[ownProps.draggableId].shouldAnimate)
          return {
            x: impact.displacedBy.point.x,
            y: 0
          };
        return {
          x: nextDraggableDimensions.x - ownDimensions.x,
          y: nextDraggableDimensions.y - ownDimensions.y
        };
      }
    }
    return null;
  };
  const selector = (state, ownProps) => {
    // Dragging
    if (state.isDragging) {
      // we do not care about the dragging item
      if (state.critical.draggable.id === ownProps.draggableId) {
        return null;
      }
      if (state.critical.droppable && state.dimensions.droppables[state.critical.droppable.id].axis.grid) {
        return getProps(
          ownProps.draggableId,
          state.critical.draggable.id,
          state.impact,
          state.afterCritical,
          getDisplacementOverride(state, ownProps)
        );
      }
      return getProps(ownProps.draggableId, state.critical.draggable.id, state.impact, state.afterCritical);
    }

    // Dropping
    if (state.phase === "DROP_ANIMATING") {
      const completed = state.completed;
      // do nothing if this was the dragging item
      if (completed.result.draggableId === ownProps.draggableId) {
        return null;
      }
      if (
        state.completed.critical.droppable &&
        state.dimensions.droppables[state.completed.critical.droppable.id].axis.grid
      ) {
        return getProps(
          ownProps.draggableId,
          completed.result.draggableId,
          completed.impact,
          completed.afterCritical,
          getDisplacementOverride(state, ownProps)
        );
      }
      return getProps(ownProps.draggableId, completed.result.draggableId, completed.impact, completed.afterCritical);
    }

    // Otherwise
    return null;
  };
  return selector;
}

// Returning a function to ensure each
// Draggable gets its own selector
export const makeMapStateToProps = () => {
  const draggingSelector = getDraggableSelector();
  const secondarySelector = getSecondarySelector();
  const selector = (state, ownProps) =>
    draggingSelector(state, ownProps) || secondarySelector(state, ownProps) || atRest;
  return selector;
};
const mapDispatchToProps = {
  dropAnimationFinished: dropAnimationFinishedAction
};

// Leaning heavily on the default shallow equality checking
// that `connect` provides.
// It avoids needing to do it own within `<Draggable />`
const ConnectedDraggable = connect(
  // returning a function so each component can do its own memoization
  makeMapStateToProps,
  mapDispatchToProps,
  // mergeProps: use default
  null,
  // options
  // $FlowFixMe: current react-redux type does not know about context property
  {
    // Using our own context for the store to avoid clashing with consumers
    context: StoreContext,
    // Default value, but being really clear
    // When pure, compares the result of mapStateToProps to its previous value.
    // Default value: shallowEqual
    // Switching to a strictEqual as we return a memoized object on changes
    areStatePropsEqual: isStrictEqual
  }
)(Draggable);
export default ConnectedDraggable;
