import { target, targetable } from '@github/catalyst/lib/targetable'
import { actionable } from '@github/catalyst/lib/actionable'

function loadTiptap () {
  return Promise.all([
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-bold'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-italic'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-paragraph'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-link'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-underline'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/markdown'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
    import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
  ]).then(([core, bold, italic, paragraph, text, hardBreak, document, link, underline, extensions, markdown, pmState, pmView]) => ({
    Editor: core.Editor,
    Extension: core.Extension,
    Bold: bold.default || bold,
    Italic: italic.default || italic,
    Paragraph: paragraph.default || paragraph,
    Text: text.default || text,
    HardBreak: hardBreak.default || hardBreak,
    Document: document.default || document,
    Link: link.default || link,
    Underline: underline.default || underline,
    UndoRedo: extensions.UndoRedo,
    Markdown: markdown.Markdown,
    Plugin: pmState.Plugin,
    Decoration: pmView.Decoration,
    DecorationSet: pmView.DecorationSet
  }))
}

class LinkTooltip {
  constructor (container, editor, templateEl) {
    this.container = container
    this.editor = editor

    this.tooltip = templateEl.content.firstElementChild.cloneNode(true)

    this.input = this.tooltip.querySelector('input')
    this.saveButton = this.tooltip.querySelector('[data-role="link-save"]')
    this.removeButton = this.tooltip.querySelector('[data-role="link-remove"]')

    container.style.position = 'relative'
    container.appendChild(this.tooltip)
  }

  isVisible () {
    return !this.tooltip.classList.contains('hidden')
  }

  normalizeUrl (url) {
    if (!url) return url
    if (/^{/i.test(url)) return url
    if (/^https?:\/\//i.test(url)) return url
    if (/^mailto:/i.test(url)) return url

    return `https://${url}`
  }

  show (url, pos, { focus = false } = {}) {
    this.input.value = url || ''
    this.removeButton.classList.toggle('hidden', !url)

    this.tooltip.classList.remove('hidden')

    const coords = this.editor.view.coordsAtPos(pos)
    const containerRect = this.container.getBoundingClientRect()

    this.tooltip.style.left = `${coords.left - containerRect.left}px`
    this.tooltip.style.top = `${coords.bottom - containerRect.top + 4}px`

    if (focus) this.input.focus()

    this.saveHandler = () => {
      const inputUrl = this.input.value.trim()

      if (inputUrl) {
        this.editor.chain().focus().extendMarkRange('link').setLink({ href: this.normalizeUrl(inputUrl) }).run()
      }

      this.hide()
    }

    this.removeHandler = () => {
      this.editor.chain().focus().extendMarkRange('link').unsetLink().run()

      this.hide()
    }

    this.keyHandler = (e) => {
      if (e.key === 'Enter') {
        e.preventDefault()
        this.saveHandler()
      } else if (e.key === 'Escape') {
        e.preventDefault()
        this.hide()
      }
    }

    this.saveButton.addEventListener('click', this.saveHandler, { once: true })
    this.removeButton.addEventListener('click', this.removeHandler, { once: true })
    this.input.addEventListener('keydown', this.keyHandler)
  }

  hide () {
    if (this.saveHandler) {
      this.saveButton.removeEventListener('click', this.saveHandler)
      this.saveHandler = null
    }

    if (this.removeHandler) {
      this.removeButton.removeEventListener('click', this.removeHandler)
      this.removeHandler = null
    }

    if (this.keyHandler) {
      this.input.removeEventListener('keydown', this.keyHandler)
      this.keyHandler = null
    }

    this.tooltip.classList.add('hidden')
    this.currentMark = null
  }
}

export default actionable(targetable(class extends HTMLElement {
  static [target.static] = [
    'textarea',
    'editorElement',
    'boldButton',
    'italicButton',
    'underlineButton',
    'linkButton',
    'linkTooltipTemplate'
  ]

  async connectedCallback () {
    if (!this.textarea || !this.editorElement) return

    this.textarea.style.display = 'none'
    this.adjustShortcutsForPlatform()

    const { Editor, Extension, Bold, Italic, Paragraph, Text, HardBreak, UndoRedo, Document, Link, Underline, Markdown, Plugin, Decoration, DecorationSet } = await loadTiptap()

    const buildDecorations = (doc) => {
      const decorations = []
      const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g

      doc.descendants((node, pos) => {
        if (!node.isText) return

        let match

        while ((match = regex.exec(node.text)) !== null) {
          decorations.push(
            Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
              class: 'bg-amber-100 py-0.5 px-1 rounded'
            })
          )
        }
      })

      return DecorationSet.create(doc, decorations)
    }

    const VariableHighlight = Extension.create({
      name: 'variableHighlight',
      addProseMirrorPlugins () {
        return [new Plugin({
          state: {
            init (_, { doc }) {
              return buildDecorations(doc)
            },
            apply (tr, oldSet) {
              return tr.docChanged ? buildDecorations(tr.doc) : oldSet
            }
          },
          props: {
            decorations (state) {
              return this.getState(state)
            }
          }
        })]
      }
    })

    this.editor = new Editor({
      element: this.editorElement,
      extensions: [
        Markdown,
        Document,
        Paragraph,
        Text,
        Bold,
        Italic,
        HardBreak.extend({
          addKeyboardShortcuts () {
            return {
              Enter: () => this.editor.commands.setHardBreak()
            }
          }
        }),
        UndoRedo,
        Link.extend({
          inclusive: true,
          addKeyboardShortcuts: () => ({
            'Mod-k': () => {
              this.toggleLink()

              return true
            }
          })
        }).configure({
          openOnClick: false,
          HTMLAttributes: {
            class: 'link',
            'data-turbo': 'false',
            style: 'color: #2563eb; text-decoration: underline; cursor: text;'
          }
        }),
        Underline,
        VariableHighlight
      ],
      content: (this.textarea.value || '').trim().replace(/ *\n/g, '<br>'),
      contentType: 'markdown',
      editorProps: {
        attributes: {
          style: 'min-height: 220px',
          dir: 'auto',
          class: 'p-3 outline-none focus:outline-none'
        }
      },
      onUpdate: ({ editor }) => {
        this.textarea.value = editor.getMarkdown()
        this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
      },
      onSelectionUpdate: ({ editor }) => {
        this.updateToolbarState()
        this.handleLinkTooltip(editor)
      },
      onBlur: () => {
        setTimeout(() => {
          if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
            this.linkTooltip.hide()
          }
        }, 0)
      }
    })

    this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
  }

  adjustShortcutsForPlatform () {
    if ((navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')) {
      this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
        const tip = tooltip.getAttribute('data-tip')

        if (tip && tip.includes('Ctrl')) {
          tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
        }
      })
    }
  }

