import {
  computed,
  getCurrentInstance,
  h,
  onActivated,
  onBeforeUnmount,
  onDeactivated,
  ref,
  watch
} from 'vue'

import useQuasar from '../../composables/use-quasar/use-quasar.js'
import useDark, {
  useDarkProps
} from '../../composables/private.use-dark/use-dark.js'

import ScrollAreaControls from './ScrollAreaControls.js'
import QResizeObserver from '../resize-observer/QResizeObserver.js'
import QScrollObserver from '../scroll-observer/QScrollObserver.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 {
  setHorizontalScrollPosition,
  setVerticalScrollPosition
} from '../../utils/scroll/scroll.js'
import { hMergeSlot } from '../../utils/private.render/render.js'
import { rtlHasScrollBug } from '../../utils/private.rtl/rtl.js'
import debounce from '../../utils/debounce/debounce.js'

const axisList = ['vertical', 'horizontal']
const dirProps = {
  vertical: { offset: 'offsetY', scroll: 'scrollTop', dir: 'down', dist: 'y' },
  horizontal: {
    offset: 'offsetX',
    scroll: 'scrollLeft',
    dir: 'right',
    dist: 'x'
  }
}
const panOpts = {
  prevent: true,
  mouse: true,
  mouseAllDir: true
}

const getMinThumbSize = size => (size >= 250 ? 50 : Math.ceil(size / 5))

function isValidAxis(axis, methodName) {
  if (axisList.includes(axis)) return true

  console.error(
    `[QScrollArea]: wrong first param of ${methodName} (vertical/horizontal)`
  )
  return false
}

