/*
 * HSScrollspy
 * @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 { getClassProperty, dispatch } from '../../utils'

import { IScrollspy, IScrollspyOptions } from '../scrollspy/interfaces'

import HSBasePlugin from '../base-plugin'
import { ICollectionItem } from '../../interfaces'

class HSScrollspy extends HSBasePlugin<IScrollspyOptions> implements IScrollspy {
  private readonly ignoreScrollUp: boolean

  private readonly links: NodeListOf<HTMLAnchorElement> | null
  private readonly sections: HTMLElement[] | null
  private readonly scrollableId: string | null
  private readonly scrollable: HTMLElement | Document

  private isScrollingDown: boolean = false
  private lastScrollTop: number = 0

  private onScrollableScrollListener: (evt: Event) => void
  private onLinkClickListener:
    | {
        el: HTMLAnchorElement
        fn: (evt: Event) => void
      }[]
    | null

  constructor(el: HTMLElement, options = {}) {
    super(el, options)

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

    this.ignoreScrollUp = typeof concatOptions.ignoreScrollUp !== 'undefined' ? concatOptions.ignoreScrollUp : false

    this.links = this.el.querySelectorAll('[href]')
    this.sections = []
    this.scrollableId = this.el.getAttribute('data-scrollspy-scrollable-parent')
    this.scrollable = this.scrollableId
      ? (document.querySelector(this.scrollableId) as HTMLElement)
      : (document as Document)

    this.onLinkClickListener = []

    this.init()
  }

  private scrollableScroll(evt: Event) {
    const currentScrollTop = this.scrollable instanceof HTMLElement ? this.scrollable.scrollTop : window.scrollY
    this.isScrollingDown = currentScrollTop > this.lastScrollTop
    this.lastScrollTop = currentScrollTop <= 0 ? 0 : currentScrollTop

    Array.from(this.sections).forEach((section: HTMLElement) => {
      if (!section.getAttribute('id')) return false

      this.update(evt, section)
    })
  }

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

    this.links.forEach(el => {
      this.sections.push(this.scrollable.querySelector(el.getAttribute('href')))
    })

    this.onScrollableScrollListener = evt => this.scrollableScroll(evt)

    this.scrollable.addEventListener('scroll', this.onScrollableScrollListener)

    this.links.forEach(el => {
      this.onLinkClickListener.push({
        el,
        fn: (evt: Event) => this.linkClick(evt, el)
      })

      el.addEventListener('click', this.onLinkClickListener.find(link => link.el === el).fn)
    })
  }

  private determineScrollDirection(target: HTMLAnchorElement): boolean {
    const activeLink = this.el.querySelector('a.active') as HTMLAnchorElement | null

    if (!activeLink) {
      return true
    }

    const activeIndex = Array.from(this.links).indexOf(activeLink)
    const targetIndex = Array.from(this.links).indexOf(target)

    if (targetIndex === -1) {
      return true
    }

    return targetIndex > activeIndex
  }

  private linkClick(evt: Event, el: HTMLAnchorElement) {
    evt.preventDefault()

    const href = el.getAttribute('href')
    if (!href || href === 'javascript:;') return

    const target: HTMLElement | null = href ? document.querySelector(href) : null
    if (!target) return

    this.isScrollingDown = this.determineScrollDirection(el)

    this.scrollTo(el)
  }

  private update(evt: Event, section: HTMLElement) {
    const globalOffset = parseInt(getClassProperty(this.el, '--scrollspy-offset', '0'))
    const userOffset = parseInt(getClassProperty(section, '--scrollspy-offset')) || globalOffset
    const scrollableParentOffset =
      evt.target === document ? 0 : parseInt(String((evt.target as HTMLElement).getBoundingClientRect().top))
    const topOffset = parseInt(String(section.getBoundingClientRect().top)) - userOffset - scrollableParentOffset
    const height = section.offsetHeight
    const statement = this.ignoreScrollUp
      ? topOffset <= 0 && topOffset + height > 0
      : this.isScrollingDown
        ? topOffset <= 0 && topOffset + height > 0
        : topOffset <= 0 && topOffset < height

    if (statement) {
      this.links.forEach(el => el.classList.remove('active'))

      const current = this.el.querySelector(`[href="#${section.getAttribute('id')}"]`)

      if (current) {
        current.classList.add('active')

        const group = current.closest('[data-scrollspy-group]')

        if (group) {
          const parentLink = group.querySelector('[href]')

          if (parentLink) parentLink.classList.add('active')
        }
      }

      this.fireEvent('afterScroll', current)
      dispatch('afterScroll.scrollspy', current, this.el)
    }
  }

  private scrollTo(link: HTMLAnchorElement) {
    const targetId = link.getAttribute('href')
    const target: HTMLElement = document.querySelector(targetId)
    const globalOffset = parseInt(getClassProperty(this.el, '--scrollspy-offset', '0'))
    const userOffset = parseInt(getClassProperty(target, '--scrollspy-offset')) || globalOffset
    const scrollableParentOffset = this.scrollable === document ? 0 : (this.scrollable as HTMLElement).offsetTop
    const topOffset = target.offsetTop - userOffset - scrollableParentOffset
    const view = this.scrollable === document ? window : this.scrollable
    const scrollFn = () => {
      window.history.replaceState(null, null, link.getAttribute('href'))

      if ('scrollTo' in view) {
        view.scrollTo({
          top: topOffset,
          left: 0,
          behavior: 'smooth'
        })
      }
    }

    const beforeScroll = this.fireEvent('beforeScroll', this.el)
    dispatch('beforeScroll.scrollspy', this.el, this.el)

    if (beforeScroll instanceof Promise) beforeScroll.then(() => scrollFn())
    else scrollFn()
  }

  // Public methods
  public destroy() {
    // Remove classes
    const activeLink = this.el.querySelector('[href].active')
    activeLink.classList.remove('active')

    // Remove listeners
    this.scrollable.removeEventListener('scroll', this.onScrollableScrollListener)
    if (this.onLinkClickListener.length)
      this.onLinkClickListener.forEach(({ el, fn }) => {
        el.removeEventListener('click', fn)
      })

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

  // Static methods
  static getInstance(target: HTMLElement, isInstance = false) {
    const elInCollection = window.$hsScrollspyCollection.find(
      el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target)
    )

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

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

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

    document.querySelectorAll('[data-scrollspy]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => {
      if (!window.$hsScrollspyCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) new HSScrollspy(el)
    })
  }
}

declare global {
  interface Window {
    HSScrollspy: Function
    $hsScrollspyCollection: ICollectionItem<HSScrollspy>[]
  }
}

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

  // Uncomment for debug
  // console.log('Scrollspy collection:', window.$hsScrollspyCollection);
})

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

export default HSScrollspy
