<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'
import type { BagelInputShellProps } from './bagelInputShell'
import { useBagelInputShell } from './bagelInputShell'

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

interface NumberInputProps extends BagelInputShellProps {
	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,
	frame,
	outline,
	minWidth,
	maxWidth,
	labelColor,
	labelActiveColor,
} = defineProps<NumberInputProps>()

const { shellClass, shellStyle } = useBagelInputShell({
	frame, outline, minWidth, maxWidth, labelColor, labelActiveColor,
})

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']

const TRAILING_ZEROS_RE = /\.\d*0$/

function getPrecision() {
	return step.toString().split('.')[1]?.length || 0
}

function add(...numbers: (number | undefined)[]) {
	const numArr: number[] = numbers.map(n => n || 0)
	return Number.parseFloat(numArr.reduce((acc, curr) => acc + curr, 0).toFixed(getPrecision()))
}

function subtract(...numbers: (number | undefined)[]) {
	const numArr: number[] = numbers.map(n => n || 0)
	const firstNum = numArr.shift() || 0
	return Number.parseFloat(numArr.reduce((acc, curr) => acc - curr, firstNum).toFixed(getPrecision()))
}

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

function increment() {
	if (!canIncrement.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)
}

const numberFormatter = computed(() => new Intl.NumberFormat('en-US', {
	minimumFractionDigits: 0,
	maximumFractionDigits: 20,
	useGrouping,
}))

function formatNumber(num: number) {
	if (Number.isNaN(num)) { return '' }

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

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

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

const formattedValue = ref('')
function inputHandler() {
	const numeric = formattedValue.value.replace(/[^\d.-]/g, '')
	const isIncomplete = ['', '-', '.', '-.'].includes(numeric) || numeric.endsWith('.') || TRAILING_ZEROS_RE.test(numeric)

	if (isIncomplete) {
		emit('update:modelValue', numeric === '-' ? '-' : numeric)
		return
	}

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

watch(() => numberValue.value, () => {
	nextTick(() => {
		if (formattedValue.value.endsWith('.') || TRAILING_ZEROS_RE.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(() => formattedValue.value.length > 0)
</script>

<template>
	<div
		class="bagel-input num-input" :class="[
			shellClass,
			{
				underlined,
				'has-error': !!error,
				'has-value': hasValue,
			},
		]" :style="shellStyle"
	>
		<div>
			<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 display-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"
				/>
				<div class="input-icon-wrap flex-grow-1">
					<Icon v-if="iconStart" class="iconStart" size="0.9" :icon="iconStart" />
					<input
						:id v-model.trim="formattedValue" v-pattern.number type="text" :class="{
							'txt-center': center,
							'min0': layout,
						}" :placeholder="underlined ? ' ' : (resolveI18n(placeholder) || resolveI18n(label))" :disabled :required 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"
					>
					<Icon v-if="icon" size="0.9" :icon />
					<div v-if="spinner && (!layout || !btnLayouts.includes(layout))" class="flex column spinner-btns">
						<Btn
							icon="add" flat thin class="bgl-ctrl-num-btn ctrl-add top-bgl-ctrl-num-btn" :disabled="!canIncrement"
							tabindex="-1" @click="increment"
						/>
						<Btn
							icon="remove" flat thin class="bgl-ctrl-num-btn ctrl-remove" :disabled="!canDecrement"
							tabindex="-1" @click="decrement"
						/>
					</div>
				</div>
				<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>
			<p v-if="helptext" class="opacity-7 light">
				{{ helptext }}
			</p>
		</div>
		<div v-if="error" class="error-message">
			{{ error }}
		</div>
	</div>
</template>

<style scoped>

.input-icon-wrap {
display: flex;
align-items: center;
background: var(--bgl-input-bg);
border-radius: var(--bgl-input-border-radius);
height: var(--bgl-input-height);
transition: box-shadow 0.2s ease, outline-color 0.2s ease;
}

.input-icon-wrap input {
flex: 1;
min-width: 0;
background: transparent !important;
outline: none !important;
box-shadow: none !important;
}

.input-icon-wrap:focus-within {
outline-color: var(--bgl-input-bg);
box-shadow: inset 0 0 8px #00000018;
}

.input-icon-wrap .iconStart,
.input-icon-wrap > .bgl_icon-font {
flex-shrink: 0;
color: var(--bgl-input-color);
line-height: 0;
padding-inline: 0.35rem;
}

/* ── frame variant ─────────────────────────────────────────────── */
.bagel-input.frame .input-icon-wrap {
background: transparent;
outline: 1.5px solid var(--bgl-border-color);
outline-offset: -1px;
}

.bagel-input.frame .input-icon-wrap:focus-within {
outline-color: var(--bgl-input-label-active-color, var(--bgl-primary)) !important;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bgl-input-label-active-color, var(--bgl-primary)) 12%, transparent) !important;
}

/* ── outline variant ───────────────────────────────────────────── */
.bagel-input.bgl-outline .input-icon-wrap {
outline: 1.5px solid var(--bgl-border-color);
outline-offset: -1px;
}

/* ── underlined variant ────────────────────────────────────────── */
.bagel-input.underlined .input-icon-wrap {
position: relative;
background: transparent;
border-radius: 0;
height: auto;
border-bottom: 1.5px solid var(--bgl-border-color);
}

.bagel-input.underlined .input-icon-wrap::after {
content: '';
position: absolute;
bottom: -1px;
inset-inline-start: 0;
width: 0;
height: 2px;
background: var(--bgl-input-label-active-color, var(--bgl-primary));
transition: width 0.6s ease;
pointer-events: none;
}

.bagel-input.underlined .input-icon-wrap:focus-within {
box-shadow: none;
}

.bagel-input.underlined .input-icon-wrap:focus-within::after {
width: 100%;
}

.bagel-input.underlined .input-icon-wrap input {
border-bottom: none !important;
}

.spinner-btns {
flex-shrink: 0;
display: flex;
flex-direction: column;
margin-inline-end: 0.25rem;
}

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

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

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

.bagel-input.has-error input {
border-color: var(--bgl-red) !important;
}

</style>
