<script setup lang="ts">
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import {
  Table,
  TableCell,
  TableHeader,
  TableRow,
} from '@tiptap/extension-table'
import TextAlign from '@tiptap/extension-text-align'
import Underline from '@tiptap/extension-underline'
import StarterKit from '@tiptap/starter-kit'
import type { Editor, JSONContent } from '@tiptap/core'
import { EditorContent, useEditor } from '@tiptap/vue-3'
import {
  AlignCenter,
  AlignJustify,
  AlignLeft,
  AlignRight,
  Bold,
  Columns3,
  Highlighter,
  Italic,
  Link2,
  List,
  ListOrdered,
  Minus,
  Plus,
  Quote,
  Redo2,
  Rows3,
  Strikethrough,
  Table2,
  TableCellsMerge,
  TableCellsSplit,
  Trash2,
  Underline as UnderlineIcon,
  Undo2,
  Unlink2,
} from 'lucide-vue-next'
import { computed, watch } from 'vue'

const props = defineProps<{
  modelValue: string
  disabled: boolean
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

type HeadingLevel = 2 | 3 | 4
type TextAlignment = 'left' | 'center' | 'right' | 'justify'

const allowedLinkProtocols = ['http:', 'https:', 'mailto:', 'tel:']

const editor = useEditor({
  content: props.modelValue,
  editable: !props.disabled,
  extensions: [
    StarterKit.configure({
      heading: {
        levels: [2, 3, 4],
      },
      link: false,
      underline: false,
    }),
    Underline,
    Highlight,
    Link.configure({
      autolink: true,
      defaultProtocol: 'https',
      HTMLAttributes: {
        rel: 'noreferrer',
        target: '_blank',
      },
      isAllowedUri: (linkUrl) => {
        return isAllowedLinkUrl(linkUrl)
      },
      linkOnPaste: true,
      openOnClick: false,
      protocols: ['http', 'https', 'mailto', 'tel'],
      shouldAutoLink: (linkUrl) => {
        return isAllowedLinkUrl(linkUrl)
      },
    }),
    TextAlign.configure({
      types: ['heading', 'paragraph'],
    }),
    Table.configure({
      resizable: true,
    }),
    TableRow,
    TableHeader,
    TableCell,
  ],
  editorProps: {
    attributes: {
      'aria-label': 'Rich text editor',
      class: 'rich-text-editor__surface',
      'data-cy': 'admin-element-rich-text',
    },
  },
  onUpdate: ({ editor: updatedEditor }) => {
    emit('update:modelValue', getEditorHtml(updatedEditor))
  },
})

const currentBlockType = computed(() => {
  const editorInstance = editor.value
  if (editorInstance === undefined) {
    return 'paragraph'
  }

  if (editorInstance.isActive('heading', { level: 2 })) {
    return 'heading-2'
  }

  if (editorInstance.isActive('heading', { level: 3 })) {
    return 'heading-3'
  }

  if (editorInstance.isActive('heading', { level: 4 })) {
    return 'heading-4'
  }

  return 'paragraph'
})

const isInTable = computed(() => {
  const editorInstance = editor.value
  if (editorInstance === undefined) {
    return false
  }

  return (
    editorInstance.isActive('table') ||
    editorInstance.isActive('tableCell') ||
    editorInstance.isActive('tableHeader') ||
    editorInstance.isActive('tableRow')
  )
})

watch(
  () => props.modelValue,
  (currentValue) => {
    const editorInstance = editor.value
    if (editorInstance === undefined) {
      return
    }

    if (getEditorHtml(editorInstance) === currentValue) {
      return
    }

    editorInstance.commands.setContent(currentValue, {
      emitUpdate: false,
    })
  },
)

watch(
  () => props.disabled,
  (isDisabled) => {
    const editorInstance = editor.value
    if (editorInstance === undefined) {
      return
    }

    editorInstance.setEditable(!isDisabled)
  },
)

function getEditorHtml(editorInstance: Editor): string {
  if (editorInstance.isEmpty) {
    return ''
  }

  return editorInstance.getHTML()
}

function isAllowedLinkUrl(linkUrl: string): boolean {
  const normalizedUrl = normalizeLinkUrl(linkUrl)

  return normalizedUrl !== null
}

function normalizeLinkUrl(linkUrl: string): string | null {
  const trimmedUrl = linkUrl.trim()
  if (trimmedUrl === '') {
    return null
  }

  let parsedUrl: URL
  try {
    parsedUrl = new URL(trimmedUrl)
  } catch {
    return null
  }

  if (!allowedLinkProtocols.includes(parsedUrl.protocol)) {
    return null
  }

  return parsedUrl.toString()
}

function isEditorDisabled(): boolean {
  return props.disabled || editor.value === undefined
}

function isMarkActive(markName: string): boolean {
  const editorInstance = editor.value
  if (editorInstance === undefined) {
    return false
  }

  return editorInstance.isActive(markName)
}

function isAlignmentActive(alignment: TextAlignment): boolean {
  const editorInstance = editor.value
  if (editorInstance === undefined) {
    return false
  }

  return editorInstance.isActive({ textAlign: alignment })
}

function handleBlockTypeChange(changeEvent: Event): void {
  const target = changeEvent.target
  if (!(target instanceof HTMLSelectElement)) {
    return
  }

  if (target.value === 'paragraph') {
    setParagraph()
    return
  }

  if (target.value === 'heading-2') {
    setHeading(2)
    return
  }

  if (target.value === 'heading-3') {
    setHeading(3)
    return
  }

  if (target.value === 'heading-4') {
    setHeading(4)
  }
}

function setParagraph(): void {
  editor.value?.chain().focus().setParagraph().run()
}

function setHeading(headingLevel: HeadingLevel): void {
  editor.value?.chain().focus().setHeading({ level: headingLevel }).run()
}

function toggleBold(): void {
  editor.value?.chain().focus().toggleBold().run()
}

function toggleItalic(): void {
  editor.value?.chain().focus().toggleItalic().run()
}

function toggleUnderline(): void {
  editor.value?.chain().focus().toggleUnderline().run()
}

function toggleStrike(): void {
  editor.value?.chain().focus().toggleStrike().run()
}

function toggleHighlight(): void {
  editor.value?.chain().focus().toggleHighlight().run()
}

function setTextAlign(alignment: TextAlignment): void {
  editor.value?.chain().focus().setTextAlign(alignment).run()
}

function toggleBulletList(): void {
  editor.value?.chain().focus().toggleBulletList().run()
}

function toggleOrderedList(): void {
  editor.value?.chain().focus().toggleOrderedList().run()
}

function toggleBlockquote(): void {
  editor.value?.chain().focus().toggleBlockquote().run()
}

function setHorizontalRule(): void {
  editor.value?.chain().focus().setHorizontalRule().run()
}

function applyLink(): void {
  const editorInstance = editor.value
  if (editorInstance === undefined) {
    return
  }

  const linkUrl = window.prompt('Link URL', currentLinkHref(editorInstance))
  if (linkUrl === null) {
    return
  }

  const normalizedUrl = normalizeLinkUrl(linkUrl)
  if (normalizedUrl === null) {
    return
  }

  if (editorInstance.state.selection.empty) {
    insertLinkedText(editorInstance, normalizedUrl)
    return
  }

  editorInstance
    .chain()
    .focus()
    .extendMarkRange('link')
    .setLink({ href: normalizedUrl })
    .run()
}

function currentLinkHref(editorInstance: Editor): string {
  const linkAttributes = editorInstance.getAttributes('link') as Record<
    string,
    unknown
  >
  const href = linkAttributes.href

  return typeof href === 'string' ? href : ''
}

function insertLinkedText(editorInstance: Editor, linkUrl: string): void {
  const linkedText: JSONContent = {
    marks: [
      {
        attrs: {
          href: linkUrl,
        },
        type: 'link',
      },
    ],
    text: linkUrl,
    type: 'text',
  }

  editorInstance.chain().focus().insertContent(linkedText).run()
}

function unsetLink(): void {
  editor.value?.chain().focus().unsetLink().run()
}

function undo(): void {
  editor.value?.chain().focus().undo().run()
}

function redo(): void {
  editor.value?.chain().focus().redo().run()
}

function insertTable(): void {
  editor.value
    ?.chain()
    .focus()
    .insertTable({
      cols: 3,
      rows: 3,
      withHeaderRow: true,
    })
    .run()
}

function addColumnAfter(): void {
  editor.value?.chain().focus().addColumnAfter().run()
}

function deleteColumn(): void {
  editor.value?.chain().focus().deleteColumn().run()
}

function addRowAfter(): void {
  editor.value?.chain().focus().addRowAfter().run()
}

function deleteRow(): void {
  editor.value?.chain().focus().deleteRow().run()
}

function toggleHeaderRow(): void {
  editor.value?.chain().focus().toggleHeaderRow().run()
}

function mergeOrSplitCells(): void {
  editor.value?.chain().focus().mergeOrSplit().run()
}

function splitCell(): void {
  editor.value?.chain().focus().splitCell().run()
}

function deleteTable(): void {
  editor.value?.chain().focus().deleteTable().run()
}
</script>

<template>
  <div class="rich-text-editor">
    <div class="rich-text-editor__toolbar" aria-label="Rich text controls">
      <select
        class="rich-text-editor__select"
        data-cy="admin-rich-text-block-type"
        :disabled="isEditorDisabled()"
        :value="currentBlockType"
        aria-label="Block type"
        @change="handleBlockTypeChange"
      >
        <option value="paragraph">Paragraph</option>
        <option value="heading-2">Heading 2</option>
        <option value="heading-3">Heading 3</option>
        <option value="heading-4">Heading 4</option>
      </select>

      <div class="rich-text-editor__button-group">
        <button
          class="rich-text-editor__button"
          data-cy="admin-rich-text-bold"
          type="button"
          :aria-pressed="isMarkActive('bold')"
          :class="{ 'rich-text-editor__button--active': isMarkActive('bold') }"
          :disabled="isEditorDisabled()"
          aria-label="Bold"
          title="Bold"
          @click="toggleBold"
        >
          <Bold :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isMarkActive('italic')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('italic'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Italic"
          title="Italic"
          @click="toggleItalic"
        >
          <Italic :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isMarkActive('underline')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('underline'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Underline"
          title="Underline"
          @click="toggleUnderline"
        >
          <UnderlineIcon :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isMarkActive('strike')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('strike'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Strike"
          title="Strike"
          @click="toggleStrike"
        >
          <Strikethrough :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isMarkActive('highlight')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('highlight'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Highlight"
          title="Highlight"
          @click="toggleHighlight"
        >
          <Highlighter :size="16" aria-hidden="true" />
        </button>
      </div>

      <div class="rich-text-editor__button-group">
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isAlignmentActive('left')"
          :class="{
            'rich-text-editor__button--active': isAlignmentActive('left'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Align left"
          title="Align left"
          @click="setTextAlign('left')"
        >
          <AlignLeft :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isAlignmentActive('center')"
          :class="{
            'rich-text-editor__button--active': isAlignmentActive('center'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Align center"
          title="Align center"
          @click="setTextAlign('center')"
        >
          <AlignCenter :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isAlignmentActive('right')"
          :class="{
            'rich-text-editor__button--active': isAlignmentActive('right'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Align right"
          title="Align right"
          @click="setTextAlign('right')"
        >
          <AlignRight :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isAlignmentActive('justify')"
          :class="{
            'rich-text-editor__button--active': isAlignmentActive('justify'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Justify"
          title="Justify"
          @click="setTextAlign('justify')"
        >
          <AlignJustify :size="16" aria-hidden="true" />
        </button>
      </div>

      <div class="rich-text-editor__button-group">
        <button
          class="rich-text-editor__button"
          data-cy="admin-rich-text-bullet-list"
          type="button"
          :aria-pressed="isMarkActive('bulletList')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('bulletList'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Bullet list"
          title="Bullet list"
          @click="toggleBulletList"
        >
          <List :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isMarkActive('orderedList')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('orderedList'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Ordered list"
          title="Ordered list"
          @click="toggleOrderedList"
        >
          <ListOrdered :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :aria-pressed="isMarkActive('blockquote')"
          :class="{
            'rich-text-editor__button--active': isMarkActive('blockquote'),
          }"
          :disabled="isEditorDisabled()"
          aria-label="Blockquote"
          title="Blockquote"
          @click="toggleBlockquote"
        >
          <Quote :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled()"
          aria-label="Horizontal rule"
          title="Horizontal rule"
          @click="setHorizontalRule"
        >
          <Minus :size="16" aria-hidden="true" />
        </button>
      </div>

      <div class="rich-text-editor__button-group">
        <button
          class="rich-text-editor__button"
          data-cy="admin-rich-text-link"
          type="button"
          :aria-pressed="isMarkActive('link')"
          :class="{ 'rich-text-editor__button--active': isMarkActive('link') }"
          :disabled="isEditorDisabled()"
          aria-label="Link"
          title="Link"
          @click="applyLink"
        >
          <Link2 :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled()"
          aria-label="Remove link"
          title="Remove link"
          @click="unsetLink"
        >
          <Unlink2 :size="16" aria-hidden="true" />
        </button>
      </div>

      <div class="rich-text-editor__button-group">
        <button
          class="rich-text-editor__button"
          data-cy="admin-rich-text-insert-table"
          type="button"
          :disabled="isEditorDisabled()"
          aria-label="Insert table"
          title="Insert table"
          @click="insertTable"
        >
          <Table2 :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Add column"
          title="Add column"
          @click="addColumnAfter"
        >
          <Columns3 :size="16" aria-hidden="true" />
          <Plus :size="11" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Delete column"
          title="Delete column"
          @click="deleteColumn"
        >
          <Columns3 :size="16" aria-hidden="true" />
          <Trash2 :size="11" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Add row"
          title="Add row"
          @click="addRowAfter"
        >
          <Rows3 :size="16" aria-hidden="true" />
          <Plus :size="11" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Delete row"
          title="Delete row"
          @click="deleteRow"
        >
          <Rows3 :size="16" aria-hidden="true" />
          <Trash2 :size="11" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Toggle header row"
          title="Toggle header row"
          @click="toggleHeaderRow"
        >
          <Rows3 :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Merge cells"
          title="Merge cells"
          @click="mergeOrSplitCells"
        >
          <TableCellsMerge :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Split cell"
          title="Split cell"
          @click="splitCell"
        >
          <TableCellsSplit :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled() || !isInTable"
          aria-label="Delete table"
          title="Delete table"
          @click="deleteTable"
        >
          <Trash2 :size="16" aria-hidden="true" />
        </button>
      </div>

      <div class="rich-text-editor__button-group">
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled()"
          aria-label="Undo"
          title="Undo"
          @click="undo"
        >
          <Undo2 :size="16" aria-hidden="true" />
        </button>
        <button
          class="rich-text-editor__button"
          type="button"
          :disabled="isEditorDisabled()"
          aria-label="Redo"
          title="Redo"
          @click="redo"
        >
          <Redo2 :size="16" aria-hidden="true" />
        </button>
      </div>
    </div>

    <EditorContent :editor="editor" class="rich-text-editor__content" />
  </div>
</template>

<style scoped>
.rich-text-editor {
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-white);
}

.rich-text-editor:focus-within {
  border-color: var(--color-slate);
}

.rich-text-editor__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem;
  border-bottom: 1px solid var(--color-border);
  background: #fbfaf6;
}

.rich-text-editor__select {
  min-height: 2.2rem;
  padding: 0 0.55rem;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  color: var(--color-text);
  background: var(--color-white);
  font-family: var(--font-sans);
  font-size: 0.86rem;
}

.rich-text-editor__button-group {
  display: inline-flex;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: 4px;
  background: var(--color-white);
}

.rich-text-editor__button {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.15rem;
  height: 2.15rem;
  padding: 0 0.45rem;
  border: 0;
  border-right: 1px solid var(--color-border);
  color: var(--color-text-muted);
  background: transparent;
}

.rich-text-editor__button:last-child {
  border-right: 0;
}

.rich-text-editor__button:hover:not(:disabled),
.rich-text-editor__button:focus-visible,
.rich-text-editor__button--active {
  color: var(--color-white);
  background: var(--color-slate);
  outline: none;
}

.rich-text-editor__button:disabled,
.rich-text-editor__select:disabled {
  cursor: not-allowed;
  opacity: 0.5;
}

.rich-text-editor__content {
  background: var(--color-white);
}

.rich-text-editor__content :deep(.rich-text-editor__surface) {
  min-height: 15rem;
  padding: 0.85rem 0.95rem;
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: 0.98rem;
  line-height: 1.65;
  outline: none;
}

.rich-text-editor__content :deep(.rich-text-editor__surface > *:first-child) {
  margin-top: 0;
}

.rich-text-editor__content :deep(.rich-text-editor__surface > *:last-child) {
  margin-bottom: 0;
}

.rich-text-editor__content :deep(h2),
.rich-text-editor__content :deep(h3),
.rich-text-editor__content :deep(h4) {
  margin: 1.1rem 0 0.45rem;
  color: #2c2c2c;
  font-family: var(--font-serif);
  font-weight: 500;
  line-height: 1.25;
}

.rich-text-editor__content :deep(h2) {
  font-size: 1.55rem;
}

.rich-text-editor__content :deep(h3) {
  font-size: 1.3rem;
}

.rich-text-editor__content :deep(h4) {
  font-size: 1.1rem;
}

.rich-text-editor__content :deep(p) {
  margin: 0 0 0.75rem;
}

.rich-text-editor__content :deep(ul),
.rich-text-editor__content :deep(ol) {
  margin: 0 0 0.85rem;
  padding-left: 1.45rem;
}

.rich-text-editor__content :deep(blockquote) {
  margin: 1rem 0;
  padding: 0.1rem 0 0.1rem 1rem;
  border-left: 3px solid var(--color-olive);
  color: var(--color-text-muted);
}

.rich-text-editor__content :deep(mark) {
  padding: 0 0.16em;
  background: #fff3a7;
}

.rich-text-editor__content :deep(a) {
  color: var(--color-slate);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 0.16em;
}

.rich-text-editor__content :deep(hr) {
  margin: 1.2rem 0;
  border: 0;
  border-top: 1px solid var(--color-border);
}

.rich-text-editor__content :deep(.tableWrapper) {
  max-width: 100%;
  margin: 1rem 0;
  overflow-x: auto;
}

.rich-text-editor__content :deep(table) {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

.rich-text-editor__content :deep(th),
.rich-text-editor__content :deep(td) {
  min-width: 4rem;
  padding: 0.45rem 0.55rem;
  border: 1px solid var(--color-border);
  vertical-align: top;
}

.rich-text-editor__content :deep(th) {
  background: #f6f3eb;
  font-weight: 700;
}

.rich-text-editor__content :deep(.selectedCell) {
  position: relative;
}

.rich-text-editor__content :deep(.selectedCell::after) {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  content: '';
  background: rgb(43 63 78 / 12%);
}

@media (max-width: 768px) {
  .rich-text-editor__toolbar {
    align-items: stretch;
  }

  .rich-text-editor__select {
    width: 100%;
  }
}
</style>
