<script setup lang="ts">
import {
  VNode,
  DirectiveBinding,
  ref,
  computed
} from 'vue'
import clamp from 'lodash/clamp'

import type { ButtonVariant } from 'bootstrap-vue-next'
import AppIcon from '@/components/App/AppIcon.vue'
import { useRangeControl } from '@/composables/useRangeControl'
import IPhCaretLeftBold from '~icons/ph/caret-left-bold'
import IPhCaretRightBold from '~icons/ph/caret-right-bold'
import type { Component } from 'vue'

interface DragDropValue { detail: number }

defineOptions({
  name: 'RangePicker'
})

const vDraggable = {
  mounted(el: HTMLElement, binding: DirectiveBinding, vnode: VNode): void {
    let startX: number, initialClientX: number
    const relative = binding.modifiers?.relative ?? false

    // Emit an event to the parent component
    function emitEvent({
      name,
      data = null
    }: {
      name: string
      data?: any
    }) {
      vnode.el?.dispatchEvent(new CustomEvent(name, { detail: data }))
    }

    // Handle the dragging of the element
    function move(event: MouseEvent | TouchEvent) {
      const clientX
        = event instanceof MouseEvent
          ? event.clientX
          : event.touches[0].clientX
      const offset = relative ? el.offsetWidth : 0
      // binding.instance is the directive's host component (this one, via
      // defineExpose({ rangeWidth })); narrow to just what we call on it.
      const maxX = ((binding.instance as { rangeWidth: () => number } | null)?.rangeWidth() ?? 0) - offset
      const data = clamp(startX + clientX - initialClientX, 0, maxX)
      emitEvent({ name: 'dragged', data })
      return false
    }

    // Clean up listeners once the dragging ends
    function end(event: MouseEvent | TouchEvent) {
      emitEvent({ name: 'ended' })
      if (event instanceof MouseEvent) {
        document.removeEventListener('mousemove', move)
        document.removeEventListener('mouseup', end)
      }
      else {
        document.removeEventListener('touchmove', move)
        document.removeEventListener('touchend', end)
      }
    }

    // Register listeners when dragging start
    function start(event: MouseEvent | TouchEvent) {
      emitEvent({ name: 'started' })
      startX = el.offsetLeft
      if (event instanceof MouseEvent) {
        initialClientX = event.clientX
        document.addEventListener('mousemove', move)
        document.addEventListener('mouseup', end)
      }
      else {
        initialClientX = event.touches[0].clientX
        document.addEventListener('touchmove', move)
        document.addEventListener('touchend', end)
      }
      return false
    }

    // Register the drag and touch event handlers
    el.addEventListener('mousedown', start)
    el.addEventListener('touchstart', start)
  }
}

export interface FormControlRangeProps {
  /**
   * Initial values of the range bounds. Should contain two numbers.
   * indicating the start and end of the range.
   */
  range: [number, number]
  /**
   * Enables hover styling on rows.
   */
  hover?: boolean
  /**
   * Offset from the left side of the component
   * where the dragging for the start value begins.
   */
  startOffset?: number | string
  /**
   * Offset from the right side of the component where
   * the dragging for the end value ends.
   */
  endOffset?: number | string
  /**
   * Number of decimal places to which values should be rounded.
   */
  precision?: number
  /**
   * Snap increment value. For instance,
   * if snap is 0.1, values will snap to 0, 0.1, 0.2, and so on.
   */
  snap?: number
  /**
   * Minimum distance between the two range bounds to ensure they
   * don't get too close to each other.
   */
  minDistance?: number
  /**
   * Variant style of the component. Expected to be one
   * of the predefined Bootstrap theme (e.g., 'primary', 'secondary', etc.).
   */
  variant?: ButtonVariant
  /**
   * Rounds corner edges of the range boundaries. If
   * true, the component will have rounded corners.
   */
  rounded?: boolean
  boundStartIcon?: string | Component
  boundEndIcon?: string | Component
}

const props = withDefaults(defineProps<FormControlRangeProps>(), {
  hover: false,
  startOffset: 0,
  endOffset: 0,
  precision: 4,
  snap: 0.0001,
  minDistance: 0.01,
  variant: 'primary',
  rounded: false,
  boundStartIcon: () => IPhCaretLeftBold,
  boundEndIcon: () => IPhCaretRightBold
})

const emit = defineEmits<{
  'update:range': [[number, number]]
}>()

const rangePickerBounds = ref<HTMLElement | null>(null)
const moving = ref(false)
const resizing = ref(false)

// Bounds math (snapping, rounding, minimum-distance constraint) lives in a
// dedicated composable; the component keeps the DOM and emit wiring.
const { start, end, disabled, moveStartTo, moveEndTo, moveBoundsTo }
  = useRangeControl({
    range: () => props.range,
    snap: () => props.snap,
    precision: () => props.precision,
    minDistance: () => props.minDistance
  })

const overlayStyle = computed((): { left: string, right: string } => {
  return {
    left: `${start.value * 100}%`,
    right: `${(1 - end.value) * 100}%`
  }
})

const boundsStyle = computed((): { left: string, right: string } => {
  return {
    left: startOffsetWithUnit.value,
    right: endOffsetWithUnit.value
  }
})

const startOffsetWithUnit = computed((): string => {
  return valueWithUnit(props.startOffset)
})

const endOffsetWithUnit = computed((): string => {
  return valueWithUnit(props.endOffset)
})

