/**
 * --------------------------------------------------------------------------
 * Bootstrap (v5.2.0-beta1): carousel.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 * --------------------------------------------------------------------------
 */

import {
    defineJQueryPlugin,
    getElementFromSelector,
    getNextActiveElement,
    isRTL,
    isVisible,
    reflow,
    triggerTransitionEnd
} from './util/index'
import EventHandler from './dom/event-handler'
import Manipulator from './dom/manipulator'
import SelectorEngine from './dom/selector-engine'
import Swipe from './util/swipe'
import BaseComponent from './base-component'

/**
 * Constants
 */

const NAME = 'carousel'
const DATA_KEY = 'bs.carousel'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'

const ARROW_LEFT_KEY = 'ArrowLeft'
const ARROW_RIGHT_KEY = 'ArrowRight'
const TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch

const ORDER_NEXT = 'next'
const ORDER_PREV = 'prev'
const DIRECTION_LEFT = 'left'
const DIRECTION_RIGHT = 'right'

const EVENT_SLIDE = `slide${EVENT_KEY}`
const EVENT_SLID = `slid${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_MOUSEENTER = `mouseenter${EVENT_KEY}`
const EVENT_MOUSELEAVE = `mouseleave${EVENT_KEY}`
const EVENT_DRAG_START = `dragstart${EVENT_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`

const CLASS_NAME_CAROUSEL = 'carousel'
const CLASS_NAME_ACTIVE = 'active'
const CLASS_NAME_SLIDE = 'slide'
const CLASS_NAME_END = 'carousel-item-end'
const CLASS_NAME_START = 'carousel-item-start'
const CLASS_NAME_NEXT = 'carousel-item-next'
const CLASS_NAME_PREV = 'carousel-item-prev'

const SELECTOR_ACTIVE = '.active'
const SELECTOR_ITEM = '.carousel-item'
const SELECTOR_ACTIVE_ITEM = SELECTOR_ACTIVE + SELECTOR_ITEM
const SELECTOR_ITEM_IMG = '.carousel-item img'
const SELECTOR_INDICATORS = '.carousel-indicators'
const SELECTOR_DATA_SLIDE = '[data-bs-slide], [data-bs-slide-to]'
const SELECTOR_DATA_RIDE = '[data-bs-ride="carousel"]'

const KEY_TO_DIRECTION = {
    [ARROW_LEFT_KEY]: DIRECTION_RIGHT,
    [ARROW_RIGHT_KEY]: DIRECTION_LEFT
}

const Default = {
    interval: 5000,
    keyboard: true,
    pause: 'hover',
    ride: false,
    touch: true,
    wrap: true
}

const DefaultType = {
    interval: '(number|boolean)',
    keyboard: 'boolean',
    ride: '(boolean|string)',
    pause: '(string|boolean)',
    touch: 'boolean',
    wrap: 'boolean'
}

/**
 * Class definition
 */

class Carousel extends BaseComponent {
    constructor(element, config) {
        super(element, config)

        this._interval = null
        this._activeElement = null
        this._isSliding = false
        this.touchTimeout = null
        this._swipeHelper = null

        this._indicatorsElement = SelectorEngine.findOne(SELECTOR_INDICATORS, this._element)
        this._addEventListeners()

        if (this._config.ride === CLASS_NAME_CAROUSEL) {
            this.cycle()
        }
    }

    // Getters
    static get Default() {
        return Default
    }

    static get DefaultType() {
        return DefaultType
    }

    static get NAME() {
        return NAME
    }

    // Public
    next() {
        this._slide(ORDER_NEXT)
    }

    nextWhenVisible() {
        // FIXME TODO use `document.visibilityState`
        // Don't call next when the page isn't visible
        // or the carousel or its parent isn't visible
        if (!document.hidden && isVisible(this._element)) {
            this.next()
        }
    }

    prev() {
        this._slide(ORDER_PREV)
    }

    pause() {
        if (this._isSliding) {
            triggerTransitionEnd(this._element)
        }

        this._clearInterval()
    }

    cycle() {
        this._clearInterval()
        this._updateInterval()

        this._interval = setInterval(() => this.nextWhenVisible(), this._config.interval)
    }

    _maybeEnableCycle() {
        if (!this._config.ride) {
            return
        }

        if (this._isSliding) {
            EventHandler.one(this._element, EVENT_SLID, () => this.cycle())
            return
        }

        this.cycle()
    }

    to(index) {
        const items = this._getItems()
        if (index > items.length - 1 || index < 0) {
            return
        }

        if (this._isSliding) {
            EventHandler.one(this._element, EVENT_SLID, () => this.to(index))
            return
        }

        const activeIndex = this._getItemIndex(this._getActive())
        if (activeIndex === index) {
            return
        }

        const order = index > activeIndex ? ORDER_NEXT : ORDER_PREV

        this._slide(order, items[index])
    }

    dispose() {
        if (this._swipeHelper) {
            this._swipeHelper.dispose()
        }

        super.dispose()
    }

    // Private
    _configAfterMerge(config) {
        config.defaultInterval = config.interval
        return config
    }

    _addEventListeners() {
        if (this._config.keyboard) {
            EventHandler.on(this._element, EVENT_KEYDOWN, event => this._keydown(event))
        }

        if (this._config.pause === 'hover') {
            EventHandler.on(this._element, EVENT_MOUSEENTER, () => this.pause())
            EventHandler.on(this._element, EVENT_MOUSELEAVE, () => this._maybeEnableCycle())
        }

        if (this._config.touch && Swipe.isSupported()) {
            this._addTouchEventListeners()
        }
    }

    _addTouchEventListeners() {
        for (const img of SelectorEngine.find(SELECTOR_ITEM_IMG, this._element)) {
            EventHandler.on(img, EVENT_DRAG_START, event => event.preventDefault())
        }

        const endCallBack = () => {
            if (this._config.pause !== 'hover') {
                return
            }

            // If it's a touch-enabled device, mouseenter/leave are fired as
            // part of the mouse compatibility events on first tap - the carousel
            // would stop cycling until user tapped out of it;
            // here, we listen for touchend, explicitly pause the carousel
            // (as if it's the second time we tap on it, mouseenter compat event
            // is NOT fired) and after a timeout (to allow for mouse compatibility
            // events to fire) we explicitly restart cycling

            this.pause()
            if (this.touchTimeout) {
                clearTimeout(this.touchTimeout)
            }

            this.touchTimeout = setTimeout(() => this._maybeEnableCycle(), TOUCHEVENT_COMPAT_WAIT + this._config.interval)
        }

        const swipeConfig = {
            leftCallback: () => this._slide(this._directionToOrder(DIRECTION_LEFT)),
            rightCallback: () => this._slide(this._directionToOrder(DIRECTION_RIGHT)),
            endCallback: endCallBack
        }

        this._swipeHelper = new Swipe(this._element, swipeConfig)
    }

    _keydown(event) {
        if (/input|textarea/i.test(event.target.tagName)) {
            return
        }

        const direction = KEY_TO_DIRECTION[event.key]
        if (direction) {
            event.preventDefault()
            this._slide(this._directionToOrder(direction))
        }
    }

    _getItemIndex(element) {
        return this._getItems().indexOf(element)
    }

    _setActiveIndicatorElement(index) {
        if (!this._indicatorsElement) {
            return
        }

        const activeIndicator = SelectorEngine.findOne(SELECTOR_ACTIVE, this._indicatorsElement)

        activeIndicator.classList.remove(CLASS_NAME_ACTIVE)
        activeIndicator.removeAttribute('aria-current')

        const newActiveIndicator = SelectorEngine.findOne(`[data-bs-slide-to="${index}"]`, this._indicatorsElement)

        if (newActiveIndicator) {
            newActiveIndicator.classList.add(CLASS_NAME_ACTIVE)
            newActiveIndicator.setAttribute('aria-current', 'true')
        }
    }

    _updateInterval() {
        const element = this._activeElement || this._getActive()

        if (!element) {
            return
        }

        const elementInterval = Number.parseInt(element.getAttribute('data-bs-interval'), 10)

        this._config.interval = elementInterval || this._config.defaultInterval
    }

    _slide(order, element = null) {
        if (this._isSliding) {
            return
        }

        const activeElement = this._getActive()
        const isNext = order === ORDER_NEXT
        const nextElement = element || getNextActiveElement(this._getItems(), activeElement, isNext, this._config.wrap)

        if (nextElement === activeElement) {
            return
        }

        const nextElementIndex = this._getItemIndex(nextElement)

        const triggerEvent = eventName => {
            return EventHandler.trigger(this._element, eventName, {
                relatedTarget: nextElement,
                direction: this._orderToDirection(order),
                from: this._getItemIndex(activeElement),
                to: nextElementIndex
            })
        }

        const slideEvent = triggerEvent(EVENT_SLIDE)

        if (slideEvent.defaultPrevented) {
            return
        }

        if (!activeElement || !nextElement) {
            // Some weirdness is happening, so we bail
            // todo: change tests that use empty divs to avoid this check
            return
        }

        const isCycling = Boolean(this._interval)
        this.pause()

        this._isSliding = true

        this._setActiveIndicatorElement(nextElementIndex)
        this._activeElement = nextElement

        const directionalClassName = isNext ? CLASS_NAME_START : CLASS_NAME_END
        const orderClassName = isNext ? CLASS_NAME_NEXT : CLASS_NAME_PREV

        nextElement.classList.add(orderClassName)

        reflow(nextElement)

        activeElement.classList.add(directionalClassName)
        nextElement.classList.add(directionalClassName)

        const completeCallBack = () => {
            nextElement.classList.remove(directionalClassName, orderClassName)
            nextElement.classList.add(CLASS_NAME_ACTIVE)

            activeElement.classList.remove(CLASS_NAME_ACTIVE, orderClassName, directionalClassName)

            this._isSliding = false

            triggerEvent(EVENT_SLID)
        }

        this._queueCallback(completeCallBack, activeElement, this._isAnimated())

        if (isCycling) {
            this.cycle()
        }
    }

    _isAnimated() {
        return this._element.classList.contains(CLASS_NAME_SLIDE)
    }

    _getActive() {
        return SelectorEngine.findOne(SELECTOR_ACTIVE_ITEM, this._element)
    }

    _getItems() {
        return SelectorEngine.find(SELECTOR_ITEM, this._element)
    }

    _clearInterval() {
        if (this._interval) {
            clearInterval(this._interval)
            this._interval = null
        }
    }

    _directionToOrder(direction) {
        if (isRTL()) {
            return direction === DIRECTION_LEFT ? ORDER_PREV : ORDER_NEXT
        }

        return direction === DIRECTION_LEFT ? ORDER_NEXT : ORDER_PREV
    }

    _orderToDirection(order) {
        if (isRTL()) {
            return order === ORDER_PREV ? DIRECTION_LEFT : DIRECTION_RIGHT
        }

        return order === ORDER_PREV ? DIRECTION_RIGHT : DIRECTION_LEFT
    }

    // Static
    static jQueryInterface(config) {
        return this.each(function() {
            const data = Carousel.getOrCreateInstance(this, config)

            if (typeof config === 'number') {
                data.to(config)
                return
            }

            if (typeof config === 'string') {
                if (data[config] === undefined || config.startsWith('_') || config === 'constructor') {
                    throw new TypeError(`No method named "${config}"`)
                }

                data[config]()
            }
        })
    }
}

/**
 * Data API implementation
 */

EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_SLIDE, function(event) {
    const target = getElementFromSelector(this)

    if (!target || !target.classList.contains(CLASS_NAME_CAROUSEL)) {
        return
    }

    event.preventDefault()

    const carousel = Carousel.getOrCreateInstance(target)
    const slideIndex = this.getAttribute('data-bs-slide-to')

    if (slideIndex) {
        carousel.to(slideIndex)
        carousel._maybeEnableCycle()
        return
    }

    if (Manipulator.getDataAttribute(this, 'slide') === 'next') {
        carousel.next()
        carousel._maybeEnableCycle()
        return
    }

    carousel.prev()
    carousel._maybeEnableCycle()
})

EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
    const carousels = SelectorEngine.find(SELECTOR_DATA_RIDE)

    for (const carousel of carousels) {
        Carousel.getOrCreateInstance(carousel)
    }
})

/**
 * jQuery
 */

defineJQueryPlugin(Carousel)

export default Carousel