import './dom/aria'
import './form/FormFilter'
import './Board'
import './BroadcastAlertBanner'
import './BulkWorkflow'
import './ChartTable'
import './ContentEditDrawer'
import './ContentEditSites'
import './ContentForm'
import './ContentInputGroup'
import './ContentSummary'
import './Conversation'
import './theme'
import './dialog'
import './Diff'
import './ExternalCalendars'
import './ExternalItemObjects'
import './LiveBlog'
import './Notifications'
import './PostPublish'
import './PrePublish'
import './SearchWidget'
import './Suggestions'
import './Translation'
import './graphql/GraphQL'
import './graphql/GraphQLPreview'
import ContentSelectorActions from './ContentSelectorActions'
import Cookies from 'js-cookie'
import RadialProgressBar from './RadialProgressBar'
import { announce } from './dom/aria'
import debounce from './util/debounce'
import getComponentKey from './util/getComponentKey'
import { setupMenuForAria } from './dom/popupMenu.js'
import find from './dom/find'
import onFind from './dom/onFind'
import onVisible from './dom/onVisible'
import { wrap } from './WrappingMixin'
import CodeInput from './form/CodeInput'
import CIGCluster from './CIGCluster'
import CopyToClipboard from './CopyToClipboard'
import EmbeddedInputGroup from './EmbeddedInputGroup'
import Input from './form/Input'
import SearchInput from './form/SearchInput'
import Table from './table'
import TextAreaInput from './form/TextAreaInput'
import Toggleable from './Toggleable'
import Widget from './widget'
import throttle from './util/throttle'

// Set the Brightspot header for all XHR and fetch requests.
const BRIGHTSPOT_HEADER_NAME = 'X-Brightspot-Requested-With'
const BRIGHTSPOT_HEADER_VALUE = 'XMLHttpRequest'
const originalSend = XMLHttpRequest.prototype.send
const originalFetch = window.fetch

XMLHttpRequest.prototype.send = function (...args) {
  this.setRequestHeader(BRIGHTSPOT_HEADER_NAME, BRIGHTSPOT_HEADER_VALUE)
  originalSend.apply(this, args)
}

window.fetch = (
  input: RequestInfo | URL,
  init?: RequestInit,
): Promise<Response> => {
  const headers = new Headers(init?.headers)
  headers.set(BRIGHTSPOT_HEADER_NAME, BRIGHTSPOT_HEADER_VALUE)

  const newInit: RequestInit = {
    ...init,
    headers: headers,
  }
  return originalFetch(input, newInit)
}

onVisible('textarea[data-code-type]', (input: HTMLTextAreaElement) => {
  const code = new CodeInput()

  code.type = input.dataset.codeType
  code.placeholder = input.placeholder
  code.placeholderEditable = 'editablePlaceholder' in input.dataset
  code.value = input.value

  // Debounce the bubbled change event so we don't fire 10+ document-level
  // jQuery change delegates (several with `:has()`) per keystroke.
  //
  // Lives here, not in CodeInput: moving it would either debounce the
  // value-sync (form-submit sees stale data) or split CodeInput's API for
  // one consumer. Remove when the v3 jQuery delegates are retired.
  const CHANGE_DEBOUNCE_MS = 200
  const dispatchChange = debounce(CHANGE_DEBOUNCE_MS, () => {
    input.dispatchEvent(new Event('change', { bubbles: true }))
  })
  code.addEventListener('change', () => {
    input.value = code.value
    dispatchChange()
  })

  // We decided word count isn't meaningful on code. Opt the external textarea
  // out of the v3 word-count delegate, which writes to a sibling via jQuery
  // `.text()` on every keystroke and invalidates layout up the `:has()`
  // ancestor chain. CM uses `inputStyle: 'contenteditable'` (set in
  // CodeInput), so there is no internal textarea for the delegate to find —
  // the delegate's `textarea` selector cannot match CM's editable `<div>`.
  input.dataset.hideWordCount = 'true'

  input.hidden = true
  input.parentNode?.insertBefore(code, input)
})

