import './VTimePickerClock.sass'

// Mixins
import Colorable from '../../mixins/colorable'
import Themeable from '../../mixins/themeable'

// Types
import mixins, { ExtractVue } from '../../util/mixins'
import Vue, { VNode, PropType, VNodeData } from 'vue'
import { PropValidator } from 'vue/types/options'

interface Point {
  x: number
  y: number
}

interface options extends Vue {
  $refs: {
    clock: HTMLElement
    innerClock: HTMLElement
  }
}

export default mixins<options &
/* eslint-disable indent */
  ExtractVue<[
    typeof Colorable,
    typeof Themeable
  ]>
/* eslint-enable indent */
>(
  Colorable,
  Themeable
/* @vue/component */
).extend({
  name: 'v-time-picker-clock',

  props: {
    allowedValues: Function as PropType<(value: number) => boolean>,
    ampm: Boolean,
    disabled: Boolean,
    double: Boolean,
    format: {
      type: Function,
      default: (val: string | number) => val,
    } as PropValidator<(val: string | number) => string | number>,
    max: {
      type: Number,
      required: true,
    },
    min: {
      type: Number,
      required: true,
    },
    scrollable: Boolean,
    readonly: Boolean,
    rotate: {
      type: Number,
      default: 0,
    },
    step: {
      type: Number,
      default: 1,
    },
    value: Number,
  },

  data () {
    return {
      inputValue: this.value,
      isDragging: false,
      valueOnMouseDown: null as number | null,
      valueOnMouseUp: null as number | null,
    }
  },

  computed: {
    count (): number {
      return this.max - this.min + 1
    },
    degreesPerUnit (): number {
      return 360 / this.roundCount
    },
    degrees (): number {
      return this.degreesPerUnit * Math.PI / 180
    },
    displayedValue (): number {
      return this.value == null ? this.min : this.value
    },
    innerRadiusScale (): number {
      return 0.62
    },
    roundCount (): number {
      return this.double ? (this.count / 2) : this.count
    },
  },

  watch: {
    value (value) {
      this.inputValue = value
    },
  },

  methods: {
    wheel (e: WheelEvent) {
      e.preventDefault()

      const delta = Math.sign(-e.deltaY || 1)
      let value = this.displayedValue
      do {
        value = value + delta
        value = (value - this.min + this.count) % this.count + this.min
      } while (!this.isAllowed(value) && value !== this.displayedValue)

      if (value !== this.displayedValue) {
        this.update(value)
      }
    },
    isInner (value: number) {
      return this.double && (value - this.min >= this.roundCount)
    },
    handScale (value: number) {
      return this.isInner(value) ? this.innerRadiusScale : 1
    },
    isAllowed (value: number) {
      return !this.allowedValues || this.allowedValues(value)
    },
    genValues () {
      const children: VNode[] = []

      for (let value = this.min; value <= this.max; value = value + this.step) {
        const color = value === this.value && (this.color || 'accent')
        children.push(this.$createElement('span', this.setBackgroundColor(color, {
          staticClass: 'v-time-picker-clock__item',
          class: {
            'v-time-picker-clock__item--active': value === this.displayedValue,
            'v-time-picker-clock__item--disabled': this.disabled || !this.isAllowed(value),
          },
          style: this.getTransform(value),
          domProps: { innerHTML: `<span>${this.format(value)}</span>` },
        })))
      }

      return children
    },
    genHand () {
      const scale = `scaleY(${this.handScale(this.displayedValue)})`
      const angle = this.rotate + this.degreesPerUnit * (this.displayedValue - this.min)
      const color = (this.value != null) && (this.color || 'accent')
      return this.$createElement('div', this.setBackgroundColor(color, {
        staticClass: 'v-time-picker-clock__hand',
        class: {
          'v-time-picker-clock__hand--inner': this.isInner(this.value),
        },
        style: {
          transform: `rotate(${angle}deg) ${scale}`,
        },
      }))
    },
    getTransform (i: number) {
      const { x, y } = this.getPosition(i)
      return {
        left: `${50 + x * 50}%`,
        top: `${50 + y * 50}%`,
      }
    },
    getPosition (value: number) {
      const rotateRadians = this.rotate * Math.PI / 180
      return {
        x: Math.sin((value - this.min) * this.degrees + rotateRadians) * this.handScale(value),
        y: -Math.cos((value - this.min) * this.degrees + rotateRadians) * this.handScale(value),
      }
    },
    onMouseDown (e: MouseEvent | TouchEvent) {
      e.preventDefault()

      this.valueOnMouseDown = null
      this.valueOnMouseUp = null
      this.isDragging = true
      this.onDragMove(e)
    },
    onMouseUp (e: MouseEvent | TouchEvent) {
      e.stopPropagation()

      this.isDragging = false
      if (this.valueOnMouseUp !== null && this.isAllowed(this.valueOnMouseUp)) {
        this.$emit('change', this.valueOnMouseUp)
      }
    },
    onDragMove (e: MouseEvent | TouchEvent) {
      e.preventDefault()
      if (!this.isDragging && e.type !== 'click') return

      const { width, top, left } = this.$refs.clock.getBoundingClientRect()
      const { width: innerWidth } = this.$refs.innerClock.getBoundingClientRect()
      const { clientX, clientY } = 'touches' in e ? e.touches[0] : e
      const center = { x: width / 2, y: -width / 2 }
      const coords = { x: clientX - left, y: top - clientY }
      const handAngle = Math.round(this.angle(center, coords) - this.rotate + 360) % 360
      const insideClick = this.double && this.euclidean(center, coords) < (innerWidth + innerWidth * this.innerRadiusScale) / 4
      const checksCount = Math.ceil(15 / this.degreesPerUnit)
      let value

      for (let i = 0; i < checksCount; i++) {
        value = this.angleToValue(handAngle + i * this.degreesPerUnit, insideClick)
        if (this.isAllowed(value)) return this.setMouseDownValue(value)

        value = this.angleToValue(handAngle - i * this.degreesPerUnit, insideClick)
        if (this.isAllowed(value)) return this.setMouseDownValue(value)
      }
    },
    angleToValue (angle: number, insideClick: boolean): number {
      const value = (
        Math.round(angle / this.degreesPerUnit) +
        (insideClick ? this.roundCount : 0)
      ) % this.count + this.min

      // Necessary to fix edge case when selecting left part of the value(s) at 12 o'clock
      if (angle < (360 - this.degreesPerUnit / 2)) return value

      return insideClick ? this.max - this.roundCount + 1 : this.min
    },
    setMouseDownValue (value: number) {
      if (this.valueOnMouseDown === null) {
        this.valueOnMouseDown = value
      }

      this.valueOnMouseUp = value
      this.update(value)
    },
    update (value: number) {
      if (this.inputValue !== value) {
        this.inputValue = value
        this.$emit('input', value)
      }
    },
    euclidean (p0: Point, p1: Point) {
      const dx = p1.x - p0.x
      const dy = p1.y - p0.y

      return Math.sqrt(dx * dx + dy * dy)
    },
    angle (center: Point, p1: Point) {
      const value = 2 * Math.atan2(p1.y - center.y - this.euclidean(center, p1), p1.x - center.x)
      return Math.abs(value * 180 / Math.PI)
    },
  },

  render (h): VNode {
    const data: VNodeData = {
      staticClass: 'v-time-picker-clock',
      class: {
        'v-time-picker-clock--indeterminate': this.value == null,
        ...this.themeClasses,
      },
      on: (this.readonly || this.disabled) ? undefined : {
        mousedown: this.onMouseDown,
        mouseup: this.onMouseUp,
        mouseleave: (e: MouseEvent) => (this.isDragging && this.onMouseUp(e)),
        touchstart: this.onMouseDown,
        touchend: this.onMouseUp,
        mousemove: this.onDragMove,
        touchmove: this.onDragMove,
      },
      ref: 'clock',
    }

    if (this.scrollable && data.on) {
      data.on.wheel = this.wheel
    }

    return h('div', data, [
      h('div', {
        staticClass: 'v-time-picker-clock__inner',
        ref: 'innerClock',
      }, [
        this.genHand(),
        this.genValues(),
      ]),
    ])
  },
})