const startBoundStyle = computed((): { left: string } => {
  return { left: `${start.value * 100}% ` }
})

const endBoundStyle = computed((): { left: string } => {
  return { left: `${end.value * 100}%` }
})

const classList = computed((): Record<string, boolean> => {
  return {
    [`range-picker--${props.variant}`]: !!props.variant,
    'range-picker--hover': props.hover,
    'range-picker--disabled': disabled.value,
    'range-picker--rounded': props.rounded,
    'range-picker--resizing': resizing.value,
    'range-picker--moving': moving.value
  }
})

function toggleMoving(value: boolean) {
  moving.value = value ?? !moving.value
}

function toggleResizing(value: boolean) {
  resizing.value = value ?? !resizing.value
}

function rangeWidth(): number {
  return rangePickerBounds.value?.getBoundingClientRect().width ?? 0
}

// Emit the current bounds as the canonical range update.
function emitRangeUpdate() {
  /**
   * Update the values of the range (both start and end)
   * @event update
   * @param Number[] New value of the range
   */
  emit('update:range', [start.value, end.value])
}

function dragStartBound({ detail: dx }: DragDropValue) {
  if (moveStartTo(dx, rangeWidth())) {
    emitRangeUpdate()
  }
}

function dragEndBound({ detail: dx }: DragDropValue) {
  if (moveEndTo(dx, rangeWidth())) {
    emitRangeUpdate()
  }
}

function dragBounds({ detail: dx }: DragDropValue) {
  moveBoundsTo(dx, rangeWidth())
  emitRangeUpdate()
}

function valueWithUnit(value: number | string): string {
  return typeof value === 'number' ? `${value}px` : `${value}`
}

defineExpose({
  rangeWidth
})
</script>

<template>
  <div
    class="range-picker"
    :class="classList"
  >
    <div class="range-picker__wrapper">
      <slot />
    </div>
    <div
      v-show="!disabled"
      ref="rangePickerBounds"
      class="range-picker__bounds"
      :style="boundsStyle"
    >
      <div
        v-draggable.relative
        class="range-picker__bounds__overlay"
        :style="overlayStyle"
        @dragged="dragBounds"
        @started="toggleMoving(true)"
        @ended="toggleMoving(false)"
      />
      <button
        v-draggable
        :style="startBoundStyle"
        class="range-picker__bounds__start btn"
        @dragged="dragStartBound"
        @started="toggleResizing(true)"
        @ended="toggleResizing(false)"
      >
        <app-icon>
          <component :is="boundStartIcon" />
        </app-icon>
      </button>
      <button
        v-draggable
        class="range-picker__bounds__end btn"
        :style="endBoundStyle"
        @dragged="dragEndBound"
        @started="toggleResizing(true)"
        @ended="toggleResizing(false)"
      >
        <app-icon>
          <component :is="boundEndIcon" />
        </app-icon>
      </button>
    </div>
  </div>
</template>

<style scoped lang="scss">

.range-picker {
  min-height: 1rem;
  position: relative;

  --bg: #{$component-active-bg};
  --fg: #{$component-active-color};

  @each $color, $value in $theme-colors {
    &--#{$color} {
      --bg: var(--#{$color}, #{$value});
      --fg: #{color-contrast($value)};
    }
  }

  &--rounded {
    border-radius: $border-radius;
  }

  &--hover:hover &__bounds:after {
    pointer-events: none;
    content: '';
    z-index: -1;
    width: 100%;
    height: 100%;
    display: block;
    background: var(--bg);
    opacity: 0.1;
    border-radius: inherit;
  }

  &--moving &__wrapper,
  &--resizing &__wrapper {
    &,
    * {
      pointer-events: none;
    }
  }

  &--moving,
  &__bounds__overlay,
  &--moving &__bounds__start.btn:not(:disabled):not(.disabled),
  &--moving &__bounds__end.btn:not(:disabled):not(.disabled) {
    cursor: move;
  }

  &--resizing,
  &--resizing &__bounds__overlay,
  &__bounds__start.btn:not(:disabled):not(.disabled),
  &__bounds__end.btn:not(:disabled):not(.disabled) {
    cursor: col-resize;
  }

  &__bounds {
    pointer-events: none;
    position: absolute;
    top: 0;
    height: 100%;
    border-radius: inherit;

    &__overlay {
      pointer-events: all;
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      display: flex;
      border-radius: inherit;
      border: 1px solid var(--bg);
      overflow: hidden;

      &:after {
        content: '';
        width: 100%;
        height: 100%;
        display: block;
        background: var(--bg);
        opacity: 0.3;
      }
    }

    &__start,
    &__end {
      pointer-events: all;
      position: absolute;
      top: 50%;
      transform: translate(-50%, -50%);
      border-radius: 50%;
      font-size: 0.6rem;
      width: 1.2rem;
      height: 1.2rem;
      line-height: 1.2rem;
      padding: 0;
      background: var(--bg, transparent);
      color: var(--fg);
      transform: translate(-50%, -50%);
      display: inline-flex;
      align-items: center;
      justify-content: center;

      &:hover,
      &:active,
      &:focus {
        color: var(--fg);
      }

      &:active, &:focus {
        background: var(--bg, transparent);
      }
    }

    &__start {
      left: 0;
    }

    &__end {
      left: 100%;
    }
  }
}
</style>