/*
  Only wrap visible inputs which are "text" or "password" types,
  with the following exclusions:
  `:not([type])` because <input>s without a "type" default to "text".
  `:not(:is(.objectId))` because those classes of inputs are hidden with css after the fact (object.js).
  `:not(:is(.ComboInput-filter))` because for some reason the selects option values don't change when "data-dynamic-value-generator"s are used (state.js).
*/
onVisible(
  [
    'input:not([type])',
    'input[type="text"]:not(:is(.objectId, .ComboInput-filter))',
    'input[type="password"]:not(:is(.objectId))',
  ],
  (input: HTMLInputElement) => {
    if (input.classList.contains('secret')) {
      input.type = 'password'
    }
    wrap(input, Input, (btuInput) => {
      if (btuInput instanceof Input) {
        // Preserve focus if initial input has focus.
        if (document.activeElement === input) {
          btuInput.autofocusInput = true
        }
        btuInput.type = input.type ?? undefined
        btuInput.placeholder = input.placeholder
        btuInput.placeholderEditable = 'editablePlaceholder' in input.dataset
      }
    })
  },
)

onVisible(
  '.inputSmall-text > textarea:not(.richtext):not([data-code-type])',
  (input: HTMLTextAreaElement) => {
    wrap(input, TextAreaInput, (btuTextareaInput) => {
      if (btuTextareaInput instanceof TextAreaInput) {
        btuTextareaInput.placeholder = input.placeholder
        btuTextareaInput.placeholderEditable =
          'editablePlaceholder' in input.dataset
      }
    })
  },
)

// Initialize radial progress bars.
onFind('.p-workStreams .progressBar', RadialProgressBar.onFindFunction(48, 6))
onFind(
  '.ContentEdit-workstreamProgress .progressBar',
  RadialProgressBar.onFindFunction(24, 4),
)

onFind(
  '.ContentEdit div.Widget > .Widget-title',
  (legacyWidgetTitle: HTMLElement) => {
    const legacyWidget = legacyWidgetTitle.parentElement

    // Only initialize widgets whose direct child is the widget title.
    if (
      legacyWidget instanceof HTMLElement &&
      legacyWidget.classList.contains('Widget')
    ) {
      const widget = new Widget()

      // Legacy widgets are collapsible if they have a data-internal-name attribute.
      if (legacyWidget.dataset.internalName) {
        widget.collapsible = true
      }

      widget.classList.add(...legacyWidget.classList)

      for (const d in legacyWidget.dataset) {
        widget.dataset[d] = legacyWidget.dataset[d]
      }

      widget.internalname = legacyWidget.dataset.internalName
      widget.widgetid = legacyWidget.dataset.widgetId
      widget.initialClosed = legacyWidget.hasAttribute('data-closed')

      // Assign the header title
      const title = legacyWidget.querySelector<HTMLElement>('.Widget-title')
      if (title) {
        widget.heading = title.innerText
      }

      // Move the content when the WC is ready.
      widget.addEventListener('btu-widget-ready', (evt) => {
        const contents = legacyWidget.querySelectorAll(
          ':scope > *:not(.Widget-title)',
        )
        if (contents != null) {
          widget.append(...contents)
        }
      })

      // Update the DOM.
      legacyWidget.parentNode?.insertBefore(widget, legacyWidget)
      legacyWidget.remove()
    }
  },
)

/*
  Makes the last state of the fullscreen search accessible via the back button.
  Adds fullscreen search parameters to history before navigating away.
*/
window.addEventListener('beforeunload', (event) => {
  if (
    event.target instanceof Document &&
    event.target.querySelector(
      '.SearchWidget[name="searchAdvancedFull"]:not(.AssignmentDeskDashboard)',
    )
  ) {
    const fullscreenButton = event.target.querySelector(
      '.SearchResult-action-link[href*="/searchAdvancedFull"]',
    )
    const searchParams = fullscreenButton?.getAttribute('href')?.split('?')[1]
    window.history.replaceState(
      null,
      '',
      window.location.pathname + '?' + searchParams,
    )
  }
})

onFind(
  [
    '.SearchWidget.is-global .SavedSearches-links a',
    '.SearchWidget.is-global .SavedSearches-links btu-copy-to-clipboard',
  ],
  (a: HTMLElement) => {
    a.addEventListener('click', () => {
      const input = document.querySelector(
        '.Page-search .SearchInput input',
      ) as HTMLInputElement

      if (input) {
        input.value = ''
      }
    })
  },
)

