/*
 * HSOverlay
 * @version: 3.0.1
 * @author: Preline Labs Ltd.
 * @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
 * Copyright 2024 Preline Labs Ltd.
 */

import {
  afterTransition,
  dispatch,
  getClassProperty,
  getHighestZIndex,
  isDirectChild,
  isParentOrElementHidden,
  stringToBoolean
} from '../../utils'

import { IOverlay, IOverlayOptions } from './interfaces'
import { TOverlayOptionsAutoCloseEqualityType } from './types'
import { ICollectionItem } from '../../interfaces'
import { BREAKPOINTS } from '../../constants'

import HSBasePlugin from '../base-plugin'

class HSOverlay extends HSBasePlugin<{}> implements IOverlay {
  private readonly hiddenClass: string | null
  private readonly emulateScrollbarSpace: boolean
  private readonly isClosePrev: boolean
  private readonly backdropClasses: string | null
  private readonly backdropParent: string | HTMLElement | Document
  private readonly backdropExtraClasses: string | null
  private readonly animationTarget: HTMLElement | null

  private openNextOverlay: boolean
  private autoHide: ReturnType<typeof setTimeout> | null
  private toggleButtons: HTMLElement[]

  static openedItemsQty = 0

  public initContainer: HTMLElement | null
  public isCloseWhenClickInside: boolean
  public isTabAccessibilityLimited: boolean
  public isLayoutAffect: boolean
  public hasAutofocus: boolean
  public hasDynamicZIndex: boolean
  public hasAbilityToCloseOnBackdropClick: boolean
  public openedBreakpoint: number | null
  public autoClose: number | null
  public autoCloseEqualityType: TOverlayOptionsAutoCloseEqualityType | null
  public moveOverlayToBody: number | null

  private backdrop: HTMLElement | null
  private initialZIndex = 0
  static currentZIndex = 0

  private onElementClickListener:
    | {
        el: HTMLElement
        fn: () => void
      }[]
    | null
  private onOverlayClickListener: (evt: Event) => void
  private onBackdropClickListener: () => void

  constructor(el: HTMLElement, options?: IOverlayOptions, events?: {}) {
    super(el, options, events)

    // Collect all data options from toggles
    this.toggleButtons = Array.from(document.querySelectorAll(`[data-overlay="#${this.el.id}"]`))
    const toggleDataOptions = this.collectToggleParameters(this.toggleButtons)

    const data = el.getAttribute('data-overlay-options')
    const dataOptions: IOverlayOptions = data ? JSON.parse(data) : {}
    const concatOptions = {
      ...dataOptions,
      ...toggleDataOptions,
      ...options
    }

    this.hiddenClass = concatOptions?.hiddenClass || 'hidden'
    this.emulateScrollbarSpace = concatOptions?.emulateScrollbarSpace || false
    this.isClosePrev = concatOptions?.isClosePrev ?? true
    this.backdropClasses =
      concatOptions?.backdropClasses ??
      'overlay-backdrop transition duration-300 fixed inset-0 bg-base-300/60 overflow-y-auto'
    this.backdropParent =
      typeof concatOptions.backdropParent === 'string'
        ? (document.querySelector(concatOptions.backdropParent) as HTMLElement)
        : document.body
    this.backdropExtraClasses = concatOptions?.backdropExtraClasses ?? ''
    this.moveOverlayToBody = concatOptions?.moveOverlayToBody || null

    this.openNextOverlay = false
    this.autoHide = null
    this.initContainer = this.el?.parentElement || null
    this.isCloseWhenClickInside = stringToBoolean(
      getClassProperty(this.el, '--close-when-click-inside', 'false') || 'false'
    )
    this.isTabAccessibilityLimited = stringToBoolean(
      getClassProperty(this.el, '--tab-accessibility-limited', 'true') || 'true'
    )
    this.isLayoutAffect = stringToBoolean(getClassProperty(this.el, '--is-layout-affect', 'false') || 'false')
    this.hasAutofocus = stringToBoolean(getClassProperty(this.el, '--has-autofocus', 'true') || 'true')
    this.hasDynamicZIndex = stringToBoolean(getClassProperty(this.el, '--has-dynamic-z-index', 'false') || 'false')
    this.hasAbilityToCloseOnBackdropClick = stringToBoolean(this.el.getAttribute('data-overlay-keyboard') || 'true')

    const autoCloseBreakpoint = getClassProperty(this.el, '--auto-close')
    const autoCloseEqualityType = getClassProperty(this.el, '--auto-close-equality-type')
    const openedBreakpoint = getClassProperty(this.el, '--opened')

    this.autoClose =
      !isNaN(+autoCloseBreakpoint) && isFinite(+autoCloseBreakpoint)
        ? +autoCloseBreakpoint
        : BREAKPOINTS[autoCloseBreakpoint] || null
    this.autoCloseEqualityType = (autoCloseEqualityType as TOverlayOptionsAutoCloseEqualityType) ?? null
    this.openedBreakpoint =
      (!isNaN(+openedBreakpoint) && isFinite(+openedBreakpoint) ? +openedBreakpoint : BREAKPOINTS[openedBreakpoint]) ||
      null

    this.animationTarget = this?.el?.querySelector('.overlay-animation-target') || this.el

    this.initialZIndex = parseInt(getComputedStyle(this.el).zIndex, 10)

    this.onElementClickListener = []

    this.init()
  }