  bold (e) {
    e.preventDefault()

    this.editor.chain().focus().toggleBold().run()
    this.updateToolbarState()
  }

  italic (e) {
    e.preventDefault()

    this.editor.chain().focus().toggleItalic().run()
    this.updateToolbarState()
  }

  underline (e) {
    e.preventDefault()

    this.editor.chain().focus().toggleUnderline().run()
    this.updateToolbarState()
  }

  linkSelection (e) {
    e.preventDefault()

    this.toggleLink()
    this.updateToolbarState()
  }

  undo (e) {
    e.preventDefault()

    this.editor.chain().focus().undo().run()
    this.updateToolbarState()
  }

  redo (e) {
    e.preventDefault()

    this.editor.chain().focus().redo().run()
    this.updateToolbarState()
  }

  updateToolbarState () {
    this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
    this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
    this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
    this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
  }

  handleLinkTooltip (editor) {
    const { from } = editor.state.selection
    const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')

    if (!mark) {
      if (this.linkTooltip.isVisible()) this.linkTooltip.hide()

      return
    }

    if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return

    let linkStart = from
    const start = editor.state.doc.resolve(from).start()

    for (let i = from - 1; i >= start; i--) {
      if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
        linkStart = i
      } else {
        break
      }
    }

    this.linkTooltip.hide()
    this.linkTooltip.show(mark.attrs.href, linkStart > start ? linkStart - 1 : linkStart)
    this.linkTooltip.currentMark = mark
  }

  toggleLink () {
    if (this.editor.isActive('link')) {
      this.linkTooltip.hide()
      this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
      this.updateToolbarState()
    } else {
      const { from } = this.editor.state.selection

      this.linkTooltip.hide()
      this.linkTooltip.show(this.editor.getAttributes('link').href, from, { focus: true })
    }
  }

  insertVariable (e) {
    const variable = e.target.closest('[data-variable]')?.dataset.variable

    if (variable) {
      const { from, to } = this.editor.state.selection

      if (variable.includes('link') && from !== to) {
        this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
      } else {
        this.editor.chain().focus().insertContent(`{${variable}}`).run()
      }
    }
  }

  disconnectedCallback () {
    this.linkTooltip.hide()

    if (this.editor) {
      this.editor.destroy()
    }
  }
}))
