<template>
    <component v-if="!asChild" :is="as" :class="cx('root')" :style="sx('root')" v-bind="attrs">
        <slot />
    </component>
    <slot v-else :class="cx('root')" :a11yAttrs="a11yAttrs" />
</template>

<script>
import { isElement } from '@primeuix/utils/dom';
import { mergeProps } from 'vue';
import BaseScrollArea from './BaseScrollArea.vue';

const SCROLL_TIMEOUT = 500;
const MIN_HANDLE_SIZE = 20;

export default {
    name: 'ScrollArea',
    extends: BaseScrollArea,
    inheritAttrs: false,
    resizeObserver: null,
    resizeObserverRaf: null,
    rafId: null,
    scrollXTimeout: null,
    scrollYTimeout: null,
    isDragging: false,
    dragOrientation: null,
    dragStart: { pointerPos: 0, scrollPos: 0 },
    prevScroll: { top: 0, left: 0 },
    data() {
        return {
            viewportEl: null,
            scrollbarYEl: null,
            scrollbarXEl: null,
            hovering: false,
            scrollingX: false,
            scrollingY: false,
            scrollMeta: {
                hiddenX: true,
                hiddenY: true,
                cornerWidth: 0,
                cornerHeight: 0,
                xBefore: false,
                xAfter: false,
                yBefore: false,
                yAfter: false
            }
        };
    },
    watch: {
        viewportEl(newEl, oldEl) {
            if (oldEl && oldEl !== newEl) {
                this.unbindResizeObserver();
            }

            if (newEl) {
                this.bindResizeObserver();
                this.$nextTick(this.computeHandle);
            } else {
                this.unbindResizeObserver();
            }
        }
    },
    beforeUnmount() {
        this.unbindResizeObserver();

        if (this.scrollXTimeout) {
            clearTimeout(this.scrollXTimeout);
            this.scrollXTimeout = null;
        }

        if (this.scrollYTimeout) {
            clearTimeout(this.scrollYTimeout);
            this.scrollYTimeout = null;
        }

        if (this.rafId) {
            cancelAnimationFrame(this.rafId);
            this.rafId = null;
        }
    },
    methods: {
        setViewportEl(el) {
            this.viewportEl = isElement(el) ? el : null;
        },
        setScrollbarEl(el, orientation) {
            const value = isElement(el) ? el : null;

            if (orientation === 'vertical') this.scrollbarYEl = value;
            else this.scrollbarXEl = value;

            if (this.viewportEl) {
                this.$nextTick(this.computeHandle);
            }
        },
        getHandleSize(scrollbar, isVertical) {
            if (!scrollbar) return 0;
            const value = scrollbar.style.getPropertyValue(isVertical ? '--px-handle-height' : '--px-handle-width');

            return parseFloat(value) || 0;
        },
        bindResizeObserver() {
            if (!this.viewportEl || typeof ResizeObserver === 'undefined') return;

            this.unbindResizeObserver();

            this.resizeObserver = new ResizeObserver(() => {
                if (this.resizeObserverRaf) return;

                this.resizeObserverRaf = requestAnimationFrame(() => {
                    this.resizeObserverRaf = null;
                    this.computeHandle();
                });
            });

            this.resizeObserver.observe(this.viewportEl);

            if (this.viewportEl.firstElementChild) {
                this.resizeObserver.observe(this.viewportEl.firstElementChild);
            }
        },
        unbindResizeObserver() {
            if (this.resizeObserverRaf) {
                cancelAnimationFrame(this.resizeObserverRaf);
                this.resizeObserverRaf = null;
            }

            if (this.resizeObserver) {
                this.resizeObserver.disconnect();
                this.resizeObserver = null;
            }
        },
        computeHandle() {
            const vp = this.viewportEl;

            if (!vp) return;

            const { scrollHeight, scrollWidth, clientHeight, clientWidth, scrollTop, scrollLeft } = vp;
            const yHidden = clientHeight >= scrollHeight;
            const xHidden = clientWidth >= scrollWidth;

            const scrollbarY = this.scrollbarYEl;
            const scrollbarX = this.scrollbarXEl;

            const cw = !yHidden && !xHidden && scrollbarY ? scrollbarY.offsetWidth : 0;
            const ch = !yHidden && !xHidden && scrollbarX ? scrollbarX.offsetHeight : 0;
            const scrollbarYHeight = scrollbarY && !yHidden ? scrollbarY.offsetHeight : 0;
            const scrollbarXWidth = scrollbarX && !xHidden ? scrollbarX.offsetWidth : 0;

            const next = {
                hiddenX: xHidden,
                hiddenY: yHidden,
                cornerWidth: cw,
                cornerHeight: ch,
                xBefore: !xHidden && scrollLeft > 0,
                xAfter: !xHidden && scrollWidth - clientWidth - scrollLeft > 1,
                yBefore: !yHidden && scrollTop > 0,
                yAfter: !yHidden && scrollHeight - clientHeight - scrollTop > 1
            };

            const prev = this.scrollMeta;
            const changed =
                prev.hiddenX !== next.hiddenX ||
                prev.hiddenY !== next.hiddenY ||
                prev.cornerWidth !== next.cornerWidth ||
                prev.cornerHeight !== next.cornerHeight ||
                prev.xBefore !== next.xBefore ||
                prev.xAfter !== next.xAfter ||
                prev.yBefore !== next.yBefore ||
                prev.yAfter !== next.yAfter;

            if (changed) {
                this.scrollMeta = next;

                if (prev.hiddenX !== next.hiddenX || prev.hiddenY !== next.hiddenY) {
                    this.$nextTick(this.computeHandle);
                }
            }

            if (scrollbarY && !yHidden && scrollbarYHeight > 0) {
                const handleRatio = clientHeight / scrollHeight;
                const handleHeight = Math.max(handleRatio * scrollbarYHeight, MIN_HANDLE_SIZE);
                const availableTrack = scrollbarYHeight - handleHeight;
                const maxScrollTop = scrollHeight - clientHeight;
                const scrollRatio = maxScrollTop > 0 ? scrollTop / maxScrollTop : 0;
                const offsetPx = scrollRatio * availableTrack;

                scrollbarY.style.setProperty('--px-handle-height', `${handleHeight}px`);
                scrollbarY.style.setProperty('--px-handle-offset', `${offsetPx}px`);
            } else if (scrollbarY) {
                scrollbarY.style.removeProperty('--px-handle-height');
                scrollbarY.style.removeProperty('--px-handle-offset');
            }

            if (scrollbarX && !xHidden && scrollbarXWidth > 0) {
                const handleRatio = clientWidth / scrollWidth;
                const handleWidth = Math.max(handleRatio * scrollbarXWidth, MIN_HANDLE_SIZE);
                const availableTrack = scrollbarXWidth - handleWidth;
                const maxScrollLeft = scrollWidth - clientWidth;
                const scrollRatio = maxScrollLeft > 0 ? scrollLeft / maxScrollLeft : 0;
                const offsetPx = scrollRatio * availableTrack;

                scrollbarX.style.setProperty('--px-handle-width', `${handleWidth}px`);
                scrollbarX.style.setProperty('--px-handle-offset', `${offsetPx}px`);
            } else if (scrollbarX) {
                scrollbarX.style.removeProperty('--px-handle-width');
                scrollbarX.style.removeProperty('--px-handle-offset');
            }
        },
        onScroll() {
            if (!this.rafId) {
                this.rafId = requestAnimationFrame(() => {
                    this.rafId = null;
                    this.computeHandle();
                });
            }

            const vp = this.viewportEl;

            if (!vp) return;

            const { scrollTop, scrollLeft } = vp;

            if (!this.prevScroll) this.prevScroll = { top: 0, left: 0 };
            const prev = this.prevScroll;

            if (scrollTop !== prev.top) {
                this.scrollingY = true;
                if (this.scrollYTimeout) clearTimeout(this.scrollYTimeout);
                this.scrollYTimeout = setTimeout(() => {
                    this.scrollingY = false;
                }, SCROLL_TIMEOUT);
            }

            if (scrollLeft !== prev.left) {
                this.scrollingX = true;
                if (this.scrollXTimeout) clearTimeout(this.scrollXTimeout);
                this.scrollXTimeout = setTimeout(() => {
                    this.scrollingX = false;
                }, SCROLL_TIMEOUT);
            }

            prev.top = scrollTop;
            prev.left = scrollLeft;
        },
        onPointerEnter(event) {
            if (event.pointerType !== 'touch') {
                this.hovering = true;
            }
        },
        onPointerLeave() {
            this.hovering = false;
        },
        onScrollbarPointerDown(event, orientation) {
            if (event.pointerType === 'mouse' && event.button !== 0) return;

            event.preventDefault();

            const isVertical = orientation === 'vertical';
            const scrollbar = isVertical ? this.scrollbarYEl : this.scrollbarXEl;
            const vp = this.viewportEl;

            if (!scrollbar || !vp) return;

            if (scrollbar.setPointerCapture && event.pointerId != null) {
                scrollbar.setPointerCapture(event.pointerId);
            }

            this.isDragging = true;
            this.dragOrientation = orientation;

            const rect = scrollbar.getBoundingClientRect();
            const handleSize = this.getHandleSize(scrollbar, isVertical);
            const trackSize = isVertical ? rect.height : rect.width;
            const effectiveTrack = trackSize - handleSize;

            if (effectiveTrack > 0) {
                const clickPos = (isVertical ? event.clientY - rect.top : event.clientX - rect.left) - handleSize / 2;
                const ratio = Math.max(0, Math.min(1, clickPos / effectiveTrack));
                const maxScroll = isVertical ? vp.scrollHeight - vp.clientHeight : vp.scrollWidth - vp.clientWidth;

                if (isVertical) {
                    vp.scrollTop = ratio * maxScroll;
                } else {
                    vp.scrollLeft = ratio * maxScroll;
                }
            }

            this.dragStart = {
                pointerPos: isVertical ? event.clientY : event.clientX,
                scrollPos: isVertical ? vp.scrollTop : vp.scrollLeft
            };
        },
        onScrollbarPointerMove(event) {
            if (!this.isDragging) return;

            event.preventDefault();

            const isVertical = this.dragOrientation === 'vertical';
            const vp = this.viewportEl;
            const scrollbar = isVertical ? this.scrollbarYEl : this.scrollbarXEl;

            if (!vp || !scrollbar) return;

            const rect = scrollbar.getBoundingClientRect();
            const handleSize = this.getHandleSize(scrollbar, isVertical);
            const trackSize = isVertical ? rect.height : rect.width;
            const effectiveTrack = trackSize - handleSize;

            if (effectiveTrack <= 0) return;

            const currentPos = isVertical ? event.clientY : event.clientX;
            const delta = currentPos - this.dragStart.pointerPos;
            const maxScroll = isVertical ? vp.scrollHeight - vp.clientHeight : vp.scrollWidth - vp.clientWidth;
            const scrollDelta = (delta / effectiveTrack) * maxScroll;

            if (isVertical) {
                vp.scrollTop = this.dragStart.scrollPos + scrollDelta;
            } else {
                vp.scrollLeft = this.dragStart.scrollPos + scrollDelta;
            }
        },
        onScrollbarPointerUp(event) {
            if (!this.isDragging) return;

            event.preventDefault();

            if (event.currentTarget?.hasPointerCapture?.(event.pointerId)) {
                event.currentTarget.releasePointerCapture(event.pointerId);
            }

            this.isDragging = false;
            this.dragOrientation = null;
        },
        onScrollbarWheel(event, orientation) {
            const vp = this.viewportEl;

            if (!vp || event.ctrlKey) return;

            event.preventDefault();

            if (orientation === 'vertical') {
                vp.scrollTop += event.deltaY;
            } else {
                vp.scrollLeft += event.deltaX || event.deltaY;
            }
        },
        onHandlePointerDown(event, orientation) {
            if (event.pointerType === 'mouse' && event.button !== 0) return;

            event.preventDefault();
            event.stopPropagation();

            const isVertical = orientation === 'vertical';
            const scrollbar = isVertical ? this.scrollbarYEl : this.scrollbarXEl;
            const vp = this.viewportEl;

            if (!scrollbar || !vp) return;

            if (scrollbar.setPointerCapture && event.pointerId != null) {
                scrollbar.setPointerCapture(event.pointerId);
            }

            this.isDragging = true;
            this.dragOrientation = orientation;
            this.dragStart = {
                pointerPos: isVertical ? event.clientY : event.clientX,
                scrollPos: isVertical ? vp.scrollTop : vp.scrollLeft
            };
        }
    },
    computed: {
        rootStyle() {
            return {
                '--px-corner-height': `${this.scrollMeta.cornerHeight}px`,
                '--px-corner-width': `${this.scrollMeta.cornerWidth}px`
            };
        },
        hasOverflowX() {
            return !this.scrollMeta.hiddenX;
        },
        hasOverflowY() {
            return !this.scrollMeta.hiddenY;
        },
        attrs() {
            return mergeProps(this.a11yAttrs, this.ptmi('root'));
        },
        a11yAttrs() {
            return {
                'data-variant': this.variant,
                'data-mask': this.mask ? '' : undefined,
                'data-scrolling': this.scrollingX || this.scrollingY ? '' : undefined,
                'data-has-overflow-x': this.hasOverflowX ? '' : undefined,
                'data-has-overflow-y': this.hasOverflowY ? '' : undefined,
                'data-scroll-x-before': this.scrollMeta.xBefore ? '' : undefined,
                'data-scroll-x-after': this.scrollMeta.xAfter ? '' : undefined,
                'data-scroll-y-before': this.scrollMeta.yBefore ? '' : undefined,
                'data-scroll-y-after': this.scrollMeta.yAfter ? '' : undefined,
                role: 'presentation',
                style: this.rootStyle,
                onPointerenter: this.onPointerEnter,
                onPointerleave: this.onPointerLeave
            };
        }
    }
};
</script>