  private elementClick() {
    const payloadFn = () => {
      const payload = {
        el: this.el,
        isOpened: !!this.el.classList.contains('open')
      }

      this.fireEvent('toggleClicked', payload)
      dispatch('toggleClicked.overlay', this.el, payload)
    }

    if (this.el.classList.contains('opened')) this.close(false, payloadFn)
    else this.open(payloadFn)
  }

  private overlayClick(evt: Event) {
    if (
      (evt.target as HTMLElement).id &&
      `#${(evt.target as HTMLElement).id}` === this.el.id &&
      this.isCloseWhenClickInside &&
      this.hasAbilityToCloseOnBackdropClick
    ) {
      this.close()
    }
  }

  private backdropClick() {
    this.close()
  }

  private init() {
    this.createCollection(window.$hsOverlayCollection, this)

    if (this.isLayoutAffect && this.openedBreakpoint) {
      const instance = HSOverlay.getInstance(this.el, true)

      HSOverlay.setOpened(this.openedBreakpoint, instance as ICollectionItem<HSOverlay>)
    }

    this.onOverlayClickListener = evt => this.overlayClick(evt)

    this.el.addEventListener('click', this.onOverlayClickListener)

    if (this.toggleButtons.length) this.buildToggleButtons()
  }

  private getElementsByZIndex() {
    return window.$hsOverlayCollection.filter(el => el.element.initialZIndex === this.initialZIndex)
  }

  private buildToggleButtons() {
    this.toggleButtons.forEach(el => {
      if (this.el.classList.contains('opened')) el.ariaExpanded = 'true'
      else el.ariaExpanded = 'false'

      this.onElementClickListener.push({
        el,
        fn: () => this.elementClick()
      })

      el.addEventListener('click', this.onElementClickListener.find(toggleButton => toggleButton.el === el).fn)
    })
  }

  private hideAuto() {
    const time = parseInt(getClassProperty(this.el, '--auto-hide', '0'))

    if (time) {
      this.autoHide = setTimeout(() => {
        this.close()
      }, time)
    }
  }

  private checkTimer() {
    if (this.autoHide) {
      clearTimeout(this.autoHide)

      this.autoHide = null
    }
  }

