import { Directive } from 'vue';

const HANDLERS_PROPERTY = '__v-click-outside';
const HAS_WINDOWS = typeof window !== 'undefined';
const HAS_NAVIGATOR = typeof navigator !== 'undefined';
const IS_TOUCH =
  HAS_WINDOWS &&
  ('ontouchstart' in window ||
    (HAS_NAVIGATOR && (navigator as any).msMaxTouchPoints > 0));
const EVENTS = IS_TOUCH ? ['touchstart'] : ['click'];

const processDirectiveArguments = (bindingValue: any) => {
  const isFunction = typeof bindingValue === 'function';
  if (!isFunction && typeof bindingValue !== 'object') {
    throw new Error(
      'v-click-outside: Binding value must be a function or an object',
    );
  }

  return {
    handler: isFunction ? bindingValue : bindingValue.handler,
    middleware: bindingValue.middleware || ((item: any) => item),
    events: bindingValue.events || EVENTS,
    isActive: !(bindingValue.isActive === false),
  };
};

const onEvent = ({ el, event, handler, middleware }: any) => {
  const path = event.path || (event.composedPath && event.composedPath());
  const isClickOutside = path ? path.indexOf(el) < 0 : !el.contains(event.target);

  if (!isClickOutside) {
    return;
  }

  if (middleware(event)) {
    handler(event);
  }
};

const beforeMount = (el: any, { value }: any) => {
  const { events, handler, middleware, isActive } = processDirectiveArguments(value);
  if (!isActive) {
    return;
  }

  el[HANDLERS_PROPERTY] = events.map((eventName: any) => ({
    event: eventName,
    handler: (event: any) => onEvent({ event, el, handler, middleware }),
  }));

  el[HANDLERS_PROPERTY].forEach(({ event, handler }: any) =>
    setTimeout(() => {
      if (!el[HANDLERS_PROPERTY]) {
        return;
      }
      document.documentElement.addEventListener(event, handler, false);
    }, 0),
  );
};

const unmounted = (el: any) => {
  const handlers = el[HANDLERS_PROPERTY] || [];
  handlers.forEach(({ event, handler }: any) =>
    document.documentElement.removeEventListener(event, handler, false),
  );
  delete el[HANDLERS_PROPERTY];
};

const updated = (el: any, { value, oldValue }: any) => {
  if (JSON.stringify(value) === JSON.stringify(oldValue)) {
    return;
  }
  unmounted(el);
  beforeMount(el, { value });
};

const directive: Directive = {
  beforeMount,
  updated,
  unmounted,
};

export default directive;