onFind(
  '.SearchWidget .SavedSearches-links .SavedSearches-more-dropdown',
  (dropdown: HTMLElement) => {
    const button = dropdown.parentNode?.querySelector('.SavedSearches-more')
    setupMenuForAria(dropdown, button)
  },
)

onFind(
  [
    'div.SearchWidget-moreFilters > h2.SearchWidget-moreFiltersHeading',
    'div.SearchWidget-recent > h2.searchRecent-heading',
    'div.SearchWidget-saved > h2.searchSaved-heading',
    'div.SearchWidget-create > h2:first-child',
  ],
  (legacyWidgetTitle: HTMLElement) => {
    const legacyWidget = legacyWidgetTitle.parentElement

    if (!(legacyWidget instanceof HTMLElement)) return

    const widget = new Widget()

    widget.collapsible = true

    widget.classList.add(...legacyWidget.classList)

    widget.setAttribute('internalname', legacyWidget.classList[0])
    widget.internalname = legacyWidget.classList[0]
    widget.setAttribute('widgetid', legacyWidget.classList[0])

    widget.heading = legacyWidgetTitle.innerText

    for (const d in legacyWidgetTitle.dataset) {
      widget.dataset[d] = legacyWidgetTitle.dataset[d]
    }

    // Move the content when the WC is ready.
    widget.addEventListener('btu-widget-ready', (evt) => {
      const contents = legacyWidget.querySelectorAll(':scope > *:not(h2)')
      if (contents != null) {
        widget.append(...contents)
      }
    })

    // Update the DOM.
    legacyWidget.parentNode?.insertBefore(widget, legacyWidget)
    legacyWidget.remove()
  },
)

onFind('a.SavedSearches-more-dropdown-copy', (legacyElement: HTMLElement) => {
  const copyToClipboard = new CopyToClipboard()
  copyToClipboard.classList.add(...legacyElement.classList)
  copyToClipboard.copydata = legacyElement.dataset.searchString
  copyToClipboard.internallink = true
  copyToClipboard.textContent = legacyElement.textContent
  copyToClipboard.setAttribute('role', 'menuitem')
  copyToClipboard.title = legacyElement.title

  legacyElement.parentNode?.insertBefore(copyToClipboard, legacyElement)
  legacyElement.remove()

  copyToClipboard.addEventListener('click', () => {
    const button = copyToClipboard
      .closest('.SavedSearches-links')
      ?.querySelector<HTMLElement>('button.SavedSearches-more')
    if (button?.ariaExpanded === 'true') {
      button.click()
      button.focus()
    }

    const search = find(document.body, '.Page-search .SearchInput input')
    if (search instanceof HTMLInputElement) {
      search.value = ''
    }

    copyToClipboard.copy()
  })

  copyToClipboard.addEventListener('keydown', ({ key }) => {
    if (key === 'Enter') copyToClipboard.copy()
  })

  copyToClipboard.addEventListener('btu-copy-to-clipboard-copied', () => {
    const copySuccessMessage = legacyElement.dataset.copiedLink ?? ''
    if (!copySuccessMessage) return

    announce(copySuccessMessage)
    const currentText = copyToClipboard.firstChild?.nodeValue
    if (copyToClipboard.firstChild) {
      copyToClipboard.firstChild.nodeValue = copySuccessMessage
      setTimeout(() => {
        if (copyToClipboard.firstChild) {
          copyToClipboard.firstChild.nodeValue = currentText ?? ''
        }
      }, 3000)
    }
  })
})

onFind('a.ApiKey-copy', (legacyElement: HTMLElement) => {
  const copyToClipboard = new CopyToClipboard()
  copyToClipboard.classList.add(...legacyElement.classList)
  const key = legacyElement
    .closest('.ApiKey-apiKey')
    ?.querySelector('.ApiKey-value')?.textContent
  if (!key) return
  copyToClipboard.copydata = key
  copyToClipboard.title = legacyElement.title

  legacyElement.parentNode?.insertBefore(copyToClipboard, legacyElement)
  legacyElement.remove()

  copyToClipboard.addEventListener('btu-copy-to-clipboard-copied', () => {
    const copySuccessMessage =
      legacyElement.querySelector('.ApiKey-copyConfirmed')?.textContent ?? ''
    if (copySuccessMessage) {
      announce(copySuccessMessage)
    }
  })
})