  private buildBackdrop() {
    const overlayClasses = this.el.classList.value.split(' ')
    const overlayZIndex = parseInt(window.getComputedStyle(this.el).getPropertyValue('z-index'))
    const backdropId = this.el.getAttribute('data-overlay-backdrop-container') || false
    this.backdrop = document.createElement('div')
    let backdropClasses = `${this.backdropClasses} ${this.backdropExtraClasses}`
    const closeOnBackdrop = getClassProperty(this.el, '--overlay-backdrop', 'true') !== 'static'
    const disableBackdrop = getClassProperty(this.el, '--overlay-backdrop', 'true') === 'false'

    this.backdrop.id = `${this.el.id}-backdrop`
    if ('style' in this.backdrop) {
      this.backdrop.style.zIndex = `${overlayZIndex - 1}`
    }

    for (const value of overlayClasses) {
      if (value.startsWith('overlay-backdrop-open:') || value.includes(':overlay-backdrop-open:')) {
        backdropClasses += ` ${value}`
      }
    }

    if (disableBackdrop) return

    if (backdropId) {
      this.backdrop = document.querySelector(backdropId).cloneNode(true) as HTMLElement
      this.backdrop.classList.remove('hidden')

      backdropClasses = `${(this.backdrop as HTMLElement).classList.toString()}`
      this.backdrop.classList.value = ''
    }

    if (closeOnBackdrop) {
      this.onBackdropClickListener = () => this.backdropClick()

      this.backdrop.addEventListener('click', this.onBackdropClickListener, true)
    }

    this.backdrop.setAttribute('data-overlay-backdrop-template', '')
    ;(this.backdropParent as HTMLElement).appendChild(this.backdrop)

    setTimeout(() => {
      this.backdrop.classList.value = backdropClasses
    })
  }

  private destroyBackdrop() {
    const backdrop: HTMLElement = document.querySelector(`#${this.el.id}-backdrop`)

    if (!backdrop) return

    if (this.openNextOverlay) {
      backdrop.style.transitionDuration = `${
        parseFloat(window.getComputedStyle(backdrop).transitionDuration.replace(/[^\d.-]/g, '')) * 1.8
      }s`
    }

    backdrop.classList.add('opacity-0')

    afterTransition(backdrop, () => {
      backdrop.remove()
    })
  }

  private focusElement() {
    const input: HTMLInputElement = this.el.querySelector('[autofocus]')

    if (!input) return false
    else input.focus()
  }

  private getScrollbarSize() {
    let div = document.createElement('div')
    div.style.overflow = 'scroll'
    div.style.width = '100px'
    div.style.height = '100px'
    document.body.appendChild(div)

    let scrollbarSize = div.offsetWidth - div.clientWidth

    document.body.removeChild(div)

    return scrollbarSize
  }

  private collectToggleParameters(buttons: HTMLElement[]) {
    let toggleData = {}

    buttons.forEach(el => {
      const data = el.getAttribute('data-overlay-options')
      const dataOptions: IOverlayOptions = data ? JSON.parse(data) : {}
      toggleData = {
        ...toggleData,
        ...dataOptions
      }
    })

    return toggleData
  }

  // Public methods
  public open(cb: Function | null = null) {
    if (this.hasDynamicZIndex) {
      if (HSOverlay.currentZIndex < this.initialZIndex) {
        HSOverlay.currentZIndex = this.initialZIndex
      }

      HSOverlay.currentZIndex++
      this.el.style.zIndex = `${HSOverlay.currentZIndex}`
    }

    const openedOverlays = document.querySelectorAll('.overlay.open')
    const currentlyOpened = window.$hsOverlayCollection.find(
      el => Array.from(openedOverlays).includes(el.element.el) && !el.element.isLayoutAffect
    )
    const toggles = document.querySelectorAll(`[data-overlay="#${this.el.id}"]`)
    const disabledScroll = getClassProperty(this.el, '--body-scroll', 'false') !== 'true'

    if (this.isClosePrev && currentlyOpened) {
      this.openNextOverlay = true

      return currentlyOpened.element.close().then(() => {
        this.open()

        this.openNextOverlay = false
      })
    }

    if (disabledScroll) {
      document.body.style.overflow = 'hidden'
      if (this.emulateScrollbarSpace) {
        document.body.style.paddingRight = `${this.getScrollbarSize()}px`
      }
    }

    this.buildBackdrop()
    this.checkTimer()
    this.hideAuto()

    toggles.forEach(toggle => {
      if (toggle.ariaExpanded) toggle.ariaExpanded = 'true'
    })
    this.el.classList.remove(this.hiddenClass)
    this.el.setAttribute('aria-overlay', 'true')
    this.el.setAttribute('tabindex', '-1')

    setTimeout(() => {
      if (this.el.classList.contains('opened')) return false

      this.el.classList.add('open', 'opened')
      if (this.isLayoutAffect) {
        document.body.classList.add('overlay-body-open')
      }

      this.fireEvent('open', this.el)
      dispatch('open.overlay', this.el, this.el)

      if (this.hasAutofocus) this.focusElement()

      if (typeof cb === 'function') cb()

      HSOverlay.openedItemsQty++
    }, 50)
  }

