<template>
    <div
        :class="cx('root')"
        :style="sx('root')"
        @click="onBarClick"
        v-bind="ptmi('root')"
        :data-p="dataP"
        :data-orientation="orientation"
        :data-disabled="disabled ? '' : undefined"
        :data-invalid="$invalid ? '' : undefined"
        :data-dragging="d_dragging ? '' : undefined"
    >
        <div :class="cx('track')" :style="sx('track')" v-bind="ptm('track')" :data-orientation="orientation" :data-disabled="disabled ? '' : undefined" :data-invalid="$invalid ? '' : undefined" :data-dragging="d_dragging ? '' : undefined">
            <span
                :class="cx('range')"
                :style="sx('range')"
                v-bind="ptm('range')"
                :data-p="dataP"
                :data-orientation="orientation"
                :data-disabled="disabled ? '' : undefined"
                :data-invalid="$invalid ? '' : undefined"
                :data-dragging="d_dragging ? '' : undefined"
            ></span>
        </div>
        <span
            v-if="!range"
            :class="cx('handle')"
            :style="sx('handle')"
            @touchstart.passive="onDragStart($event)"
            @touchmove.passive="onDrag($event)"
            @touchend="onDragEnd($event)"
            @mousedown="onMouseDown($event)"
            v-bind="ptm('handle')"
            :data-p="dataP"
            :data-index="0"
            :data-orientation="orientation"
            :data-disabled="disabled ? '' : undefined"
            :data-invalid="$invalid ? '' : undefined"
            :data-dragging="d_dragging ? '' : undefined"
        >
            <input
                type="range"
                :class="[cx('input'), inputClass]"
                :style="inputStyle"
                :id="inputId"
                :name="name"
                :min="min"
                :max="max"
                :step="step ?? 1"
                :value="getHandleValue(0)"
                :disabled="disabled"
                :readonly="readonly"
                :tabindex="disabled ? -1 : tabindex"
                :aria-valuemin="min"
                :aria-valuenow="getHandleValue(0)"
                :aria-valuemax="max"
                :aria-labelledby="ariaLabelledby"
                :aria-label="ariaLabel"
                :aria-orientation="orientation"
                @input="onInputChange($event, 0)"
                @change="onInputChange($event, 0)"
                @focus="onInputFocus($event, 0)"
                @blur="onInputBlur($event, 0)"
                v-bind="ptm('input')"
            />
        </span>
        <span
            v-if="range"
            :class="cx('handle')"
            :style="sx('startHandler')"
            @touchstart.passive="onDragStart($event, 0)"
            @touchmove.passive="onDrag($event)"
            @touchend="onDragEnd($event)"
            @mousedown="onMouseDown($event, 0)"
            v-bind="ptm('startHandler')"
            :data-p="dataP"
            :data-index="0"
            :data-orientation="orientation"
            :data-disabled="isHandleDisabled(0) ? '' : undefined"
            :data-invalid="$invalid ? '' : undefined"
            :data-dragging="d_dragging ? '' : undefined"
        >
            <input
                type="range"
                :class="[cx('input'), inputClass]"
                :style="inputStyle"
                :id="inputId"
                :name="name"
                :min="min"
                :max="max"
                :step="step ?? 1"
                :value="getHandleValue(0)"
                :disabled="isHandleDisabled(0)"
                :readonly="readonly"
                :tabindex="isHandleDisabled(0) ? -1 : tabindex"
                :aria-valuemin="min"
                :aria-valuenow="getHandleValue(0)"
                :aria-valuemax="max"
                :aria-labelledby="ariaLabelledby"
                :aria-label="ariaLabel"
                :aria-orientation="orientation"
                @input="onInputChange($event, 0)"
                @change="onInputChange($event, 0)"
                @focus="onInputFocus($event, 0)"
                @blur="onInputBlur($event, 0)"
                v-bind="ptm('input')"
            />
        </span>
        <span
            v-if="range"
            :class="cx('handle')"
            :style="sx('endHandler')"
            @touchstart.passive="onDragStart($event, 1)"
            @touchmove.passive="onDrag($event)"
            @touchend="onDragEnd($event)"
            @mousedown="onMouseDown($event, 1)"
            v-bind="ptm('endHandler')"
            :data-p="dataP"
            :data-index="1"
            :data-orientation="orientation"
            :data-disabled="isHandleDisabled(1) ? '' : undefined"
            :data-invalid="$invalid ? '' : undefined"
            :data-dragging="d_dragging ? '' : undefined"
        >
            <input
                type="range"
                :class="[cx('input'), inputClass]"
                :style="inputStyle"
                :name="name"
                :min="min"
                :max="max"
                :step="step ?? 1"
                :value="getHandleValue(1)"
                :disabled="isHandleDisabled(1)"
                :readonly="readonly"
                :tabindex="isHandleDisabled(1) ? -1 : tabindex"
                :aria-valuemin="min"
                :aria-valuenow="getHandleValue(1)"
                :aria-valuemax="max"
                :aria-labelledby="ariaLabelledby"
                :aria-label="ariaLabel"
                :aria-orientation="orientation"
                @input="onInputChange($event, 1)"
                @change="onInputChange($event, 1)"
                @focus="onInputFocus($event, 1)"
                @blur="onInputBlur($event, 1)"
                v-bind="ptm('input')"
            />
        </span>
    </div>
