<template>
  <div class="pitcher-numpad-input" v-bind="containerAttrs">
    <!-- Input -->
    <div @click="focus(true)">
      <VTextField
        ref="input"
        :value="localValue"
        v-bind="[textFieldDefaults, parentAttrs]"
        class="number-input"
        v-on="parentListeners"
        @keypress.prevent
        @keydown.prevent="handleKeydown"
      >
        <!-- Dynamically inherit slots from parent. -->
        <!-- Here we could use #[slot]="slotProps" but it then does not inherit slots without slot props -->
        <template v-for="slot in parentSlots" #[slot]>
          <slot :name="slot" />
        </template>
      </VTextField>
    </div>

    <!-- Numpad -->
    <VSheet
      v-if="numpadIsVisible"
      ref="numpad"
      v-click-outside="determineOuterClick"
      class="numpad"
      elevation="5"
      outlined
    >
      <div class="numpad__keys">
        <div class="numpad__keys--row">
          <VBtn v-bind="numpadAttrs.button" class="row--key rounded-tl-lg" @click="addVal(1)">1</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(2)">2</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(3)">3</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key rounded-tr-lg" @click="reset">
            <VIcon v-bind="numpadAttrs.icon" class="row--key">fal fa-times</VIcon>
          </VBtn>
        </div>
        <div class="numpad__keys--row">
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(4)">4</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(5)">5</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(6)">6</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="backspace">
            <VIcon v-bind="numpadAttrs.icon">fal fa-backspace</VIcon>
          </VBtn>
        </div>
        <div class="numpad__keys--row">
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(7)">7</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(8)">8</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(9)">9</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="jumpNextSibling">
            <VIcon v-bind="numpadAttrs.icon">fal fa-chevron-right</VIcon>
          </VBtn>
        </div>
        <div class="numpad__keys--row">
          <VBtn v-bind="numpadAttrs.button" class="row--key rounded-bl-lg" @click="incDec('inc')">
            <VIcon v-bind="numpadAttrs.icon">fal fa-plus</VIcon>
          </VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="addVal(0)">0</VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key" @click="incDec('dec')">
            <VIcon v-bind="numpadAttrs.icon">fal fa-minus</VIcon>
          </VBtn>
          <VBtn v-bind="numpadAttrs.button" class="row--key rounded-br-lg" @click="jumpNextGroup">
            <VIcon v-bind="numpadAttrs.icon" class="row--key">fal fa-chevron-down</VIcon>
          </VBtn>
        </div>
      </div>
    </VSheet>
  </div>
</template>

<script>
import { computed, defineComponent, onMounted, onUnmounted, reactive, toRefs, watch } from '@vue/composition-api'

// Px & Percentage parser
const parsePxStyle = (val) => {
  return val.toString().includes('%') || val.toString().includes('px') ? val : `${parseInt(val)}px`
}

// Custom event with polyfill
const customEvent = (name) => {
  // Modern browsers
  if (typeof window.CustomEvent === 'function') return new CustomEvent(name)
  // IE11
  const customEvent = document.createEvent('CustomEvent')

  customEvent.initEvent(name, true, true)

  return customEvent
}
/**
 * Pitcher Numpad Input component
 * @inherits v-text-field
 */