  public close(forceClose = false, cb: Function | null = null) {
    HSOverlay.openedItemsQty = HSOverlay.openedItemsQty <= 0 ? 0 : HSOverlay.openedItemsQty - 1

    if (HSOverlay.openedItemsQty === 0 && this.isLayoutAffect) {
      document.body.classList.remove('overlay-body-open')
    }

    const closeFn = (_cb: Function) => {
      if (this.el.classList.contains('open')) return false
      const toggles = document.querySelectorAll(`[data-overlay="#${this.el.id}"]`)

      toggles.forEach(toggle => {
        if (toggle.ariaExpanded) toggle.ariaExpanded = 'false'
      })
      this.el.classList.add(this.hiddenClass)
      if (this.hasDynamicZIndex) this.el.style.zIndex = ''

      this.destroyBackdrop()

      this.fireEvent('close', this.el)
      dispatch('close.overlay', this.el, this.el)

      if (!document.querySelector('.overlay.opened')) {
        document.body.style.overflow = ''
        if (this.emulateScrollbarSpace) document.body.style.paddingRight = ''
      }

      _cb(this.el)
      if (typeof cb === 'function') cb()

      if (HSOverlay.openedItemsQty === 0) {
        document.body.classList.remove('overlay-body-open')
        if (this.hasDynamicZIndex) HSOverlay.currentZIndex = 0
      }
    }

    return new Promise(resolve => {
      this.el.classList.remove('open', 'opened')
      this.el.removeAttribute('aria-overlay')
      this.el.removeAttribute('tabindex')

      if (forceClose) closeFn(resolve)
      else afterTransition(this.animationTarget, () => closeFn(resolve))
    })
  }

  public destroy() {
    // Remove classes
    this.el.classList.remove('open', 'opened', this.hiddenClass)
    if (this.isLayoutAffect) {
      document.body.classList.remove('overlay-body-open')
    }

    // Remove listeners
    this.el.removeEventListener('click', this.onOverlayClickListener)
    if (this.onElementClickListener.length) {
      this.onElementClickListener.forEach(({ el, fn }) => {
        el.removeEventListener('click', fn)
      })
      this.onElementClickListener = null
    }
    if (this.backdrop) {
      this.backdrop.removeEventListener('click', this.onBackdropClickListener)
    }

    if (this.backdrop) {
      this.backdrop.remove()
      this.backdrop = null
    }

    window.$hsOverlayCollection = window.$hsOverlayCollection.filter(({ element }) => element.el !== this.el)
  }

  // Static methods
  private static findInCollection(target: HSOverlay | HTMLElement | string): ICollectionItem<HSOverlay> | null {
    return (
      window.$hsOverlayCollection.find(el => {
        if (target instanceof HSOverlay) return el.element.el === target.el
        else if (typeof target === 'string') {
          return el.element.el === document.querySelector(target)
        } else return el.element.el === target
      }) || null
    )
  }