// Convert legacy LiveBlogPostList filter widget to WC
onFind(
  '.LiveBlogPostList .LiveBlogPostList-filters > .LiveBlogPostList-filtersTitle',
  (postListTitle: HTMLElement) => {
    const postFilters = postListTitle.parentElement
    const postNav = postFilters?.closest<HTMLElement>('.LiveBlogPostListNav')

    postNav?.querySelector<HTMLElement>('.Widget-collapse')?.remove()

    // Only initialize widgets whose direct child is the widget title.
    if (postFilters instanceof HTMLElement) {
      const widget = new Widget()

      widget.collapsible = true

      const key = getComponentKey(postFilters, 'data-internal-name')

      if (key) {
        widget.collapsed = localStorage.getItem(key) != null
      }

      widget.classList.add(...postFilters.classList)

      for (const d in postFilters.dataset) {
        widget.dataset[d] = postFilters.dataset[d]
      }

      const widgetId = postFilters.dataset.widgetId
      const internalName = postFilters.dataset.internalName

      if (widgetId && internalName) {
        widget.setAttribute('widgetid', widgetId)
        widget.setAttribute('internalname', internalName)
      }

      // Assign the header title
      widget.heading = postListTitle.innerText

      // Move the content when the WC is ready.
      widget.addEventListener('btu-widget-ready', (evt) => {
        const contents = postFilters.querySelectorAll(
          ':scope > *:not(.LiveBlogPostList-filtersTitle)',
        )
        if (contents != null) {
          widget.append(...contents)
        }
      })

      // Update the DOM.
      postFilters.parentNode?.insertBefore(widget, postFilters)
      postFilters.remove()
    }
  },
)

onFind('.toggleable', (select: HTMLSelectElement) => {
  wrap(select, Toggleable, (btuToggleable) => {
    if (btuToggleable instanceof Toggleable) {
      btuToggleable.rootSelector = select.dataset.root
    }
  })
})

onFind('.TranslationFrame .LinkTable', (legacyTable: HTMLElement) => {
  wrap(legacyTable, Table, (btuTable) => {
    if (btuTable instanceof Table) {
      btuTable.classList.add(...legacyTable.classList)

      for (const d in legacyTable.dataset) {
        btuTable.dataset[d] = legacyTable.dataset[d]
      }

      // Trigger table form submit.
      btuTable.addEventListener('btu-table-checked-all', (evt) => {
        const $form = $(legacyTable as any)

        $form.trigger('submit')
      })
    }
  })
})

onFind(
  '.objectId-select, .RCIG-item .repeatableLabel, .EIG-item .repeatableLabel',
  (element: HTMLElement) => {
    // eslint-disable-next-line no-new
    // Only collapse actions if item is an RCIG
    if (element.parentElement?.classList.contains('RCIG-item'))
      new ContentSelectorActions(element, {})
  },
)

onFind('.CIG > .CIG-title', (legacyTitle: HTMLElement) => {
  const cluster = new CIGCluster()
  cluster.clusterTitle = legacyTitle.innerText
  cluster.tab = legacyTitle.dataset.tab ?? undefined

  cluster.addEventListener('btu-cig-cluster-ready', (evt) => {
    const contents = []
    let sibling = cluster.nextElementSibling
    while (sibling instanceof HTMLElement) {
      if (sibling.matches('.CIG-row') && sibling.dataset.tab === cluster.tab) {
        contents.push(sibling)
        sibling = sibling.nextElementSibling
      } else {
        break
      }
    }

    if (contents != null) {
      cluster.append(...contents)
    }
    requestAnimationFrame(() => cluster.requestUpdate())
  })

  legacyTitle.parentNode?.insertBefore(cluster, legacyTitle)
  legacyTitle.remove()
})

