import { html, LitElement, nothing } from 'lit'
import { customElement, property, state } from 'lit/decorators.js'
import onFind from '../../dom/onFind'
import debounce from '../../util/debounce'
import { v1 as uuid } from 'uuid'

/**
 * Will display a search button that opens an input for users to filter the CIG-row(s) of a CIG form container.
 *
 * @element btu-form-filter
 * @property {string} noMatches - Localized message to display when no matches are found. Default: 'None found'
 * @property {string} placeholder - Placeholder text for the input field. Default: 'Filter content'
 * @property {string} selector - CSS selector to find the container to filter. Default: 'form > .CIG'
 */
@customElement('btu-form-filter')
export default class FormFilter extends LitElement {
  static noFilterTypes = new Set(['option', 'textarea'])

  @property()
  noMatches = 'No Results!'

  @property()
  placeholder = 'Find in form'

  @property()
  selector = 'form > .CIG'

  @state()
  private container?: Element

  constructor() {
    super()
  }

  connectedCallback(): void {
    super.connectedCallback()

    if (this.selector) {
      onFind(this.selector, (container) => {
        this.container = container
        new FormFilter()
      })
    }
  }

  // Renders the template into the main DOM tree and not shadow root.
  createRenderRoot() {
    return this
  }

  handleClick = () => {
    const filterContainer = this.querySelector('.FormFilter-container')
    const input = this.querySelector('input')

    if (input) {
      input.focus()
      const isOpen = this.classList.toggle('is-open')
      this.querySelector('.FormFilter-icon')?.classList.toggle(
        'is-open',
        isOpen,
      )
      filterContainer?.classList.toggle('is-open', isOpen)

      if (filterContainer?.classList.contains('is-open')) {
        input.focus()
      } else {
        this.removeMessage()
        this.container?.classList.remove('is-filtering')
        this.container
          ?.querySelectorAll('.is-filterResult')
          .forEach((element) =>
            element.classList.remove('is-filterResult', 'has-title'),
          )

        input.blur()
        input.value = ''
        unmarkText(this)
      }
    }
  }

  handleInput = () => {
    const input = this.querySelector('input')

    if (this.container && input) {
      this.removeMessage()

      this.querySelectorAll('.is-filterResult').forEach((element) =>
        element.classList.remove('is-filterResult'),
      )

      this.container
        .querySelectorAll('.is-filterResult')
        .forEach((element) => element.classList.remove('is-filterResult'))

      unmarkText(this)

      if (input.value.length === 0) {
        this.container.classList.remove('is-filtering')
      } else {
        this.container.classList.add('is-filtering', 'is-loading')
        if (walkDom instanceof Function) {
          walkDom(this, input?.value)
        }
      }
    }
  }

  getContainer(): Element | undefined {
    return this.container
  }

  private removeMessage(): void {
    this.container?.parentElement
      ?.querySelector('.FormFilter-message')
      ?.remove()
  }

  render() {
    const id = `i${uuid()}`
    return html`<div class="FormFilter-container">
        <label aria-label="${this.placeholder}"></label>
        <input
          type="text"
          id="${id}"
          name="formFilter"
          @input="${this.handleInput}"
          placeholder="${this.placeholder ?? nothing}"
          autocomplete="off"
        />
      </div>
      <button
        class="FormFilter-icon"
        data-icon-name="search"
        type="button"
        aria-controls="${id}"
        aria-expanded="false"
        aria-label="${this.placeholder}"
        title="${this.placeholder}"
        @click="${this.handleClick}"
      ></button>`
  }
}

