import katex from 'katex'
import Quill from 'quill'
import { getChromeVersion, htmlEscape } from '../utils/index'
import { getBaseStyleText, getFontStyleText } from './FormulaStyle'
import MindMap from '..'
class BaseQuillFormula {
  static create(value: string): HTMLElement {
    return document.createElement('span');
  }
}
let extended = false
const QuillFormula = Quill.import('formats/formula') as typeof BaseQuillFormula

interface FormulaOptions {
  mindMap: MindMap
  enableEditFormulaInRichTextEdit?: boolean
  transformRichTextOnEnterEdit?: (text: string) => string
  beforeHideRichTextEdit?: (richText: any) => void
  katexFontPath?: string
  getKatexOutputType?: () => string
  openRealtimeRenderOnNodeTextEdit?: boolean
}
declare global {
  interface Window {
    katex: typeof katex;
  }
}

// 数学公式支持插件
// 该插件在富文本模式下可用
class Formula {
  static instanceName = 'formula'

  private opt: FormulaOptions
  private mindMap: MindMap
  private config: {
    throwOnError: boolean
    errorColor: string
    output: 'mathml' | 'html'
  }
  private cssEl: HTMLStyleElement | null

  constructor(opt: FormulaOptions) {
    this.opt = opt
    this.mindMap = opt.mindMap
    window.katex = katex
    this.init()
    this.config = this.getKatexConfig()
    this.cssEl = null
    this.addStyle()
    this.extendQuill()
    this.onDestroy = this.onDestroy.bind(this)
    this.mindMap.on('beforeDestroy', this.onDestroy)
  }

  private onDestroy(): void {
    const instanceCount = Object.getPrototypeOf(this.mindMap).constructor
      .instanceCount
    if (instanceCount <= 1) {
      extended = false
      Quill.register('formats/formula', QuillFormula, true)
    }
  }

  private init(): void {
    if (this.mindMap.opt.enableEditFormulaInRichTextEdit) {
      this.mindMap.opt.transformRichTextOnEnterEdit =
        this.latexRichToText.bind(this)
      this.mindMap.opt.beforeHideRichTextEdit = this.formatLatex.bind(this)
    }
  }

  private getKatexConfig() {
    const config = {
      throwOnError: false,
      errorColor: '#f00',
      output: 'mathml' as 'mathml' | 'html'
    }

    let { getKatexOutputType } = this.mindMap.opt
    getKatexOutputType =
      getKatexOutputType ||
      function () {
        const chromeVersion = getChromeVersion()
        if (chromeVersion && chromeVersion <= 100) {
          return 'html'
        }
        return undefined
      }
    const output = getKatexOutputType() || 'mathml'
    config.output = ['mathml', 'html'].includes(output) ? output as 'mathml' | 'html' : 'mathml'
    return config
  }

  private extendQuill(): void {
    if (extended) return
    extended = true

    const self = this

    class CustomFormulaBlot extends QuillFormula {
      static create(value: string) {
        let node = super.create(value)
        if (typeof value === 'string') {
          katex.render(value, node, self.config)
          node.setAttribute('data-value', htmlEscape(value))
        }
        return node
      }
    }

    Quill.register('formats/formula', CustomFormulaBlot, true)
  }

  private getStyleText(): string {
    const { katexFontPath } = this.mindMap.opt
    let text = ''
    if (this.config.output === 'html') {
      text = getFontStyleText(katexFontPath)
    }
    text += getBaseStyleText()
    return text
  }

  private addStyle(): void {
    this.cssEl = document.createElement('style')
    this.cssEl.type = 'text/css'
    this.cssEl.innerHTML = this.getStyleText()
    document.head.appendChild(this.cssEl)
  }

  private removeStyle(): void {
    if (this.cssEl) {
      document.head.removeChild(this.cssEl)
    }
  }

  public insertFormulaToNode(node: any, formula: string): void {
    const richTextPlugin = this.mindMap.richText
    richTextPlugin.showEditText({ node })
    richTextPlugin.quill.insertEmbed(
      richTextPlugin.quill.getLength() - 1,
      'formula',
      formula
    )
    richTextPlugin.hideEditText([node])
  }

  public latexRichToText(nodeText: string): string {
    if (nodeText.indexOf('class="ql-formula"') !== -1) {
      const parser = new DOMParser()
      const doc = parser.parseFromString(nodeText, 'text/html')
      const els = doc.getElementsByClassName('ql-formula')
      for (const el of els) {
        nodeText = nodeText.replace(
          el.outerHTML,
          `$${el.getAttribute('data-value')}$`
        )
      }
      if (this.mindMap.opt.openRealtimeRenderOnNodeTextEdit) {
        setTimeout(() => {
          this.mindMap.emit('node_text_edit_change', {
            node: this.mindMap.richText.node,
            text: this.mindMap.richText.getEditText(),
            richText: true
          })
        }, 0)
      }
    }
    return nodeText
  }

  public formatLatex(richText: any): void {
    const contents = richText.quill.getContents()
    const ops = contents.ops
    let mod = false
    for (let i = ops.length - 1; i >= 0; i--) {
      const op = ops[i]
      const insert = op.insert
      if (insert && typeof insert !== 'object' && insert !== '\n') {
        if (/\$.+?\$/g.test(insert)) {
          const m = [...insert.matchAll(/\$.+?\$/g)]
          const arr = insert.split(/\$.+?\$/g)
          for (let j = m.length - 1; j >= 0; j--) {
            const exp = m[j] && m[j][0] ? m[j][0].slice(1, -1) || null : null
            if (exp !== null && exp.trim().length > 0) {
              const isLegal = this.checkFormulaIsLegal(exp)
              if (isLegal) {
                arr.splice(j + 1, 0, { insert: { formula: exp } })
                mod = true
              } else {
                arr.splice(j + 1, 0, '')
              }
            } else arr.splice(j + 1, 0, '')
          }
          while (arr.length > 0) {
            let v = arr.pop()
            if (typeof v === 'string') {
              if (v.length < 1) continue
              v = { insert: v }
            }
            v['attributes'] = ops[i]['attributes']
            ops.splice(i + 1, 0, v)
          }
          ops.splice(i, 1)
        }
      }
    }
    if (mod) richText.quill.setContents(contents)
  }

  private checkFormulaIsLegal(str: string): boolean {
    try {
      katex.renderToString(str)
      return true
    } catch (e) {
      return false
    }
  }

  public beforePluginRemove(): void {
    this.removeStyle()
    this.mindMap.off('beforeDestroy', this.onDestroy)
  }

  public beforePluginDestroy(): void {
    this.removeStyle()
    this.mindMap.off('beforeDestroy', this.onDestroy)
  }
}

export default Formula