import ifClick from '../dom/ifClick'
import TetherLayout from './TetherLayout'

type ToggleFn = () => boolean
type ToggleFnOrBoolean = ToggleFn | boolean

interface Toggle {
  tether: Tether
  closeListener?: (evt: Event) => void
}

export default class Tether {
  static #layout = new TetherLayout()
  static #toggles: WeakMap<HTMLElement, Toggle> = new WeakMap()

  static get toggles() {
    return this.#toggles
  }

  static toggle(
    reference: HTMLElement,
    element: HTMLElement,
    toggle: ToggleFnOrBoolean,
  ) {
    let toggleFn: ToggleFn
    let toggled = typeof toggle === 'boolean' ? toggle : false

    function isToggleFn(
      fnOrBoolean: ToggleFnOrBoolean,
    ): fnOrBoolean is ToggleFn {
      return typeof fnOrBoolean === 'function'
    }

    // When toggle arg is a function, use it to "close" the element
    // automatically when the user clicks outside.
    if (isToggleFn(toggle)) {
      toggleFn = toggle
      toggled = toggleFn()
    }

    if (toggled) {
      const tether = new Tether(reference, element)
      let closeListener: undefined | ((evt: Event) => void) = undefined

      if (isToggleFn(toggle)) {
        window.addEventListener(
          'click',
          (closeListener = ifClick((args) => {
            if (!(args instanceof Event)) return
            const target = args.target
            if (!(target instanceof HTMLElement)) return
            if (!reference.contains(target) && !element.contains(target)) {
              const parent = target.closest('[data-tether-rx]')

              if (!parent || parent.contains(reference)) {
                toggleFn()
                if (closeListener !== undefined) {
                  window.removeEventListener('click', closeListener, true)
                }
                tether.destroy()
                Tether.#toggles.delete(reference)
              }
            }
          })),
          true,
        )
      }

      Tether.#toggles.set(reference, {
        tether,
        closeListener,
      })
    } else {
      const t = Tether.#toggles.get(reference)

      if (t) {
        if (t.closeListener !== undefined) {
          window.removeEventListener('click', t.closeListener, true)
        }

        t.tether.destroy()
        Tether.#toggles.delete(reference)
      }
    }

    return toggled
  }

  /* Given a dom element, find the Tether reference element attached to it. */
  static getReference(element: HTMLElement) {
    for (const tether of Tether.#layout.tethers) {
      if (tether.#element === element) {
        return tether.#reference
      }
    }

    return null
  }

  #reference: HTMLElement
  #element: HTMLElement

  get element() {
    return this.#element
  }

  get reference() {
    return this.#reference
  }

  constructor(reference: HTMLElement, element: HTMLElement) {
    this.#reference = reference
    this.#element = element

    // conditionally moves tethered element to end of Page or Popup to avoid z-index issues
    if (
      reference.classList.contains('ComboInput') &&
      !element.parentElement?.hasAttribute('data-tether-anchor')
    ) {
      if (reference.closest('dialog') !== null) {
        reference.closest('dialog')?.append(element)
      } else if (reference.closest('.Popup') === null) {
        document.querySelector('.Page')?.append(element)
      } else {
        reference.closest('.Popup')?.append(element)
      }
    }

    Tether.#layout.add(this)
  }

  destroy() {
    Tether.#layout.remove(this)
  }
}
