(function() {
  const loadingStatesUndoQueue = []

  function loadingStateContainer(target) {
    return htmx.closest(target, '[data-loading-states]') || document.body
  }

  function mayProcessUndoCallback(target, callback) {
    if (document.body.contains(target)) {
      callback()
    }
  }

  function mayProcessLoadingStateByPath(elt, requestPath) {
    const pathElt = htmx.closest(elt, '[data-loading-path]')
    if (!pathElt) {
      return true
    }

    return pathElt.getAttribute('data-loading-path') === requestPath
  }

  function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
    const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
    if (delayElt) {
      const delayInMilliseconds =
        delayElt.getAttribute('data-loading-delay') || 200
      const timeout = setTimeout(function() {
        doCallback()

        loadingStatesUndoQueue.push(function() {
          mayProcessUndoCallback(targetElt, undoCallback)
        })
      }, delayInMilliseconds)

      loadingStatesUndoQueue.push(function() {
        mayProcessUndoCallback(targetElt, function() { clearTimeout(timeout) })
      })
    } else {
      doCallback()
      loadingStatesUndoQueue.push(function() {
        mayProcessUndoCallback(targetElt, undoCallback)
      })
    }
  }

  function getLoadingStateElts(loadingScope, type, path) {
    return Array.from(htmx.findAll(loadingScope, '[' + type + ']')).filter(
      function(elt) { return mayProcessLoadingStateByPath(elt, path) }
    )
  }

  function getLoadingTarget(elt) {
    if (elt.getAttribute('data-loading-target')) {
      return Array.from(
        htmx.findAll(elt.getAttribute('data-loading-target'))
      )
    }
    return [elt]
  }

  htmx.defineExtension('loading-states', {
    onEvent: function(name, evt) {
      if (name === 'htmx:beforeRequest') {
        const container = loadingStateContainer(evt.target)

        const loadingStateTypes = [
          'data-loading',
          'data-loading-class',
          'data-loading-class-remove',
          'data-loading-disable',
          'data-loading-aria-busy'
        ]

        const loadingStateEltsByType = {}

        loadingStateTypes.forEach(function(type) {
          loadingStateEltsByType[type] = getLoadingStateElts(
            container,
            type,
            evt.detail.pathInfo.requestPath
          )
        })

        loadingStateEltsByType['data-loading'].forEach(function(sourceElt) {
          getLoadingTarget(sourceElt).forEach(function(targetElt) {
            queueLoadingState(
              sourceElt,
              targetElt,
              function() {
                targetElt.style.display =
                  sourceElt.getAttribute('data-loading') ||
                  'inline-block'
              },
              function() { targetElt.style.display = 'none' }
            )
          })
        })

        loadingStateEltsByType['data-loading-class'].forEach(
          function(sourceElt) {
            const classNames = sourceElt
              .getAttribute('data-loading-class')
              .split(' ')

            getLoadingTarget(sourceElt).forEach(function(targetElt) {
              queueLoadingState(
                sourceElt,
                targetElt,
                function() {
                  classNames.forEach(function(className) {
                    targetElt.classList.add(className)
                  })
                },
                function() {
                  classNames.forEach(function(className) {
                    targetElt.classList.remove(className)
                  })
                }
              )
            })
          }
        )

        loadingStateEltsByType['data-loading-class-remove'].forEach(
          function(sourceElt) {
            const classNames = sourceElt
              .getAttribute('data-loading-class-remove')
              .split(' ')

            getLoadingTarget(sourceElt).forEach(function(targetElt) {
              queueLoadingState(
                sourceElt,
                targetElt,
                function() {
                  classNames.forEach(function(className) {
                    targetElt.classList.remove(className)
                  })
                },
                function() {
                  classNames.forEach(function(className) {
                    targetElt.classList.add(className)
                  })
                }
              )
            })
          }
        )

        loadingStateEltsByType['data-loading-disable'].forEach(
          function(sourceElt) {
            getLoadingTarget(sourceElt).forEach(function(targetElt) {
              queueLoadingState(
                sourceElt,
                targetElt,
                function() { targetElt.disabled = true },
                function() { targetElt.disabled = false }
              )
            })
          }
        )

        loadingStateEltsByType['data-loading-aria-busy'].forEach(
          function(sourceElt) {
            getLoadingTarget(sourceElt).forEach(function(targetElt) {
              queueLoadingState(
                sourceElt,
                targetElt,
                function() { targetElt.setAttribute('aria-busy', 'true') },
                function() { targetElt.removeAttribute('aria-busy') }
              )
            })
          }
        )
      }

      if (name === 'htmx:beforeOnLoad') {
        while (loadingStatesUndoQueue.length > 0) {
          loadingStatesUndoQueue.shift()()
        }
      }
    }
  })
})()
