import {
  h,
  ref,
  computed,
  watch,
  onMounted,
  onBeforeUnmount,
  nextTick,
  getCurrentInstance
} from 'vue'

import Caret from './editor-caret.js'
import { getToolbar, getFonts, getLinkEditor } from './editor-utils.js'

import useDark, {
  useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import useFullscreen, {
  useFullscreenProps,
  useFullscreenEmits
} from '../../composables/private.use-fullscreen/use-fullscreen.js'
import useSplitAttrs from '../../composables/use-split-attrs/use-split-attrs.js'

import { createComponent } from '../../utils/private.create/create.js'
import { stopAndPrevent } from '../../utils/event/event.js'
import extend from '../../utils/extend/extend.js'
import { shouldIgnoreKey } from '../../utils/private.keyboard/key-composition.js'
import { addFocusFn } from '../../utils/private.focus/focus-manager.js'

export default createComponent({
  name: 'QEditor',

  props: {
    ...useDarkProps,
    ...useFullscreenProps,

    modelValue: {
      type: String,
      required: true
    },
    readonly: Boolean,
    disable: Boolean,
    minHeight: {
      type: String,
      default: '10rem'
    },
    maxHeight: String,
    height: String,
    definitions: Object,
    fonts: Object,
    placeholder: String,

    toolbar: {
      type: Array,
      validator: v => v.length === 0 || v.every(group => group.length),
      // long line on purpose for API validation purposes:
      // oxfmt-ignore
      default: () => [['left', 'center', 'right', 'justify'], ['bold', 'italic', 'underline', 'strike'], ['undo', 'redo']]
    },
    toolbarColor: String,
    toolbarBg: String,
    toolbarTextColor: String,
    toolbarToggleColor: {
      type: String,
      default: 'primary'
    },
    toolbarOutline: Boolean,
    toolbarPush: Boolean,
    toolbarRounded: Boolean,

    paragraphTag: {
      type: String,
      validator: v => ['div', 'p'].includes(v),
      default: 'div'
    },

    contentStyle: Object,
    contentClass: [Object, Array, String],

    square: Boolean,
    flat: Boolean,
    dense: Boolean
  },

  emits: [
    ...useFullscreenEmits,
    'update:modelValue',
    'keydown',
    'click',
    'focus',
    'blur',
    'dropdownShow',
    'dropdownHide',
    'dropdownBeforeShow',
    'dropdownBeforeHide',
    'linkShow',
    'linkHide'
  ],

  setup(props, { slots, emit }) {
    const { proxy } = getCurrentInstance()
    const { $q } = proxy

    const isDark = useDark(props, $q)
    const { inFullscreen, toggleFullscreen } = useFullscreen()
    const splitAttrs = useSplitAttrs()

    const rootRef = ref(null)
    const contentRef = ref(null)

    const editLinkUrl = ref(null)
    const isViewingSource = ref(false)

    const editable = computed(() => !props.readonly && !props.disable)

    let defaultFont, offsetBottom
    let lastEmit = props.modelValue

    if (__QUASAR_SSR_SERVER__ !== true) {
      document.execCommand(
        'defaultParagraphSeparator',
        false,
        props.paragraphTag
      )
      defaultFont = window.getComputedStyle(document.body).fontFamily
    }

    const toolbarBackgroundClass = computed(() =>
      props.toolbarBg ? ` bg-${props.toolbarBg}` : ''
    )

    const buttonProps = computed(() => {
      const flat = props.toolbarOutline !== true && props.toolbarPush !== true

      return {
        type: 'a',
        flat,
        noWrap: true,
        outline: props.toolbarOutline,
        push: props.toolbarPush,
        rounded: props.toolbarRounded,
        dense: true,
        color: props.toolbarColor,
        disable: !editable.value,
        size: 'sm'
      }
    })

    const buttonDef = computed(() => {
      const e = $q.lang.editor,
        i = $q.iconSet.editor

      return {
        bold: { cmd: 'bold', icon: i.bold, tip: e.bold, key: 66 },
        italic: { cmd: 'italic', icon: i.italic, tip: e.italic, key: 73 },
        strike: {
          cmd: 'strikeThrough',
          icon: i.strikethrough,
          tip: e.strikethrough,
          key: 83
        },
        underline: {
          cmd: 'underline',
          icon: i.underline,
          tip: e.underline,
          key: 85
        },
        unordered: {
          cmd: 'insertUnorderedList',
          icon: i.unorderedList,
          tip: e.unorderedList
        },
        ordered: {
          cmd: 'insertOrderedList',
          icon: i.orderedList,
          tip: e.orderedList
        },
        subscript: {
          cmd: 'subscript',
          icon: i.subscript,
          tip: e.subscript,
          htmlTip: 'x<subscript>2</subscript>'
        },
        superscript: {
          cmd: 'superscript',
          icon: i.superscript,
          tip: e.superscript,
          htmlTip: 'x<superscript>2</superscript>'
        },
        link: {
          cmd: 'link',
          disable: eVm => eVm.caret && !eVm.caret.can('link'),
          icon: i.hyperlink,
          tip: e.hyperlink,
          key: 76
        },
        fullscreen: {
          cmd: 'fullscreen',
          icon: i.toggleFullscreen,
          tip: e.toggleFullscreen,
          key: 70
        },
        viewsource: {
          cmd: 'viewsource',
          icon: i.viewSource,
          tip: e.viewSource
        },

        quote: {
          cmd: 'formatBlock',
          param: 'BLOCKQUOTE',
          icon: i.quote,
          tip: e.quote,
          key: 81
        },
        left: { cmd: 'justifyLeft', icon: i.left, tip: e.left },
        center: { cmd: 'justifyCenter', icon: i.center, tip: e.center },
        right: { cmd: 'justifyRight', icon: i.right, tip: e.right },
        justify: { cmd: 'justifyFull', icon: i.justify, tip: e.justify },

        print: {
          type: 'no-state',
          cmd: 'print',
          icon: i.print,
          tip: e.print,
          key: 80
        },
        outdent: {
          type: 'no-state',
          disable: eVm => eVm.caret && !eVm.caret.can('outdent'),
          cmd: 'outdent',
          icon: i.outdent,
          tip: e.outdent
        },
        indent: {
          type: 'no-state',
          disable: eVm => eVm.caret && !eVm.caret.can('indent'),
          cmd: 'indent',
          icon: i.indent,
          tip: e.indent
        },
        removeFormat: {
          type: 'no-state',
          cmd: 'removeFormat',
          icon: i.removeFormat,
          tip: e.removeFormat
        },
        hr: {
          type: 'no-state',
          cmd: 'insertHorizontalRule',
          icon: i.hr,
          tip: e.hr
        },
        undo: {
          type: 'no-state',
          cmd: 'undo',
          icon: i.undo,
          tip: e.undo,
          key: 90
        },
        redo: {
          type: 'no-state',
          cmd: 'redo',
          icon: i.redo,
          tip: e.redo,
          key: 89
        },

        h1: {
          cmd: 'formatBlock',
          param: 'H1',
          icon: i.heading1 || i.heading,
          tip: e.heading1,
          htmlTip: `<h1 class="q-ma-none">${e.heading1}</h1>`
        },
        h2: {
          cmd: 'formatBlock',
          param: 'H2',
          icon: i.heading2 || i.heading,
          tip: e.heading2,
          htmlTip: `<h2 class="q-ma-none">${e.heading2}</h2>`
        },
        h3: {
          cmd: 'formatBlock',
          param: 'H3',
          icon: i.heading3 || i.heading,
          tip: e.heading3,
          htmlTip: `<h3 class="q-ma-none">${e.heading3}</h3>`
        },
        h4: {
          cmd: 'formatBlock',
          param: 'H4',
          icon: i.heading4 || i.heading,
          tip: e.heading4,
          htmlTip: `<h4 class="q-ma-none">${e.heading4}</h4>`
        },
        h5: {
          cmd: 'formatBlock',
          param: 'H5',
          icon: i.heading5 || i.heading,
          tip: e.heading5,
          htmlTip: `<h5 class="q-ma-none">${e.heading5}</h5>`
        },
        h6: {
          cmd: 'formatBlock',
          param: 'H6',
          icon: i.heading6 || i.heading,
          tip: e.heading6,
          htmlTip: `<h6 class="q-ma-none">${e.heading6}</h6>`
        },
        p: {
          cmd: 'formatBlock',
          param: props.paragraphTag,
          icon: i.heading,
          tip: e.paragraph
        },
        code: {
          cmd: 'formatBlock',
          param: 'PRE',
          icon: i.code,
          htmlTip: `<code>${e.code}</code>`
        },

        'size-1': {
          cmd: 'fontSize',
          param: '1',
          icon: i.size1 || i.size,
          tip: e.size1,
          htmlTip: `<font size="1">${e.size1}</font>`
        },
        'size-2': {
          cmd: 'fontSize',
          param: '2',
          icon: i.size2 || i.size,
          tip: e.size2,
          htmlTip: `<font size="2">${e.size2}</font>`
        },
        'size-3': {
          cmd: 'fontSize',
          param: '3',
          icon: i.size3 || i.size,
          tip: e.size3,
          htmlTip: `<font size="3">${e.size3}</font>`
        },
        'size-4': {
          cmd: 'fontSize',
          param: '4',
          icon: i.size4 || i.size,
          tip: e.size4,
          htmlTip: `<font size="4">${e.size4}</font>`
        },
        'size-5': {
          cmd: 'fontSize',
          param: '5',
          icon: i.size5 || i.size,
          tip: e.size5,
          htmlTip: `<font size="5">${e.size5}</font>`
        },
        'size-6': {
          cmd: 'fontSize',
          param: '6',
          icon: i.size6 || i.size,
          tip: e.size6,
          htmlTip: `<font size="6">${e.size6}</font>`
        },
        'size-7': {
          cmd: 'fontSize',
          param: '7',
          icon: i.size7 || i.size,
          tip: e.size7,
          htmlTip: `<font size="7">${e.size7}</font>`
        }
      }
    })

    const buttons = computed(() => {
      const userDef = props.definitions || {}
      const def =
        props.definitions || props.fonts
          ? extend(
              true,
              {},
              buttonDef.value,
              userDef,
              getFonts(
                defaultFont,
                $q.lang.editor.defaultFont,
                $q.iconSet.editor.font,
                props.fonts
              )
            )
          : buttonDef.value

      return props.toolbar.map(group =>
        group.map(token => {
          if (token.options) {
            return {
              type: 'dropdown',
              icon: token.icon,
              label: token.label,
              size: 'sm',
              dense: true,
              fixedLabel: token.fixedLabel,
              fixedIcon: token.fixedIcon,
              highlight: token.highlight,
              list: token.list,
              options: token.options.map(item => def[item])
            }
          }

          const obj = def[token]

          if (obj) {
            return obj.type === 'no-state' ||
              (userDef[token] &&
                (obj.cmd === void 0 ||
                  (buttonDef.value[obj.cmd] &&
                    buttonDef.value[obj.cmd].type === 'no-state')))
              ? obj
              : Object.assign({ type: 'toggle' }, obj)
          } else {
            return {
              type: 'slot',
              slot: token
            }
          }
        })
      )
    })

    const eVm = {
      $q,
      props,
      slots,
      emit,
      // caret (will get injected after mount)
      inFullscreen,
      toggleFullscreen,
      runCmd,
      isViewingSource,
      editLinkUrl,
      toolbarBackgroundClass,
      buttonProps,
      contentRef,
      buttons,
      setContent
    }

    watch(
      () => props.modelValue,
      v => {
        if (lastEmit !== v) {
          lastEmit = v
          setContent(v, true)
        }
      }
    )

    watch(editLinkUrl, v => {
      emit(`link${v ? 'Show' : 'Hide'}`)
    })

    const hasToolbar = computed(
      () => props.toolbar && props.toolbar.length !== 0
    )

    const keys = computed(() => {
      const k = {},
        add = btn => {
          if (btn.key) {
            k[btn.key] = {
              cmd: btn.cmd,
              param: btn.param
            }
          }
        }

      buttons.value.forEach(group => {
        group.forEach(token => {
          if (token.options) {
            token.options.forEach(add)
          } else {
            add(token)
          }
        })
      })
      return k
    })

    const innerStyle = computed(() =>
      inFullscreen.value
        ? props.contentStyle
        : [
            {
              minHeight: props.minHeight,
              height: props.height,
              maxHeight: props.maxHeight
            },
            props.contentStyle
          ]
    )

    const classes = computed(
      () =>
        `q-editor q-editor--${isViewingSource.value === true ? 'source' : 'default'}` +
        (props.disable === true ? ' disabled' : '') +
        (inFullscreen.value === true ? ' fullscreen column' : '') +
        (props.square === true ? ' q-editor--square no-border-radius' : '') +
        (props.flat === true ? ' q-editor--flat' : '') +
        (props.dense === true ? ' q-editor--dense' : '') +
        (isDark.value === true ? ' q-editor--dark q-dark' : '')
    )

    const innerClass = computed(() => [
      props.contentClass,
      'q-editor__content',
      {
        col: inFullscreen.value,
        'overflow-auto': inFullscreen.value || props.maxHeight
      }
    ])

    const attributes = computed(() =>
      props.disable === true ? { 'aria-disabled': 'true' } : {}
    )

    function onInput() {
      if (contentRef.value !== null) {
        const prop = `inner${isViewingSource.value === true ? 'Text' : 'HTML'}`
        const val = contentRef.value[prop]

        if (val !== props.modelValue) {
          lastEmit = val
          emit('update:modelValue', val)
        }
      }
    }

    function onKeydown(e) {
      emit('keydown', e)

      if (e.ctrlKey !== true || shouldIgnoreKey(e) === true) {
        refreshToolbar()
        return
      }

      const key = e.keyCode
      const target = keys.value[key]
      if (target !== void 0) {
        const { cmd, param } = target
        stopAndPrevent(e)
        runCmd(cmd, param, false)
      }
    }

    function onClick(e) {
      refreshToolbar()
      emit('click', e)
    }

    function onBlur(e) {
      if (contentRef.value !== null) {
        const { scrollTop, scrollHeight } = contentRef.value
        offsetBottom = scrollHeight - scrollTop
      }
      eVm.caret.save()
      emit('blur', e)
    }

    function onFocus(e) {
      nextTick(() => {
        if (contentRef.value !== null && offsetBottom !== void 0) {
          contentRef.value.scrollTop =
            contentRef.value.scrollHeight - offsetBottom
        }
      })
      emit('focus', e)
    }

    function onFocusin(e) {
      const root = rootRef.value

      if (
        root !== null &&
        root.contains(e.target) === true &&
        (e.relatedTarget === null || root.contains(e.relatedTarget) !== true)
      ) {
        const prop = `inner${isViewingSource.value === true ? 'Text' : 'HTML'}`
        eVm.caret.restorePosition(contentRef.value[prop].length)
        refreshToolbar()
      }
    }

    function onFocusout(e) {
      const root = rootRef.value

      if (
        root !== null &&
        root.contains(e.target) === true &&
        (e.relatedTarget === null || root.contains(e.relatedTarget) !== true)
      ) {
        eVm.caret.savePosition()
        refreshToolbar()
      }
    }

    function onPointerStart() {
      offsetBottom = void 0
    }

    function onSelectionchange() {
      eVm.caret.save()
    }

    function setContent(v, restorePosition) {
      if (contentRef.value !== null) {
        if (restorePosition === true) {
          eVm.caret.savePosition()
        }

        const prop = `inner${isViewingSource.value === true ? 'Text' : 'HTML'}`
        contentRef.value[prop] = v

        if (restorePosition === true) {
          eVm.caret.restorePosition(contentRef.value[prop].length)
          refreshToolbar()
        }
      }
    }

    function runCmd(cmd, param, update = true) {
      focus()
      eVm.caret.restore()
      eVm.caret.apply(cmd, param, () => {
        focus()
        eVm.caret.save()
        if (update) {
          refreshToolbar()
        }
      })
    }

    function refreshToolbar() {
      setTimeout(() => {
        editLinkUrl.value = null
        proxy.$forceUpdate()
      }, 1)
    }

    function focus() {
      addFocusFn(() => {
        contentRef.value?.focus({ preventScroll: true })
      })
    }

    function getContentEl() {
      return contentRef.value
    }

    onMounted(() => {
      eVm.caret = proxy.caret = new Caret(contentRef.value, eVm)
      setContent(props.modelValue)
      refreshToolbar()

      document.addEventListener('selectionchange', onSelectionchange)
    })

    onBeforeUnmount(() => {
      document.removeEventListener('selectionchange', onSelectionchange)
    })

    // expose public methods
    Object.assign(proxy, {
      runCmd,
      refreshToolbar,
      focus,
      getContentEl
    })

    return () => {
      let toolbars

      if (hasToolbar.value) {
        const bars = [
          h(
            'div',
            {
              key: 'qedt_top',
              class:
                'q-editor__toolbar row no-wrap scroll-x' +
                toolbarBackgroundClass.value
            },
            getToolbar(eVm)
          )
        ]

        if (editLinkUrl.value !== null) {
          bars.push(
            h(
              'div',
              {
                key: 'qedt_btm',
                class:
                  'q-editor__toolbar row no-wrap items-center scroll-x' +
                  toolbarBackgroundClass.value
              },
              getLinkEditor(eVm)
            )
          )
        }

        toolbars = h(
          'div',
          {
            key: 'toolbar_ctainer',
            class: 'q-editor__toolbars-container'
          },
          bars
        )
      }

      return h(
        'div',
        {
          ref: rootRef,
          class: classes.value,
          style: { height: inFullscreen.value === true ? '100%' : null },
          ...attributes.value,
          onFocusin,
          onFocusout
        },
        [
          toolbars,

          h('div', {
            ref: contentRef,
            style: innerStyle.value,
            class: innerClass.value,
            contenteditable: editable.value,
            placeholder: props.placeholder,
            ...(__QUASAR_SSR_SERVER__ ? { innerHTML: props.modelValue } : {}),
            ...splitAttrs.listeners.value,
            onInput,
            onKeydown,
            onClick,
            onBlur,
            onFocus,

            // clean saved scroll position
            onMousedown: onPointerStart,
            onTouchstartPassive: onPointerStart
          })
        ]
      )
    }
  }
})
