import type { MarkType } from '@tiptap/pm/model'

import { getMarksBetween } from '../helpers/getMarksBetween.js'
import type { PasteRuleFinder } from '../PasteRule.js'
import { PasteRule } from '../PasteRule.js'
import type { ExtendedRegExpMatchArray } from '../types.js'
import { callOrReturn } from '../utilities/callOrReturn.js'

/**
 * Build an paste rule that adds a mark when the
 * matched text is pasted into it.
 * @see https://tiptap.dev/docs/editor/extensions/custom-extensions/extend-existing#paste-rules
 */
export function markPasteRule(config: {
  find: PasteRuleFinder
  type: MarkType
  getAttributes?:
    | Record<string, any>
    | ((match: ExtendedRegExpMatchArray, event: ClipboardEvent) => Record<string, any>)
    | false
    | null
}) {
  return new PasteRule({
    find: config.find,
    handler: ({ state, range, match, pasteEvent }) => {
      const attributes = callOrReturn(config.getAttributes, undefined, match, pasteEvent)

      if (attributes === false || attributes === null) {
        return null
      }

      const { tr } = state
      const captureGroup = match[match.length - 1]
      const fullMatch = match[0]
      let markEnd = range.to

      if (captureGroup) {
        const startSpaces = fullMatch.search(/\S/)
        const textStart = range.from + fullMatch.indexOf(captureGroup)
        const textEnd = textStart + captureGroup.length

        const excludedMarks = getMarksBetween(range.from, range.to, state.doc)
          .filter(item => {
            // @ts-ignore
            const excluded = item.mark.type.excluded as MarkType[]

            return excluded.find(type => type === config.type && type !== item.mark.type)
          })
          .filter(item => item.to > textStart)

        if (excludedMarks.length) {
          return null
        }

        if (textEnd < range.to) {
          tr.delete(textEnd, range.to)
        }

        if (textStart > range.from) {
          tr.delete(range.from + startSpaces, textStart)
        }

        markEnd = range.from + startSpaces + captureGroup.length

        tr.addMark(range.from + startSpaces, markEnd, config.type.create(attributes || {}))

        // Only remove the stored mark if the match is not at the end of the
        // pasted text. When a mark extends to the end of the content, removing
        // it overrides the mark's `inclusive` behavior and incorrectly places
        // the cursor outside the mark.
        const isMatchAtEndOfText =
          match.index !== undefined &&
          match.input !== undefined &&
          match.index + match[0].length >= match.input.length

        if (!isMatchAtEndOfText) {
          tr.removeStoredMark(config.type)
        }
      }
    },
  })
}
