import React, {
  FC,
  MouseEvent,
  KeyboardEvent as ReactKeyboardEvent,
  ReactNode,
  RefObject,
  useEffect,
  useRef,
  WheelEvent
} from 'react'

import { formatClassList } from '../../utils'

type ModalProps = {
  blurId?: string,
  children: ReactNode,
  id: string,
  showModal: boolean,
  setModalClosed: () => void,
  setModalOpen: () => void,
  title: string,
  [other:string]: unknown
}

const DIALOG_BASE: string = `
  fixed
  h-full
  inset-0
  overflow-y-auto
  sm:w-full
  w-screen
  opacity-0
  transition
  duration-300
  ease-in-out
  -z-1
`

// const DIALOG_CLOSED: string = `
//   -z-1
// `

const BACKGROUND_BASE: string = `
  bg-black
  cursor-pointer
  duration-300
  ease-in-out
  fixed
  inset-0
  overflow-hidden
  transition-all
  w-full
  opacity-75
  -z-1
`

// const BACKGROUND_OPEN: string = `
//   opacity-75
// `

// const BACKGROUND_CLOSED: string = `
//   opacity-0
// `

const CONTENT_BASE: string = `
  bg-white
  duration-300
  ease-in-out
  ml-0
  lg:ml-1/4
  lg:w-1/2
  md:ml-2/12
  md:rounded-lg
  md:w-8/12
  ml-1/12
  px-5
  py-5
  rounded
  sm:rounded-md
  transition-all
  w-10/12
  m-20
`

  // opacity-0
// const CONTENT_CLOSED: string = `
//   // -m-200
// `

// const CONTENT_OPEN: string = `
//   m-20
// `

const CONTENT_HEADER: string = `
  -mt-5
  -mx-5
  bg-bscs-gray-200
  flex
  mb-3
  pl-5
  pr-5
  py-3
  rounded-t-lg
  shadow-sm
`

const CONTENT_TITLE: string = `
  font-semibold
  text-bscs-gray-800
  text-xl
  m-0
  sm:text-2xl
  tracking-normal
  self-center
`

const CONTENT_BODY: string = `
  tracking-normal
  leading-tight
  text-left
  py-2
  sm:py-3
  text-bscs-gray-600
`

const CLOSE_BUTTON: string = `
  cursor-pointer
  focus:outline-none
  focus:shadow-outline
  ml-auto
`

const CLOSE_ICON: string = `
  fas
  fa-times
  text-bscs-gray-500
  text-2xl
`

const handleBodyModalClose = (): void => {
  document.body.style.overflow = ''
}

const handleBodyModalOpen = (): void => {
  document.body.style.overflow = 'hidden'
}

const addFilterBlur = (blurId: string): void => {
  if (blurId) {
    const blurElem: HTMLElement | null = document.getElementById(blurId)

    if (blurElem) {
      blurElem.style.filter = 'blur(3px)'
    }
  }
}

const removeFilterBlur = (blurId: string) => {
  if (blurId) {
    const blurElem: HTMLElement | null = document.getElementById(blurId)

    if (blurElem) {
      blurElem.style.filter = ''
    }
  }
}

const openModal = (
  setModalOpen: () => void,
  ref: RefObject<HTMLDivElement> | null,
  id: string,
  blurId: string
): void => {
  if (!ref || !ref.current) {
    return
  }

  setModalOpen()

  // ref.current.classList.remove(formatClassList(DIALOG_CLOSED))
  ref.current.classList.remove('-z-1')
  ref.current.classList.remove('opacity-0')
  
  handleBodyModalOpen()
  addFilterBlur(blurId)

  // const backgroundElem: Element = ref.current.children[0]
  // backgroundElem.classList.value = formatClassList(
  //   joinStrings(' ', BACKGROUND_BASE, BACKGROUND_OPEN)
  // )
  
  // const contentElem: Element = ref.current.children[1]
  // contentElem.classList.value = formatClassList(
  //   joinStrings(' ', CONTENT_BASE, CONTENT_OPEN)
  // )

  /*
    COSTLY FPS
  */

  const activeElement: Element | null = document.activeElement

  if (!activeElement) {
    return
  }
  
  const selectableElements: NodeListOf<Element> = (
    ref.current.querySelectorAll('input, textarea, button, select, [role="button"]')
  )
  
  //Focuses first non-close button focusable element in Modal on open
  if (selectableElements.length === 1) {
    (selectableElements[0] as HTMLElement).focus()
  }
  
  if (
    selectableElements.length > 1
    && !Array.from(selectableElements).includes(activeElement)
  ) {
    const nextElement = getNextNonDisabledElement(selectableElements, 0)

    if (nextElement) {
      (nextElement as HTMLElement).focus()
    }
  }
}