</template>

<script>
import { cn } from '@primeuix/utils';
import { getAttribute, getWindowScrollLeft, getWindowScrollTop, isRTL } from '@primeuix/utils/dom';
import BaseSlider from './BaseSlider.vue';

export default {
    name: 'Slider',
    extends: BaseSlider,
    inheritAttrs: false,
    emits: ['change', 'slideend'],
    handleIndex: null,
    initX: null,
    initY: null,
    barWidth: null,
    barHeight: null,
    dragListener: null,
    dragEndListener: null,
    data() {
        return {
            d_dragging: false,
            d_focusedIndex: null
        };
    },
    beforeUnmount() {
        this.unbindDragListeners();
    },
    methods: {
        updateDomData() {
            let rect = this.$el.getBoundingClientRect();

            this.initX = rect.left + getWindowScrollLeft();
            this.initY = rect.top + getWindowScrollTop();
            this.barWidth = this.$el.offsetWidth;
            this.barHeight = this.$el.offsetHeight;
        },
        setValue(event) {
            let handleValue;
            let pageX = event.touches ? event.touches[0].pageX : event.pageX;
            let pageY = event.touches ? event.touches[0].pageY : event.pageY;

            if (this.orientation === 'horizontal') {
                if (isRTL(this.$el)) {
                    handleValue = ((this.initX + this.barWidth - pageX) * 100) / this.barWidth;
                } else {
                    handleValue = ((pageX - this.initX) * 100) / this.barWidth;
                }
            } else {
                handleValue = ((this.initY + this.barHeight - pageY) * 100) / this.barHeight;
            }

            let newValue = (this.max - this.min) * (handleValue / 100) + this.min;

            if (!this.step) {
                newValue = Math.floor(newValue);
            }

            this.updateModel(event, newValue);
        },
        clamp(value, minValue, maxValue) {
            return Math.min(Math.max(value, minValue), maxValue);
        },
        getPrecision(stepValue) {
            const stepString = stepValue.toString();

            if (stepString.includes('e-')) {
                return Number(stepString.split('e-')[1] || 0);
            }

            const dotIndex = stepString.indexOf('.');

            return dotIndex >= 0 ? stepString.length - dotIndex - 1 : 0;
        },
        roundToStep(value, stepValue, minValue) {
            if (!stepValue) return value;

            const precision = this.getPrecision(stepValue);
            const rounded = Math.round((value - minValue) / stepValue) * stepValue + minValue;

            return Number(rounded.toFixed(precision));
        },
        updateModel(event, value) {
            let newValue = this.step ? this.roundToStep(value, this.step, this.min) : Math.round(value * 100) / 100;
            let modelValue;

            if (this.range) {
                modelValue = this.value ? [...this.value] : [];

                const gap = Math.max((this.minStepsBetweenHandles || 0) * (this.step || 1), 0);

                if (this.handleIndex == 0) {
                    const upperBound = modelValue[1] !== undefined ? modelValue[1] - gap : this.max;

                    newValue = this.clamp(newValue, this.min, upperBound);

                    modelValue[0] = newValue;
                } else {
                    const lowerBound = modelValue[0] !== undefined ? modelValue[0] + gap : this.min;

                    newValue = this.clamp(newValue, lowerBound, this.max);

                    modelValue[1] = newValue;
                }
            } else {
                newValue = this.clamp(newValue, this.min, this.max);

                modelValue = newValue;
            }

            this.writeValue(modelValue, event);
            this.$emit('change', modelValue);
        },
        onDragStart(event, index) {
            if (this.disabled || this.readonly) {
                return;
            }

            if (this.range && this.isHandleDisabled(index)) {
                return;
            }

            this.d_dragging = true;
            this.updateDomData();

            if (this.range && this.value[0] === this.max) {
                this.handleIndex = 0;
            } else {
                this.handleIndex = index;
            }

            const input = event.currentTarget?.querySelector?.('.p-slider-input');

            input?.focus?.();
        },
        onDrag(event) {
            if (this.d_dragging) {
                this.setValue(event);
            }
        },
        onDragEnd(event) {
            if (this.d_dragging) {
                this.d_dragging = false;
                this.$emit('slideend', { originalEvent: event, value: this.value });
            }
        },
        onBarClick(event) {
            if (this.disabled || this.readonly) {
                return;
            }

            if (getAttribute(event.target, 'data-pc-section') !== 'handle') {
                this.updateDomData();
                this.setValue(event);
            }
        },
        onMouseDown(event, index) {
            this.bindDragListeners();
            this.onDragStart(event, index);
        },
        onInputChange(event, index) {
            if (this.disabled || this.readonly) return;

            if (this.range && this.isHandleDisabled(index)) return;

            this.handleIndex = index;
            const newValue = parseFloat(event.target.value);

            if (Number.isNaN(newValue)) return;

            this.updateModel(event, newValue);
            this.$emit('slideend', { originalEvent: event, value: this.value });
        },
        onInputFocus(event, index) {
            this.d_focusedIndex = index;
        },
        onInputBlur(event, index) {
            if (this.d_focusedIndex === index) {
                this.d_focusedIndex = null;
            }

            this.formField.onBlur?.(event);
            this.$emit('slideend', { originalEvent: event, value: this.value });
        },
        bindDragListeners() {
            if (!this.dragListener) {
                this.dragListener = this.onDrag.bind(this);
                document.addEventListener('mousemove', this.dragListener);
            }

            if (!this.dragEndListener) {
                this.dragEndListener = this.onDragEnd.bind(this);
                document.addEventListener('mouseup', this.dragEndListener);
            }
        },
        unbindDragListeners() {
            if (this.dragListener) {
                document.removeEventListener('mousemove', this.dragListener);
                this.dragListener = null;
            }

            if (this.dragEndListener) {
                document.removeEventListener('mouseup', this.dragEndListener);
                this.dragEndListener = null;
            }
        },
        isRange() {
            return this.range;
        },
        isHorizontal() {
            return this.orientation === 'horizontal';
        },
        values() {
            if (this.range) {
                return [this.d_value?.[0] ?? this.min, this.d_value?.[1] ?? this.max];
            }

            return [this.d_value ?? this.min];
        },
        getHandleValue(index) {
            const vals = this.values();

            return vals[index] ?? this.min;
        },
        isHandleDisabled(index) {
            if (this.disabled) return true;

            if (this.range) {
                if (index === 0 && this.disabledMinHandle) return true;

                if (index === 1 && this.disabledMaxHandle) return true;
            }

            return false;
        },
        getValuePercent(val) {
            const range = this.max - this.min;

            if (!range) return 0;

            return Math.min(Math.max(((val - this.min) / range) * 100, 0), 100);
        }
    },
    computed: {
        value() {
            if (this.range) {
                return [this.d_value?.[0] ?? this.min, this.d_value?.[1] ?? this.max];
            }

            return this.d_value ?? this.min;
        },
        horizontal() {
            return this.orientation === 'horizontal';
        },
        vertical() {
            return this.orientation === 'vertical';
        },
        handlePosition() {
            return this.getValuePercent(this.getHandleValue(0));
        },
        rangeStartPosition() {
            if (this.value && this.value[0] !== undefined) {
                return this.getValuePercent(this.value[0]);
            }

            return 0;
        },
        rangeEndPosition() {
            if (this.value && this.value.length === 2 && this.value[1] !== undefined) {
                return this.getValuePercent(this.value[1]);
            }

            return 100;
        },
        dataP() {
            return cn({
                [this.orientation]: this.orientation
            });
        }
    }
};
</script>