export default defineComponent({
  name: 'PNumpadInput',
  inheritAttrs: false,
  props: {
    value: {
      type: [String, Number],
    },
    max: {
      type: Number,
    },
    group: {
      type: String,
      default: 'no-group',
    },
    initialValue: {
      type: [String, Number],
    },
    resetBefore: {
      type: Boolean,
      default: true,
    },
    decimals: {
      type: Number,
      default: 2,
      validator: (val) => {
        if (val > 5) {
          console.error("[Vue warn]: The value of decimals can't be higher than 5!")

          return false
        } else if (val < 0) {
          console.error("[Vue warn]: The value of decimals can't be lower than 0!")

          return false
        }

        return true
      },
    },
    textAlignRight: {
      type: Boolean,
    },
    lazy: Boolean,
    disableKeyboard: {
      type: Boolean,
      default: false,
    },
    minWidth: {
      type: [Number, String],
      default: 50,
    },
    maxWidth: {
      type: [Number, String],
      default: '100%',
    },
    allowNegative: {
      type: Boolean,
      default: false,
    },
  },
  emits: ['input'],
  setup(props, ctx) {
    const localState = reactive({
      localValue: props.value,
      numpadIsVisible: false,
      shouldReset: false,
    })

    // Container attrs
    const containerAttrs = computed(() => ({
      'data-group': props.group,
      class: {
        [props.group]: true,
      },
      style: {
        minWidth: parsePxStyle(props.minWidth),
        maxWidth: parsePxStyle(props.maxWidth),
      },
    }))

    // Default Numpad Attrs
    const numpadAttrs = {
      button: {
        text: true,
        tile: true,
        minWidth: 66,
        maxWidth: 66,
        height: 66,
        style: {
          fontSize: '28px',
          fontWeight: '400',
        },
      },
      icon: {
        size: 28,
      },
    }

    // Default TextField attrs
    // must be computed to keep reactivity on prop changes
    const textFieldDefaults = computed(() => ({
      // required
      readonly: true,
      // default style
      outlined: true,
      dense: true,
      hideDetails: true,
      class: {
        // active class
        'v-input--is-focused primary--text': localState.numpadIsVisible,
        // text alignment
        'text-align-right': props.textAlignRight,
      },
    }))

    // Get from parent component
    const parentAttrs = computed(() => ctx.attrs)
    const parentListeners = computed(() => ctx.listeners)
    const parentSlots = computed(() => Object.keys(ctx.slots))

    // set divider value connected to decimals
    // 10, 100, 1000 etc.
    const dividerValue = parseInt(`1${'0'.repeat(props.decimals)}`)
    // default zero value
    const defaultValue = props.decimals === 0 ? '0' : `0.${'0'.repeat(props.decimals)}`

    function calculateNewValue(oldValue, val) {
      let parsed = parseInt(oldValue.toString().replace(/\D/g, ''))

      if (oldValue < 0) parsed *= -1

      const value = parsed + val.toString()
      const masked = (value / dividerValue).toFixed(props.decimals)

      return masked
    }

    function addVal(val) {
      // if resetBefore prop is set, reset the value once to default zero value before first user input
      if (localState.shouldReset) {
        emit(defaultValue)
        localState.localValue = defaultValue
        localState.shouldReset = false
      }
      const newVal = calculateNewValue(localState.localValue, val)

      emit(newVal)
    }

    function incDec(action) {
      if (!localState.localValue) {
        emit(defaultValue)
      }
      // parse the number with decimals
      let parsedNumber = parseFloat(localState.localValue).toFixed(props.decimals)

      // should not reset if only increase/decrease will be clicked
      if (localState.shouldReset) {
        localState.shouldReset = false
      }

      if (action === 'dec' && (parsedNumber >= 1 || props.allowNegative)) {
        parsedNumber--
        emit(parsedNumber.toFixed(props.decimals))
      } else if (action === 'inc') {
        parsedNumber++
        emit(parsedNumber.toFixed(props.decimals))
      }
    }

    function backspace() {
      // parse current value as Integer & convert to string
      const parsed = parseInt(localState.localValue.toString().replace(/\D/g, '')).toString()
      // remove last char
      const value = parsed.slice(0, -1)
      // mask the value with decimals
      const masked = value / dividerValue

      if (masked > 0) {
        // emit value as fixed
        emit(masked.toFixed(props.decimals))
      } else {
        // set default value as it is now 0
        emit(defaultValue)
      }
    }

    function reset() {
      emit(defaultValue)
    }

    function focus(visibility) {
      // set visibility
      localState.numpadIsVisible = visibility

      if (visibility) {
        blurOthers()
        checkOverlap()

        // visible, if value empty set default
        if (!localState.localValue) {
          // Emit first time default initial value
          if (props.initialValue) {
            emit(props.initialValue)
          } else {
            emit(defaultValue)
          }
        }

        if (props.resetBefore && parseFloat(localState.localValue) !== 0) {
          localState.shouldReset = true
        }

        ctx.refs.input.focus()

        return
      }

      // if blur & lazy model is active
      if (props.lazy) {
        ctx.emit('input', localState.localValue)
      }

      ctx.refs.input.blur()
    }

    function jumpNextSibling() {
      // get current group from the groups in view
      const { [props.group]: group } = getNumpadGroups()

      // find input element in this component and destruct selfIndex
      const { index: selfIndex } = group.find((i) => i.input === ctx.refs.input.$el)

      const isLast = selfIndex === group.length - 1

      if (isLast) {
        // jump next group if this is the last one
        jumpNextGroup()

        return
      }

      // not last item, jump next in same group
      group[selfIndex + 1].input.dispatchEvent(customEvent('customFocus'))
    }

    function jumpNextGroup() {
      // get groups in view
      const groups = getNumpadGroups()
      // convert object model to array
      const groupsArray = Object.keys(groups).map((k) => groups[k])
      // get group index of this component
      const groupIndex = Object.keys(groups).indexOf(props.group)
      // get next group index
      const nextIndex = groupIndex === groupsArray.length - 1 ? 0 : groupIndex + 1

      groupsArray[nextIndex][0].input.dispatchEvent(customEvent('customFocus'))
    }

    function blurOthers() {
      // get groups in view
      const groups = getNumpadGroups()
      // convert object model to array
      const groupsArray = Object.keys(groups).map((k) => groups[k])
      // get all items except self
      const otherNumpads = [].concat(...groupsArray).filter((i) => i.input !== ctx.refs.input.$el)

      // remove focus from all the other items
      otherNumpads.forEach((i) => i.input.dispatchEvent(customEvent('customBlur')))
    }

    // Get numpads as groups in view
    function getNumpadGroups() {
      const inputsInView = document.querySelectorAll('.pitcher-numpad-input')
      const groups = {}

      inputsInView.forEach((elem) => {
        const groupName = elem.getAttribute('data-group')

        if (!groups[groupName]) {
          groups[groupName] = []
        }

        groups[groupName].push({
          id: `${groupName}_${groups[groupName].length}`,
          index: groups[groupName].length,
          input: elem.querySelector('.number-input'),
        })
      })

      return groups
    }

    function handleKeydown(event) {
      event.preventDefault()
      const keyCode = event.keyCode ? event.keyCode : event.which

      if (props.disableKeyboard) {
        return
      } else if (keyCode === 8) {
        // backspace
        backspace()
      } else if (keyCode === 9) {
        // tab
        jumpNextSibling()
      } else if (/\d/.test(event.key)) {
        // numbers
        addVal(parseInt(event.key))
      }

      return
    }

    function checkOverlap() {
      ctx.root.$nextTick(() => {
        if (!ctx.refs.input || !ctx.refs.numpad) return

        const inputRect = ctx.refs.input.$el.getBoundingClientRect()
        const numpadRect = ctx.refs.numpad.$el.getBoundingClientRect()
        const isColliding = !(
          inputRect.top + inputRect.height < numpadRect.top ||
          inputRect.top > numpadRect.top + numpadRect.height ||
          inputRect.left + inputRect.width < numpadRect.left ||
          inputRect.left > numpadRect.left + numpadRect.width
        )

        if (isColliding) {
          ctx.refs.numpad.$el.classList.add('top')
        }
      })
    }

    // function to emit data if not lazy
    function emit(val) {
      let newVal = val

      if (props.max && parseFloat(val) > props.max) {
        newVal = props.max.toFixed(props.decimals)
      }
      const parsed = typeof localState.localValue === 'number' ? parseFloat(newVal) : newVal

      // keep focus on input
      ctx.refs.input.focus()

      // if not lazy emit immediately
      if (!props.lazy) {
        ctx.emit('input', parsed)

        return
      }

      // lazy: keep the value inside localValue
      localState.localValue = parsed
    }

    function determineOuterClick(e) {
      if (
        e.target.tagName.includes('INPUT') ||
        e.target.className.includes('numpad') ||
        e.target.className.includes('v-text-field')
      ) {
        return
      }
      focus(false)
    }

    onMounted(() => {
      // hide numpad & blur if visible & event called
      ctx.refs.input.$el.addEventListener('customBlur', () => localState.numpadIsVisible && focus(false))
      ctx.refs.input.$el.addEventListener('customFocus', () => focus(true))
    })

    onUnmounted(() => {
      if (ctx.refs.input) {
        ctx.refs.input.$el.removeEventListener('customBlur', () => focus(false))
        ctx.refs.input.$el.removeEventListener('customFocus', () => focus(true))
      }
    })

    // Watch props.value
    watch(
      () => props.value,
      (val) => {
        localState.localValue = val
      }
    )

    return {
      ...toRefs(localState),
      parentAttrs,
      parentListeners,
      parentSlots,
      containerAttrs,
      textFieldDefaults,
      numpadAttrs,
      focus,
      addVal,
      incDec,
      backspace,
      reset,
      jumpNextSibling,
      jumpNextGroup,
      handleKeydown,
      determineOuterClick,
    }
  },
})
</script>

<style lang="scss" scoped>
.pitcher-numpad-input {
  .number-input.text-align-right ::v-deep input {
    direction: rtl;
  }

  .numpad {
    display: inline-flex;
    position: fixed;
    z-index: 10000;
    top: auto;
    left: auto;
    right: 20px;
    bottom: 20px;

    &.top {
      bottom: auto;
      top: 20px;
    }

    &__keys {
      border-color: inherit !important;

      &--row {
        border-color: inherit !important;

        &:not(:last-child) {
          border-bottom: 1px solid;
        }

        .row--key {
          border-color: inherit !important;
          &:not(:last-child) {
            border-right: 1px solid;
          }
        }
      }
    }
  }
}
</style>