  static getInstance(target: HTMLElement | string, isInstance?: boolean) {
    // Backward compatibility
    const _temp = typeof target === 'string' ? document.querySelector(target) : target
    const _target = _temp?.getAttribute('data-overlay') ? _temp.getAttribute('data-overlay') : target

    const elInCollection = window.$hsOverlayCollection.find(
      el =>
        el.element.el === (typeof _target === 'string' ? document.querySelector(_target) : _target) ||
        el.element.el === (typeof _target === 'string' ? document.querySelector(_target) : _target)
    )

    return elInCollection ? (isInstance ? elInCollection : elInCollection.element.el) : null
  }

  static autoInit() {
    if (!window.$hsOverlayCollection) {
      window.$hsOverlayCollection = []

      document.addEventListener('keydown', evt => HSOverlay.accessibility(evt))
    }

    if (window.$hsOverlayCollection) {
      window.$hsOverlayCollection = window.$hsOverlayCollection.filter(({ element }) => document.contains(element.el))
    }

    document.querySelectorAll('.overlay:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => {
      if (!window.$hsOverlayCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) {
        new HSOverlay(el)
      }
    })
  }

  static open(target: HSOverlay | HTMLElement | string) {
    const instance = HSOverlay.findInCollection(target)

    if (instance && instance.element.el.classList.contains(instance.element.hiddenClass)) instance.element.open()
  }

  static close(target: HSOverlay | HTMLElement | string) {
    const instance = HSOverlay.findInCollection(target)

    if (instance && !instance.element.el.classList.contains(instance.element.hiddenClass)) instance.element.close()
  }

  static setOpened(breakpoint: number, el: ICollectionItem<HSOverlay>) {
    if (document.body.clientWidth >= breakpoint) {
      document.body.classList.add('overlay-body-open')
      el.element.open()
    } else el.element.close(true)
  }

  // Accessibility methods
  static accessibility(evt: KeyboardEvent) {
    const opened = document.querySelectorAll('.overlay.open')
    const highest = getHighestZIndex(Array.from(opened) as HTMLElement[])
    const targets = window.$hsOverlayCollection.filter(el => el.element.el.classList.contains('open'))
    const target = targets.find(
      el => window.getComputedStyle(el.element.el).getPropertyValue('z-index') === `${highest}`
    )
    const focusableElements = target?.element?.el?.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    )
    const notHiddenFocusableElements: HTMLElement[] = []
    if (focusableElements?.length) {
      focusableElements.forEach((el: HTMLElement) => {
        if (!isParentOrElementHidden(el)) notHiddenFocusableElements.push(el)
      })
    }
    const basicCheck = target && !evt.metaKey

    if (basicCheck && !target.element.isTabAccessibilityLimited && evt.code === 'Tab') {
      return false
    }

    if (basicCheck && notHiddenFocusableElements.length && evt.code === 'Tab') {
      evt.preventDefault()
      this.onTab(target)
    }
    if (basicCheck && evt.code === 'Escape') {
      evt.preventDefault()
      this.onEscape(target)
    }
  }

  static onEscape(target: ICollectionItem<HSOverlay>) {
    if (target && target.element.hasAbilityToCloseOnBackdropClick) {
      target.element.close()
    }
  }

  static onTab(target: ICollectionItem<HSOverlay>) {
    const overlayElement = target.element.el
    const focusableElements = Array.from(
      overlayElement.querySelectorAll<HTMLElement>(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      )
    )

    if (focusableElements.length === 0) return false

    const focusedElement = overlayElement.querySelector(':focus')

    if (focusedElement) {
      let foundCurrent = false

      for (const element of focusableElements) {
        if (foundCurrent) {
          element.focus()

          return
        }

        if (element === focusedElement) {
          foundCurrent = true
        }
      }

      focusableElements[0].focus()
    } else {
      focusableElements[0].focus()
    }
  }

  // Backward compatibility
  static on(evt: string, target: HSOverlay | HTMLElement | string, cb: Function) {
    const instance = HSOverlay.findInCollection(target)

    if (instance) instance.element.events[evt] = cb
  }
}

