<script setup>
const props = defineProps({
	min: {
		type: Number,
		default: 0
	},
	max: {
		type: Number,
		default: 1
	},
	step: {
		type: Number,
		default: 0.01
	},
	value: {
		type: Number,
		default: 0
	},
	id: {
		type: String,
	},
	label: {
		type: String,
	}
})

const emit = defineEmits(['update:modelValue'])
const modelValue = ref(props.value);

watch(modelValue, (newValue) => {
	emit('update:modelValue', newValue);
});
emit('update:modelValue', props.value);

function roundValueToStep(number) {
	const clampedNumber = Math.min(Math.max(number, props.min), props.max);
	const quotient = Math.floor((clampedNumber - props.min) / props.step);
	const lowerValue = props.min + quotient * props.step;
	const upperValue = lowerValue + props.step;

	if (clampedNumber - lowerValue < upperValue - clampedNumber) {
		return lowerValue; // Round down
	} else {
		return upperValue; // Round up
	}
}

const rangeSlider = ref(null);
const rangeThumb = ref(null);
const rangeValue = ref({
	real: ref(roundValueToStep(props.value)),
	stepped: ref(roundValueToStep(props.value))
});

const sliderPercentage = ref((rangeValue.value.real / props.max) * 100);
const steppedSliderPercentage = ref((rangeValue.value.stepped / props.max) * 100)
const steps = (props.max / props.step)

function snapToStep(percentage) {
	const pixelsPerStep = rangeSlider.value.clientWidth / steps;
	let step = roundValueToStep((((percentage / 100) * rangeSlider.value.clientWidth) / pixelsPerStep) * props.step);

	const split = step.toString().split('.');
	// fix float imprecisions by making sure the number only has at most 3 digits after the decimal point
	if (split.length > 1) {
		split[1] = split[1].slice(0,3);
		step = parseFloat(split.join('.'));
	}

	rangeValue.value.stepped = step;
	modelValue.value = step;

	return (rangeValue.value.stepped / props.max) * 100;
}

function calculateSliderPosition(pointerEvent) {
	if (!rangeSlider.value) return;
	let pointerPosFromRangeStart = pointerEvent.clientX - rangeSlider.value.getClientRects()[0].x;

	if (pointerPosFromRangeStart < 0) {
		pointerPosFromRangeStart = 0;
	}

	if (pointerPosFromRangeStart > rangeSlider.value.clientWidth) {
		pointerPosFromRangeStart = rangeSlider.value.clientWidth;
	}

	const value = (pointerPosFromRangeStart / rangeSlider.value.clientWidth);

	const newSliderPercentage = value * 100;

	if (newSliderPercentage === sliderPercentage.value) return;

	updateSliderPosition(newSliderPercentage);
}

function updateSliderPosition(newSliderPercentage) {
	sliderPercentage.value = newSliderPercentage;

	steppedSliderPercentage.value = snapToStep(newSliderPercentage);
}

function pointerDown(pointerEvent) {
	document.body.addEventListener('pointermove', calculateSliderPosition)

	calculateSliderPosition(pointerEvent);

	document.body.addEventListener('pointerup', () => {
		rangeThumb.value.focus();
		document.body.removeEventListener('pointermove', calculateSliderPosition)
	}, { once: true })
}

function keydown(keyboardEvent) {
	if (keyboardEvent.keyCode >= 37 && keyboardEvent.keyCode <= 40) {
		keyboardEvent.preventDefault();
		const direction = (keyboardEvent.key === "ArrowRight" || keyboardEvent.key === "ArrowUp") ? 'forward' : 'backward';

		if (direction === 'forward') {
			if (rangeValue.value.stepped + props.step > props.max) return;
			rangeValue.value.stepped += props.step;
		} else {
			if (rangeValue.value.stepped - props.step < props.min) return;
			rangeValue.value.stepped -= props.step;
		}

		return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
	}

	if (keyboardEvent.keyCode === 36 || keyboardEvent.keyCode === 35) {
		keyboardEvent.preventDefault();

		if (keyboardEvent.key === "Home") {
			rangeValue.value.stepped = props.min;
		} else {
			rangeValue.value.stepped = props.max;
		}

		return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
	}

	if (keyboardEvent.keyCode === 33 || keyboardEvent.keyCode === 34) {
		keyboardEvent.preventDefault();
		const direction = (keyboardEvent.key === "PageUp") ? 'forward' : 'backward';
		let multiplier = 2;

		if (direction === 'forward') {
			if (rangeValue.value.stepped + (props.step * multiplier) > props.max) multiplier = 1;
			if (rangeValue.value.stepped + (props.step * multiplier) > props.max) return;
			rangeValue.value.stepped += (props.step * multiplier);
		} else {
			if (rangeValue.value.stepped - (props.step * multiplier) > props.max) multiplier = 1;
			if (rangeValue.value.stepped - (props.step * multiplier) > props.max) return;
			rangeValue.value.stepped -= (props.step * multiplier);
		}

		return updateSliderPosition((rangeValue.value.stepped / props.max) * 100);
	}
}

onMounted(() => {
	if (props.id) {
		document.body.querySelector(`[for="${props.id}"]`).addEventListener("click", () => {
			rangeThumb.value.focus();
		})
	}
})

onUnmounted(() => {
	if (props.id) {
		document.body.querySelector(`[for="${props.id}"]`).removeEventListener("click", () => {
			rangeThumb.value.focus();
		})
	}
})
</script>

<template>
	<div class="vl-range-slider" ref="rangeSlider" @pointerdown="pointerDown($event)">
		<span class="vl-range-track">
			<span class="vl-range-track-filled" :style="'width: ' + steppedSliderPercentage + '%;'">
			</span>
		</span>
		<span class="vl-range-thumb" :aria-label="props.label" :aria-labelledby="props.id" role="slider" ref="rangeThumb"
			:aria-valuemin="props.min" :aria-valuemax="props.max" aria-orientation="horizontal" :aria-valuenow="rangeValue.stepped" @keydown="keydown($event)"
			tabindex="0" :style="'left: ' + steppedSliderPercentage + '%;'"></span>
	</div>
</template>

<style>
.vl-range-slider {
	position: relative;
	display: flex;
	align-items: center;
	width: 200px;
	height: 20px;
	margin: 4px 0;
}

.vl-range-track,
.vl-range-track-filled {
	display: block;
	height: 4px;
	width: 100%;
	background: #313234;
	border-radius: 99999px;
}

.vl-range-track-filled {
	background: hsl(226, 79%, 47.5%);
	width: 0%;
}

.vl-range-thumb {
	position: absolute;
	display: block;
	height: 20px;
	width: 20px;
	background: #f7f7f7;
	border-radius: 99999px;
	transform: translateX(-50%);
}

.vl-range-thumb:focus {
	outline: 4px solid #b7b7b74a;
}
</style>