// Support moving Styleguide fields into CIGClusters based on their data-cluster attribute.
onFind('.TemplateField[data-cluster]', (templateField: HTMLElement) => {
  const clusterTitle = templateField.dataset.cluster
  if (!clusterTitle) return
  const presetRow = templateField.closest('.StyleguidePreset-row')
  if (!presetRow) return

  let cluster = presetRow.querySelector(
    `btu-cig-cluster > .TemplateField[data-cluster="${clusterTitle}"]`,
  )?.parentElement as CIGCluster | null

  if (!cluster) {
    cluster = new CIGCluster()
    cluster.clusterTitle = clusterTitle
    cluster.tab = templateField.dataset.tab ?? ''
    templateField.insertAdjacentElement('beforebegin', cluster)

    // Handle showing/hiding of Cluster based on template selection.
    const templateRow = presetRow.closest('.CIG-row.StyleguideTemplate-row')
    if (templateRow) {
      const templateSelect = templateRow.querySelector('.CIG-small > select')
      if (templateSelect) {
        templateSelect.addEventListener('change', () => {
          if (!cluster) return
          if (
            Array.from(
              cluster.querySelectorAll<HTMLElement>('div.TemplateField'),
            ).some((field) => field.style.display !== 'none')
          ) {
            cluster.style.display = 'block'
          } else {
            cluster.style.display = 'none'
          }
        })
      }
    }
  }

  templateField.querySelector(':scope > .CIG-title')?.remove()
  cluster.append(templateField)
})

onFind('.EIG', (legacyEig: HTMLElement) => {
  const eig = new EmbeddedInputGroup()

  eig.classList.add(...legacyEig.classList)
  for (const d in legacyEig.dataset) {
    eig.dataset[d] = legacyEig.dataset[d]
  }

  // Move the content when the WC is ready.
  eig.addEventListener('btu-eig-ready', () => {
    const contents = legacyEig.querySelectorAll(':scope > *')
    if (contents != null) {
      eig.append(...contents)
    }
    requestAnimationFrame(() => eig.requestUpdate())
  })

  legacyEig.parentNode?.insertBefore(eig, legacyEig)
  legacyEig.remove()
})

// Handle the click event for the "Add Embedded Content" popup
onFind(
  ['a[data-objectId]', 'a[data-add-embedded-content-data]'],
  (element: HTMLElement) => {
    element.onclick = async (event: MouseEvent) => {
      let data
      let $popupSource
      let target
      const node = event.target

      if (node instanceof HTMLElement) {
        target = node.closest<HTMLElement>('a[data-objectId]')
      }

      if (target) {
        $popupSource = $(target).popup('source')

        let typeId = $popupSource.attr('data-add-embedded-content-data')
        if (!typeId) return
        typeId = JSON.parse(typeId).typeId

        const fieldName = $popupSource.attr('data-field')
        if (!fieldName) return

        const selected = target.dataset.objectid
        if (!selected) return

        const dataObj: { [key: string]: string } = { typeId: typeId ?? '' }
        dataObj[fieldName] = selected
        data = JSON.stringify(dataObj)
      } else {
        if (node instanceof HTMLElement) {
          target = node.closest<HTMLElement>('[data-add-embedded-content-data]')
        }

        if (!target) return

        $popupSource = $(target).popup('source')

        data = target.dataset.addEmbeddedContentData ?? undefined
        if (!data) return
      }

      if (!$popupSource) return
      const url = $popupSource.attr('href')
      if (!url || !url.includes('add-embedded-content')) return

      event.preventDefault()

      const headers = new Headers()
      const cookies = Cookies.get('bsp.csrf')

      if (cookies) {
        headers.append('Brightspot-CSRF', cookies)
      }

      const body = new FormData()
      body.append('data', data)

      try {
        const response = await fetch(url, { method: 'POST', headers, body })
        const itemHtml = await response.text()
        let toFocus

        if (
          $popupSource.hasClass('EIG-add') ||
          $popupSource.hasClass('EIG-swap') ||
          $popupSource.hasClass('EIG-search')
        ) {
          const $eig = $popupSource.closest('.EIG')

          // remove any existing embedded items
          $eig.find('.EIG-item').remove()

          $eig.append(itemHtml)

          // backward-compatibility with toggleable
          $eig.find('.toggleable').trigger('toggle')

          // Trigger a custom "create" event for the item
          // So other code can act on this if necessary (ie objectId)
          $eig.trigger('create')
          $eig.trigger('change')
          $popupSource.closest('.EIG-controls').detach()

          toFocus = $eig[0].querySelector<HTMLElement>('.EIG-title')
        } else {
          if ($popupSource.hasClass('RCIG-swap')) {
            $popupSource.closest('.RCIG-item').addClass('RCIG-replace')
          }
          $popupSource.closest('.RCIG').data('repeatable').addItem(itemHtml)

          $popupSource.closest('.RCIG-replace').remove()
        }

        $(target).popup('close')
        toFocus?.focus()
      } catch (error) {
        console.error(error)
      }
    }
  },
)

