import {
  createElement,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useLayers } from './context';
import { PopupController, PopupControllerProps } from './popup/PopupController';

import { Exactly, PickPartial, RequiredKeys } from 'types';

const POPUP_DEFAULT_PROPS: {
  trigger: 'click' | 'hover';
} = {
  trigger: 'click',
};

interface OnDismiss {
  onDismiss: () => void;
}

type PartialPopupControllerProps = Omit<
  PopupControllerProps,
  'anchor' | 'component' | 'onDismiss'
>;

// interface PopupOptions<T extends React.ComponentType<any>>
//   extends PartialPopupControllerProps {
//   defaultProps?: Partial<React.ComponentProps<T>>;
// }

interface PopupOptions<P> extends PartialPopupControllerProps {
  defaultProps?: Partial<P>;
}

export function usePopup<T extends React.ComponentType<any>>(
  component: T,
  modalOptions?: PopupOptions<undefined>
): {
  show: RequiredKeys<React.ComponentProps<T>> extends never
    ? (anchor: Element, componentProps?: React.ComponentProps<T>) => void
    : (anchor: Element, componentProps: React.ComponentProps<T>) => void;
  hide: () => void;
  toggle: RequiredKeys<React.ComponentProps<T>> extends never
    ? (anchor: Element, componentProps?: React.ComponentProps<T>) => void
    : (anchor: Element, componentProps: React.ComponentProps<T>) => void;
  bind: RequiredKeys<React.ComponentProps<T>> extends never
    ? (componentProps?: React.ComponentProps<T>) => void
    : (componentProps: React.ComponentProps<T>) => void;
  update: (componentProps?: React.ComponentProps<T>) => void;
  isOpen: boolean;
};

export function usePopup<
  T extends React.ComponentType<any>,
  P extends Exactly<React.ComponentProps<T>, P>
>(
  component: T,
  modalOptions?: PopupOptions<P>
): {
  show: OnDismiss & P extends React.ComponentProps<T>
    ? (
        anchor: Element,
        componentProps?: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void
    : (
        anchor: Element,
        componentProps: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void;
  hide: () => void;
  toggle: OnDismiss & P extends React.ComponentProps<T>
    ? (
        anchor: Element,
        componentProps?: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void
    : (
        anchor: Element,
        componentProps: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void;
  bind: OnDismiss & P extends React.ComponentProps<T>
    ? (
        componentProps?: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void
    : (
        componentProps: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void;
  update: (componentProps?: React.ComponentProps<T>) => void;
  isOpen: boolean;
};

export function usePopup<
  T extends React.ComponentType<any>,
  P extends Exactly<React.ComponentProps<T>, P>
>(component: T, options?: PopupOptions<P>) {
  const { show, hide } = useLayers();
  const [open, setOpen] = useState(false);
  const layerRef = useRef<string | null>();
  const popupRef = useRef<HTMLDivElement>(null);
  const anchorRef = useRef<Element | null>(null);

  const componentRef = useRef(component);
  const popupPropsRef = useRef(options);

  useEffect(() => {
    componentRef.current = component;
    popupPropsRef.current = options;
  }, [component, options]);

  const hidePopup = useCallback(() => {
    if (layerRef.current) {
      hide(layerRef.current);
      layerRef.current = null;
      setOpen(false);
    }
  }, [hide]);

  const createPopup = useCallback(
    (
      anchor: Element,
      componentProps?: React.ComponentProps<typeof component>
    ) => {
      const currentComponent = componentRef.current;

      const { defaultProps, ...popupProps } = popupPropsRef.current || {};

      const popupComponent = createElement(currentComponent, {
        onDismiss: hidePopup,
        ...defaultProps,
        ...componentProps,
      });

      const popupControllerProps = {
        ...POPUP_DEFAULT_PROPS,
        ...popupProps,
      };

      const popup = (
        <PopupController
          ref={popupRef}
          anchor={anchor}
          component={popupComponent}
          onDismiss={hidePopup}
          {...popupControllerProps}
        />
      );

      return popup;
    },
    [hidePopup]
  );

  const showPopup = useCallback(
    (anchor: Element, componentProps?: React.ComponentProps<T>) => {
      if (!layerRef.current) {
        anchorRef.current = anchor;

        const popup = createPopup(anchor, componentProps);

        layerRef.current = show(popup);
        setOpen(true);
      }
    },
    [show, createPopup]
  );

  const togglePopup = useCallback(
    (target: Element, componentProps?: React.ComponentProps<T>) => {
      if (layerRef.current) {
        hidePopup();
      } else {
        showPopup(target, componentProps);
      }
    },
    [showPopup, hidePopup]
  );

  const bindPopup = useCallback(
    (componentProps?: React.ComponentProps<T>) => {
      const popupControllerProps = {
        ...POPUP_DEFAULT_PROPS,
        ...popupPropsRef.current,
      };
      const { trigger } = popupControllerProps;
      if (trigger === 'hover') {
        return {
          onMouseEnter: (ev: React.MouseEvent) => {
            showPopup(ev.currentTarget, componentProps);
          },
          onMouseLeave: (ev: React.MouseEvent) => {
            if (
              popupRef.current &&
              popupRef.current.contains(ev.relatedTarget as Node)
            ) {
              return;
            }
            hidePopup();
          },
        };
      } else {
        return {
          onClick: (ev: React.MouseEvent) =>
            togglePopup(ev.currentTarget, componentProps),
        };
      }
    },
    [showPopup, hidePopup, togglePopup]
  );

  const updatePopup = useCallback(
    (componentProps?: React.ComponentProps<T>) => {
      if (!layerRef.current || !anchorRef.current) return;
      const popup = createPopup(anchorRef.current, componentProps);

      show(popup, layerRef.current);
    },
    [show, createPopup]
  );

  useEffect(() => hidePopup, [hidePopup]);

  const obj = useMemo(
    () => ({
      show: showPopup,
      hide: hidePopup,
      toggle: togglePopup,
      bind: bindPopup,
      update: updatePopup,
      isOpen: open,
    }),
    [showPopup, hidePopup, togglePopup, bindPopup, updatePopup, open]
  );

  return obj;

  // return [togglePopup, bindPopup] as const;
}
