import { invariant } from "../../../invariant";
import messagePreset from "../../../screen-reader-message-preset";
import * as timings from "../../../debug/timings";
import getExpiringAnnounce from "./expiring-announce";
import getAsyncMarshal from "./async-marshal";
import { areLocationsEqual, isCombineEqual, isCriticalEqual } from "./is-equal";
import { tryGetCombine, tryGetDestination } from "../../get-impact-location";

const withTimings = (key, fn) => {
  timings.start(key);
  fn();
  timings.finish(key);
};
const getDragStart = (critical, mode) => ({
  draggableId: critical.draggable.id,
  type: critical.droppable.type,
  source: {
    droppableId: critical.droppable.id,
    index: critical.draggable.index
  },
  mode
});
const execute = (responder, data, announce, getDefaultMessage) => {
  if (!responder) {
    announce(getDefaultMessage(data));
    return;
  }
  const willExpire = getExpiringAnnounce(announce);
  const provided = {
    announce: willExpire
  };

  // Casting because we are not validating which data type is going into which responder
  responder(data, provided);
  if (!willExpire.wasCalled()) {
    announce(getDefaultMessage(data));
  }
};
const publisher = (getResponders, announce) => {
  const asyncMarshal = getAsyncMarshal();
  let dragging = null;
  const beforeCapture = (draggableId, mode) => {
    invariant(!dragging, "Cannot fire onBeforeCapture as a drag start has already been published");
    withTimings("onBeforeCapture", () => {
      // No use of screen reader for this responder
      const fn = getResponders().onBeforeCapture;
      if (fn) {
        const before = {
          draggableId,
          mode
        };
        fn(before);
      }
    });
  };
  const beforeStart = (critical, mode) => {
    invariant(!dragging, "Cannot fire onBeforeDragStart as a drag start has already been published");
    withTimings("onBeforeDragStart", () => {
      // No use of screen reader for this responder
      const fn = getResponders().onBeforeDragStart;
      if (fn) {
        fn(getDragStart(critical, mode));
      }
    });
  };
  const start = (critical, mode) => {
    invariant(!dragging, "Cannot fire onBeforeDragStart as a drag start has already been published");
    const data = getDragStart(critical, mode);
    dragging = {
      mode,
      lastCritical: critical,
      lastLocation: data.source,
      lastCombine: null
    };

    // we will flush this frame if we receive any responder updates
    asyncMarshal.add(() => {
      withTimings("onDragStart", () => execute(getResponders().onDragStart, data, announce, messagePreset.onDragStart));
    });
  };

  // Passing in the critical location again as it can change during a drag
  const update = (critical, impact) => {
    const location = tryGetDestination(impact);
    const combine = tryGetCombine(impact);
    invariant(dragging, "Cannot fire onDragMove when onDragStart has not been called");

    // Has the critical changed? Will result in a source change
    const hasCriticalChanged = !isCriticalEqual(critical, dragging.lastCritical);
    if (hasCriticalChanged) {
      dragging.lastCritical = critical;
    }

    // Has the location changed? Will result in a destination change
    const hasLocationChanged = !areLocationsEqual(dragging.lastLocation, location);
    if (hasLocationChanged) {
      dragging.lastLocation = location;
    }
    const hasGroupingChanged = !isCombineEqual(dragging.lastCombine, combine);
    if (hasGroupingChanged) {
      dragging.lastCombine = combine;
    }

    // Nothing has changed - no update needed
    if (!hasCriticalChanged && !hasLocationChanged && !hasGroupingChanged) {
      return;
    }
    const data = {
      ...getDragStart(critical, dragging.mode),
      combine,
      destination: location
    };
    asyncMarshal.add(() => {
      withTimings("onDragUpdate", () =>
        execute(getResponders().onDragUpdate, data, announce, messagePreset.onDragUpdate)
      );
    });
  };
  const flush = () => {
    invariant(dragging, "Can only flush responders while dragging");
    asyncMarshal.flush();
  };
  const drop = (result) => {
    invariant(dragging, "Cannot fire onDragEnd when there is no matching onDragStart");
    dragging = null;
    // not adding to frame marshal - we want this to be done in the same render pass
    // we also want the consumers reorder logic to be in the same render pass
    withTimings("onDragEnd", () => execute(getResponders().onDragEnd, result, announce, messagePreset.onDragEnd));
  };

  // A non user initiated cancel
  const abort = () => {
    // aborting can happen defensively
    if (!dragging) {
      return;
    }
    const result = {
      ...getDragStart(dragging.lastCritical, dragging.mode),
      combine: null,
      destination: null,
      reason: "CANCEL"
    };
    drop(result);
  };
  return {
    beforeCapture,
    beforeStart,
    start,
    update,
    flush,
    drop,
    abort
  };
};

export default publisher;