const closeModal = (
  e: MouseEvent | KeyboardEvent,
  setModalClosed: () => void,
  ref: RefObject<HTMLDivElement> | null,
  id: string,
  blurId: string
): void => {
  e.preventDefault()

  if (!ref || !ref.current) {
    return
  }
  
  setModalClosed()

  handleBodyModalClose()
  removeFilterBlur(blurId)
  
  // waits for animation close before adding z-index to make body clickable again
  // required for overflow modal scrolling
  setTimeout(() => {
    if (ref && ref.current) {
      //ref.current.classList.add(formatClassList(DIALOG_CLOSED))
      ref.current.classList.add('-z-1')
    }
  }, 300)
  //ref.current.classList.add('-z-1')
  ref.current.classList.add('opacity-0')
  
  // const backgroundElem: Element = ref.current.children[0]
  // backgroundElem.classList.value = formatClassList(
  //   joinStrings(' ', BACKGROUND_BASE, BACKGROUND_CLOSED)
  // )
  
  // const contentElem: Element = ref.current.children[1]
  // contentElem.classList.value = formatClassList(
  //   joinStrings(' ', CONTENT_BASE, CONTENT_CLOSED)
  // )
}

const handleEscape = (
  e: KeyboardEvent,
  setModalClosed: () => void,
  ref: RefObject<HTMLDivElement> | null,
  id: string,
  blurId: string
): void => {
  if (e.key === 'Escape') {
    e.preventDefault()
    closeModal(e, setModalClosed, ref, id, blurId)
  }
}

const getPreviousNonDisabledElement = (
  elements: NodeListOf<Element>,
  index: number
): Element | null => {
  const start: number = index === 0
    ? elements.length - 1
    : index - 1
  
  for (let i: number = start; i >= 0; i--) {
    if ((elements[i] as HTMLElement).getAttribute('disabled') === null) {
      return elements[i]
    }
    
    if (i === 0) {
      i = elements.length
    }

  }

  return null
}

const getNextNonDisabledElement = (
  elements: NodeListOf<Element>,
  index: number
): Element | null => {
  const start: number = index === elements.length - 1
    ? 0
    : index + 1
  
  for (let i: number = start; i < elements.length; i++) {
    if ((elements[i] as HTMLElement).getAttribute('disabled') === null) {
      return elements[i]
    }
    
    if (i === elements.length - 1) {
      i = -1
    }
  }

  return null
}

const handlePrevious = (ref: RefObject<HTMLDivElement> | null): void => {
  if (!ref || !ref.current) {
    return
  }
  
  const selectableElements: NodeListOf<Element> = (
    ref.current.querySelectorAll('input, textarea, button, select, [role="button"]')
  )
  
  const activeElement: Element | null = document.activeElement

  if (!activeElement) {
    return
  }

  for (let i: number = 0; i < selectableElements.length; i++) {
    if (activeElement === selectableElements[i]) {
      const previousElement: Element | null = (
        getPreviousNonDisabledElement(selectableElements, i)
      )

      if (!previousElement) {
        return
      }
      
      (previousElement as HTMLElement).focus()
    }
  }
}

const handleNext = (ref: RefObject<HTMLDivElement> | null): void => {
  if (!ref || !ref.current) {
    return
  }
  
  const selectableElements: NodeListOf<Element> = (
    ref.current.querySelectorAll('input, textarea, button, select, [role="button"]')
  )

  const activeElement: Element | null = document.activeElement

  if (!activeElement) {
    return
  }
  
  for (let i: number = 0; i < selectableElements.length; i++) {
    if (activeElement === selectableElements[i]) {
      const nextElement: Element | null = (
        getNextNonDisabledElement(selectableElements, i)
      )

      if (!nextElement) {
        return
      }
      
      (nextElement as HTMLElement).focus()
    }
  }
}

