import CharacterCount from '@tiptap/extension-character-count'
import Placeholder from '@tiptap/extension-placeholder'
import {
  useEditor,
  EditorContent,
  FloatingMenu,
  JSONContent,
  Content,
  Editor,
  ReactRenderer,
} from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link'
import { Mention } from '@tiptap/extension-mention'
import clsx from 'clsx'
import { useEffect } from 'react'
import { Linkify } from './linkify'
import { uploadImage } from 'web/lib/firebase/storage'
import { useMutation } from 'react-query'
import { exhibitExts, searchInAny } from 'common/util/parse'
import { FileUploadButton } from './file-upload-button'
import { linkClass } from './site-link'
import { useUsers } from 'web/hooks/use-users'
import { MentionList } from './editor/mention-list'
import tippy from 'tippy.js'

const proseClass = clsx(
  'prose prose-p:my-0 prose-li:my-0 prose-blockquote:not-italic max-w-none prose-quoteless leading-relaxed',
  'font-light prose-a:font-light prose-blockquote:font-light'
)

export function useTextEditor(props: {
  placeholder?: string
  max?: number
  defaultValue?: Content
  disabled?: boolean
}) {
  const { placeholder, max, defaultValue = '', disabled } = props

  const users = useUsers()

  const editorClass = clsx(
    proseClass,
    'box-content min-h-[6em] textarea textarea-bordered text-base'
  )

  const editor = useEditor({
    editorProps: { attributes: { class: editorClass } },
    extensions: [
      StarterKit.configure({
        heading: { levels: [1, 2, 3] },
      }),
      Placeholder.configure({
        placeholder,
        emptyEditorClass:
          'before:content-[attr(data-placeholder)] before:text-slate-500 before:float-left before:h-0',
      }),
      CharacterCount.configure({ limit: max }),
      Image,
      Link.configure({
        HTMLAttributes: {
          class: clsx('no-underline !text-indigo-700', linkClass),
        },
      }),
      Mention.configure({
        HTMLAttributes: {
          class: clsx('not-prose text-indigo-700', linkClass),
        },
        // TODO: do a Next link instead of raw <a>
        renderLabel: ({ options, node }) =>
          [
            'a',
            { href: node.attrs.label },
            `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`,
          ] as any,
        suggestion: {
          items: ({ query }) =>
            users
              .filter((u) => searchInAny(query, u.username, u.name))
              .slice(0, 5),
          render: () => {
            let component: any
            let popup: any
            return {
              onStart: (props) => {
                component = new ReactRenderer(MentionList, {
                  props,
                  editor: props.editor,
                })
                if (!props.clientRect) {
                  return
                }

                popup = tippy('body', {
                  getReferenceClientRect: props.clientRect as any,
                  appendTo: () => document.body,
                  content: component.element,
                  showOnCreate: true,
                  interactive: true,
                  trigger: 'manual',
                  placement: 'bottom-start',
                })
              },
              onUpdate(props) {
                component.updateProps(props)

                if (!props.clientRect) {
                  return
                }

                popup[0].setProps({
                  getReferenceClientRect: props.clientRect,
                })
              },
              onKeyDown(props) {
                if (props.event.key === 'Escape') {
                  popup[0].hide()
                  return true
                }
                return component.ref?.onKeyDown(props)
              },
              onExit() {
                popup[0].destroy()
                component.destroy()
              },
            }
          },
        },
      }),
    ],
    content: defaultValue,
  })

  const upload = useUploadMutation(editor)

  editor?.setOptions({
    editorProps: {
      handlePaste(view, event) {
        const imageFiles = Array.from(event.clipboardData?.files ?? []).filter(
          (file) => file.type.startsWith('image')
        )

        if (!imageFiles.length) {
          return // if no files pasted, use default paste handler
        }

        event.preventDefault()
        upload.mutate(imageFiles)
      },
    },
  })

  useEffect(() => {
    editor?.setEditable(!disabled)
  }, [editor, disabled])

  return { editor, upload }
}

export function TextEditor(props: {
  editor: Editor | null
  upload: ReturnType<typeof useUploadMutation>
}) {
  const { editor, upload } = props

  return (
    <>
      {/* hide placeholder when focused */}
      <div className="w-full [&:focus-within_p.is-empty]:before:content-none">
        {editor && (
          <FloatingMenu
            editor={editor}
            className={clsx(proseClass, '-ml-2 mr-2 w-full text-slate-300 ')}
          >
            Type <em>*markdown*</em>. Paste or{' '}
            <FileUploadButton
              className="link text-blue-300"
              onFiles={upload.mutate}
            >
              upload
            </FileUploadButton>{' '}
            images!
          </FloatingMenu>
        )}
        <EditorContent editor={editor} />
      </div>
      {upload.isLoading && <span className="text-xs">Uploading image...</span>}
      {upload.isError && (
        <span className="text-error text-xs">Error uploading image :(</span>
      )}
    </>
  )
}

const useUploadMutation = (editor: Editor | null) =>
  useMutation(
    (files: File[]) =>
      // TODO: Images should be uploaded under a particular username
      Promise.all(files.map((file) => uploadImage('default', file))),
    {
      onSuccess(urls) {
        if (!editor) return
        let trans = editor.view.state.tr
        urls.forEach((src: any) => {
          const node = editor.view.state.schema.nodes.image.create({ src })
          trans = trans.insert(editor.view.state.selection.to, node)
        })
        editor.view.dispatch(trans)
      },
    }
  )

function RichContent(props: { content: JSONContent }) {
  const { content } = props
  const editor = useEditor({
    editorProps: { attributes: { class: proseClass } },
    extensions: exhibitExts,
    content,
    editable: false,
  })
  useEffect(() => void editor?.commands?.setContent(content), [editor, content])

  return <EditorContent editor={editor} />
}

// backwards compatibility: we used to store content as strings
export function Content(props: { content: JSONContent | string }) {
  const { content } = props
  return typeof content === 'string' ? (
    <div className="whitespace-pre-line font-light leading-relaxed">
      <Linkify text={content} />
    </div>
  ) : (
    <RichContent content={content} />
  )
}
