import {
  cloneElement,
  createElement,
  useRef,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from 'react';
import { useLayers } from './context';
import { Modal, ModalPropsType } from './Modal';
import { ModalContainer } from 'components/modals';
import { ErrorBoundary } from 'components/ErrorBoundary';
import { Exactly, PickPartial, RequiredKeys } from 'types';

type ContainerProps = Omit<ModalPropsType, 'children'>;

interface ModalOptions<P> extends Partial<ContainerProps> {
  defaultProps?: Partial<P>;
}

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

const modalFallback = (error: Error) => (
  <ModalContainer title="Error">{error.message}</ModalContainer>
);

const ModalBoundary = ({ children, ...props }: ModalPropsType) => {
  return (
    <Modal {...props}>
      <ErrorBoundary fallbackRender={modalFallback}>{children}</ErrorBoundary>
    </Modal>
  );
};

export function useModal<T extends React.ComponentType<any>>(
  component: T,
  modalOptions?: undefined | ModalOptions<undefined>
): {
  show: RequiredKeys<React.ComponentProps<T>> extends never
    ? (componentProps?: React.ComponentProps<T>) => void
    : (componentProps: React.ComponentProps<T>) => void;
  hide: () => void;
  isOpen: boolean;
};

export function useModal<
  T extends React.ComponentType<any>,
  P extends Exactly<React.ComponentProps<T>, P>
>(
  component: T,
  modalOptions?: ModalOptions<P>
): {
  show: OnDismiss & P extends React.ComponentProps<T>
    ? (
        componentProps?: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void
    : (
        componentProps: PickPartial<React.ComponentProps<T>, OnDismiss & P>
      ) => void;
  hide: () => void;
  isOpen: boolean;
};

export function useModal<
  T extends React.ComponentType<any>,
  P extends Exactly<React.ComponentProps<T>, P>
>(component: T, modalOptions?: ModalOptions<P>) {
  const [open, setOpen] = useState(false);
  const layer = useLayers();
  const layerRef = useRef<string | null>();

  const hideLayer = layer.hide;
  const showLayer = layer.show;

  const componentRef = useRef(component);
  const modalOptionsRef = useRef(modalOptions);

  useEffect(() => {
    componentRef.current = component;
    modalOptionsRef.current = modalOptions;
  }, [component, modalOptions]);

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

  const showModal = useCallback(
    (componentProps?: Partial<React.ComponentProps<T>>) => {
      if (layerRef.current) return;

      const currentComponent = componentRef.current;

      const { defaultProps, ...modalProps } = modalOptionsRef.current || {};

      const modalComponent = createElement(currentComponent, {
        onDismiss: hideModal,
        ...defaultProps,
        ...componentProps,
      });

      const containerProps: ContainerProps = Object.assign(
        { onDismiss: hideModal },
        modalProps
      );

      const modal = cloneElement(
        createElement(ModalBoundary),
        containerProps,
        modalComponent
      );

      layerRef.current = showLayer(modal);
      setOpen(true);
    },
    [showLayer, hideModal]
  );

  return useMemo(
    () => ({ show: showModal, hide: hideModal, isOpen: open }),
    [showModal, hideModal, open]
  );
}