onFind('.SearchInput:not(btu-search-input)', (legacyElement: HTMLElement) => {
  const searchInput = new SearchInput()

  searchInput.classList.add(...legacyElement.classList)

  const label = legacyElement.querySelector('label')
  if (label) {
    searchInput.label = label.innerText
  }

  const input = legacyElement.querySelector('input')
  if (input) {
    if (input.name) searchInput.name = input.name
    if (input.ariaLabel) searchInput.arialabel = input.ariaLabel
    const val = input.getAttribute('value')
    if (val) searchInput.value = val
  }

  legacyElement.parentNode?.insertBefore(searchInput, legacyElement)
  legacyElement.remove()
})

// Sync the search input in the tool header with the one in the popup
onFind(
  '.Page-search btu-search-input input[type="text"]',
  (input: HTMLInputElement) => {
    const events = ['click', 'input']

    events.forEach((event) => {
      input.addEventListener(
        event,
        debounce(500, (evt) => {
          if (!(evt instanceof Event)) return
          const form = input.closest('form')
          if (!form) return
          const inputValue = input.value
          const searchFrame = document.querySelector(
            '.frame[name="' + form.target + '"]',
          )

          if (
            !searchFrame ||
            (evt.type === 'focus' &&
              inputValue &&
              searchFrame.querySelector('.searchResultList .message-warning'))
          ) {
            form.requestSubmit()
          } else {
            $(searchFrame).popup('open')
            const searchInputs = searchFrame.querySelectorAll<HTMLInputElement>(
              '.searchFilters input[name="q"]',
            )

            searchInputs.forEach((searchInput) => {
              if (inputValue !== searchInput.value) {
                searchInput.value = inputValue
                searchInput.dispatchEvent(
                  new CustomEvent('input', { bubbles: true }),
                )
              }
            })
          }
        }),
      )
    })

    input.addEventListener('keydown', (evt) => {
      if (evt.code === 'Enter') {
        evt.target?.dispatchEvent(new Event('input', { bubbles: true }))
        evt.preventDefault()
      } else if (evt.code === 'Tab') {
        const frame = document.querySelector(
          '.frame[name="' + input.closest('form')?.target + '"]',
        )
        if (
          frame &&
          frame instanceof HTMLElement &&
          frame.style.display !== 'none'
        ) {
          frame
            .closest('.Popup-content')
            ?.querySelector<HTMLElement>('.Popup-close')
            ?.focus()
          evt.preventDefault()
        }
      }
    })
  },
)

// Move the login dialog out of any other dialog it may invoke in via XHR requests.
onFind('.Page-loginDialog', (dialog: HTMLDialogElement) => {
  document.body.appendChild(dialog)
})

onFind('.Suggestions-list', (legacySuggestionsList: HTMLElement) => {
  const labelElement = legacySuggestionsList.querySelector<HTMLElement>(
    '.Suggestions-list-icon',
  )

  const widget = new Widget()
  widget.collapsible = true

  if (labelElement) {
    const key = getComponentKey(labelElement, 'data-label')

    if (key) {
      widget.collapsed = localStorage.getItem(key) != null
      widget.setAttribute('data-internal-name', key)
    }

    widget.classList.add(...legacySuggestionsList.classList)

    for (const d in legacySuggestionsList.dataset) {
      widget.dataset[d] = legacySuggestionsList.dataset[d]
    }

    // Assign the header title
    widget.heading = labelElement.innerText

    // Move the content when the WC is ready.
    widget.addEventListener('btu-widget-ready', (evt) => {
      const contents = legacySuggestionsList.querySelectorAll(
        ':scope > *:not(.Suggestions-list-icon)',
      )
      if (contents != null) {
        widget.append(...contents)
      }
    })

    // Update the DOM.
    legacySuggestionsList.parentNode?.insertBefore(
      widget,
      legacySuggestionsList,
    )

    legacySuggestionsList.remove()
  }
})