declare global {
  interface Window {
    HSOverlay: Function
    $hsOverlayCollection: ICollectionItem<HSOverlay>[]
  }
}

const autoCloseResizeFn = () => {
  if (!window.$hsOverlayCollection.length || !window.$hsOverlayCollection.find(el => el.element.autoClose)) {
    return false
  }

  const overlays = window.$hsOverlayCollection.filter(el => el.element.autoClose)

  overlays.forEach(overlay => {
    const { autoCloseEqualityType, autoClose } = overlay.element
    const condition =
      autoCloseEqualityType === 'less-than'
        ? document.body.clientWidth <= autoClose
        : document.body.clientWidth >= autoClose

    if (condition) {
      overlay.element.close(true)
    } else {
      if (overlay.element.isLayoutAffect) {
        document.body.classList.add('overlay-body-open')
      }
    }
  })
}

const moveOverlayToBodyResizeFn = () => {
  if (!window.$hsOverlayCollection.length || !window.$hsOverlayCollection.find(el => el.element.moveOverlayToBody)) {
    return false
  }

  const overlays = window.$hsOverlayCollection.filter(el => el.element.moveOverlayToBody)

  overlays.forEach(overlay => {
    const resolution = overlay.element.moveOverlayToBody
    const initPlace = overlay.element.initContainer
    const newPlace = document.querySelector('body')
    const target = overlay.element.el

    if (!initPlace && target) return false

    if (document.body.clientWidth <= resolution && !isDirectChild(newPlace, target)) {
      newPlace.appendChild(target)
    } else if (document.body.clientWidth > resolution && !initPlace.contains(target)) {
      initPlace.appendChild(target)
    }
  })
}

const setOpenedResizeFn = () => {
  if (!window.$hsOverlayCollection.length || !window.$hsOverlayCollection.find(el => el.element.autoClose)) {
    return false
  }

  const overlays = window.$hsOverlayCollection.filter(el => el.element.autoClose)

  overlays.forEach(overlay => {
    const { autoCloseEqualityType, autoClose } = overlay.element
    const condition =
      autoCloseEqualityType === 'less-than'
        ? document.body.clientWidth <= autoClose
        : document.body.clientWidth >= autoClose

    if (condition) {
      overlay.element.close(true)
    }
  })
}

const setBackdropZIndexResizeFn = () => {
  if (
    !window.$hsOverlayCollection.length ||
    !window.$hsOverlayCollection.find(el => el.element.el.classList.contains('opened'))
  ) {
    return false
  }

  const overlays = window.$hsOverlayCollection.filter(el => el.element.el.classList.contains('opened'))

  overlays.forEach(overlay => {
    const overlayZIndex = parseInt(window.getComputedStyle(overlay.element.el).getPropertyValue('z-index'))
    const backdrop: HTMLElement = document.querySelector(`#${overlay.element.el.id}-backdrop`)
    if (!backdrop) return false

    const backdropZIndex = parseInt(window.getComputedStyle(backdrop).getPropertyValue('z-index'))
    if (overlayZIndex === backdropZIndex + 1) return false

    if ('style' in backdrop) backdrop.style.zIndex = `${overlayZIndex - 1}`

    document.body.classList.add('overlay-body-open')
  })
}

window.addEventListener('load', () => {
  HSOverlay.autoInit()

  moveOverlayToBodyResizeFn()
  // Uncomment for debug
  // console.log('Overlay collection:', window.$hsOverlayCollection);
})

window.addEventListener('resize', () => {
  autoCloseResizeFn()
  moveOverlayToBodyResizeFn()
  setOpenedResizeFn()
  setBackdropZIndexResizeFn()
})

if (typeof window !== 'undefined') {
  window.HSOverlay = HSOverlay
}

export default HSOverlay