const setNavigationTrap = (
  e: ReactKeyboardEvent,
  ref: RefObject<HTMLDivElement> | null
): void => {
  if (!ref || !ref.current) {
    return
  }

  const nextKeys: string[] = [
    'ArrowDown',
    'ArrowLeft',
    'ArrowRight',
    'ArrowUp',
    'Tab'
  ]
  
  const previousKeys: string[] = ['ArrowUp', 'ArrowLeft']

  const key: string = e.key
  
  if ((e.shiftKey && key === 'Tab') || previousKeys.includes(key)) {
    e.preventDefault();
    handlePrevious(ref)
    return
  }
  
  if (nextKeys.includes(key)) {
    e.preventDefault();
    handleNext(ref)
    return
  }
}

// Necessary because Firefox doesn't propagate wheel event to target parent node 
const handleWheelEvent = (e: WheelEvent, ref: RefObject<HTMLDivElement>) => {
  if (!ref || !ref.current) {
    return
  }

  ref.current.scrollTop += e.deltaY
}

const Modal:FC<ModalProps> = React.memo(({
  blurId="",
  children,
  id,
  showModal,
  setModalClosed,
  setModalOpen,
  title,
  ...other
}: ModalProps) => {
  const dialog: RefObject<HTMLDivElement> | null = useRef<HTMLDivElement>(null)

  const contentClassList: string = formatClassList(CONTENT_BASE)
  //   joinStrings(' ', CONTENT_BASE, CONTENT_CLOSED)
  // )
  
  const contentBodyClassList: string = formatClassList(CONTENT_BODY)
  const contentHeaderClassList: string = formatClassList(CONTENT_HEADER)
  const contentTitleClassList: string = formatClassList(CONTENT_TITLE)
  
  const backgroundClassList: string = formatClassList(BACKGROUND_BASE)
  //   joinStrings(' ', BACKGROUND_BASE, BACKGROUND_CLOSED)
  // )
  
  const closeButtonClassList: string = formatClassList(CLOSE_BUTTON)
  const closeIconClassList: string = formatClassList(CLOSE_ICON)

  const dialogClassList: string = formatClassList(DIALOG_BASE)
  //   joinStrings(' ', DIALOG_BASE, DIALOG_CLOSED)
  // )

  useEffect(() => {
    document.addEventListener(
      "keydown",
      (e: KeyboardEvent) => {
        handleEscape(e, setModalClosed, dialog, id, blurId)
      },
      false
    )
    return () => {
      document.removeEventListener(
        "keydown",
        (e: KeyboardEvent) => {
          handleEscape(e, setModalClosed, dialog, id, blurId)
        },
        false
      )
    }
  // eslint-disable-next-line
  }, [])
  
  useEffect(() => {
    if (showModal) {
      openModal(setModalOpen, dialog, id, blurId)
    }
  }, [blurId, id, setModalOpen, showModal])

  return (
    <div
      className={dialogClassList} 
      aria-hidden="true"
      aria-modal="true"
      id={id}
      onKeyDown={(e: ReactKeyboardEvent) => setNavigationTrap(e, dialog)}
      onWheel={(e: WheelEvent) => handleWheelEvent(e, dialog)}
      ref={dialog}
      role="dialog"
      {...other}
    >
      <div
        className={backgroundClassList}
        onClick={(e) => closeModal(
          e,
          setModalClosed,
          dialog,
          id,
          blurId
        )}
      />
      <div
        className={contentClassList}
      >
        <div className={contentHeaderClassList}>
          <h2 className={contentTitleClassList}>{title}</h2>
          <button
            className={closeButtonClassList}
            onClick={(e) => closeModal(
              e,
              setModalClosed,
              dialog,
              id,
              blurId
            )}
          >
            <i className={closeIconClassList} />
          </button>
        </div>
        <div className={contentBodyClassList}>
          { children }
        </div>
      </div>
    </div>
  )
})

export default Modal
