window.Components = {}

window.Components.listbox = function listbox(options) {
  let modelName = options.modelName || 'selected'
  let pointer = useTrackedPointer()

  return {
    init() {
      this.optionCount = this.$refs.listbox.children.length
      this.$watch('activeIndex', (value) => {
        if (!this.open) return

        if (this.activeIndex === null) {
          this.activeDescendant = ''
          return
        }

        this.activeDescendant = this.$refs.listbox.children[this.activeIndex].id
      })
    },
    activeDescendant: null,
    optionCount: null,
    open: false,
    activeIndex: null,
    selectedIndex: 0,
    get active() {
      return this.items[this.activeIndex]
    },
    get [modelName]() {
      return this.items[this.selectedIndex]
    },
    choose(option) {
      this.selectedIndex = option
      this.open = false
    },
    onButtonClick() {
      if (this.open) return
      this.activeIndex = this.selectedIndex
      this.open = true
      this.$nextTick(() => {
        this.$refs.listbox.focus()
        this.$refs.listbox.children[this.activeIndex].scrollIntoView({ block: 'nearest' })
      })
    },
    onOptionSelect() {
      if (this.activeIndex !== null) {
        this.selectedIndex = this.activeIndex
      }
      this.open = false
      this.$refs.button.focus()
    },
    onEscape() {
      this.open = false
      this.$refs.button.focus()
    },
    onArrowUp() {
      this.activeIndex = this.activeIndex - 1 < 0 ? this.optionCount - 1 : this.activeIndex - 1
      this.$refs.listbox.children[this.activeIndex].scrollIntoView({ block: 'nearest' })
    },
    onArrowDown() {
      this.activeIndex = this.activeIndex + 1 > this.optionCount - 1 ? 0 : this.activeIndex + 1
      this.$refs.listbox.children[this.activeIndex].scrollIntoView({ block: 'nearest' })
    },
    onMouseEnter(evt) {
      pointer.update(evt)
    },
    onMouseMove(evt, newIndex) {
      // Only highlight when the cursor has moved
      // Pressing arrow keys can otherwise scroll the container and override the selected item
      if (!pointer.wasMoved(evt)) return
      this.activeIndex = newIndex
    },
    onMouseLeave(evt) {
      // Only unhighlight when the cursor has moved
      // Pressing arrow keys can otherwise scroll the container and override the selected item
      if (!pointer.wasMoved(evt)) return
      this.activeIndex = null
    },
    ...options,
  }
}

window.Components.menu = function menu(options = { open: false }) {
  let pointer = useTrackedPointer()

  return {
    init() {
      this.items = Array.from(this.$el.querySelectorAll('[role="menuitem"]'))
      this.$watch('open', () => {
        if (this.open) {
          this.activeIndex = -1
        }
      })
    },
    activeDescendant: null,
    activeIndex: null,
    items: null,
    open: options.open,
    focusButton() {
      this.$refs.button.focus()
    },
    onButtonClick() {
      this.open = !this.open
      if (this.open) {
        this.$nextTick(() => {
          this.$refs['menu-items'].focus()
        })
      }
    },
    onButtonEnter() {
      this.open = !this.open
      if (this.open) {
        this.activeIndex = 0
        this.activeDescendant = this.items[this.activeIndex].id
        this.$nextTick(() => {
          this.$refs['menu-items'].focus()
        })
      }
    },
    onArrowUp() {
      if (!this.open) {
        this.open = true
        this.activeIndex = this.items.length - 1
        this.activeDescendant = this.items[this.activeIndex].id

        return
      }

      if (this.activeIndex === 0) {
        return
      }

      this.activeIndex = this.activeIndex === -1 ? this.items.length - 1 : this.activeIndex - 1
      this.activeDescendant = this.items[this.activeIndex].id
    },
    onArrowDown() {
      if (!this.open) {
        this.open = true
        this.activeIndex = 0
        this.activeDescendant = this.items[this.activeIndex].id

        return
      }

      if (this.activeIndex === this.items.length - 1) {
        return
      }

      this.activeIndex = this.activeIndex + 1
      this.activeDescendant = this.items[this.activeIndex].id
    },
    onClickAway($event) {
      if (this.open) {
        const focusableSelector = [
          '[contentEditable=true]',
          '[tabindex]',
          'a[href]',
          'area[href]',
          'button:not([disabled])',
          'iframe',
          'input:not([disabled])',
          'select:not([disabled])',
          'textarea:not([disabled])',
        ]
          .map((selector) => `${selector}:not([tabindex='-1'])`)
          .join(',')

        this.open = false

        if (!$event.target.closest(focusableSelector)) {
          this.focusButton()
        }
      }
    },

    onMouseEnter(evt) {
      pointer.update(evt)
    },
    onMouseMove(evt, newIndex) {
      // Only highlight when the cursor has moved
      // Pressing arrow keys can otherwise scroll the container and override the selected item
      if (!pointer.wasMoved(evt)) return
      this.activeIndex = newIndex
    },
    onMouseLeave(evt) {
      // Only unhighlight when the cursor has moved
      // Pressing arrow keys can otherwise scroll the container and override the selected item
      if (!pointer.wasMoved(evt)) return
      this.activeIndex = -1
    },
  }
}