const walkDom = debounce(800, (fn: unknown, value: unknown) => {
  if (!(fn instanceof FormFilter) || !(typeof value === 'string')) return
  const container = fn.getContainer()

  if (!container) return

  let isResults = false
  const tabs = new Set<string>()
  const nodes = new Set<Node>()

  const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
    acceptNode: function (node: Text) {
      if (
        node !== null &&
        node.textContent !== null &&
        node.parentElement !== null &&
        !FormFilter.noFilterTypes.has(
          node.parentElement.nodeName.toLocaleLowerCase(),
        ) &&
        !node.parentElement.closest('.ComboInput, .RCIG-controls') &&
        node.textContent.toLowerCase().indexOf(value.toLowerCase()) > -1
      ) {
        return NodeFilter.FILTER_ACCEPT
      }

      return NodeFilter.FILTER_REJECT
    },
  })

  while (walker.nextNode()) {
    const node = walker.currentNode
    const element = node.parentElement
    const parent = element?.parentElement
    const cigTitle = furthestClass(element, 'CIG-title')
    const cigRow = furthestClass(element, 'CIG-row')

    if (cigTitle || cigRow) {
      isResults = true
      nodes.add(node)
    }

    if (cigTitle) {
      let nextSibling = cigTitle.nextElementSibling
      cigTitle.classList.add('is-filterResult')

      if (nextSibling) {
        do {
          nextSibling.classList.add('is-filterResult', 'has-title')
          nextSibling = nextSibling.nextElementSibling
        } while (
          nextSibling &&
          nextSibling.classList &&
          nextSibling.classList.contains('CIG-row')
        )
      }
    }

    if (
      cigRow instanceof HTMLElement &&
      !cigRow.classList.contains('is-filterResult')
    ) {
      let previousSibling = cigRow.previousElementSibling
      const tab: string =
        cigRow.dataset && cigRow.dataset.tab ? cigRow.dataset.tab : ''
      let hasTitle = false

      cigRow.classList.add('has-title')

      while (previousSibling) {
        if (
          previousSibling instanceof HTMLElement &&
          previousSibling.classList &&
          previousSibling.classList.contains('CIG-title') &&
          previousSibling.dataset.tab === tab
        ) {
          previousSibling.classList.add('is-filterResult')
          previousSibling = null
          hasTitle = true
        } else {
          previousSibling = previousSibling.previousElementSibling
        }
      }

      if (!hasTitle && !tabs.has(tab)) {
        cigRow.classList.remove('has-title')
        tabs.add(tab)
      }

      cigRow.classList.add('is-filterResult')
    }

    // Checks for specific setting types.

    // Plugins
    if (parent && parent.classList && parent.classList.contains('PluginItem')) {
      const tab = parent.closest('.Tab')
      if (tab) {
        tab.classList.add('is-filterResult')
      }
      parent.classList.add('is-filterResult')
      if (parent?.parentElement?.previousElementSibling) {
        parent.parentElement.previousElementSibling.classList.add(
          'is-filterResult',
        )
      }
    }
  }

  container.classList.remove('is-loading')

  if (!isResults) {
    const message = document.createElement('div')
    message.setAttribute('class', 'Message message-warning FormFilter-message')
    message.textContent = fn.noMatches

    // V4: Maintain no-results message as a child of CIG
    // V5: Adjust message's position so that CIG doesn't restrict its width
    container.insertAdjacentElement(
      window.BRIGHTSPOT?.ui?.cms?.enableV5UI ? 'beforebegin' : 'afterbegin',
      message,
    )
  }

  // This can end after user has cleared input so always check directly from element.
  if (fn.querySelector('input')?.value) {
    nodes.forEach((node) => markText(value, node))
  } else {
    unmarkText(fn)
  }
})

/**
 * Remove any previously inserted <mark> tags.
 *
 * @param {FormFilter} instance Current instance of this plugin.
 */
function unmarkText(instance: FormFilter) {
  instance
    .getContainer()
    ?.querySelectorAll('mark')
    .forEach((element: HTMLElement) => {
      const parent = element.parentElement
      element.insertAdjacentText('afterend', element.textContent || '')
      element.remove()
      if (parent) {
        parent.normalize()
      }
    })
}

/**
 * Wrap the (case-insensitive) value in a <mark> tag.
 *
 * @param {String} value The users search string.
 * @param {Node} textNode The TextNode found by the Walker.
 */
function markText(value: string, textNode: Node) {
  const re = new RegExp(`(${value})`, 'i')
  const str = textNode?.textContent?.replace(re, '<mark>$1</mark>')
  const parent = textNode.parentElement
  if (parent) {
    parent.removeChild(textNode)
    parent.insertAdjacentHTML('afterbegin', str || '')
  }
}

/**
 * Return the outermost element in the DOM heirarchy with the matching class name.
 *
 * @param {Element} element The root Element to begin with.
 * @param {String} className The CSS class name to check against.
 */
function furthestClass(
  element: Element | null,
  className: string,
): Element | null {
  if (element) {
    const closest = element.parentElement
      ? element.parentElement.closest(`.${className}`)
      : null

    return closest
      ? furthestClass(closest, className)
      : element.classList && element.classList.contains(className)
        ? element
        : null
  }

  return null
}

declare global {
  interface HTMLElementTagNameMap {
    'btu-form-filter': FormFilter
  }
}
