// SPDX-License-Identifier: MIT
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { type Ref, useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { css, cx, keyframes } from "@emotion/css"
import { lock, unlock } from "tua-body-scroll-lock"
import { token } from "~/options/theme"
import { FocusTrap } from "./FocusTrap"

const useScrollLock = (
  refModal: React.RefObject<HTMLElement>,
  open: boolean,
  showPortal: boolean,
  blockScroll: boolean,
) => {
  const oldRef = useRef<HTMLElement | null>(null)

  useEffect(() => {
    if (open && refModal.current && blockScroll) {
      oldRef.current = refModal.current
      unlock(refModal.current)
    }
    return () => {
      if (oldRef.current) {
        lock(oldRef.current)
        oldRef.current = null
      }
    }
  }, [open, showPortal, refModal, blockScroll])
}

let modals: Ref<Element>[] = []

function useModal(ref: Ref<Element>, open: boolean) {
  useEffect(() => {
    if (open) {
      modals.push(ref)
    }
    return () => {
      modals = modals.filter(modal => modal !== ref)
    }
  }, [open, ref])
}

const overlayAnimationIn = keyframes`
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
`
const overlayAnimationOut = keyframes`
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
`
const modalAnimationIn = keyframes`
  0% {
    transform: scale(0.96);
    opacity: 0;
  }
  100% {
    transform: scale(100%);
    opacity: 1;
  }
`
const modalAnimationOut = keyframes`
  0% {
    transform: scale(100%);
    opacity: 1;
  }
  100% {
    transform: scale(0.96);
    opacity: 0;
  }
`

export interface ModalProps {
  /**
   * Control if the modal is open or not.
   */
  open: boolean
  /**
   * Should the dialog be centered.
   *
   * Default to false.
   */
  center?: boolean
  className?: string
  /**
   * Is the modal closable when user press esc key.
   *
   * Default to true.
   */
  closeOnEsc?: boolean
  /**
   * Is the modal closable when user click on overlay.
   *
   * Default to true.
   */
  closeOnOverlayClick?: boolean
  /**
   * Whether to block scrolling when dialog is open.
   *
   * Default to true.
   */
  blockScroll?: boolean
  /**
   * Show the close icon.
   *
   * Default to true.
   */
  showCloseIcon?: boolean
  /**
   * id attribute for the close icon button.
   */
  closeIconId?: string
  /**
   * Custom icon to render (svg, img, etc...).
   */
  closeIcon?: React.ReactNode
  /**
   * When the modal is open, trap focus within it.
   *
   * Default to true.
   */
  focusTrapped?: boolean
  /**
   * Element to focus when focus trap is used.
   *
   * Default to undefined.
   */
  initialFocusRef?: React.RefObject<HTMLElement>
  /**
   * You can specify a container prop which should be of type `Element`.
   * The portal will be rendered inside that element.
   * The default behavior will create a div node and render it at the at the end of document.body.
   */
  container?: Element | null
  /**
   * An object containing classNames or keyframe name to style the modal.
   */
  classNames?: {
    overlay?: string
    overlayAnimationIn?: string
    overlayAnimationOut?: string
    modalContainer?: string
    modal?: string
    modalAnimationIn?: string
    modalAnimationOut?: string
    closeButton?: string
    closeIcon?: string
  }
  /**
   * Animation duration in milliseconds.
   *
   * Default to 300.
   */
  animationDuration?: number
  /**
   * ARIA role for modal
   *
   * Default to 'dialog'.
   */
  role?: string
  /**
   * ARIA label for modal
   */
  ariaLabelledby?: string
  /**
   * ARIA description for modal
   */
  ariaDescribedby?: string
  /**
   * id attribute for modal container
   */
  containerId?: string
  /**
   * id attribute for modal
   */
  modalId?: string
  /**
   * Callback fired when the Modal is requested to be closed by a click on the overlay or when user press esc key.
   */
  onClose: () => void
  /**
   * Callback fired when the escape key is pressed.
   */
  onEscKeyDown?: (event: KeyboardEvent) => void
  /**
   * Callback fired when the overlay is clicked.
   */
  onOverlayClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void

  children?: React.ReactNode
}

export const Modal = (props: ModalProps) => {
  const {
    open,
    center,
    blockScroll = true,
    closeOnEsc = true,
    closeOnOverlayClick = true,
    container,
    className,
    classNames,
    showCloseIcon = true,
    closeIconId,
    closeIcon,
    focusTrapped = true,
    initialFocusRef = undefined,
    animationDuration = 300,
    role = "dialog",
    ariaDescribedby,
    ariaLabelledby,
    containerId,
    modalId,
    onClose,
    onEscKeyDown,
    onOverlayClick,
    children,
  } = props
  const ref = useRef<HTMLDivElement>(null)
  const refModal = useRef<HTMLDivElement>(null)
  const refShouldClose = useRef<boolean | null>(null)
  const refContainer = useRef<HTMLDivElement | null>(null)
  // Lazily create the ref instance
  // https://reactjs.org/docs/hooks-faq.html#how-to-create-expensive-objects-lazily
  if (refContainer.current === null && typeof document !== "undefined") {
    refContainer.current = document.createElement("div")
  }

  // The value should be false for srr, that way when the component is hydrated client side,
  // it will match the server rendered content
  const [showPortal, setShowPortal] = useState(false)

  // Hook used to manage multiple modals opened at the same time
  useModal(refModal, open)

  // Hook used to manage the scroll
  useScrollLock(refModal, open, showPortal, blockScroll)

  const handleOpen = () => {
    if (
      refContainer.current &&
      !container &&
      !document.body.contains(refContainer.current)
    ) {
      document.body.appendChild(refContainer.current)
    }

    document.addEventListener("keydown", handleKeydown)
  }

  const handleClose = () => {
    if (
      refContainer.current &&
      !container &&
      document.body.contains(refContainer.current)
    ) {
      document.body.removeChild(refContainer.current)
    }
    document.removeEventListener("keydown", handleKeydown)
  }

  const handleKeydown = (event: KeyboardEvent) => {
    // Only the last modal need to be escaped when pressing the esc key
    if (event.keyCode !== 27 || modals.at(-1) !== refModal) {
      return
    }

    onEscKeyDown?.(event)

    if (closeOnEsc) {
      onClose()
    }
  }

  useEffect(
    () => () => {
      if (showPortal) {
        // When the modal is closed or removed directly, cleanup the listeners
        handleClose()
      }
    },
    [showPortal],
  )

  useEffect(() => {
    // If the open prop is changing, we need to open the modal
    // This is also called on the first render if the open prop is true when the modal is created
    if (open && !showPortal) {
      setShowPortal(true)
      handleOpen()
    }
  }, [open])

  const handleClickOverlay = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    if (refShouldClose.current === null) {
      refShouldClose.current = true
    }

    if (!refShouldClose.current) {
      refShouldClose.current = null
      return
    }

    onOverlayClick?.(event)

    if (closeOnOverlayClick) {
      onClose()
    }

    refShouldClose.current = null
  }

  const handleModalEvent = () => {
    refShouldClose.current = false
  }

  const containerModal = container ?? refContainer.current

  const overlayAnimation = open
    ? classNames?.overlayAnimationIn ?? overlayAnimationIn
    : classNames?.overlayAnimationOut ?? overlayAnimationOut

  const modalAnimation = open
    ? classNames?.modalAnimationIn ?? modalAnimationIn
    : classNames?.modalAnimationOut ?? modalAnimationOut

  if (!showPortal || !containerModal) {
    return null
  }

  return createPortal(
    <div
      className={cx(
        css`
          position: fixed;
          top: 0;
          bottom: 0;
          left: 0;
          right: 0;
          z-index: 1000;
        `,
        className,
      )}
      data-testid="root"
    >
      <div
        className={cx(
          css`
            background: rgba(0, 0, 0, 0.5);
            position: fixed;
            top: 0;
            bottom: 0;
            left: 0;
            right: 0;
            z-index: -1;
            animation-fill-mode: forwards !important;
          `,
          classNames?.overlay,
        )}
        data-testid="overlay"
        aria-hidden={true}
        style={{
          animation: `${overlayAnimation} ${animationDuration}ms`,
        }}
      />
      <div
        ref={refModal}
        id={containerId}
        className={cx(
          css`
            height: 100%;
            outline: 0;
            overflow-x: hidden;
            overflow-y: auto;
            text-align: center;
            /* Used to fix a screen glitch issues with the animation see https://github.com/pradel/react-responsive-modal/issues/495 */
            animation-fill-mode: forwards !important;
          `,
          center &&
            css`
              &:after {
                width: 0;
                height: 100%;
                content: "";
                display: inline-block;
                vertical-align: middle;
              }
            `,
          classNames?.modalContainer,
        )}
        data-testid="modal-container"
        onClick={handleClickOverlay}
      >
        <div
          ref={ref}
          className={cx(
            css`
              max-width: 800px;
              display: inline-block;
              text-align: left;
              vertical-align: middle;
              background: #ffffff;
              box-shadow: 0 12px 15px 0 rgba(0, 0, 0, 0.25);
              margin: 1.2rem;
              padding: 1.2rem;
              position: relative;
              overflow-y: auto;
              animation-fill-mode: forwards !important;

              background: ${token.canvas.default};
              border-radius: 5px;

              body.dark & {
                border: 1px solid ${token.border.default};
              }
            `,
            classNames?.modal,
          )}
          style={{
            animation: `${modalAnimation} ${animationDuration}ms`,
          }}
          onMouseDown={handleModalEvent}
          onMouseUp={handleModalEvent}
          onClick={handleModalEvent}
          onAnimationEnd={() => {
            if (!open) {
              setShowPortal(false)
            }
          }}
          id={modalId}
          role={role}
          aria-modal="true"
          aria-labelledby={ariaLabelledby}
          aria-describedby={ariaDescribedby}
          tabIndex={-1}
        >
          {focusTrapped && (
            <FocusTrap container={ref} initialFocusRef={initialFocusRef} />
          )}
          {children}
          {showCloseIcon && (
            <button
              id={closeIconId}
              className={cx(
                css`
                  position: absolute;
                  top: 14px;
                  right: 14px;
                  border: none;
                  padding: 0;
                  cursor: pointer;
                  background-color: transparent;
                  display: flex;

                  fill: var(--color-fg-default);
                `,
                classNames?.closeButton,
              )}
              onClick={onClose}
              data-testid="close-button"
            >
              {closeIcon ? (
                closeIcon
              ) : (
                <svg
                  className={classNames?.closeIcon}
                  width={28}
                  height={28}
                  viewBox="0 0 36 36"
                >
                  <path d="M28.5 9.62L26.38 7.5 18 15.88 9.62 7.5 7.5 9.62 15.88 18 7.5 26.38l2.12 2.12L18 20.12l8.38 8.38 2.12-2.12L20.12 18z" />
                </svg>
              )}
            </button>
          )}
        </div>
      </div>
    </div>,
    containerModal,
  )
}

export default Modal
