/**
 * --------------------------------------------------------------------------
 * Bootstrap (v5.1.3): util/index.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
 * --------------------------------------------------------------------------
 */


define([
    "jquery",
    'domReady!'
], function() {
    'use strict';

    const MAX_UID = 1000000
    const MILLISECONDS_MULTIPLIER = 1000
    const TRANSITION_END = 'transitionend'

    // Shoutout AngusCroll (https://goo.gl/pxwQGp)
    const toType = obj => {
        if (obj === null || obj === undefined) {
            return `${obj}`
        }

        return {}.toString.call(obj).match(/\s([a-z]+)/i)[1].toLowerCase()
    }

    /**
     * --------------------------------------------------------------------------
     * Public Util Api
     * --------------------------------------------------------------------------
     */

    const getUID = prefix => {
        do {
            prefix += Math.floor(Math.random() * MAX_UID)
        } while (document.getElementById(prefix))

        return prefix
    }

    const getSelector = element => {
        let selector = element.getAttribute('data-bs-target')

        if (!selector || selector === '#') {
            let hrefAttr = element.getAttribute('href')

            // The only valid content that could double as a selector are IDs or classes,
            // so everything starting with `#` or `.`. If a "real" URL is used as the selector,
            // `document.querySelector` will rightfully complain it is invalid.
            // See https://github.com/twbs/bootstrap/issues/32273
            if (!hrefAttr || (!hrefAttr.includes('#') && !hrefAttr.startsWith('.'))) {
                return null
            }

            // Just in case some CMS puts out a full URL with the anchor appended
            if (hrefAttr.includes('#') && !hrefAttr.startsWith('#')) {
                hrefAttr = `#${hrefAttr.split('#')[1]}`
            }

            selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : null
        }

        return selector
    }

    const getSelectorFromElement = element => {
        const selector = getSelector(element)

        if (selector) {
            return document.querySelector(selector) ? selector : null
        }

        return null
    }

    const getElementFromSelector = element => {
        const selector = getSelector(element)

        return selector ? document.querySelector(selector) : null
    }

    const getTransitionDurationFromElement = element => {
        if (!element) {
            return 0
        }

        // Get transition-duration of the element
        let {transitionDuration, transitionDelay} = window.getComputedStyle(element)

        const floatTransitionDuration = Number.parseFloat(transitionDuration)
        const floatTransitionDelay = Number.parseFloat(transitionDelay)

        // Return 0 if element or transition duration is not found
        if (!floatTransitionDuration && !floatTransitionDelay) {
            return 0
        }

        // If multiple durations are defined, take the first
        transitionDuration = transitionDuration.split(',')[0]
        transitionDelay = transitionDelay.split(',')[0]

        return (Number.parseFloat(transitionDuration) + Number.parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER
    }

    const triggerTransitionEnd = element => {
        element.dispatchEvent(new Event(TRANSITION_END))
    }

    const isElement = obj => {
        if (!obj || typeof obj !== 'object') {
            return false
        }

        if (typeof obj.jquery !== 'undefined') {
            obj = obj[0]
        }

        return typeof obj.nodeType !== 'undefined'
    }

    const getElement = obj => {
        if (isElement(obj)) { // it's a jQuery object or a node element
            return obj.jquery ? obj[0] : obj
        }

        if (typeof obj === 'string' && obj.length > 0) {
            return document.querySelector(obj)
        }

        return null
    }

    const typeCheckConfig = (componentName, config, configTypes) => {
        Object.keys(configTypes).forEach(property => {
            const expectedTypes = configTypes[property]
            const value = config[property]
            const valueType = value && isElement(value) ? 'element' : toType(value)

            if (!new RegExp(expectedTypes).test(valueType)) {
                throw new TypeError(
                    `${componentName.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
                )
            }
        })
    }

    const isVisible = element => {
        if (!isElement(element) || element.getClientRects().length === 0) {
            return false
        }

        return getComputedStyle(element).getPropertyValue('visibility') === 'visible'
    }

    const isDisabled = element => {
        if (!element || element.nodeType !== Node.ELEMENT_NODE) {
            return true
        }

        if (element.classList.contains('disabled')) {
            return true
        }

        if (typeof element.disabled !== 'undefined') {
            return element.disabled
        }

        return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false'
    }

    const findShadowRoot = element => {
        if (!document.documentElement.attachShadow) {
            return null
        }

        // Can find the shadow root otherwise it'll return the document
        if (typeof element.getRootNode === 'function') {
            const root = element.getRootNode()
            return root instanceof ShadowRoot ? root : null
        }

        if (element instanceof ShadowRoot) {
            return element
        }

        // when we don't find a shadow root
        if (!element.parentNode) {
            return null
        }

        return findShadowRoot(element.parentNode)
    }

    const noop = () => {}

    /**
     * Trick to restart an element's animation
     *
     * @param {HTMLElement} element
     * @return void
     *
     * @see https://www.charistheo.io/blog/2021/02/restart-a-css-animation-with-javascript/#restarting-a-css-animation
     */
    const reflow = element => {
        // eslint-disable-next-line no-unused-expressions
        element.offsetHeight
    }

    const getjQuery = () => {
        const {jQuery} = window

        if (jQuery && !document.body.hasAttribute('data-bs-no-jquery')) {
            return jQuery
        }

        return null
    }

    const DOMContentLoadedCallbacks = []

    const onDOMContentLoaded = callback => {
        if (document.readyState === 'loading') {
            // add listener on the first call when the document is in loading state
            if (!DOMContentLoadedCallbacks.length) {
                document.addEventListener('DOMContentLoaded', () => {
                    DOMContentLoadedCallbacks.forEach(callback => callback())
                })
            }

            DOMContentLoadedCallbacks.push(callback)
        } else {
            callback()
        }
    }

    const isRTL = () => document.documentElement.dir === 'rtl'

    const defineJQueryPlugin = plugin => {
        onDOMContentLoaded(() => {
            const $ = getjQuery()
            /* istanbul ignore if */
            if ($) {
                const name = plugin.NAME
                const JQUERY_NO_CONFLICT = $.fn[name]
                $.fn[name] = plugin.jQueryInterface
                $.fn[name].Constructor = plugin
                $.fn[name].noConflict = () => {
                    $.fn[name] = JQUERY_NO_CONFLICT
                    return plugin.jQueryInterface
                }
            }
        })
    }

    const execute = callback => {
        if (typeof callback === 'function') {
            callback()
        }
    }

    const executeAfterTransition = (callback, transitionElement, waitForTransition = true) => {
        if (!waitForTransition) {
            execute(callback)
            return
        }

        const durationPadding = 5
        const emulatedDuration = getTransitionDurationFromElement(transitionElement) + durationPadding

        let called = false

        const handler = ({ target }) => {
            if (target !== transitionElement) {
                return
            }

            called = true
            transitionElement.removeEventListener(TRANSITION_END, handler)
            execute(callback)
        }

        transitionElement.addEventListener(TRANSITION_END, handler)
        setTimeout(() => {
            if (!called) {
                triggerTransitionEnd(transitionElement)
            }
        }, emulatedDuration)
    }

    /**
     * Return the previous/next element of a list.
     *
     * @param {array} list    The list of elements
     * @param activeElement   The active element
     * @param shouldGetNext   Choose to get next or previous element
     * @param isCycleAllowed
     * @return {Element|elem} The proper element
     */
    const getNextActiveElement = (list, activeElement, shouldGetNext, isCycleAllowed) => {
        let index = list.indexOf(activeElement)

        // if the element does not exist in the list return an element depending on the direction and if cycle is allowed
        if (index === -1) {
            return list[!shouldGetNext && isCycleAllowed ? list.length - 1 : 0]
        }

        const listLength = list.length

        index += shouldGetNext ? 1 : -1

        if (isCycleAllowed) {
            index = (index + listLength) % listLength
        }

        return list[Math.max(0, Math.min(index, listLength - 1))]
    }

    return {
        getElement,
        getUID,
        getSelectorFromElement,
        getElementFromSelector,
        getTransitionDurationFromElement,
        triggerTransitionEnd,
        isElement,
        typeCheckConfig,
        isVisible,
        isDisabled,
        findShadowRoot,
        noop,
        getNextActiveElement,
        reflow,
        getjQuery,
        onDOMContentLoaded,
        isRTL,
        defineJQueryPlugin,
        execute,
        executeAfterTransition
    };
});
