import { invariant } from "../invariant";
import scrollDroppable from "./droppable/scroll-droppable";
import moveInDirection from "./move-in-direction";
import { add, isEqual, origin } from "./position";
import scrollViewport from "./scroll-viewport";
import isMovementAllowed from "./is-movement-allowed";
import { toDroppableList } from "./dimension-structures";
import update from "./post-reducer/when-moving/update";
import refreshSnap from "./post-reducer/when-moving/refresh-snap";
import getLiftEffect from "./get-lift-effect";
import patchDimensionMap from "./patch-dimension-map";
import publishWhileDraggingInVirtual from "./publish-while-dragging-in-virtual";

const isSnapping = (state) => state.movementMode === "SNAP";
const postDroppableChange = (state, updated, isEnabledChanging) => {
  const dimensions = patchDimensionMap(state.dimensions, updated);

  // if the enabled state is changing, we need to force a update
  if (!isSnapping(state) || isEnabledChanging) {
    return update({
      state,
      dimensions
    });
  }
  return refreshSnap({
    state,
    dimensions
  });
};

function removeScrollJumpRequest(state) {
  if (state.isDragging && state.movementMode === "SNAP") {
    return {
      // will be overwritten by spread
      // needed for flow
      phase: "DRAGGING",
      ...state,
      scrollJumpRequest: null
    };
  }
  return state;
}

