<script lang="ts" setup>
import { composeNewDecimalValue, ncIsNaN } from 'nocodb-sdk'
import type { StyleValue } from 'vue'

interface Props {
  placeholder?: string
  inputStyle?: StyleValue
  modelValue?: number | null
  disabled?: boolean
  precision?: number
  isFocusOnMounted?: boolean
}

interface Emits {
  (event: 'update:modelValue', model: number): void
  (event: 'blur', model: FocusEvent): void
}

const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'modelValue', emits)

const { isMobileMode } = useGlobal()

const inputRef = templateRef('input-ref')

const pasteText = (target: HTMLInputElement, value: string) => {
  if (!value || value === '') {
    return { changed: false }
  }
  const selectionEnd = target.selectionEnd
  const lastValue = target.value
  const newValue = composeNewDecimalValue({
    selectionStart: target.selectionStart,
    selectionEnd: target.selectionEnd,
    lastValue,
    newValue: value,
  })
  if (target.value !== newValue) {
    target.value = newValue
  }
  if (selectionEnd || selectionEnd === 0) {
    const newCursorIndex = target.value.length - (lastValue.length - selectionEnd)
    target.setSelectionRange(newCursorIndex, newCursorIndex)
  }
}

const getFormattedModelValue = (format = true) => {
  if (vModel.value || vModel.value === 0) {
    if (typeof vModel.value === 'number') {
      if (props.precision && format) {
        return vModel.value.toFixed(props.precision) ?? ''
      } else {
        return vModel.value.toString()
      }
    } else if (typeof vModel.value === 'string') {
      const numberValue = Number(vModel.value)
      if (!ncIsNaN(numberValue)) {
        if (props.precision && format) {
          return numberValue.toFixed(props.precision) ?? ''
        } else {
          return numberValue.toString()
        }
      }
    }
  }

  return ''
}
const refreshVModel = (format = true) => {
  if (inputRef.value && (vModel.value || vModel.value === 0)) {
    inputRef.value.value = getFormattedModelValue(format)
  }
}
const saveValue = (targetValue: string) => {
  if (targetValue === '') {
    vModel.value = null
    return
  }
  const value = Number(targetValue)
  if (ncIsNaN(value)) {
    vModel.value = null
    return
  }
  vModel.value = value
}
let savingHandle: any
const onInputKeyUp = (e: KeyboardEvent, debounce = true) => {
  const target: HTMLInputElement = e.target as HTMLInputElement
  if (target) {
    // mac's double space insert period
    // not perfect, but better
    if (target.value.match(/\.\s/)?.[0]) {
      target.value = target.value.replace(/\.\s/, '')
    }
    // debounce, maybe there's some helpers in vue?
    if (savingHandle) {
      clearTimeout(savingHandle)
    }
    if (!debounce) {
      saveValue(target.value)
    } else {
      savingHandle = setTimeout(() => {
        saveValue(target.value)
      }, 100)
    }
  }
}
// Handle the arrow keys as its default behavior is to increment/decrement the value
const onInputKeyDown = (e: KeyboardEvent) => {
  const target: HTMLInputElement = e.target as HTMLInputElement
  if (!target) {
    return
  }
  const functionKeys = ['F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']
  if (
    [
      'ArrowLeft',
      'ArrowRight',
      'Enter',
      'Escape',
      'Home',
      'End',
      'PageUp',
      'PageDown',
      'Delete',
      'Backspace',
      'Tab',
      ...functionKeys,
    ].includes(e.key) ||
    e.ctrlKey ||
    e.altKey ||
    e.metaKey
  ) {
    return
  }
  if (e.key === 'ArrowDown') {
    target.setSelectionRange(target.value.length, target.value.length)
    return
  } else if (e.key === 'ArrowUp') {
    target.setSelectionRange(0, 0)
    return
  } else if (e.key.match('[^-0-9\.]')) {
    // prevent everything non ctrl / alt and non . and non number
    e.preventDefault()
    e.stopPropagation()
    return
  }

  pasteText(target, e.key)
  e.preventDefault()
  e.stopPropagation()
}

const onInputPaste = (e: ClipboardEvent) => {
  if (e.clipboardData === null || typeof e.clipboardData === 'undefined') {
    return
  }
  const target: HTMLInputElement = e.target as HTMLInputElement
  if (!target) {
    return
  }
  const value = e.clipboardData.getData('text/plain')
  if (value === null || value === '' || typeof value === 'undefined') {
    return
  }
  e.preventDefault()
  e.stopPropagation()
  pasteText(target, value)
}

const onInputBlur = (e: FocusEvent) => {
  emits('blur', e)
  if (e.target) {
    const targetValue = (e.target as HTMLInputElement).value
    saveValue(targetValue)
    setTimeout(() => {
      // allow for debouncing to clear first
      refreshVModel()
    }, 100)
  }
}

const onInputFocus = () => {
  refreshVModel(false)
}

const registerEvents = (input: HTMLInputElement) => {
  input.addEventListener('keydown', onInputKeyDown)
  input.addEventListener('keyup', onInputKeyUp)
  input.addEventListener('paste', onInputPaste)
  input.addEventListener('blur', onInputBlur)
  input.addEventListener('focus', onInputFocus)
}

const removeEvents = (input: HTMLInputElement) => {
  input.removeEventListener('keydown', onInputKeyDown)
  input.removeEventListener('keyup', onInputKeyUp)
  input.removeEventListener('paste', onInputPaste)
  input.removeEventListener('blur', onInputBlur)
  input.removeEventListener('focus', onInputFocus)
}

const onBeforeInput = (e: InputEvent) => {
  if (!e.data || !isMobileMode.value) return // may be null for deletions etc.

  // allow only digits, minus, dot
  if (!/^[0-9.\-]$/.test(e.data)) {
    e.preventDefault()
  }
}

onMounted(() => {
  if (inputRef.value) {
    registerEvents(inputRef.value as HTMLInputElement)
    nextTick(() => {
      refreshVModel()
      if (props.isFocusOnMounted) {
        inputRef.value.focus()
      }
    })
  }
})

onBeforeUnmount(() => {
  if (inputRef.value) {
    removeEvents(inputRef.value as HTMLInputElement)
  }
})

watch(vModel, (newValue) => {
  if (
    !inputRef.value ||
    newValue ||
    inputRef.value.value === getFormattedModelValue() ||
    inputRef.value.value === (newValue?.toString() || '')
  ) {
    return
  }

  // Clear input value if vModel is null and input value is not empty and not a dot or minus
  if (!newValue && inputRef.value.value && !['.', '-'].includes(inputRef.value.value)) {
    inputRef.value.value = ''
  }
})
</script>

<template>
  <!-- eslint-disable vue/use-v-on-exact -->
  <input
    ref="input-ref"
    class="nc-cell-field outline-none rounded-md w-full"
    :placeholder="placeholder"
    style="letter-spacing: 0.06rem; height: 24px !important"
    :style="inputStyle"
    inputmode="decimal"
    :disabled="disabled"
    @keydown.enter.exact="onInputKeyUp($event, false)"
    @keydown.left.stop
    @keydown.right.stop
    @keydown.delete.stop
    @keydown.alt.stop
    @selectstart.capture.stop
    @mousedown.stop
    @beforeinput="onBeforeInput"
  />
</template>

<style scoped lang="scss">
input[type='number']:focus {
  @apply ring-transparent;
}

/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Firefox */
input[type='number'] {
  -moz-appearance: textfield;
}
</style>
