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

import useHistory from '../../composables/private.use-history/use-history.js'
import useModelToggle, {
  useModelToggleProps,
  useModelToggleEmits
} from '../../composables/private.use-model-toggle/use-model-toggle.js'
import usePreventScroll from '../../composables/private.use-prevent-scroll/use-prevent-scroll.js'
import useTimeout from '../../composables/use-timeout/use-timeout.js'
import useDark, {
  useDarkProps
} from '../../composables/private.use-dark/use-dark.js'

import TouchPan from '../../directives/touch-pan/TouchPan.js'

import { createComponent } from '../../utils/private.create/create.js'
import { between } from '../../utils/format/format.js'
import { hSlot, hDir } from '../../utils/private.render/render.js'
import {
  layoutKey,
  emptyRenderFn
} from '../../utils/private.symbols/symbols.js'

const duration = 150

export default createComponent({
  name: 'QDrawer',

  inheritAttrs: false,

  props: {
    ...useModelToggleProps,
    ...useDarkProps,

    side: {
      type: String,
      default: 'left',
      validator: v => ['left', 'right'].includes(v)
    },

    width: {
      type: Number,
      default: 300
    },

    mini: Boolean,
    miniToOverlay: Boolean,
    miniWidth: {
      type: Number,
      default: 57
    },
    noMiniAnimation: Boolean,

    breakpoint: {
      type: Number,
      default: 1023
    },
    showIfAbove: Boolean,

    behavior: {
      type: String,
      validator: v => ['default', 'desktop', 'mobile'].includes(v),
      default: 'default'
    },

    bordered: Boolean,
    elevated: Boolean,

    overlay: Boolean,
    persistent: Boolean,
    noSwipeOpen: Boolean,
    noSwipeClose: Boolean,
    noSwipeBackdrop: Boolean
  },

  emits: [...useModelToggleEmits, 'onLayout', 'miniState'],

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

    const isDark = useDark(props, $q)
    const { preventBodyScroll } = usePreventScroll()
    const { registerTimeout, removeTimeout } = useTimeout()

    const $layout = inject(layoutKey, emptyRenderFn)
    if ($layout === emptyRenderFn) {
      console.error('QDrawer needs to be child of QLayout')
      return emptyRenderFn
    }

    let lastDesktopState,
      timerMini = null,
      layoutTotalWidthWatcher

    const belowBreakpoint = ref(
      props.behavior === 'mobile' ||
        (props.behavior !== 'desktop' &&
          $layout.totalWidth.value <= props.breakpoint)
    )

    const isMini = computed(
      () => props.mini === true && belowBreakpoint.value !== true
    )

    const size = computed(() =>
      isMini.value === true ? props.miniWidth : props.width
    )

    const showing = ref(
      props.showIfAbove === true && belowBreakpoint.value === false
        ? true
        : props.modelValue === true
    )

    const hideOnRouteChange = computed(
      () =>
        props.persistent !== true &&
        (belowBreakpoint.value === true || onScreenOverlay.value === true)
    )

    function handleShow(evt, noEvent) {
      addToHistory()

      if (evt !== false) $layout.animate()
      applyPosition(0)

      if (belowBreakpoint.value === true) {
        const otherInstance = $layout.instances[otherSide.value]
        if (otherInstance?.belowBreakpoint === true) {
          otherInstance.hide(false)
        }

        applyBackdrop(1)
        if ($layout.isContainer.value !== true) preventBodyScroll(true)
      } else {
        applyBackdrop(0)
        if (evt !== false) setScrollable(false)
      }

      registerTimeout(() => {
        if (evt !== false) setScrollable(true)
        if (noEvent !== true) emit('show', evt)
      }, duration)
    }

    function handleHide(evt, noEvent) {
      removeFromHistory()

      if (evt !== false) $layout.animate()

      applyBackdrop(0)
      applyPosition(stateDirection.value * size.value)

      cleanup()

      if (noEvent !== true) {
        registerTimeout(() => {
          emit('hide', evt)
        }, duration)
      } else {
        removeTimeout()
      }
    }

    const { show, hide } = useModelToggle({
      showing,
      hideOnRouteChange,
      handleShow,
      handleHide
    })

    const { addToHistory, removeFromHistory } = useHistory(
      showing,
      hide,
      hideOnRouteChange
    )

    const instance = {
      belowBreakpoint,
      hide
    }

    const rightSide = computed(() => props.side === 'right')

    const stateDirection = computed(
      () =>
        ($q.lang.rtl === true ? -1 : 1) * (rightSide.value === true ? 1 : -1)
    )

    const flagBackdropBg = ref(0)
    const flagPanning = ref(false)
    const flagMiniAnimate = ref(false)
    const flagContentPosition = ref(
      // starting with "hidden" for SSR
      size.value * stateDirection.value
    )

    const otherSide = computed(() =>
      rightSide.value === true ? 'left' : 'right'
    )
    const offset = computed(() =>
      showing.value === true &&
      belowBreakpoint.value === false &&
      props.overlay === false
        ? props.miniToOverlay === true
          ? props.miniWidth
          : size.value
        : 0
    )

    const fixed = computed(
      () =>
        props.overlay === true ||
        props.miniToOverlay === true ||
        $layout.view.value.indexOf(rightSide.value ? 'R' : 'L') !== -1 ||
        ($q.platform.is.ios === true && $layout.isContainer.value === true)
    )

    const onLayout = computed(
      () =>
        props.overlay === false &&
        showing.value === true &&
        belowBreakpoint.value === false
    )

    const onScreenOverlay = computed(
      () =>
        props.overlay === true &&
        showing.value === true &&
        belowBreakpoint.value === false
    )

    const backdropClass = computed(
      () =>
        'fullscreen q-drawer__backdrop' +
        (showing.value === false && flagPanning.value === false
          ? ' hidden'
          : '')
    )

    const backdropStyle = computed(() => ({
      backgroundColor: `rgba(0,0,0,${flagBackdropBg.value * 0.4})`
    }))

    const headerSlot = computed(() =>
      rightSide.value === true
        ? $layout.rows.value.top[2] === 'r'
        : $layout.rows.value.top[0] === 'l'
    )

    const footerSlot = computed(() =>
      rightSide.value === true
        ? $layout.rows.value.bottom[2] === 'r'
        : $layout.rows.value.bottom[0] === 'l'
    )

    const aboveStyle = computed(() => {
      const css = {}

      if ($layout.header.space === true && headerSlot.value === false) {
        if (fixed.value === true) {
          css.top = `${$layout.header.offset}px`
        } else if ($layout.header.space === true) {
          css.top = `${$layout.header.size}px`
        }
      }

      if ($layout.footer.space === true && footerSlot.value === false) {
        if (fixed.value === true) {
          css.bottom = `${$layout.footer.offset}px`
        } else if ($layout.footer.space === true) {
          css.bottom = `${$layout.footer.size}px`
        }
      }

      return css
    })

    const style = computed(() => {
      const acc = {
        width: `${size.value}px`,
        transform: `translateX(${flagContentPosition.value}px)`
      }

      return belowBreakpoint.value === true
        ? acc
        : Object.assign(acc, aboveStyle.value)
    })

    const contentClass = computed(
      () =>
        'q-drawer__content fit ' +
        ($layout.isContainer.value !== true ? 'scroll' : 'overflow-auto')
    )

    const classes = computed(
      () =>
        `q-drawer q-drawer--${props.side}` +
        (flagMiniAnimate.value === true ? ' q-drawer--mini-animate' : '') +
        (props.bordered === true ? ' q-drawer--bordered' : '') +
        (isDark.value === true ? ' q-drawer--dark q-dark' : '') +
        (flagPanning.value === true
          ? ' no-transition'
          : showing.value === true
            ? ''
            : ' q-layout--prevent-focus') +
        (belowBreakpoint.value === true
          ? ' fixed q-drawer--on-top q-drawer--mobile q-drawer--top-padding'
          : ` q-drawer--${isMini.value === true ? 'mini' : 'standard'}` +
            (fixed.value === true || onLayout.value !== true ? ' fixed' : '') +
            (props.overlay === true || props.miniToOverlay === true
              ? ' q-drawer--on-top'
              : '') +
            (headerSlot.value === true ? ' q-drawer--top-padding' : ''))
    )

    const openDirective = computed(() => {
      // if props.noSwipeOpen !== true
      const dir = $q.lang.rtl === true ? props.side : otherSide.value

      return [
        [
          TouchPan,
          onOpenPan,
          void 0,
          {
            [dir]: true,
            mouse: true
          }
        ]
      ]
    })

    const contentCloseDirective = computed(() => {
      // if belowBreakpoint.value === true && props.noSwipeClose !== true
      const dir = $q.lang.rtl === true ? otherSide.value : props.side

      return [
        [
          TouchPan,
          onClosePan,
          void 0,
          {
            [dir]: true,
            mouse: true
          }
        ]
      ]
    })

    const backdropCloseDirective = computed(() => {
      // if showing.value === true && props.noSwipeBackdrop !== true
      const dir = $q.lang.rtl === true ? otherSide.value : props.side

      return [
        [
          TouchPan,
          onClosePan,
          void 0,
          {
            [dir]: true,
            mouse: true,
            mouseAllDir: true
          }
        ]
      ]
    })

    function updateBelowBreakpoint() {
      updateLocal(
        belowBreakpoint,
        props.behavior === 'mobile' ||
          (props.behavior !== 'desktop' &&
            $layout.totalWidth.value <= props.breakpoint)
      )
    }

    watch(belowBreakpoint, val => {
      if (val === true) {
        // from lg to xs
        lastDesktopState = showing.value
        if (showing.value === true) hide(false)
      } else if (
        props.overlay === false &&
        props.behavior !== 'mobile' &&
        lastDesktopState !== false
      ) {
        // from xs to lg
        if (showing.value === true) {
          applyPosition(0)
          applyBackdrop(0)
          cleanup()
        } else {
          show(false)
        }
      }
    })

    watch(
      () => props.side,
      (newSide, oldSide) => {
        if ($layout.instances[oldSide] === instance) {
          $layout.instances[oldSide] = void 0
          $layout[oldSide].space = false
          $layout[oldSide].offset = 0
        }

        $layout.instances[newSide] = instance
        $layout[newSide].size = size.value
        $layout[newSide].space = onLayout.value
        $layout[newSide].offset = offset.value
      }
    )

    watch($layout.totalWidth, () => {
      if (
        $layout.isContainer.value === true ||
        document.qScrollPrevented !== true
      ) {
        updateBelowBreakpoint()
      }
    })

    watch(() => props.behavior + props.breakpoint, updateBelowBreakpoint)

    watch($layout.isContainer, val => {
      if (showing.value === true) preventBodyScroll(val !== true)
      if (val === true) updateBelowBreakpoint()
    })

    watch($layout.scrollbarWidth, () => {
      applyPosition(showing.value === true ? 0 : void 0)
    })

    watch(offset, val => {
      updateLayout('offset', val)
    })

    watch(onLayout, val => {
      emit('onLayout', val)
      updateLayout('space', val)
    })

    watch(rightSide, () => {
      applyPosition()
    })

    watch(size, val => {
      applyPosition()
      updateSizeOnLayout(props.miniToOverlay, val)
    })

    watch(
      () => props.miniToOverlay,
      val => {
        updateSizeOnLayout(val, size.value)
      }
    )

    watch(
      () => $q.lang.rtl,
      () => {
        applyPosition()
      }
    )

    watch(
      () => props.mini,
      () => {
        if (props.noMiniAnimation) return
        if (props.modelValue === true) {
          animateMini()
          $layout.animate()
        }
      }
    )

    watch(isMini, val => {
      emit('miniState', val)
    })

    function applyPosition(position) {
      if (position === void 0) {
        nextTick(() => {
          position = showing.value === true ? 0 : size.value
          applyPosition(stateDirection.value * position)
        })
      } else {
        if (
          $layout.isContainer.value === true &&
          rightSide.value === true &&
          (belowBreakpoint.value === true || Math.abs(position) === size.value)
        ) {
          position += stateDirection.value * $layout.scrollbarWidth.value
        }

        flagContentPosition.value = position
      }
    }

    function applyBackdrop(x) {
      flagBackdropBg.value = x
    }

    function setScrollable(v) {
      const action =
        v === true ? 'remove' : $layout.isContainer.value !== true ? 'add' : ''

      if (action !== '') {
        document.body.classList[action]('q-body--drawer-toggle')
      }
    }

    function animateMini() {
      if (timerMini !== null) clearTimeout(timerMini)

      if (vm.proxy && vm.proxy.$el) {
        // need to speed it up and apply it immediately,
        // even faster than Vue's nextTick!
        vm.proxy.$el.classList.add('q-drawer--mini-animate')
      }

      flagMiniAnimate.value = true
      timerMini = setTimeout(() => {
        timerMini = null
        flagMiniAnimate.value = false
        vm?.proxy?.$el?.classList.remove('q-drawer--mini-animate')
      }, 150)
    }

    function onOpenPan(evt) {
      if (showing.value !== false) {
        // some browsers might capture and trigger this
        // even if Drawer has just been opened (but animation is still pending)
        return
      }

      const width = size.value,
        position = between(evt.distance.x, 0, width)

      if (evt.isFinal === true) {
        const opened = position >= Math.min(75, width)

        if (opened === true) {
          show()
        } else {
          $layout.animate()
          applyBackdrop(0)
          applyPosition(stateDirection.value * width)
        }

        flagPanning.value = false
        return
      }

      applyPosition(
        ($q.lang.rtl === true ? rightSide.value !== true : rightSide.value)
          ? Math.max(width - position, 0)
          : Math.min(0, position - width)
      )
      applyBackdrop(between(position / width, 0, 1))

      if (evt.isFirst === true) {
        flagPanning.value = true
      }
    }

    function onClosePan(evt) {
      if (showing.value !== true) {
        // some browsers might capture and trigger this
        // even if Drawer has just been closed (but animation is still pending)
        return
      }

      const width = size.value,
        dir = evt.direction === props.side,
        position = ($q.lang.rtl === true ? dir !== true : dir)
          ? between(evt.distance.x, 0, width)
          : 0

      if (evt.isFinal === true) {
        const opened = Math.abs(position) < Math.min(75, width)

        if (opened === true) {
          $layout.animate()
          applyBackdrop(1)
          applyPosition(0)
        } else {
          hide()
        }

        flagPanning.value = false
        return
      }

      applyPosition(stateDirection.value * position)
      applyBackdrop(between(1 - position / width, 0, 1))

      if (evt.isFirst === true) {
        flagPanning.value = true
      }
    }

    function cleanup() {
      preventBodyScroll(false)
      setScrollable(true)
    }

    function updateLayout(prop, val) {
      $layout.update(props.side, prop, val)
    }

    function updateLocal(prop, val) {
      if (prop.value !== val) {
        prop.value = val
      }
    }

    function updateSizeOnLayout(miniToOverlay, newSize) {
      updateLayout('size', miniToOverlay === true ? props.miniWidth : newSize)
    }

    $layout.instances[props.side] = instance
    updateSizeOnLayout(props.miniToOverlay, size.value)
    updateLayout('space', onLayout.value)
    updateLayout('offset', offset.value)

    if (
      props.showIfAbove === true &&
      props.modelValue !== true &&
      showing.value === true &&
      props['onUpdate:modelValue'] !== void 0
    ) {
      emit('update:modelValue', true)
    }

    onMounted(() => {
      emit('onLayout', onLayout.value)
      emit('miniState', isMini.value)

      lastDesktopState = props.showIfAbove === true

      const fn = () => {
        const action = showing.value === true ? handleShow : handleHide
        action(false, true)
      }

      if ($layout.totalWidth.value !== 0) {
        // make sure that all computed properties
        // have been updated before calling handleShow/handleHide()
        nextTick(fn)
        return
      }

      layoutTotalWidthWatcher = watch($layout.totalWidth, () => {
        layoutTotalWidthWatcher()
        layoutTotalWidthWatcher = void 0

        if (
          showing.value === false &&
          props.showIfAbove === true &&
          belowBreakpoint.value === false
        ) {
          show(false)
        } else {
          fn()
        }
      })
    })

    onBeforeUnmount(() => {
      layoutTotalWidthWatcher?.()

      if (timerMini !== null) {
        clearTimeout(timerMini)
        timerMini = null
      }

      if (showing.value === true) cleanup()

      if ($layout.instances[props.side] === instance) {
        $layout.instances[props.side] = void 0
        updateLayout('size', 0)
        updateLayout('offset', 0)
        updateLayout('space', false)
      }
    })

    return () => {
      const child = []

      if (belowBreakpoint.value === true) {
        if (props.noSwipeOpen === false) {
          child.push(
            withDirectives(
              h('div', {
                key: 'open',
                class: `q-drawer__opener fixed-${props.side}`,
                'aria-hidden': 'true'
              }),
              openDirective.value
            )
          )
        }

        child.push(
          hDir(
            'div',
            {
              ref: 'backdrop',
              class: backdropClass.value,
              style: backdropStyle.value,
              'aria-hidden': 'true',
              onClick: hide
            },
            void 0,
            'backdrop',
            props.noSwipeBackdrop !== true && showing.value === true,
            () => backdropCloseDirective.value
          )
        )
      }

      const mini = isMini.value === true && slots.mini !== void 0
      const content = [
        h(
          'div',
          {
            ...attrs,
            key: String(mini), // required otherwise Vue will not diff correctly
            class: [contentClass.value, attrs.class]
          },
          mini === true ? slots.mini() : hSlot(slots.default)
        )
      ]

      if (props.elevated === true && showing.value === true) {
        content.push(
          h('div', {
            class:
              'q-layout__shadow absolute-full overflow-hidden no-pointer-events'
          })
        )
      }

      child.push(
        hDir(
          'aside',
          { ref: 'content', class: classes.value, style: style.value },
          content,
          'contentclose',
          props.noSwipeClose !== true && belowBreakpoint.value === true,
          () => contentCloseDirective.value
        )
      )

      return h('div', { class: 'q-drawer-container' }, child)
    }
  }
})
