import type { Position } from 'css-box-model';
import rafSchd from 'raf-schd';
import { invariant } from '../invariant';
import bindEvents from './event-bindings/bind-events';
import type { UIEventBinding } from './event-bindings/event-types';
import getWindowScroll from './window/get-window-scroll';
import { noop } from '../empty';

type OnWindowScroll = (newScroll: Position) => void;

interface Args {
  onWindowScroll: OnWindowScroll;
}

interface Result {
  start: () => void;
  stop: () => void;
  isActive: () => boolean;
}

function getWindowScrollBinding(update: () => void): UIEventBinding {
  return {
    eventName: 'scroll',
    // ## Passive: true
    // Eventual consistency is fine because we use position: fixed on the item
    // ## Capture: false
    // Scroll events on elements do not bubble, but they go through the capture phase
    // https://twitter.com/alexandereardon/status/985994224867819520
    // Using capture: false here as we want to avoid intercepting droppable scroll requests
    options: { passive: true, capture: false },
    fn: (event: UIEvent) => {
      // IE11 fix
      // All scrollable events still bubble up and are caught by this handler in ie11.
      // On a window scroll the event.target should be the window or the document.
      // If this is not the case then it is not a 'window' scroll event and can be ignored
      if (event.target !== window && event.target !== window.document) {
        return;
      }

      update();
    },
  };
}

export default function getScrollListener({ onWindowScroll }: Args): Result {
  function updateScroll() {
    // letting the update function read the latest scroll when called
    onWindowScroll(getWindowScroll());
  }

  const scheduled = rafSchd(updateScroll);
  const binding: UIEventBinding = getWindowScrollBinding(scheduled);
  let unbind: () => void = noop;

  function isActive(): boolean {
    return unbind !== noop;
  }

  function start() {
    invariant(!isActive(), 'Cannot start scroll listener when already active');
    unbind = bindEvents(window, [binding]);
  }
  function stop() {
    invariant(isActive(), 'Cannot stop scroll listener when not active');
    scheduled.cancel();
    unbind();
    unbind = noop;
  }

  return { start, stop, isActive };
}