/*
  Handle the deferred rendering of elements. For example the Styles tabs will only render
  when the user clicks on the Styles tab and all [data-tab="Styles"] elements are shown.

  Note: For performance optimization to work correctly, the deferred elements must be
  hidden with CSS on the markup returned from the server (e.g., `display: none;` or a CSS
  class equivalent) and not hidden with JavaScript.
*/
onVisible('.Deferred', (deferred) => {
  const parent = deferred.parentElement
  const template = deferred.querySelector(':scope > template')
  if (template instanceof HTMLTemplateElement && parent) {
    const clone = template.content.cloneNode(true)
    parent.insertBefore(clone, deferred)
    deferred.remove()

    // Trigger change events for all selects in the cloned content.
    parent
      .querySelectorAll('.CIG-row.StyleguidePreset-row .CIG-small > select')
      .forEach((select) => {
        select.dispatchEvent(new CustomEvent('change', { bubbles: true }))
      })

    // Initialize ContentSelector elements from the cloned content.
    parent
      .querySelectorAll(
        '.CIG-row.StyleguidePreset-row .CIG-small > input.objectId',
      )
      .forEach((input) => {
        const row = input.closest('.CIG-row')
        row?.dispatchEvent(new CustomEvent('create', { bubbles: true }))
      })

    $(parent).trigger('create')
  }
})

/* Frame JS removes the Widget class from frames so we need to re-add it */
onFind(
  [
    '.Popup[name="edit-guide"] .frame',
    '.Popup[name="PostPublishActionsPage"] .frame',
    '.Popup[name^="create-new-object-"] .frame',
    '.Popup[name^="objectId-create-"] .frame',
    '.Popup[name="AddEmbeddedContent"] .frame',
    '.Popup[name^="conversation-"] .frame',
    '.Popup[name^="_copy"] > .Popup-content > .frame',
  ],
  (frame: HTMLElement) => {
    const config = { attributeFilter: ['class'] }
    const observer = new MutationObserver((mutations) => {
      mutations.forEach((mutation) => {
        const target = mutation.target as HTMLElement
        if (!target.classList.contains('Widget')) {
          target.classList.add('Widget')
        }
      })
    })

    observer.observe(frame, config)
  },
)

// Handle Content Edit left pane scrolling to hide and collapse the top bar.
// Note: Scroll event is used instead of IntersectionObserver because of dynamic content in scroll area.
onFind('.ContentEdit-left', (leftPane: HTMLElement) => {
  if (leftPane.closest('.Popup')) return
  const contentEdit = leftPane.closest<HTMLElement>('.ContentEdit')
  if (!contentEdit) return

  const topBar = contentEdit.querySelector<HTMLElement>('.ContentEdit-top')

  leftPane.addEventListener(
    'scroll',
    throttle(600, () => {
      if (leftPane.scrollTop > 0) {
        // Only collapse if the total scroll capacity exceeds the full height of the top bar.
        const maxScroll = leftPane.scrollHeight - leftPane.clientHeight
        if (maxScroll > (topBar?.offsetHeight ?? 0)) {
          contentEdit.classList.add('is-scrolled')
        }
      } else if (leftPane.scrollTop === 0) {
        contentEdit.classList.remove('is-scrolled')
      }
    }),
  )

  // Scroll to top when clicking Title bar
  topBar?.addEventListener('click', (event) => {
    const nodeName = (event.target as HTMLElement).nodeName.toLowerCase()
    if (nodeName !== 'span' && nodeName !== 'div') return
    leftPane.scrollTo({ top: 0, behavior: 'smooth' })
  })
})
