import { memo, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
import { createPortal } from 'react-dom';

import type { CSSProperties, ReactNode, RefObject } from 'react';
import type { TransitionInOut2Api } from '../TransitionInOut2';

import useIsLargeScreen from '~/src/hooks/useIsLargeScreen';
import useKeyboard from '~/src/hooks/useKeyboard';
import darkTheme from '~/src/lib/theme/dark';
import Box from '../Box';
import Loading from '../Loading';
import Scroller2 from '../Scroller2';
import TransitionInOut2 from '../TransitionInOut2';
import { DIALOG_BOX_HEADER_HEIGHT } from './DialogBoxHeader';

export const DIALOG_BOX_CONTENT_PADDING_X = '2rem';
export const DIALOG_BOX_CONTENT_PADDING_Y = '2.2rem';
export const DIALOG_BOX_MAX_HEIGHT = '40rem';

export const DIALOG_TRANSITION_DURATION = 180;

export type CloseDialogBox<TParams = any> = (params?: TParams) => Promise<void>;

export interface DialogBoxApi<TCloseParams = any> {
  close: CloseDialogBox<TCloseParams>;
}

export type DialogBoxRenderHeader<TOnClose extends (params?: any) => void> =
  (params: { close: CloseDialogBox<Parameters<TOnClose>> }) => ReactNode;

export type DialogBoxRenderContent<TOnClose extends (params?: any) => void> =
  (params: {
    close: CloseDialogBox<Parameters<TOnClose>[0]>;
    paddingX: string;
    paddingY: string;
    isOverflowing: boolean;
  }) => ReactNode;

export interface DialogBoxProps<
  TOnClose extends (params?: any) => void = (params?: any) => void,
> {
  onClose: TOnClose;

  renderContent: DialogBoxRenderContent<TOnClose>;

  /**
   * An optional key used to refresh `useKeyboard()` so that
   * new content is processed and any `autoFocus` elements
   * are focused.
   */
  contentKey?: string;

  apiRef?: RefObject<DialogBoxApi | null>;

  /**
   * Render a fixed header that floats above the content.
   *
   * You must provide `headerHeight` also if your header isn't a <DialogBoxHeader>
   * so we can adjust content padding accordingly.
   *
   * You can alternatively wrap your header content in <Sticky> and place it
   * inside the content area if you need more control. But this isn't always
   * suitable if you have top-anchored sticky elements inside the DialogBox
   * content as they could push the header out of the scroll viewport.
   */
  renderHeader?: DialogBoxRenderHeader<TOnClose>;

  /**
   * Provide when renderHeader() content is not a <DialogBoxHeader>
   */
  headerHeight?: string;

  fillViewport?: boolean;
  maxWidth?: string | number;
  maxHeight?: string | number;
  withPaddingX?: boolean;
  fillViewportOnSmallScreen?: boolean;
  withOverflowGradients?: boolean;
  testId?: string;
  zIndex?: number;
  isLoading?: boolean;
  isDisabled?: boolean;
  style?: CSSProperties;
}

const DialogBox = <TOnClose extends (...params: any[]) => void>({
  apiRef,
  onClose,
  renderContent,
  renderHeader,
  contentKey,
  headerHeight,
  fillViewport = false,
  maxHeight,
  maxWidth,
  testId,
  withPaddingX,
  withOverflowGradients = true,
  fillViewportOnSmallScreen,
  zIndex = 1,
  isLoading = false,
  isDisabled = false,
  style,
}: DialogBoxProps<TOnClose>) => {
  const isLargeScreen = useIsLargeScreen();
  const transitionApiRef = useRef<TransitionInOut2Api>(null);
  const rootElRef = useRef<HTMLDivElement>(null);

  const close = useCallback<CloseDialogBox<any>>(
    async (param) => {
      await transitionApiRef.current?.setVisible(false);
      onClose?.(param);
    },
    [onClose]
  );

  useImperativeHandle(
    apiRef,
    () => ({
      close,
    }),
    [close]
  );

  useKeyboard(
    {
      rootElRef,
      onEscape: () => {
        close();
      },
    },
    [contentKey, close]
  );

  if (fillViewportOnSmallScreen) {
    fillViewport = !isLargeScreen;
  }

  const styleInternal: CSSProperties = {
    // dialog text slightly off white - less garish
    color: '#f2f2f2',

    borderRadius: !fillViewport ? '.8rem' : undefined,
    border: !fillViewport ? `solid 1px #333` : undefined,

    // HACK: we were using box-shadow here but it was killing the chrome GPU
    // the Performance Devtools didn't lead to any clues but flipping to filter seems to fix
    filter: !fillViewport
      ? `drop-shadow(0 0 10rem #000) drop-shadow(0 1rem 5rem rgba(0,0,0,.5))`
      : undefined,

    // enable GPU acceleration to fix some rendering issues on iOS Safari (most likely dues to filter above)
    willChange: 'transform',

    // prevent content overflowing rounded corners
    overflow: 'hidden',

    fontSize: '1rem',

    ...style,
  };

  if (isDisabled) {
    styleInternal.opacity = 0.7;
    styleInternal.pointerEvents = 'none';
  }

  // prettier-ignore
  const contentPaddingTop =
    typeof headerHeight !== 'undefined'
      ? headerHeight
      : (renderHeader
      ? DIALOG_BOX_HEADER_HEIGHT
      : undefined);

  const Component = (
    <TransitionInOut2
      nodeRef={rootElRef}
      apiRef={transitionApiRef}
      duration={DIALOG_TRANSITION_DURATION}
      transitionOnMount
      coverParent
      centerContent
      tabIndex={0}
      className="dialogBox"
      padding={!fillViewport ? '2rem' : undefined}
      styleFrom={useMemo(
        () => ({
          transform: 'translateY(1.5rem)',
          opacity: 0,
        }),
        []
      )}
      zIndex={zIndex}
      role="dialog"
      aria-modal="true"
    >
      <Box coverParent onClick={useCallback(() => close(), [close])} />
      <Scroller2
        positionRelative
        isCentered
        fullHeight={fillViewport}
        fullWidth
        maxWidth={!fillViewport ? (maxWidth ?? '42rem') : undefined}
        maxHeight={
          !fillViewport
            ? `min(${maxHeight ?? DIALOG_BOX_MAX_HEIGHT}, 90vh)`
            : undefined
        }
        renderBackground={() => (
          // we render background separately here to fix the issue on some iOS devices
          // where the modal background is sometimes transparent when applied to the
          // same element alongside with filter property
          <Box
            fullWidth
            fullHeight
            style={{ backgroundColor: darkTheme.background }}
          />
        )}
        renderBefore={useCallback(() => {
          return <>{renderHeader?.({ close })}</>;
        }, [renderHeader, close, isLoading])}
        renderAfter={useCallback(() => {
          return (
            <>
              {isLoading && (
                <Loading
                  coverParent
                  zIndex={10}
                  size="3rem"
                  style={{
                    background: 'rgba(0,0,0,0.8)',
                  }}
                />
              )}
            </>
          );
        }, [isLoading])}
        testId={testId}
        withOverflowGradients={withOverflowGradients}
        style={styleInternal}
        renderContent={useCallback(
          ({ contentContainerProps, isOverflowing }) => {
            return (
              <>
                <Box
                  {...contentContainerProps}
                  minHeight="100%"
                  style={{
                    padding: withPaddingX
                      ? `0 ${DIALOG_BOX_CONTENT_PADDING_X}`
                      : undefined,
                    paddingTop: contentPaddingTop,
                  }}
                >
                  {renderContent({
                    close,
                    paddingX: DIALOG_BOX_CONTENT_PADDING_X,
                    paddingY: DIALOG_BOX_CONTENT_PADDING_Y,
                    isOverflowing,
                  })}
                </Box>
              </>
            );
          },
          [renderContent, contentPaddingTop, close]
        )}
      />
      {/* ensure elements under the header scroll into view on tab */}
      <style jsx global>{`
        .dialogBox input {
          ${contentPaddingTop
            ? `scroll-margin: calc(${contentPaddingTop} * 1.1);`
            : ''}
        }
      `}</style>
    </TransitionInOut2>
  );

  return createPortal(Component, document.body);
};

const MemoizedDialogBox = memo(DialogBox) as unknown as typeof DialogBox;

export default MemoizedDialogBox;
