import { nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useMainStore, useSlidesStore } from '@/store'
import type { PPTTableElement } from '@/types/slides'
import message from '@/utils/message'

interface SearchTextResult {
  elType: 'text' | 'shape'
  slideId: string
  elId: string
}
interface SearchTableResult {
  elType: 'table'
  slideId: string
  elId: string
  cellIndex: [number, number]
}

type SearchResult = SearchTextResult | SearchTableResult

type Modifiers = 'g' | 'gi'

export default () => {
  const mainStore = useMainStore()
  const slidesStore = useSlidesStore()
  const { handleElement } = storeToRefs(mainStore)
  const { slides, slideIndex, currentSlide } = storeToRefs(slidesStore)

  const searchWord = ref('')
  const replaceWord = ref('')
  const searchResults = ref<SearchResult[]>([])
  const searchIndex = ref(-1)

  const modifiers = ref<Modifiers>('g')
  
  const search = () => {
    const textList: SearchResult[] = []
    const matchRegex = new RegExp(searchWord.value, modifiers.value)
    const textRegex = /(<([^>]+)>)/g
  
    for (const slide of slides.value) {
      for (const el of slide.elements) {
        if (el.type === 'text') {
          const text = el.content.replace(textRegex, '')
          const rets = text.match(matchRegex)
          rets && textList.push(...new Array(rets.length).fill({
            slideId: slide.id,
            elId: el.id,
            elType: el.type,
          }))
        }
        else if (el.type === 'shape' && el.text && el.text.content) {
          const text = el.text.content.replace(textRegex, '')
          const rets = text.match(matchRegex)
          rets && textList.push(...new Array(rets.length).fill({
            slideId: slide.id,
            elId: el.id,
            elType: el.type,
          }))
        }
        else if (el.type === 'table') {
          for (let i = 0; i < el.data.length; i++) {
            const row = el.data[i]
            for (let j = 0; j < row.length; j++) {
              const cell = row[j]
              if (!cell.text) continue
              const text = cell.text.replace(textRegex, '')
              const rets = text.match(matchRegex)
              rets && textList.push(...new Array(rets.length).fill({
                slideId: slide.id,
                elId: el.id,
                elType: el.type,
                cellIndex: [i, j],
              }))
            }
          }
        }
      }
    }
    if (textList.length) {
      searchResults.value = textList
      searchIndex.value = 0
      highlightCurrentSlide()
    }
    else {
      message.warning('未查找到匹配项')
      clearMarks()
    }
  }
  
  const getTextNodeList = (dom: Node): Text[] => {
    const nodeList = [...dom.childNodes]
    const textNodes = []
    while (nodeList.length) {
      const node = nodeList.shift()!
      if (node.nodeType === node.TEXT_NODE) {
        (node as Text).wholeText && textNodes.push(node as Text)
      } 
      else {
        nodeList.unshift(...node.childNodes)
      }
    }
    return textNodes
  }
  
  const getTextInfoList = (textNodes: Text[]) => {
    let length = 0
    const textList = textNodes.map(node => {
      const startIdx = length, endIdx = length + node.wholeText.length
      length = endIdx
      return {
        text: node.wholeText,
        startIdx,
        endIdx
      }
    })
    return textList
  }
  
  type TextInfoList = ReturnType<typeof getTextInfoList>
  
  const getMatchList = (content: string, keyword: string) => {
    const reg = new RegExp(keyword, modifiers.value)
    const matchList = []
    let match = reg.exec(content)
    while (match) {
      matchList.push(match)
      match = reg.exec(content)
    }
    return matchList
  }
  
  const highlight = (textNodes: Text[], textList: TextInfoList, matchList: RegExpExecArray[], index: number) => {
    for (let i = matchList.length - 1; i >= 0; i--) {
      const match = matchList[i]
      const matchStart = match.index
      const matchEnd = matchStart + match[0].length
  
      for (let textIdx = 0; textIdx < textList.length; textIdx++) {
        const { text, startIdx, endIdx } = textList[textIdx]
        if (endIdx < matchStart) continue
        if (startIdx >= matchEnd) break
  
        let textNode = textNodes[textIdx]
        const nodeMatchStartIdx = Math.max(0, matchStart - startIdx)
        const nodeMatchLength = Math.min(endIdx, matchEnd) - startIdx - nodeMatchStartIdx
  
        if (nodeMatchStartIdx > 0) textNode = textNode.splitText(nodeMatchStartIdx)
        if (nodeMatchLength < textNode.wholeText.length) textNode.splitText(nodeMatchLength)
  
        const mark = document.createElement('mark')
        mark.dataset.index = index + i + ''
        mark.innerText = text.substring(nodeMatchStartIdx, nodeMatchStartIdx + nodeMatchLength)
        textNode.parentNode!.replaceChild(mark, textNode)
      }
    }
  }
  
  const highlightTableText = (nodes: NodeListOf<Element>, index: number) => {
    for (const node of nodes) {
      node.innerHTML = node.innerHTML.replace(new RegExp(searchWord.value, modifiers.value), () => {
        return `<mark data-index=${index++}>${searchWord.value}</mark>`
      })
    }
  }
  
  const clearMarks = () => {
    const markNodes = document.querySelectorAll('.editable-element mark')
    for (const mark of markNodes) {
      setTimeout(() => {
        const parentNode = mark.parentNode!
        const text = mark.textContent!
        parentNode.replaceChild(document.createTextNode(text), mark)
      }, 0)
    }
  }
  
  const highlightCurrentSlide = () => {
    clearMarks()
    
    setTimeout(() => {
      for (let i = 0; i < searchResults.value.length; i++) {
        const lastTarget = searchResults.value[i - 1]
        const target = searchResults.value[i]
        if (target.slideId !== currentSlide.value.id) continue
        if (lastTarget && lastTarget.elId === target.elId) continue
  
        const node = document.querySelector(`#editable-element-${target.elId}`)
        if (node) {
          if (target.elType === 'table') {
            const cells = node.querySelectorAll('.cell-text')
            highlightTableText(cells, i)
          }
          else {
            const textNodes = getTextNodeList(node)
            const textList = getTextInfoList(textNodes)
            const content = textList.map(({ text }) => text).join('')
            const matchList = getMatchList(content, searchWord.value)
            highlight(textNodes, textList, matchList, i)
          }
        }
      }
    }, 0)
  }
  
  const setActiveMark = () => {
    const markNodes = document.querySelectorAll('mark[data-index]')
    for (const node of markNodes) {
      setTimeout(() => {
        const index = (node as HTMLElement).dataset.index
        if (index !== undefined && +index === searchIndex.value) {
          node.classList.add('active')
        }
        else node.classList.remove('active')
      }, 0)
    }
  }
  
  const turnTarget = () => {
    if (searchIndex.value === -1) return
    
    const target = searchResults.value[searchIndex.value]
  
    if (target.slideId === currentSlide.value.id) setTimeout(setActiveMark, 0)
    else {
      const index = slides.value.findIndex(slide => slide.id === target.slideId)
      if (index !== -1) slidesStore.updateSlideIndex(index)
    }
  }
  
  const searchNext = () => {
    if (!searchWord.value) return message.warning('请先输入查找内容')
    mainStore.setActiveElementIdList([])
    if (searchIndex.value === -1) search()
    else if (searchIndex.value < searchResults.value.length - 1) searchIndex.value += 1
    else searchIndex.value = 0
    turnTarget()
  }
  
  const searchPrev = () => {
    if (!searchWord.value) return message.warning('请先输入查找内容')
    mainStore.setActiveElementIdList([])
    if (searchIndex.value === -1) search()
    else if (searchIndex.value > 0) searchIndex.value -= 1
    else searchIndex.value = searchResults.value.length - 1
    turnTarget()
  }
  
  const replace = () => {
    if (!searchWord.value) return
    if (searchIndex.value === -1) {
      searchNext()
      return
    }
  
    const target = searchResults.value[searchIndex.value]
    let targetElement = null
    if (target.elType === 'table') {
      const [i, j] = target.cellIndex
      targetElement = document.querySelector(`#editable-element-${target.elId} .cell[data-cell-index="${i}_${j}"] .cell-text`)
    }
    else targetElement = document.querySelector(`#editable-element-${target.elId} .ProseMirror`)
    if (!targetElement) return
  
    const fakeElement = document.createElement('div')
    fakeElement.innerHTML = targetElement.innerHTML
  
    let replaced = false
    const marks = fakeElement.querySelectorAll('mark[data-index]')
    for (const mark of marks) {
      const parentNode = mark.parentNode!
      if (mark.classList.contains('active')) {
        if (replaced) parentNode.removeChild(mark)
        else {
          parentNode.replaceChild(document.createTextNode(replaceWord.value), mark)
          replaced = true
        }
      }
      else {
        const text = mark.textContent!
        parentNode.replaceChild(document.createTextNode(text), mark)
      }
    }
  
    if (target.elType === 'text') {
      const props = { content: fakeElement.innerHTML }
      slidesStore.updateElement({ id: target.elId, props })
    }
    else if (target.elType === 'shape') {
      const el = currentSlide.value.elements.find(item => item.id === target.elId)
      if (el && el.type === 'shape' && el.text) {
        const props = { text: { ...el.text, content: fakeElement.innerHTML } }
        slidesStore.updateElement({ id: target.elId, props })
      }
    }
    else if (target.elType === 'table') {
      const el = currentSlide.value.elements.find(item => item.id === target.elId)
      if (el && el.type === 'table') {
        const data = el.data.map((row, i) => {
          if (i === target.cellIndex[0]) {
            return row.map((cell, j) => {
              if (j === target.cellIndex[1]) {
                return {
                  ...cell,
                  text: fakeElement.innerHTML,
                }
              }
              return cell
            })
          }
          return row
        })
        const props = { data }
        slidesStore.updateElement({ id: target.elId, props })
      }
    }
  
    searchResults.value.splice(searchIndex.value, 1)
    if (searchResults.value.length) {
      if (searchIndex.value > searchResults.value.length - 1) {
        searchIndex.value = 0
      }
      nextTick(() => {
        highlightCurrentSlide()
        turnTarget()
      })
    }
    else searchIndex.value = -1
  }
  
  const replaceAll = () => {
    if (!searchWord.value) return
    if (searchIndex.value === -1) {
      searchNext()
      return
    }
  
    for (let i = 0; i < searchResults.value.length; i++) {
      const lastTarget = searchResults.value[i - 1]
      const target = searchResults.value[i]
      if (lastTarget && lastTarget.elId === target.elId) continue
  
      const targetSlide = slides.value.find(item => item.id === target.slideId)
      if (!targetSlide) continue
      const targetElement = targetSlide.elements.find(item => item.id === target.elId)
      if (!targetElement) continue
  
      const fakeElement = document.createElement('div')
      if (targetElement.type === 'text') fakeElement.innerHTML = targetElement.content
      else if (targetElement.type === 'shape') fakeElement.innerHTML = targetElement.text?.content || ''
  
      if (target.elType === 'table') {
        const data = (targetElement as PPTTableElement).data.map(row => {
          return row.map(cell => {
            if (!cell.text) return cell
            return {
              ...cell,
              text: cell.text.replaceAll(searchWord.value, replaceWord.value),
            }
          })
        })
        const props = { data }
        slidesStore.updateElement({ id: target.elId, slideId: target.slideId, props })
      }
      else {
        const textNodes = getTextNodeList(fakeElement)
        const textList = getTextInfoList(textNodes)
        const content = textList.map(({ text }) => text).join('')
        const matchList = getMatchList(content, searchWord.value)
        highlight(textNodes, textList, matchList, i)
  
        const marks = fakeElement.querySelectorAll('mark[data-index]')
        let lastMarkIndex = -1
        for (const mark of marks) {
          const markIndex = +(mark as HTMLElement).dataset.index!
          const parentNode = mark.parentNode!
          if (markIndex === lastMarkIndex) parentNode.removeChild(mark)
          else {
            parentNode.replaceChild(document.createTextNode(replaceWord.value), mark)
            lastMarkIndex = markIndex
          }
        }
  
        if (target.elType === 'text') {
          const props = { content: fakeElement.innerHTML }
          slidesStore.updateElement({ id: target.elId, slideId: target.slideId, props })
        }
        else if (target.elType === 'shape') {
          const el = currentSlide.value.elements.find(item => item.id === target.elId)
          if (el && el.type === 'shape' && el.text) {
            const props = { text: { ...el.text, content: fakeElement.innerHTML } }
            slidesStore.updateElement({ id: target.elId, slideId: target.slideId, props })
          }
        }
      }
    }
    searchResults.value = []
    searchIndex.value = -1
  }

  const reset = () => {
    searchIndex.value = -1
    searchResults.value = []
  
    if (!searchWord.value) clearMarks()
  }
  
  watch(searchWord, reset)
  
  watch(slideIndex, () => {
    nextTick(() => {
      highlightCurrentSlide()
      setTimeout(setActiveMark, 0)
    })
  })
  
  watch(handleElement, () => {
    if (handleElement.value) {
      searchIndex.value = -1
      searchResults.value = []
      clearMarks()
    }
  })
  
  onBeforeUnmount(clearMarks)
  
  const toggleModifiers = () => {
    modifiers.value = modifiers.value === 'g' ? 'gi' : 'g'
    reset()
  }

  return {
    searchWord,
    replaceWord,
    searchResults,
    searchIndex,
    modifiers,
    searchNext,
    searchPrev,
    replace,
    replaceAll,
    toggleModifiers,
  }
}