import { ReactNode, cloneElement, useCallback, useState } from 'react';

import { assertEmptyObject } from '../../../utils/assertEmptyObject';
import { uuidv4 } from '../../../utils/uuidv4';

import { ToastItem } from './components/ToastItem';
import { DEFAULT_TOAST_VISIBLE_DURATION } from './constants';
import { ToastManagerContextProvider } from './contexts/ToastManagerContext';
import { StyledToastPortal } from './styled';
import { ShowToastThroughManagerOptions, WithToastIdProps } from './types';

/** Props for {@link ToastManager} */
export interface ToastManagerProps {
  children: ReactNode;
}

/**
 * Component that allow to show and hide Toast items.
 *
 * To use Toasts you don't need to write app with this component (because {@link HiveUI} already do it).
 * Just use {@link useToastManager} hook to show any React component in Toast.
 *
 * ```tsx
 * import { useToastManager, ToastProps, Toast, ToastAction } from 'ui-kit';
 *
 * function ButtonThatShouldShowToast() {
 *   const { showToast, hideToast } = useToastManager();
 *
 *   const ToastComponent = ({ ToastId }: ToastProps) => {
 *     return (
 *       <Toast onDismiss={() => hideToast(ToastId)}>
 *         Some very important message, that user should to see.
 *         <ToastAction>Hide</ToastAction>
 *       </Toast>
 *     );
 *   };
 *
 *   const show = () => {
 *     showToast(ToastComponent);
 *   };
 *
 *   return <Button onClick={show}>Show Toast</Button>;
 * }
 * ```
 *
 * ## Show standard toast
 *
 * You can show standard {@link Toast} by {@link useToast} hook.
 *
 * ```tsx
 * import { useToast, ToastAppearance } from 'ui-kit';
 *
 * // Somewhere in the Component
 * const showToast = useToast();
 *
 * showToast('You\'ve wan $100 000 000', {
 *  appearance: ToastAppearance.Success,
 *  action: {
 *    label: 'Send me to PayPal',
 *    onClick: () => {},
 *  },
 * });
 * ```
 */
export function ToastManager(props: ToastManagerProps) {
  const { children, ...rest } = props;
  assertEmptyObject(rest);

  const [Toasts, setToasts] = useState<JSX.Element[]>([]);

  // Remove Toast from controlled Toasts
  const removeToast = useCallback((ToastId: string) => {
    setToasts((current) => {
      const ToastToDeleteIndex = current.findIndex((Toast) => Toast.key === ToastId);
      /* istanbul ignore next */
      if (ToastToDeleteIndex === -1) {
        return current;
      }

      // We use such delete algorithm to get a new array, because array.splice make deletion on the place
      return [...current.slice(0, ToastToDeleteIndex), ...current.slice(ToastToDeleteIndex + 1)];
    });
  }, []);

  // This function just start hiding animation, removing will be finished after hiding animation by
  // removeToast
  const hideToast = useCallback((ToastId: string) => {
    setToasts((current) => {
      const ToastToDeleteIndex = current.findIndex((Toast) => Toast.key === ToastId);
      /* istanbul ignore next */
      if (ToastToDeleteIndex === -1) {
        return current;
      }

      return [
        ...current.slice(0, ToastToDeleteIndex),
        cloneElement(current[ToastToDeleteIndex], { visible: false }),
        ...current.slice(ToastToDeleteIndex + 1),
      ];
    });
  }, []);

  const showToast = useCallback(
    (ToastElement: (props: WithToastIdProps) => JSX.Element, options?: ShowToastThroughManagerOptions) => {
      const ToastId = uuidv4();
      const duration = options?.duration ?? DEFAULT_TOAST_VISIBLE_DURATION;

      setToasts((current) => {
        return [
          ...current,
          <ToastItem key={ToastId} onHidden={() => removeToast(ToastId)} visible={true}>
            <ToastElement toastId={ToastId} />
          </ToastItem>,
        ];
      });

      setTimeout(() => {
        hideToast(ToastId);
      }, duration);
    },
    [hideToast, removeToast],
  );

  return (
    <ToastManagerContextProvider hideToast={hideToast} showToast={showToast}>
      {children}
      <StyledToastPortal>{Toasts}</StyledToastPortal>
    </ToastManagerContextProvider>
  );
}