export default /*#__PURE__*/ createComponent({
  name: 'QScrollArea',

  props: {
    ...useDarkProps,

    thumbStyle: Object,
    verticalThumbStyle: Object,
    horizontalThumbStyle: Object,

    barStyle: [Array, String, Object],
    verticalBarStyle: [Array, String, Object],
    horizontalBarStyle: [Array, String, Object],

    verticalOffset: {
      type: Array,
      default: () => [0, 0]
    },
    horizontalOffset: {
      type: Array,
      default: () => [0, 0]
    },

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

    delay: {
      type: [String, Number],
      default: 1000
    },

    visible: {
      type: Boolean,
      default: null
    },

    tabindex: [String, Number],

    onScroll: Function
  },

  setup(props, { slots, emit }) {
    // state management
    const tempShowing = ref(false)
    const panning = ref(false)
    const hover = ref(false)

    // other...
    const container = {
      vertical: ref(0),
      horizontal: ref(0)
    }

    const scroll = {
      vertical: {
        ref: ref(null),
        position: ref(0),
        size: ref(0)
      },

      horizontal: {
        ref: ref(null),
        position: ref(0),
        size: ref(0)
      }
    }

    const { proxy } = getCurrentInstance()
    const $q = useQuasar()

    const isDark = useDark(props, $q)

    let timer = null,
      panRefPos

    const targetRef = ref(null)

    const classes = computed(
      () => 'q-scrollarea' + (isDark() ? ' q-scrollarea--dark' : '')
    )

    Object.assign(container, {
      verticalInner: computed(
        () =>
          container.vertical.value -
          props.verticalOffset[0] -
          props.verticalOffset[1]
      ),

      horizontalInner: computed(
        () =>
          container.horizontal.value -
          props.horizontalOffset[0] -
          props.horizontalOffset[1]
      )
    })

    scroll.vertical.percentage = computed(() => {
      const diff = scroll.vertical.size.value - container.vertical.value
      if (diff <= 0) {
        return 0
      }
      const p = between(scroll.vertical.position.value / diff, 0, 1)
      return Math.round(p * 10_000) / 10_000
    })
    scroll.vertical.thumbHidden = computed(
      () =>
        (!(props.visible === null ? hover.value : props.visible) &&
          !tempShowing.value &&
          !panning.value) ||
        scroll.vertical.size.value <= container.vertical.value + 1
    )
    scroll.vertical.thumbStart = computed(
      () =>
        props.verticalOffset[0] +
        scroll.vertical.percentage.value *
          (container.verticalInner.value - scroll.vertical.thumbSize.value)
    )
    scroll.vertical.thumbSize = computed(() =>
      Math.round(
        between(
          (container.verticalInner.value * container.verticalInner.value) /
            scroll.vertical.size.value,
          getMinThumbSize(container.verticalInner.value),
          container.verticalInner.value
        )
      )
    )
    scroll.vertical.style = computed(() => ({
      ...props.thumbStyle,
      ...props.verticalThumbStyle,
      top: `${scroll.vertical.thumbStart.value}px`,
      height: `${scroll.vertical.thumbSize.value}px`,
      right: `${props.horizontalOffset[1]}px`
    }))
    scroll.vertical.thumbClass = computed(
      () =>
        'q-scrollarea__thumb q-scrollarea__thumb--v absolute-right' +
        (scroll.vertical.thumbHidden.value
          ? ' q-scrollarea__thumb--invisible'
          : '')
    )
    scroll.vertical.barClass = computed(
      () =>
        'q-scrollarea__bar q-scrollarea__bar--v absolute-right' +
        (scroll.vertical.thumbHidden.value
          ? ' q-scrollarea__bar--invisible'
          : '')
    )

    scroll.horizontal.percentage = computed(() => {
      const diff = scroll.horizontal.size.value - container.horizontal.value
      if (diff <= 0) {
        return 0
      }
      const p = between(
        getHorizontalLogicalPosition(scroll.horizontal.position.value) / diff,
        0,
        1
      )
      return Math.round(p * 10_000) / 10_000
    })
    scroll.horizontal.thumbHidden = computed(
      () =>
        (!(props.visible === null ? hover.value : props.visible) &&
          !tempShowing.value &&
          !panning.value) ||
        scroll.horizontal.size.value <= container.horizontal.value + 1
    )
    scroll.horizontal.thumbStart = computed(
      () =>
        props.horizontalOffset[$q.lang.rtl ? 1 : 0] +
        scroll.horizontal.percentage.value *
          (container.horizontalInner.value - scroll.horizontal.thumbSize.value)
    )
    scroll.horizontal.thumbSize = computed(() =>
      Math.round(
        between(
          (container.horizontalInner.value * container.horizontalInner.value) /
            scroll.horizontal.size.value,
          getMinThumbSize(container.horizontalInner.value),
          container.horizontalInner.value
        )
      )
    )
    scroll.horizontal.style = computed(() => ({
      ...props.thumbStyle,
      ...props.horizontalThumbStyle,
      [$q.lang.rtl ? 'right' : 'left']:
        `${scroll.horizontal.thumbStart.value}px`,
      width: `${scroll.horizontal.thumbSize.value}px`,
      bottom: `${props.verticalOffset[1]}px`
    }))
    scroll.horizontal.thumbClass = computed(
      () =>
        'q-scrollarea__thumb q-scrollarea__thumb--h absolute-bottom' +
        (scroll.horizontal.thumbHidden.value
          ? ' q-scrollarea__thumb--invisible'
          : '')
    )
    scroll.horizontal.barClass = computed(
      () =>
        'q-scrollarea__bar q-scrollarea__bar--h absolute-bottom' +
        (scroll.horizontal.thumbHidden.value
          ? ' q-scrollarea__bar--invisible'
          : '')
    )

    // a scrollable region must be keyboard-operable (WCAG 2.1.1), but a tab
    // stop on a region with nothing to scroll is only noise. The render also
    // re-runs on hover (mainStyle -> thumbHidden), so this stays a computed:
    // the overflow state only ever changes when one of the sizes does.
    const tabindex = computed(() =>
      props.tabindex !== void 0
        ? props.tabindex
        : scroll.vertical.size.value > container.vertical.value + 1 ||
            scroll.horizontal.size.value > container.horizontal.value + 1
          ? 0
          : void 0
    )

    const mainStyle = computed(() =>
      scroll.vertical.thumbHidden.value && scroll.horizontal.thumbHidden.value
        ? props.contentStyle
        : props.contentActiveStyle
    )

    // spelled out (not built by concatenating the axis) on two counts: these
    // 10 keys are public API, so they should be greppable in the component
    // that produces them; and a static-key literal is one allocation with a
    // cached shape, against 10 string internalizations per scroll event
    function getScroll() {
      return {
        verticalPosition: scroll.vertical.position.value,
        verticalPercentage: scroll.vertical.percentage.value,
        verticalSize: scroll.vertical.size.value,
        verticalContainerSize: container.vertical.value,
        verticalContainerInnerSize: container.verticalInner.value,

        horizontalPosition: scroll.horizontal.position.value,
        horizontalPercentage: scroll.horizontal.percentage.value,
        horizontalSize: scroll.horizontal.size.value,
        horizontalContainerSize: container.horizontal.value,
        horizontalContainerInnerSize: container.horizontalInner.value
      }
    }

    // we have lots of listeners, so
    // ensure we're not emitting same info
    // multiple times
    const emitScroll = debounce(() => {
      const info = getScroll()
      info.ref = proxy
      emit('scroll', info)
    }, 0)

    function localSetScrollPosition(axis, offset, duration) {
      if (!isValidAxis(axis, 'setScrollPosition')) return

      const fn =
        axis === 'vertical'
          ? setVerticalScrollPosition
          : setHorizontalScrollPosition

      fn(targetRef.value, offset, duration)
    }

    function updateContainer({ height, width }) {
      let change = false

      if (container.vertical.value !== height) {
        container.vertical.value = height
        change = true
      }

      if (container.horizontal.value !== width) {
        container.horizontal.value = width
        change = true
      }

      if (change) startTimer()
    }

    function updateScroll({ position }) {
      let change = false

      if (scroll.vertical.position.value !== position.top) {
        scroll.vertical.position.value = position.top
        change = true
      }

      if (scroll.horizontal.position.value !== position.left) {
        scroll.horizontal.position.value = position.left
        change = true
      }

      if (change) startTimer()
    }

    function updateScrollSize({ height, width }) {
      if (scroll.horizontal.size.value !== width) {
        scroll.horizontal.size.value = width
        startTimer()
      }

      if (scroll.vertical.size.value !== height) {
        scroll.vertical.size.value = height
        startTimer()
      }
    }

    function onPanThumb(e, axis) {
      const data = scroll[axis]

      if (e.isFirst) {
        if (data.thumbHidden.value) return

        panRefPos =
          axis === 'horizontal'
            ? getHorizontalLogicalPosition(data.position.value)
            : data.position.value
        panning.value = true
      } else if (!panning.value) {
        return
      }

      if (e.isFinal) panning.value = false

      const dProp = dirProps[axis]

      const multiplier =
        (data.size.value - container[axis].value) /
        (container[axis + 'Inner'].value - data.thumbSize.value)
      const distance = e.distance[dProp.dist]
      const direction =
        (e.direction === dProp.dir ? 1 : -1) *
        (axis === 'horizontal' && $q.lang.rtl ? -1 : 1)
      const pos = panRefPos + direction * distance * multiplier

      setScroll(
        axis === 'horizontal' ? getHorizontalNativePosition(pos) : pos,
        axis
      )
    }

    function onMousedown(evt, axis) {
      const data = scroll[axis]

      if (!data.thumbHidden.value) {
        const isHorizontalRtl = axis === 'horizontal' && $q.lang.rtl
        const startOffset =
          axis === 'vertical'
            ? props.verticalOffset[0]
            : props.horizontalOffset[isHorizontalRtl ? 1 : 0]

        const pointerOffset = isHorizontalRtl
          ? container.horizontal.value - evt.offsetX
          : evt[dirProps[axis].offset]
        const offset = pointerOffset - startOffset
        const thumbStart = data.thumbStart.value - startOffset

        if (offset < thumbStart || offset > thumbStart + data.thumbSize.value) {
          const targetThumbStart = offset - data.thumbSize.value / 2
          const percentage = between(
            targetThumbStart /
              (container[axis + 'Inner'].value - data.thumbSize.value),
            0,
            1
          )
          setScroll(
            isHorizontalRtl
              ? getHorizontalNativePosition(
                  percentage *
                    Math.max(0, data.size.value - container[axis].value)
                )
              : percentage *
                  Math.max(0, data.size.value - container[axis].value),
            axis
          )
        }

        // activate thumb pan
        if (data.ref.value !== null) {
          data.ref.value.dispatchEvent(new MouseEvent(evt.type, evt))
        }
      }
    }

    function startTimer() {
      tempShowing.value = true

      if (timer !== null) clearTimeout(timer)
      timer = setTimeout(() => {
        timer = null
        tempShowing.value = false
      }, props.delay)

      if (props.onScroll !== void 0) emitScroll()
    }

    function setScroll(offset, axis) {
      targetRef.value[dirProps[axis].scroll] = offset
    }

    function getHorizontalLogicalPosition(position, rtl = $q.lang.rtl) {
      if (!rtl) return position

      return rtlHasScrollBug
        ? Math.max(
            0,
            scroll.horizontal.size.value - container.horizontal.value
          ) - position
        : -position
    }

    function getHorizontalNativePosition(position, rtl = $q.lang.rtl) {
      if (!rtl) return position

      return rtlHasScrollBug
        ? Math.max(
            0,
            scroll.horizontal.size.value - container.horizontal.value
          ) - position
        : -position
    }

    let mouseEventTimer = null

    function onMouseenter() {
      if (mouseEventTimer !== null) {
        clearTimeout(mouseEventTimer)
      }

      // setTimeout needed for iOS; see ticket #16210
      mouseEventTimer = setTimeout(
        () => {
          mouseEventTimer = null
          hover.value = true
        },
        $q.platform.is.ios ? 50 : 0
      )
    }

    function onMouseleave() {
      if (mouseEventTimer !== null) {
        clearTimeout(mouseEventTimer)
        mouseEventTimer = null
      }

      hover.value = false
    }

    let scrollPosition = null

    watch(
      () => $q.lang.rtl,
      (rtl, oldRtl) => {
        if (targetRef.value !== null) {
          setHorizontalScrollPosition(
            targetRef.value,
            getHorizontalNativePosition(
              getHorizontalLogicalPosition(
                scroll.horizontal.position.value,
                oldRtl
              ),
              rtl
            )
          )
        }
      }
    )

    onDeactivated(() => {
      scrollPosition = {
        top: scroll.vertical.position.value,
        left: scroll.horizontal.position.value
      }
    })

    onActivated(() => {
      if (scrollPosition === null) return

      const scrollTarget = targetRef.value

      if (scrollTarget !== null) {
        setHorizontalScrollPosition(scrollTarget, scrollPosition.left)
        setVerticalScrollPosition(scrollTarget, scrollPosition.top)
      }
    })

    onBeforeUnmount(emitScroll.cancel)

    // expose public methods
    Object.assign(proxy, {
      getScrollTarget: () => targetRef.value,
      getScroll,
      getScrollPosition: () => ({
        top: scroll.vertical.position.value,
        left: scroll.horizontal.position.value
      }),
      getScrollPercentage: () => ({
        top: scroll.vertical.percentage.value,
        left: scroll.horizontal.percentage.value
      }),
      setScrollPosition: localSetScrollPosition,
      setScrollPercentage(axis, percentage, duration) {
        if (!isValidAxis(axis, 'setScrollPercentage')) return

        const offset =
          percentage * (scroll[axis].size.value - container[axis].value)

        localSetScrollPosition(
          axis,
          axis === 'horizontal' ? getHorizontalNativePosition(offset) : offset,
          duration
        )
      }
    })

    const store = {
      scroll,

      thumbVertDir: [
        [
          TouchPan,
          e => {
            onPanThumb(e, 'vertical')
          },
          void 0,
          { vertical: true, ...panOpts }
        ]
      ],

      thumbHorizDir: [
        [
          TouchPan,
          e => {
            onPanThumb(e, 'horizontal')
          },
          void 0,
          { horizontal: true, ...panOpts }
        ]
      ],

      onVerticalMousedown(evt) {
        onMousedown(evt, 'vertical')
      },

      onHorizontalMousedown(evt) {
        onMousedown(evt, 'horizontal')
      }
    }

    return () =>
      h(
        'div',
        {
          class: classes.value,
          onMouseenter,
          onMouseleave
        },
        [
          h(
            'div',
            {
              ref: targetRef,
              class:
                'q-scrollarea__container scroll relative-position fit hide-scrollbar',
              tabindex: tabindex.value
            },
            [
              h(
                'div',
                {
                  class: 'q-scrollarea__content absolute',
                  style: mainStyle.value
                },
                hMergeSlot(slots.default, [
                  h(QResizeObserver, {
                    debounce: 0,
                    onResize: updateScrollSize
                  })
                ])
              ),

              h(QScrollObserver, {
                axis: 'both',
                onScroll: updateScroll
              })
            ]
          ),

          h(QResizeObserver, {
            debounce: 0,
            onResize: updateContainer
          }),

          h(ScrollAreaControls, {
            store,
            barStyle: props.barStyle,
            verticalBarStyle: props.verticalBarStyle,
            horizontalBarStyle: props.horizontalBarStyle
          })
        ]
      )
  }
})