window.Components.popoverGroup = function popoverGroup() {
  return {
    __type: 'popoverGroup',
    init() {
      let handler = (e) => {
        if (!document.body.contains(this.$el)) {
          window.removeEventListener('focus', handler, true)
          return
        }
        if (e.target instanceof Element && !this.$el.contains(e.target)) {
          window.dispatchEvent(
            new CustomEvent('close-popover-group', {
              detail: this.$el,
            })
          )
        }
      }
      window.addEventListener('focus', handler, true)
    },
  }
}

window.Components.popover = function popover({ open = false, focus = false } = {}) {
  const focusableSelector = [
    '[contentEditable=true]',
    '[tabindex]',
    'a[href]',
    'area[href]',
    'button:not([disabled])',
    'iframe',
    'input:not([disabled])',
    'select:not([disabled])',
    'textarea:not([disabled])',
  ]
    .map((selector) => `${selector}:not([tabindex='-1'])`)
    .join(',')

  function focusFirst(container) {
    const focusableElements = Array.from(container.querySelectorAll(focusableSelector))

    function tryFocus(element) {
      if (element === undefined) return

      element.focus({ preventScroll: true })

      if (document.activeElement !== element) {
        tryFocus(focusableElements[focusableElements.indexOf(element) + 1])
      }
    }

    tryFocus(focusableElements[0])
  }

  return {
    __type: 'popover',
    open,
    init() {
      if (focus) {
        this.$watch('open', (open) => {
          if (open) {
            this.$nextTick(() => {
              focusFirst(this.$refs.panel)
            })
          }
        })
      }

      let handler = (e) => {
        if (!document.body.contains(this.$el)) {
          window.removeEventListener('focus', handler, true)
          return
        }
        let ref = focus ? this.$refs.panel : this.$el
        if (this.open && e.target instanceof Element && !ref.contains(e.target)) {
          let node = this.$el
          while (node.parentNode) {
            node = node.parentNode
            if (node.__x instanceof this.constructor) {
              if (node.__x.$data.__type === 'popoverGroup') return
              if (node.__x.$data.__type === 'popover') break
            }
          }
          this.open = false
        }
      }

      window.addEventListener('focus', handler, true)
    },
    onEscape() {
      this.open = false
      if (this.restoreEl) {
        this.restoreEl.focus()
      }
    },
    onClosePopoverGroup(e) {
      if (e.detail.contains(this.$el)) {
        this.open = false
      }
    },
    toggle(e) {
      this.open = !this.open
      if (this.open) {
        this.restoreEl = e.currentTarget
      } else if (this.restoreEl) {
        this.restoreEl.focus()
      }
    },
  }
}

