<script setup lang="ts">
import type { IconType } from '@bagelink/vue'
import { Icon, Btn, resolveI18n } from '@bagelink/vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'

type NumberLayout = 'default' | 'vertical' | 'horizontal'

interface NumberInputProps {
	modelValue?: number | string
	min?: number
	max?: number
	step?: number
	spinner?: boolean
	iconStart?: IconType
	icon?: IconType
	label?: string
	defaultValue?: number | string
	placeholder?: string
	disabled?: boolean
	required?: boolean
	id?: string
	helptext?: string
	layout?: NumberLayout
	center?: boolean
	padZero?: number
	useGrouping?: boolean
	underlined?: boolean
	error?: string
}

const {
	modelValue,
	min,
	max,
	step = 1,
	spinner = true,
	iconStart,
	icon,
	label,
	placeholder,
	disabled,
	required,
	helptext,
	layout,
	id,
	padZero = 1,
	defaultValue,
	useGrouping = true,
	error
} = defineProps<NumberInputProps>()

const emit = defineEmits(['update:modelValue'])

const numberValue = ref<number>()

onMounted(() => {
	const num = modelValue !== undefined ? Number.parseFloat(`${modelValue}`) : undefined
	const defaultNum = defaultValue !== undefined ? Number.parseFloat(`${defaultValue}`) : undefined
	numberValue.value = num ?? defaultNum
})

const btnLayouts: NumberLayout[] = ['horizontal', 'vertical']

function add(...numbers: (number | undefined)[]) {
	const numArr: number[] = numbers.map(n => n || 0)
	const precision = step.toString().split('.')[1]?.length || 0
	return Number.parseFloat(numArr.reduce((acc, curr) => acc + curr, 0).toFixed(precision))
}

function subtract(...numbers: (number | undefined)[]) {
	const numArr: number[] = numbers.map(n => n || 0)
	const firstNum = numArr.shift() || 0
	const precision = step.toString().split('.')[1]?.length || 0
	return Number.parseFloat(numArr.reduce((acc, curr) => acc - curr, firstNum).toFixed(precision))
}

const canAdd = computed(() => max === undefined || add(numberValue.value, step) <= max)
const canDecrement = computed(() => min === undefined || subtract(numberValue.value, step) >= min)

// Methods
function increment() {
	if (!canAdd.value) { return }
	numberValue.value = add(numberValue.value ?? 0, step)
	emit('update:modelValue', numberValue.value)
}

function decrement() {
	if (!canDecrement.value) { return }
	numberValue.value = subtract(numberValue.value || 0, step)
	emit('update:modelValue', numberValue.value)
}

function formatNumber(num: number) {
	if (Number.isNaN(num)) { return '' }
	const formatter = new Intl.NumberFormat('en-US', {
		minimumFractionDigits: 0,
		maximumFractionDigits: 20,
		useGrouping
	})

	const formattedNum = formatter.format(num)
	const [integerPart, decimalPart] = formattedNum.split('.')

	// Add padding zeros if needed
	const padding = padZero && padZero > integerPart.replace(/,/g, '').length
		? '0'.repeat(padZero - integerPart.replace(/,/g, '').length)
		: ''

	return decimalPart
		? `${padding}${integerPart}.${decimalPart}`
		: `${padding}${integerPart}`
}

const formattedValue = ref('')
function inputHandler() {
	const numeric = formattedValue.value.replace(/[^\d.-]/g, '')
	const emptyValue = ['', '-', '.', '-.'].includes(numeric)
	const isTypingDecimal = numeric.endsWith('.')
	const isTypingTrailingZeros = /\.\d*0$/.test(numeric) // Check if ending with decimal followed by digits ending in 0

	if (emptyValue || isTypingDecimal || isTypingTrailingZeros) {
		emit('update:modelValue', numeric === '-' ? '-' : numeric)
		// Don't reformat while typing decimal values
		return
	}

	numberValue.value = Number.parseFloat(numeric)
	formattedValue.value = emptyValue ? '' : formatNumber(numberValue.value)
	emit('update:modelValue', numberValue.value)
}

watch(() => numberValue.value, () => {
	nextTick(() => {
		// Don't reformat if user is currently typing a decimal or trailing zeros
		if (formattedValue.value.endsWith('.') || /\.\d*0$/.test(formattedValue.value)) { return }
		formattedValue.value = numberValue.value !== undefined ? formatNumber(numberValue.value) : ''
	})
})