const idle = {
  phase: "IDLE",
  completed: null,
  shouldFlush: false
};
const reducer = (state = idle, action) => {
  if (action.type === "FLUSH") {
    return {
      ...idle,
      shouldFlush: true
    };
  }
  if (action.type === "INITIAL_PUBLISH") {
    invariant(state.phase === "IDLE", "INITIAL_PUBLISH must come after a IDLE phase");
    const { critical, clientSelection, viewport, dimensions, movementMode } = action.payload;
    const draggable = dimensions.draggables[critical.draggable.id];
    const home = dimensions.droppables[critical.droppable.id];
    const client = {
      selection: clientSelection,
      borderBoxCenter: draggable.client.borderBox.center,
      offset: origin
    };
    const initial = {
      client,
      page: {
        selection: add(client.selection, viewport.scroll.initial),
        borderBoxCenter: add(client.selection, viewport.scroll.initial),
        offset: add(client.selection, viewport.scroll.diff.value)
      }
    };

    // Can only auto scroll the window if every list is not fixed on the page
    const isWindowScrollAllowed = toDroppableList(dimensions.droppables).every((item) => !item.isFixedOnPage);
    const { impact, afterCritical } = getLiftEffect({
      draggable,
      home,
      draggables: dimensions.draggables,
      viewport
    });
    const result = {
      phase: "DRAGGING",
      isDragging: true,
      critical,
      movementMode,
      dimensions,
      initial,
      current: initial,
      isWindowScrollAllowed,
      impact,
      afterCritical,
      onLiftImpact: impact,
      viewport,
      scrollJumpRequest: null,
      forceShouldAnimate: null
    };
    return result;
  }
  if (action.type === "COLLECTION_STARTING") {
    // A collection might have restarted. We do not care as we are already in the right phase
    // TODO: remove?
    if (state.phase === "COLLECTING" || state.phase === "DROP_PENDING") {
      return state;
    }
    invariant(state.phase === "DRAGGING", `Collection cannot start from phase ${state.phase}`);
    const result = {
      // putting phase first to appease flow
      phase: "COLLECTING",
      ...state,
      // eslint-disable-next-line
      phase: "COLLECTING"
    };
    return result;
  }
  if (action.type === "PUBLISH_WHILE_DRAGGING") {
    // Unexpected bulk publish
    invariant(
      state.phase === "COLLECTING" || state.phase === "DROP_PENDING",
      `Unexpected ${action.type} received in phase ${state.phase}`
    );
    return publishWhileDraggingInVirtual({
      state,
      published: action.payload
    });
  }
  if (action.type === "MOVE") {
    // Not allowing any more movements
    if (state.phase === "DROP_PENDING") {
      return state;
    }
    invariant(isMovementAllowed(state), `${action.type} not permitted in phase ${state.phase}`);
    const { client: clientSelection } = action.payload;

    // nothing needs to be done
    if (isEqual(clientSelection, state.current.client.selection)) {
      return state;
    }
    return update({
      state,
      clientSelection,
      // If we are snap moving - manual movements should not update the impact
      impact: isSnapping(state) ? state.impact : null
    });
  }
  if (action.type === "UPDATE_DROPPABLE_SCROLL") {
    // Not allowing changes while a drop is pending
    // Cannot get this during a DROP_ANIMATING as the dimension
    // marshal will cancel any pending scroll updates
    if (state.phase === "DROP_PENDING") {
      return removeScrollJumpRequest(state);
    }

    // We will be updating the scroll in response to dynamic changes
    // manually on the droppable so we can ignore this change
    if (state.phase === "COLLECTING") {
      return removeScrollJumpRequest(state);
    }
    invariant(isMovementAllowed(state), `${action.type} not permitted in phase ${state.phase}`);
    const { id, newScroll } = action.payload;
    const target = state.dimensions.droppables[id];

    // This is possible if a droppable has been asked to watch scroll but
    // the dimension has not been published yet
    if (!target) {
      return state;
    }
    const scrolled = scrollDroppable(target, newScroll);
    return postDroppableChange(state, scrolled, false);
  }
  if (action.type === "UPDATE_DROPPABLE_IS_ENABLED") {
    // Things are locked at this point
    if (state.phase === "DROP_PENDING") {
      return state;
    }
    invariant(isMovementAllowed(state), `Attempting to move in an unsupported phase ${state.phase}`);
    const { id, isEnabled } = action.payload;
    const target = state.dimensions.droppables[id];
    invariant(target, `Cannot find Droppable[id: ${id}] to toggle its enabled state`);
    invariant(
      target.isEnabled !== isEnabled,
      `Trying to set droppable isEnabled to ${String(isEnabled)}
      but it is already ${String(target.isEnabled)}`
    );
    const updated = {
      ...target,
      isEnabled
    };
    return postDroppableChange(state, updated, true);
  }
  if (action.type === "UPDATE_DROPPABLE_IS_COMBINE_ENABLED") {
    // Things are locked at this point
    if (state.phase === "DROP_PENDING") {
      return state;
    }
    invariant(isMovementAllowed(state), `Attempting to move in an unsupported phase ${state.phase}`);
    const { id, isCombineEnabled } = action.payload;
    const target = state.dimensions.droppables[id];
    invariant(target, `Cannot find Droppable[id: ${id}] to toggle its isCombineEnabled state`);
    invariant(
      target.isCombineEnabled !== isCombineEnabled,
      `Trying to set droppable isCombineEnabled to ${String(isCombineEnabled)}
      but it is already ${String(target.isCombineEnabled)}`
    );
    const updated = {
      ...target,
      isCombineEnabled
    };
    return postDroppableChange(state, updated, true);
  }
  if (action.type === "MOVE_BY_WINDOW_SCROLL") {
    // No longer accepting changes
    if (state.phase === "DROP_PENDING" || state.phase === "DROP_ANIMATING") {
      return state;
    }
    invariant(isMovementAllowed(state), `Cannot move by window in phase ${state.phase}`);
    invariant(state.isWindowScrollAllowed, "Window scrolling is currently not supported for fixed lists");
    const newScroll = action.payload.newScroll;

    // nothing needs to be done
    if (isEqual(state.viewport.scroll.current, newScroll)) {
      return removeScrollJumpRequest(state);
    }
    const viewport = scrollViewport(state.viewport, newScroll);
    if (isSnapping(state)) {
      return refreshSnap({
        state,
        viewport
      });
    }
    return update({
      state,
      viewport
    });
  }
  if (action.type === "UPDATE_VIEWPORT_MAX_SCROLL") {
    // Could occur if a transitionEnd occurs after a drag ends
    if (!isMovementAllowed(state)) {
      return state;
    }
    const maxScroll = action.payload.maxScroll;
    if (isEqual(maxScroll, state.viewport.scroll.max)) {
      return state;
    }
    const withMaxScroll = {
      ...state.viewport,
      scroll: {
        ...state.viewport.scroll,
        max: maxScroll
      }
    };

    // don't need to recalc any updates
    return {
      // phase will be overridden - appeasing flow
      phase: "DRAGGING",
      ...state,
      viewport: withMaxScroll
    };
  }
  if (
    action.type === "MOVE_UP" ||
    action.type === "MOVE_DOWN" ||
    action.type === "MOVE_LEFT" ||
    action.type === "MOVE_RIGHT"
  ) {
    // Not doing keyboard movements during these phases
    if (state.phase === "COLLECTING" || state.phase === "DROP_PENDING") {
      return state;
    }
    invariant(state.phase === "DRAGGING", `${action.type} received while not in DRAGGING phase`);
    const result = moveInDirection({
      state,
      type: action.type
    });

    // cannot move in that direction
    if (!result) {
      return state;
    }
    return update({
      state,
      impact: result.impact,
      clientSelection: result.clientSelection,
      scrollJumpRequest: result.scrollJumpRequest
    });
  }
  if (action.type === "DROP_PENDING") {
    const reason = action.payload.reason;
    invariant(state.phase === "COLLECTING", "Can only move into the DROP_PENDING phase from the COLLECTING phase");
    const newState = {
      // appeasing flow
      phase: "DROP_PENDING",
      ...state,
      // eslint-disable-next-line
      phase: "DROP_PENDING",
      isWaiting: true,
      reason
    };
    return newState;
  }
  if (action.type === "DROP_ANIMATE") {
    const { completed, dropDuration, newHomeClientOffset } = action.payload;
    invariant(
      state.phase === "DRAGGING" || state.phase === "DROP_PENDING",
      `Cannot animate drop from phase ${state.phase}`
    );

    // Moving into a new phase
    const result = {
      phase: "DROP_ANIMATING",
      completed,
      dropDuration,
      newHomeClientOffset,
      dimensions: state.dimensions
    };
    return result;
  }

  // Action will be used by responders to call consumers
  // We can simply return to the idle state
  if (action.type === "DROP_COMPLETE") {
    const { completed } = action.payload;
    return {
      phase: "IDLE",
      completed,
      shouldFlush: false
    };
  }
  return state;
};

export default reducer;