window.Components.radioGroup = function radioGroup({ initialCheckedIndex = 0 } = {}) {
  return {
    value: undefined,
    active: undefined,
    init() {
      let options = Array.from(this.$el.querySelectorAll('input'))

      this.value = options[initialCheckedIndex]?.value

      for (let option of options) {
        option.addEventListener('change', () => {
          this.active = option.value
        })
        option.addEventListener('focus', () => {
          this.active = option.value
        })
      }

      window.addEventListener(
        'focus',
        () => {
          console.log('Focus change')
          if (!options.includes(document.activeElement)) {
            console.log('HIT')
            this.active = undefined
          }
        },
        true
      )
    },
  }
}

window.Components.tabs = function tabs() {
  return {
    selectedIndex: 0,
    onTabClick(event) {
      if (!this.$el.contains(event.detail)) return

      let tabs = Array.from(this.$el.querySelectorAll('[x-data^="Components.tab("]'))
      let panels = Array.from(this.$el.querySelectorAll('[x-data^="Components.tabPanel("]'))

      let idx = tabs.indexOf(event.detail)
      this.selectedIndex = idx

      window.dispatchEvent(
        new CustomEvent('tab-select', {
          detail: {
            tab: event.detail,
            panel: panels[idx],
          },
        })
      )
    },
    onTabKeydown(event) {
      if (!this.$el.contains(event.detail.tab)) return

      let tabs = Array.from(this.$el.querySelectorAll('[x-data^="Components.tab("]'))
      let tabIndex = tabs.indexOf(event.detail.tab)

      if (event.detail.key === 'ArrowLeft') {
        this.onTabClick({ detail: tabs[(tabIndex - 1 + tabs.length) % tabs.length] })
      } else if (event.detail.key === 'ArrowRight') {
        this.onTabClick({ detail: tabs[(tabIndex + 1) % tabs.length] })
      } else if (event.detail.key === 'Home' || event.detail.key === 'PageUp') {
        this.onTabClick({ detail: tabs[0] })
      } else if (event.detail.key === 'End' || event.detail.key === 'PageDown') {
        this.onTabClick({ detail: tabs[tabs.length - 1] })
      }
    },
  }
}

window.Components.tab = function tab(defaultIndex = 0) {
  return {
    selected: false,
    init() {
      let tabs = Array.from(
        this.$el
          .closest('[x-data^="Components.tabs("]')
          .querySelectorAll('[x-data^="Components.tab("]')
      )
      this.selected = tabs.indexOf(this.$el) === defaultIndex
      this.$watch('selected', (selected) => {
        if (selected) {
          this.$el.focus()
        }
      })
    },
    onClick() {
      window.dispatchEvent(
        new CustomEvent('tab-click', {
          detail: this.$el,
        })
      )
    },
    onKeydown(event) {
      if (['ArrowLeft', 'ArrowRight', 'Home', 'PageUp', 'End', 'PageDown'].includes(event.key)) {
        event.preventDefault()
      }

      window.dispatchEvent(
        new CustomEvent('tab-keydown', {
          detail: {
            tab: this.$el,
            key: event.key,
          },
        })
      )
    },
    onTabSelect(event) {
      this.selected = event.detail.tab === this.$el
    },
  }
}

window.Components.tabPanel = function tabPanel(defaultIndex = 0) {
  return {
    selected: false,
    init() {
      let panels = Array.from(
        this.$el
          .closest('[x-data^="Components.tabs("]')
          .querySelectorAll('[x-data^="Components.tabPanel("]')
      )
      this.selected = panels.indexOf(this.$el) === defaultIndex
    },
    onTabSelect(event) {
      this.selected = event.detail.panel === this.$el
    },
  }
}

function useTrackedPointer() {
  /** @type {[x: number, y: number]} */
  let lastPos = [-1, -1]

  return {
    /**
     * @param {PointerEvent} evt
     */
    wasMoved(evt) {
      let newPos = [evt.screenX, evt.screenY]

      if (lastPos[0] === newPos[0] && lastPos[1] === newPos[1]) {
        return false
      }

      lastPos = newPos
      return true
    },

    /**
     * @param {PointerEvent} evt
     */
    update(evt) {
      lastPos = [evt.screenX, evt.screenY]
    },
  }
}