watch(() => modelValue, (newVal) => {
	if (newVal !== numberValue.value) {
		if (typeof newVal === 'string' && (newVal === '-' || newVal.endsWith('.'))) {
			formattedValue.value = newVal
			return
		}
		numberValue.value = Number.parseFloat(`${newVal}`) || 0
	}
}, { immediate: true })

const hasValue = computed(() => {
	const val = formattedValue.value
	return Boolean(val && String(val).length > 0)
})
</script>

<template>
	<div
		class="bagel-input num-input" :class="{
			'textInputSpinnerWrap': icon || spinner,
			'txtInputIconStart': iconStart,
			underlined,
			'has-error': !!error,
			'has-value': hasValue,
		}"
	>
		<div :for="id">
			<label v-if="label || (underlined && placeholder)" class="block">
				<span class="label-text">{{ resolveI18n(label) || resolveI18n(placeholder) }}<span v-if="required"> *</span></span>
			</label>
			<div class="gap-025" :class="{ 'column flex': layout === 'vertical', 'flex': layout === 'horizontal' }">
				<Btn
					v-if="layout && btnLayouts.includes(layout)" flat icon="add" class="radius"
					:class="[{ 'bgl-big-ctrl-num-btn': layout === 'vertical' }]" tabindex="-1" @click="increment"
				/>
				<input
					:id v-model.trim="formattedValue" v-pattern.number type="text" :class="{
						'txt-center': center,
						'min0': layout,
						'bgl-number-input': layout !== 'vertical' && layout !== 'horizontal',
					}" :placeholder="underlined ? ' ' : (resolveI18n(placeholder) || resolveI18n(label))" :disabled :required :readonly="disabled" inputmode="decimal"
					autocomplete="off" :pattern="useGrouping ? '^-?\\d{1,3}(,\\d{3})*(\.\\d+)?$' : '^-?\\d*\.?\d*$'"
					autocorrect="off" spellcheck="false" @input="inputHandler" @keydown.up.prevent="increment"
					@keydown.down.prevent="decrement"
				>
				<p v-if="helptext" class="opacity-7 light">
					{{ helptext }}
				</p>
				<Icon v-if="iconStart" :class="{ ' bottom-1-25 ': underlined }" class="iconStart" size="0.9" :icon="iconStart" />
				<Icon v-if="icon" :class="{ ' bottom-1-25 ': underlined }" :icon size="0.9" />
				<Btn
					v-if="layout && btnLayouts.includes(layout)" flat icon="remove" class="radius"
					:class="[{ 'bgl-big-ctrl-num-btn': layout === 'vertical' }]" tabindex="-1" @click="decrement"
				/>

				<div v-if="spinner && (!layout || !btnLayouts.includes(layout))" class="flex column spinner">
					<Btn
						icon="add" flat thin class="bgl-ctrl-num-btn ctrl-add top-bgl-ctrl-num-btn" :disabled="!canAdd"
						tabindex="-1" @click="increment"
					/>
					<Btn
						icon="remove" flat thin class="bgl-ctrl-num-btn ctrl-remove" :disabled="!canDecrement"
						tabindex="-1" @click="decrement"
					/>
				</div>
			</div>
		</div>
		<div v-if="error" class="error-message">
			{{ error }}
		</div>
	</div>
</template>

<style scoped>
.txtInputIconStart input {
	padding-inline-start: calc(var(--input-height) / 1.5);
}

.txtInputIconStart .iconStart {
	color: var(--input-color);
	position: absolute;
	inset-inline-start: calc(var(--input-height) / 3 - 0.25rem);
	margin-top: calc(var(--input-height) / 2);
	line-height: 0;
}

.textInputSpinnerWrap .spinner {
	color: var(--input-color);
	position: absolute;
	inset-inline-end: 0;
	/* margin-top: calc(var(--input-height) / -1 + 0.15rem); */
	margin-top: calc(var(--input-height) / -1 + (var(--input-height) / 40));
	line-height: 0;
	margin-inline-end: 0.25rem;
	display: flex;
	flex-direction: column;
	gap: 0;
}

.top-bgl-ctrl-num-btn {
	margin-top: calc(var(--input-height) / 10) !important;
}

.bgl-ctrl-num-btn {
	height: calc(var(--input-height) / 2.5) !important;
	isolation: isolate;
}

.bgl-big-ctrl-num-btn {
	width: 100% !important;
	isolation: isolate;
}

.bgl-number-input {
	padding-inline-end: 1.75rem !important;
}

.bagel-input.has-error input {
	border-color: var(--bgl-red, #dc3545) !important;
}
</style>
