import { useCallback, useMemo, useRef } from "react";
import { invariant } from "../../../invariant";
import bindEvents from "../../event-bindings/bind-events";
import * as keyCodes from "../../key-codes";
import preventStandardKeyEvents from "./util/prevent-standard-key-events";
import supportedPageVisibilityEventName from "./util/supported-page-visibility-event-name";
import useLayoutEffect from "../../use-isomorphic-layout-effect";
import { noop } from "../../../empty";

// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
export const primaryButton = 0;
export const sloppyClickThreshold = 5;

function isSloppyClickThresholdExceeded(original, current) {
  return (
    Math.abs(current.x - original.x) >= sloppyClickThreshold || Math.abs(current.y - original.y) >= sloppyClickThreshold
  );
}

const idle = {
  type: "IDLE"
};

function getCaptureBindings({ cancel, completed, getPhase, setPhase }) {
  return [
    {
      eventName: "mousemove",
      fn: (event) => {
        const { button, clientX, clientY } = event;
        if (button !== primaryButton) {
          return;
        }
        const point = {
          x: clientX,
          y: clientY
        };
        const phase = getPhase();

        // Already dragging
        if (phase.type === "DRAGGING") {
          // preventing default as we are using this event
          event.preventDefault();
          phase.actions.move(point);
          return;
        }

        // There should be a pending drag at this point
        invariant(phase.type === "PENDING", "Cannot be IDLE");
        const pending = phase.point;

        // threshold not yet exceeded
        if (!isSloppyClickThresholdExceeded(pending, point)) {
          return;
        }

        // preventing default as we are using this event
        event.preventDefault();

        // Lifting at the current point to prevent the draggable item from
        // jumping by the sloppyClickThreshold
        const actions = phase.actions.fluidLift(point);
        setPhase({
          type: "DRAGGING",
          actions
        });
      }
    },
    {
      eventName: "mouseup",
      fn: (event) => {
        const phase = getPhase();
        if (phase.type !== "DRAGGING") {
          cancel();
          return;
        }

        // preventing default as we are using this event
        event.preventDefault();
        phase.actions.drop({
          shouldBlockNextClick: true
        });
        completed();
      }
    },
    {
      eventName: "mousedown",
      fn: (event) => {
        // this can happen during a drag when the user clicks a button
        // other than the primary mouse button
        if (getPhase().type === "DRAGGING") {
          event.preventDefault();
        }
        cancel();
      }
    },
    {
      eventName: "keydown",
      fn: (event) => {
        const phase = getPhase();
        // Abort if any keystrokes while a drag is pending
        if (phase.type === "PENDING") {
          cancel();
          return;
        }

        // cancelling a drag
        if (event.keyCode === keyCodes.escape) {
          event.preventDefault();
          cancel();
          return;
        }
        preventStandardKeyEvents(event);
      }
    },
    {
      eventName: "resize",
      fn: cancel
    },
    {
      eventName: "scroll",
      // kill a pending drag if there is a window scroll
      options: {
        passive: true,
        capture: false
      },
      fn: () => {
        if (getPhase().type === "PENDING") {
          cancel();
        }
      }
    },
    // Need to opt out of dragging if the user is a force press
    // Only for safari which has decided to introduce its own custom way of doing things
    // https://developer.apple.com/library/content/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html
    {
      eventName: "webkitmouseforcedown",
      // it is considered a indirect cancel so we do not
      // prevent default in any situation.
      fn: (event) => {
        const phase = getPhase();
        invariant(phase.type !== "IDLE", "Unexpected phase");
        if (phase.actions.shouldRespectForcePress()) {
          cancel();
          return;
        }

        // This technically doesn't do anything.
        // It won't do anything if `webkitmouseforcewillbegin` is prevented.
        // But it is a good signal that we want to opt out of this

        event.preventDefault();
      }
    },
    // Cancel on page visibility change
    {
      eventName: supportedPageVisibilityEventName,
      fn: cancel
    }
  ];
}

export default function useMouseSensor(api) {
  const phaseRef = useRef(idle);
  const unbindEventsRef = useRef(noop);
  const startCaptureBinding = useMemo(
    () => ({
      eventName: "mousedown",
      fn: function onMouseDown(event) {
        // Event already used
        if (event.defaultPrevented) {
          return;
        }
        // only starting a drag if dragging with the primary mouse button
        if (event.button !== primaryButton) {
          return;
        }

        // Do not start a drag if any modifier key is pressed
        if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
          return;
        }
        const draggableId = api.findClosestDraggableId(event);
        if (!draggableId) {
          return;
        }
        const actions = api.tryGetLock(
          draggableId,
          // stop is defined later
           
          stop,
          {
            sourceEvent: event
          }
        );
        if (!actions) {
          return;
        }

        // consuming the event
        event.preventDefault();
        const point = {
          x: event.clientX,
          y: event.clientY
        };

        // unbind this listener
        unbindEventsRef.current();
        // using this function before it is defined as their is a circular usage pattern
         
        startPendingDrag(actions, point);
      }
    }),
    // not including startPendingDrag as it is not defined initially
    [api]
  );
  const preventForcePressBinding = useMemo(
    () => ({
      eventName: "webkitmouseforcewillbegin",
      fn: (event) => {
        if (event.defaultPrevented) {
          return;
        }
        const id = api.findClosestDraggableId(event);
        if (!id) {
          return;
        }
        const options = api.findOptionsForDraggable(id);
        if (!options) {
          return;
        }
        if (options.shouldRespectForcePress) {
          return;
        }
        if (!api.canGetLock(id)) {
          return;
        }
        event.preventDefault();
      }
    }),
    [api]
  );
  const listenForCapture = useCallback(
    function listenForCapture() {
      const options = {
        passive: false,
        capture: true
      };
      unbindEventsRef.current = bindEvents(window, [preventForcePressBinding, startCaptureBinding], options);
    },
    [preventForcePressBinding, startCaptureBinding]
  );
  const stop = useCallback(() => {
    const current = phaseRef.current;
    if (current.type === "IDLE") {
      return;
    }
    phaseRef.current = idle;
    unbindEventsRef.current();
    listenForCapture();
  }, [listenForCapture]);
  const cancel = useCallback(() => {
    const phase = phaseRef.current;
    stop();
    if (phase.type === "DRAGGING") {
      phase.actions.cancel({
        shouldBlockNextClick: true
      });
    }
    if (phase.type === "PENDING") {
      phase.actions.abort();
    }
  }, [stop]);
  const bindCapturingEvents = useCallback(
    function bindCapturingEvents() {
      const options = {
        capture: true,
        passive: false
      };
      const bindings = getCaptureBindings({
        cancel,
        completed: stop,
        getPhase: () => phaseRef.current,
        setPhase: (phase) => {
          phaseRef.current = phase;
        }
      });
      unbindEventsRef.current = bindEvents(window, bindings, options);
    },
    [cancel, stop]
  );
  const startPendingDrag = useCallback(
    function startPendingDrag(actions, point) {
      invariant(phaseRef.current.type === "IDLE", "Expected to move from IDLE to PENDING drag");
      phaseRef.current = {
        type: "PENDING",
        point,
        actions
      };
      bindCapturingEvents();
    },
    [bindCapturingEvents]
  );
  useLayoutEffect(
    function mount() {
      listenForCapture();

      // kill any pending window events when unmounting
      return function unmount() {
        unbindEventsRef.current();
      };
    },
    [listenForCapture]
  );
}
