text: import { EVENT_KEYS } from '../config'
  import selection from '../selection'
  import { findNearestParagraph } from '../selection/dom'
  import { getParagraphReference, getImageInfo } from '../utils'
  import { checkEditEmoji } from '../ui/emojis'
  
  class Keyboard {
    constructor (muya) {
      this.muya = muya
      this.isComposed = false
      this.shownFloat = new Set()
      this.recordIsComposed()
      this.dispatchEditorState()
      this.keydownBinding()
      this.keyupBinding()
      this.inputBinding()
      this.listen()
    }
  
    listen () {
      // cache shown float box
      this.muya.eventCenter.subscribe('muya-float', (tool, status) => {
        status ? this.shownFloat.add(tool) : this.shownFloat.delete(tool)
        if (tool.name === 'ag-front-menu' && !status) {
          const seletedParagraph = this.muya.container.querySelector('.ag-selected')
          if (seletedParagraph) {
            this.muya.contentState.selectedBlock = null
            // prevent rerender, so change the class manually.
            seletedParagraph.classList.toggle('ag-selected')
          }
        }
      })
    }
  
    hideAllFloatTools () {
      for (const tool of this.shownFloat) {
        tool.hide()
      }
    }
  
    recordIsComposed () {
      const { container, eventCenter, contentState } = this.muya
      const handler = event => {
        if (event.type === 'compositionstart') {
          this.isComposed = true
        } else if (event.type === 'compositionend') {
          this.isComposed = false
          // Because the compose event will not cause `input` event, So need call `inputHandler` by ourself
          contentState.inputHandler(event)
          eventCenter.dispatch('stateChange')
        }
      }
  
      eventCenter.attachDOMEvent(container, 'compositionend', handler)
      // eventCenter.attachDOMEvent(container, 'compositionupdate', handler)
      eventCenter.attachDOMEvent(container, 'compositionstart', handler)
    }
  
    dispatchEditorState () {
      const { container, eventCenter } = this.muya
  
      let timer = null
      const changeHandler = event => {
        if (
          event.type === 'keyup' &&
          (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown) &&
          this.shownFloat.size > 0
        ) {
          return
        }
        // Cursor outside editor area or over not editable elements.
        if (event.target.closest('[contenteditable=false]')) {
          return
        }
  
        // We need check cursor is null, because we may copy the html preview content,
        // and no need to dispatch change.
        const { start, end } = selection.getCursorRange()
        if (!start || !end) {
          return
        }
  
        if (timer) clearTimeout(timer)
        timer = setTimeout(() => {
          this.muya.dispatchSelectionChange()
          this.muya.dispatchSelectionFormats()
          if (!this.isComposed && event.type === 'click') {
            this.muya.dispatchChange()
          }
        })
      }
  
      eventCenter.attachDOMEvent(container, 'click', changeHandler)
      eventCenter.attachDOMEvent(container, 'keyup', changeHandler)
    }
  
    keydownBinding () {
      const { container, eventCenter, contentState } = this.muya
      const docHandler = event => {
        switch (event.code) {
          case EVENT_KEYS.Enter:
            return contentState.docEnterHandler(event)
          case EVENT_KEYS.Space: {
            if (contentState.selectedImage) {
              const { token } = contentState.selectedImage
              const { src } = getImageInfo(token.src || token.attrs.src)
              if (src) {
                eventCenter.dispatch('preview-image', {
                  data: src
                })
              }
            }
            break
          }
          case EVENT_KEYS.Backspace: {
            return contentState.docBackspaceHandler(event)
          }
          case EVENT_KEYS.Delete: {
            return contentState.docDeleteHandler(event)
          }
          case EVENT_KEYS.ArrowUp: // fallthrough
          case EVENT_KEYS.ArrowDown: // fallthrough
          case EVENT_KEYS.ArrowLeft: // fallthrough
          case EVENT_KEYS.ArrowRight: // fallthrough
            return contentState.docArrowHandler(event)
        }
      }
  
      const handler = event => {
        if (event.metaKey || event.ctrlKey) {
          container.classList.add('ag-meta-or-ctrl')
        }
  
        if (
          this.shownFloat.size > 0 &&
          (
            event.key === EVENT_KEYS.Enter ||
            event.key === EVENT_KEYS.Escape ||
            event.key === EVENT_KEYS.Tab ||
            event.key === EVENT_KEYS.ArrowUp ||
            event.key === EVENT_KEYS.ArrowDown
          )
        ) {
          let needPreventDefault = false
  
          for (const tool of this.shownFloat) {
            if (
              tool.name === 'ag-format-picker' ||
              tool.name === 'ag-table-picker' ||
              tool.name === 'ag-quick-insert' ||
              tool.name === 'ag-emoji-picker' ||
              tool.name === 'ag-front-menu' ||
              tool.name === 'ag-list-picker' ||
              tool.name === 'ag-image-selector'
            ) {
              needPreventDefault = true
              break
            }
          }
          if (needPreventDefault) {
            event.preventDefault()
          }
          // event.stopPropagation()
          return
        }
        switch (event.key) {
          case EVENT_KEYS.Backspace:
            contentState.backspaceHandler(event)
            break
          case EVENT_KEYS.Delete:
            contentState.deleteHandler(event)
            break
          case EVENT_KEYS.Enter:
            if (!this.isComposed) {
              contentState.enterHandler(event)
              this.muya.dispatchChange()
            }
            break
          case EVENT_KEYS.ArrowUp: // fallthrough
          case EVENT_KEYS.ArrowDown: // fallthrough
          case EVENT_KEYS.ArrowLeft: // fallthrough
          case EVENT_KEYS.ArrowRight: // fallthrough
            if (!this.isComposed) {
              contentState.arrowHandler(event)
            }
            break
          case EVENT_KEYS.Tab:
            contentState.tabHandler(event)
            break
          default:
            break
        }
      }
  
      eventCenter.attachDOMEvent(container, 'keydown', handler)
      eventCenter.attachDOMEvent(document, 'keydown', docHandler)
    }
  
    inputBinding () {
      const { container, eventCenter, contentState } = this.muya
      const inputHandler = event => {
        if (!this.isComposed) {
          contentState.inputHandler(event)
          this.muya.dispatchChange()
        }
  
        const { lang, paragraph } = contentState.checkEditLanguage()
        if (lang) {
          eventCenter.dispatch('muya-code-picker', {
            reference: getParagraphReference(paragraph, paragraph.id),
            lang,
            cb: item => {
              contentState.selectLanguage(paragraph, item.name)
            }
          })
        } else {
          // hide code picker float box
          eventCenter.dispatch('muya-code-picker', { reference: null })
        }
      }
  
      eventCenter.attachDOMEvent(container, 'input', inputHandler)
    }
  
    keyupBinding () {
      const { container, eventCenter, contentState } = this.muya
      const handler = event => {
        container.classList.remove('ag-meta-or-ctrl')
        // check if edit emoji
        const node = selection.getSelectionStart()
        const paragraph = findNearestParagraph(node)
        const emojiNode = checkEditEmoji(node)
        contentState.selectedImage = null
        if (
          paragraph &&
          emojiNode &&
          event.key !== EVENT_KEYS.Enter &&
          event.key !== EVENT_KEYS.ArrowDown &&
          event.key !== EVENT_KEYS.ArrowUp &&
          event.key !== EVENT_KEYS.Tab &&
          event.key !== EVENT_KEYS.Escape
        ) {
          const reference = getParagraphReference(emojiNode, paragraph.id)
          eventCenter.dispatch('muya-emoji-picker', {
            reference,
            emojiNode
          })
        }
        if (!emojiNode) {
          eventCenter.dispatch('muya-emoji-picker', {
            emojiNode
          })
        }
  
        const { anchor, focus, start, end } = selection.getCursorRange()
        if (!anchor || !focus) {
          return
        }
        if (
          !this.isComposed
        ) {
          const { anchor: oldAnchor, focus: oldFocus } = contentState.cursor
          if (
            anchor.key !== oldAnchor.key ||
            anchor.offset !== oldAnchor.offset ||
            focus.key !== oldFocus.key ||
            focus.offset !== oldFocus.offset
          ) {
            const needRender = contentState.checkNeedRender(contentState.cursor) || contentState.checkNeedRender({ start, end })
            contentState.cursor = { anchor, focus }
            if (needRender) {
              return contentState.partialRender()
            }
          }
        }
  
        const block = contentState.getBlock(anchor.key)
        if (
          anchor.key === focus.key &&
          anchor.offset !== focus.offset &&
          block.functionType !== 'codeContent' &&
          block.functionType !== 'languageInput'
        ) {
          const reference = contentState.getPositionReference()
          const { formats } = contentState.selectionFormats()
          eventCenter.dispatch('muya-format-picker', { reference, formats })
        } else {
          eventCenter.dispatch('muya-format-picker', { reference: null })
        }
      }
  
      eventCenter.attachDOMEvent(container, 'keyup', handler) // temp use input event
    }
  }
  
  export default Keyboard
  