/**
 * @file katex.ts
 * @description markdown渲染插件，用于解析渲染katex数学公式, 依赖katex库, 请确保已经引入katex库
 * @description 该插件支持公式行内公式$...$、\(...\)和块内公式$$...$$、\[...\]两种形式
 */
import katex, { KatexOptions } from 'katex'
import { MarkedExtension, TokenizerAndRendererExtension, Tokens } from 'marked'

export interface MarkedKatexOptions extends KatexOptions {
  nonStandard?: boolean
}

const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\\$]))\1(?=[\s?!\\.,:？！。，：]|$)/
const inlineRuleNonStandard = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\\$]))\1/

const blockRule = /^(?:(\${2})\n([\s\S]+?)\n\1|\\\[([\s\S]+?)\\\](?:\n|$))/

export default function (options: MarkedKatexOptions = {}): MarkedExtension {
  return {
    extensions: [
      inlineKatex(options, createRenderer(options, false)),
      blockKatex(options, createRenderer(options, true)),
    ],
  }
}

function createRenderer(options: MarkedKatexOptions, newlineAfter: boolean) {
  return (token: Tokens.Generic) => {
    return katex.renderToString(token.text, { ...options, displayMode: token.displayMode }) + (newlineAfter ? '\n' : '')
  }
}

function inlineKatex(
  options: MarkedKatexOptions,
  renderer: (token: Tokens.Generic) => string,
): TokenizerAndRendererExtension {
  const nonStandard = options && options.nonStandard
  const ruleReg = nonStandard ? inlineRuleNonStandard : inlineRule
  return {
    name: 'inlineKatex',
    level: 'inline',
    start(src: string) {
      let index
      let indexSrc = src

      while (indexSrc) {
        index = indexSrc.search(/(?:\\\(|\$)/)
        if (index === -1) {
          return
        }
        const char = indexSrc[index]
        const possibleKatex = indexSrc.substring(index)
        const isParen = char === '\\' && indexSrc[index + 1] === '('
        const isDollar = char === '$'
        const f = nonStandard ? true : index === 0 || indexSrc.charAt(index - 1).match(/\s/)
        if (f) {
          if (isDollar && possibleKatex.match(ruleReg)) {
            return index
          } else if (isParen && possibleKatex.match(/^\\\(((?:\\[^]|[^\\\n)])*?)\\\)/)) {
            return index
          }
        }
        indexSrc = indexSrc.substring(index + (isParen ? 2 : 1))
      }
    },
    tokenizer(src: string) {
      const dollarMatch = src.match(ruleReg)
      if (dollarMatch) {
        return {
          type: 'inlineKatex',
          raw: dollarMatch[0],
          text: dollarMatch[2].trim(),
          displayMode: dollarMatch[1].length === 2,
        }
      }
      // 匹配\(...\)包裹的内容
      const parenMatch = src.match(/^\\\(((?:\\[^]|[^\\\n)])*?)\\\)/)
      if (parenMatch) {
        return {
          type: 'inlineKatex',
          raw: parenMatch[0],
          text: parenMatch[1].trim(),
          displayMode: false,
        }
      }
    },
    renderer,
  }
}

function blockKatex(
  _options: KatexOptions,
  renderer: (token: Tokens.Generic) => string,
): TokenizerAndRendererExtension {
  return {
    name: 'blockKatex',
    level: 'block',
    tokenizer(src: string) {
      const match = src.match(blockRule)
      if (match) {
        // 处理 $$...$$
        if (match[1]) {
          return {
            type: 'blockKatex',
            raw: match[0],
            text: match[2].trim(),
            displayMode: true,
          }
        } else {
          // 处理 \[...\]
          return {
            type: 'blockKatex',
            raw: match[0],
            text: match[3].trim(),
            displayMode: true,
          }
        }
      }
      return undefined
    },
    renderer,
  }
}
