import * as Ariakit from '@ariakit/react';
import { getClassNames } from '@websolutespa/bom-core';
import { forwardRef, ReactNode, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { getVariantKey } from '../../variants';
import { Role } from '../role/role';
import { UIStyledComponentProps } from '../types';
import { getCssResponsive } from '../utils';
import { ModalDialogContext, ModalForceMountContext } from './modal-dialog.context';
import { ModalContext } from './modal.context';
import { IModalVariants, modalVariants } from './modal.variants';

const StyledModalDialog = styled(Ariakit.Dialog) <ModalDialogProps>`
  ${props => {
    const vs = getVariantKey('dialog', props.variant, props.variants);
    // console.log(props.variant, props.variants, vs);
    return vs;
  }}
  ${props => getCssResponsive(props)}
`;

export type ModalDialogPlacement = 'center' | 'top' | 'right' | 'bottom' | 'left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

export type ModalDialogProps = Ariakit.DialogProps & UIStyledComponentProps<{
  placement?: ModalDialogPlacement;
  className?: string;
  variant?: keyof typeof modalVariants | (string & {});
  variants?: IModalVariants;
  children?: ReactNode;
  asChild?: boolean;
  forceMount?: boolean;
  onOpenAutoFocus?: (event: Event) => void;
  onCloseAutoFocus?: (event: Event) => void;
  onEscapeKeyDown?: (event: KeyboardEvent) => void;
  onPointerDownOutside?: (event: Event) => void;
  onInteractOutside?: (event: Event) => void;
}>;

export const ModalDialog = forwardRef<HTMLDivElement, ModalDialogProps>(
  function ModalDialog({
    asChild,
    forceMount,
    onOpenAutoFocus,
    onCloseAutoFocus,
    onEscapeKeyDown,
    onPointerDownOutside,
    onInteractOutside,
    className,
    variant,
    variants,
    placement = 'center',
    ...props
  }, ref) {
    const { store, modal } = useContext(ModalDialogContext)!;
    const forceMountContext = useContext(ModalForceMountContext);
    forceMount = forceMount ?? forceMountContext;
    const mounted = Ariakit.useStoreState(store, (state) => forceMount || state.mounted);
    const contentElement = Ariakit.useStoreState(store, 'contentElement');
    const open = Ariakit.useStoreState(store, 'open');
    const [state, setState] = useState('closed');
    // Sets the state on the next animation frame so that the transition can be
    // triggered.
    useEffect(() => {
      const raf = requestAnimationFrame(() => {
        setState(open ? 'open' : 'closed');
      });
      return () => cancelAnimationFrame(raf);
    }, [open]);
    const classNames = getClassNames('modal__dialog', className, placement);
    const context = useContext(ModalContext);
    if (!mounted) {
      return null;
    }
    return (
      <StyledModalDialog
        {...props}
        className={classNames}
        variant={variant || context.variant}
        variants={variants || context.variants}
        ref={ref}
        store={store}
        modal={modal}
        portal={false}
        backdrop={false}
        data-state={state}
        alwaysVisible={forceMount}
        render={asChild ? <Role /> : <div />}
        autoFocusOnShow={() => {
          const type = 'focusScope.autoFocusOnMount';
          const event = new CustomEvent(type, { cancelable: true });
          const cb = onOpenAutoFocus || (() => { });
          contentElement?.addEventListener(type, cb, { once: true });
          contentElement?.dispatchEvent(event);
          return !event.defaultPrevented;
        }}
        autoFocusOnHide={() => {
          const type = 'focusScope.autoFocusOnUnmount';
          const event = new CustomEvent(type, { cancelable: true });
          const cb = onCloseAutoFocus || (() => { });
          contentElement?.addEventListener(type, cb, { once: true });
          contentElement?.dispatchEvent(event);
          return !event.defaultPrevented;
        }}
        hideOnEscape={(event: KeyboardEvent | React.KeyboardEvent) => {
          if (!onEscapeKeyDown) return true;
          const nativeEvent =
            event instanceof KeyboardEvent ? event : event.nativeEvent;
          onEscapeKeyDown(nativeEvent);
          return !event.defaultPrevented;
        }}
        hideOnInteractOutside={(originalEvent: Event | React.SyntheticEvent) => {
          // Ariakit doesn't hide dialogs on pointer down (although this can be
          // implemented in userland). So we treat onPointerDownOutside and
          // onInteractOutside the same.
          const type = 'dismissableLayer.pointerDownOutside';
          const detail = { originalEvent };
          const event = new CustomEvent(type, { detail, cancelable: true });
          const cb = (evt: Event) => {
            onPointerDownOutside?.(evt);
            onInteractOutside?.(evt);
          };
          contentElement?.addEventListener(type, cb, { once: true });
          contentElement?.dispatchEvent(event);
          return !event.defaultPrevented;
        }}
      />
    );
  }
);

