export const HIDDEN_TEXT_NODE_TYPE = 'hiddenText'

function escapeHtml(value = '') {
  return String(value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

export function normalizeHiddenTextValue(value = '') {
  return String(value ?? '').replace(/\r\n?/g, ' ')
}

export function escapeHiddenTextSegment(value = '', closingChar) {
  const normalized = normalizeHiddenTextValue(value)
  const closingCharPattern = new RegExp(`\\${closingChar}`, 'g')
  return normalized
    .replace(/\\/g, '\\\\')
    .replace(closingCharPattern, `\\${closingChar}`)
}

export function serializeHiddenTextSyntax(displayed = '', hidden = '') {
  const safeDisplayed = escapeHiddenTextSegment(displayed, ')')
  const safeHidden = escapeHiddenTextSegment(hidden, '}')
  return `(${safeDisplayed}){${safeHidden}}`
}

function readHiddenTextSegment(text = '', start = 0, closingChar = ')') {
  let index = start
  let value = ''

  while (index < text.length) {
    const char = text[index]

    if (char === '\\') {
      const nextChar = text[index + 1]
      if (nextChar === undefined) return null
      value += nextChar
      index += 2
      continue
    }

    if (char === '\n' || char === '\r') return null
    if (char === closingChar) {
      return {
        value,
        end: index + 1,
      }
    }

    value += char
    index += 1
  }

  return null
}

export function parseHiddenTextAt(text = '', start = 0) {
  if (text[start] !== '(') return null

  const displayed = readHiddenTextSegment(text, start + 1, ')')
  if (!displayed) return null
  if (text[displayed.end] !== '{') return null

  const hidden = readHiddenTextSegment(text, displayed.end + 1, '}')
  if (!hidden) return null

  return {
    start,
    end: hidden.end,
    displayed: displayed.value,
    hidden: hidden.value,
    raw: text.slice(start, hidden.end),
  }
}

export function extractHiddenTextMatches(text = '') {
  const matches = []
  let index = 0

  while (index < text.length) {
    const match = parseHiddenTextAt(text, index)
    if (match) {
      matches.push(match)
      index = match.end
      continue
    }

    index += 1
  }

  return matches
}

export function splitTextWithHiddenSyntax(text = '') {
  const matches = extractHiddenTextMatches(text)
  if (matches.length === 0) return null

  const segments = []
  let cursor = 0

  for (const match of matches) {
    if (match.start > cursor) {
      segments.push({
        type: 'text',
        value: text.slice(cursor, match.start),
      })
    }

    segments.push({
      type: HIDDEN_TEXT_NODE_TYPE,
      displayed: match.displayed,
      hidden: match.hidden,
    })

    cursor = match.end
  }

  if (cursor < text.length) {
    segments.push({
      type: 'text',
      value: text.slice(cursor),
    })
  }

  return segments.filter((segment) => segment.type !== 'text' || segment.value)
}

export function renderHiddenTextPreviewHtml(displayed = '', hidden = '') {
  const summary = escapeHtml(displayed || '未命名文本')

  return [
    `<span class="hidden-text-preview" data-hidden-text="true" title="${escapeHtml(serializeHiddenTextSyntax(displayed, hidden))}">`,
    `<span class="hidden-text-preview__summary">${summary}</span>`,
    '</span>',
  ].join('')
}

export function hiddenTextMarkdownItPlugin(md) {
  md.inline.ruler.before('emphasis', 'hidden_text', (state, silent) => {
    const match = parseHiddenTextAt(state.src, state.pos)
    if (!match) return false

    if (!silent) {
      const token = state.push('hidden_text', '', 0)
      token.meta = {
        displayed: match.displayed,
        hidden: match.hidden,
      }
    }

    state.pos = match.end
    return true
  })

  md.renderer.rules.hidden_text = (tokens, index) => {
    const meta = tokens[index]?.meta || {}
    return renderHiddenTextPreviewHtml(meta.displayed, meta.hidden)
  }
}