import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import dynamic from 'next/dynamic';

import type { FC, ReactNode } from 'react';
import type { AlertModalProps } from './AlertModal';
import type { ConfirmDialogProps } from './ConfirmDialog';
import type { ToasterProps } from './Toaster';

import withLazyModal from '~/src/lib/hocs/withLazyModal';
import onceIdle from '~/src/lib/utils/onceIdle';

const LazyAlertModal = withLazyModal<AlertModalProps>(
  () => import('./AlertModal')
);

const ConfirmDialogDynamic = dynamic(() => import('./ConfirmDialog'), {
  ssr: false,
});

const ToasterDynamic = dynamic(() => import('./Toaster'), { ssr: false });

const AppLoadingDynamic = dynamic(() => import('./AppLoading'), {
  ssr: false,
});

export type AlertParams = Omit<AlertModalProps, 'onClose'>;
export type ConfirmParams = Omit<
  ConfirmDialogProps,
  'onConfirm' | 'onClose' | 'onCancel'
> & { onConfirm?: () => void | Promise<void> };
export type ToastParams = ToasterProps['item'];

interface CoreUiContextValue {
  alert: (params: AlertParams) => void;
  confirm: (params: ConfirmParams) => Promise<boolean>;
  toast: (params: ToastParams) => void;
  setIsLoading: (value: boolean) => void;
}

const CoreUiContext = createContext<CoreUiContextValue>({} as any);
let appLoadingCounter = 0;

export const CoreUiProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const [alertParams, setAlertParams] = useState<AlertParams>();
  const [confirmProps, setConfirmProps] = useState<ConfirmDialogProps>();
  const [toastParams, setToastParams] = useState<ToastParams>();
  const [appLoading, setAppLoading] = useState<undefined | true | false>();

  return (
    <CoreUiContext.Provider
      value={useMemo<CoreUiContextValue>(
        () => ({
          alert: setAlertParams,

          toast: (params) => {
            // don't show toast when main-thread is busy to avoid dropping frames
            onceIdle(() => setToastParams(params));
          },

          confirm: (params) =>
            new Promise<boolean>((resolve) => {
              setConfirmProps({
                ...params,

                onClose: (params) => {
                  setConfirmProps(undefined);
                  resolve(!!params?.confirmed);
                },
              });
            }),

          /**
           * Request app to show/hide loading indicator.
           *
           * We use a counter here to keep track of how many requests for
           * loading have been made. We only hide the indicator once all
           * clients have requested to hide/stop loading.
           */
          setIsLoading: (value: boolean) => {
            if (value) {
              appLoadingCounter++;
              setAppLoading(true);
            } else {
              appLoadingCounter = Math.max(0, appLoadingCounter - 1);
              if (!appLoadingCounter) setAppLoading(false);
            }
          },
        }),
        []
      )}
    >
      {children}
      {appLoading !== undefined && <AppLoadingDynamic visible={appLoading} />}
      {confirmProps && <ConfirmDialogDynamic {...confirmProps} />}
      <LazyAlertModal
        // stack above <ConfirmDialog>
        zIndex={2}
        {...alertParams}
        isOpen={!!alertParams}
        onClose={useCallback(() => setAlertParams(undefined), [])}
      />
      {toastParams && (
        <ToasterDynamic
          // stack above <Alert> and <ConfirmDialog>
          zIndex={3}
          item={toastParams}
          onHidden={() => setToastParams(undefined)}
        />
      )}
    </CoreUiContext.Provider>
  );
};

export const useAppAlert = () => useContext(CoreUiContext).alert;
export const useAppToast = () => useContext(CoreUiContext).toast;
export const useAppConfirm = () => useContext(CoreUiContext).confirm;

export const useAppLoading = () => {
  const setIsLoading = useContext(CoreUiContext).setIsLoading;

  // used to keep reference of last known loading request state
  const isLoadingRef = useRef(false);

  // always request hide on unmount
  useEffect(
    () => () => {
      if (isLoadingRef.current) {
        setIsLoading(false);
      }
    },
    []
  );

  // Return a function caller can use to set app loading state.
  // Write to ensure that repeat requests are ignored and setIsLoading
  // is only called when the value *changes*
  return useCallback((isLoading: boolean | undefined) => {
    // don't request show if already requested
    if (isLoading && !isLoadingRef.current) {
      setIsLoading(true);
      isLoadingRef.current = true;
      // don't request hide if already requested
    } else if (!isLoading && isLoadingRef.current) {
      setIsLoading(false);
      isLoadingRef.current = false;
    }
  }, []);
};
