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

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

export default {
    name: 'GalleryItem',
    extends: BaseGalleryItem,
    inheritAttrs: false,
    inject: ['$pcGallery'],
    itemEl: null,
    pointerStart: { x: 0, y: 0 },
    isPinching: false,
    isDragging: false,
    dragStart: { x: 0, y: 0 },
    hasDragged: false,
    wheelSyncTimer: null,
    rotateTimer: null,
    wheelHandler: null,
    resizeHandler: null,
    data() {
        return {
            index: -1,
            position: { x: 0, y: 0 },
            scale: 1,
            rotation: 0,
            isRotating: false,
            flip: { x: 1, y: 1 }
        };
    },
    created() {
        this.pointerData = new Map();
        this.initialPinchDistance = 0;
        this.initialPinchScale = 1;
        this.liveScale = 1;
        this.livePosition = { x: 0, y: 0 };
    },
    mounted() {
        if (this.$pcGallery) {
            const newIndex = this.$pcGallery.registerItem(this.index >= 0 ? this.index : null);

            if (newIndex !== this.index) this.index = newIndex;
        }

        this.bindWheelListener();
        this.bindResizeListener();
        this.$nextTick(() => this.calculateItemSize());
    },
    beforeUnmount() {
        this.unbindWheelListener();
        this.unbindResizeListener();
        this.clearTimers();
    },
    watch: {
        scale(value) {
            this.liveScale = value;

            if (value <= this.normalScale) {
                this.position = { x: 0, y: 0 };
                this.livePosition = { x: 0, y: 0 };
            }

            this.reportState();
        },
        position(value) {
            this.livePosition = value;
        },
        rotation() {
            if (this.active) this.calculateItemSize();
            this.reportState();
        },
        flip: {
            handler() {
                this.reportState();
            },
            deep: true
        },
        '$pcGallery.d_activeIndex'() {
            if (this.active) this.calculateItemSize();
        },
        '$pcGallery.isFullscreen'() {
            if (this.active) this.calculateItemSize();
        },
        '$pcGallery.pendingAction'(action) {
            if (!action || !this.active) return;

            switch (action.type) {
                case 'zoom-in':
                    this.zoomIn();
                    break;

                case 'zoom-out':
                    this.zoomOut();
                    break;

                case 'rotate-left':
                    this.rotateLeft();
                    break;

                case 'rotate-right':
                    this.rotateRight();
                    break;

                case 'flip-x':
                    this.flipX();
                    break;

                case 'flip-y':
                    this.flipY();
                    break;

                case 'download':
                    this.download();
                    break;
            }

            this.$pcGallery?.clearPendingAction();
        },
        active(value) {
            if (value) this.reportState();
        }
    },
    computed: {
        active() {
            return this.index >= 0 && (this.$pcGallery?.isItemActive(this.index) ?? false);
        },
        cssVars() {
            return {
                '--px-position-x': `${this.position.x}px`,
                '--px-position-y': `${this.position.y}px`,
                '--px-scale': `${this.scale}`,
                '--px-rotation': `${this.rotation}deg`,
                '--px-flip-x': this.flip.x,
                '--px-flip-y': this.flip.y
            };
        },
        attrs() {
            return mergeProps(this.a11yAttrs, this.ptmi('root'));
        },
        a11yAttrs() {
            return {
                ref: this.setRef,
                'data-pc-name': 'galleryitem',
                'data-pc-section': 'root',
                'data-active': this.active ? '' : undefined,
                'data-rotating': this.isRotating ? '' : undefined,
                'data-index': this.index,
                style: this.cssVars,
                onClick: this.onClick,
                onPointerdown: this.onPointerDown,
                onPointermove: this.onPointerMove,
                onPointerup: this.onPointerUp,
                onDragstart: this.onDragStartPrevent
            };
        }
    },
    methods: {
        reportState() {
            if (!this.active || !this.$pcGallery) return;

            this.$pcGallery.reportItemState({
                zoomed: this.scale > this.normalScale,
                rotated: this.rotation !== 0,
                flipped: this.flip.x === -1 || this.flip.y === -1,
                scale: this.scale,
                rotation: this.rotation,
                flip: this.flip
            });
        },
        getEl() {
            return this.itemEl;
        },
        setRef(el) {
            const resolved = isElement(el) ? el : (el?.$el ?? null);

            this.itemEl = isElement(resolved) ? resolved : null;
        },
        onDragStartPrevent(event) {
            event?.preventDefault?.();
        },
        clearTimers() {
            if (this.wheelSyncTimer) {
                clearTimeout(this.wheelSyncTimer);
                this.wheelSyncTimer = null;
            }

            if (this.rotateTimer) {
                clearTimeout(this.rotateTimer);
                this.rotateTimer = null;
            }
        },
        calculateItemSize() {
            const el = this.itemEl;
            const contentEl = this.$pcGallery?.contentEl;

            if (!el || !contentEl) return;

            const imageElement = el.firstElementChild;

            if (!imageElement) return;

            const contentRect = contentEl.getBoundingClientRect();

            let naturalWidth = imageElement.naturalWidth || imageElement.offsetWidth;
            let naturalHeight = imageElement.naturalHeight || imageElement.offsetHeight;

            if (naturalWidth === 0 || naturalHeight === 0) return;

            const isRotated = Math.abs(this.rotation) % 180 === 90;

            if (isRotated) {
                [naturalWidth, naturalHeight] = [naturalHeight, naturalWidth];
            }

            const naturalAspectRatio = naturalWidth / naturalHeight;
            const contentAspectRatio = contentRect.width / contentRect.height;

            let targetWidth;
            let targetHeight;

            if (naturalAspectRatio > contentAspectRatio) {
                targetWidth = Math.min(contentRect.width * 0.99, naturalWidth);
                targetHeight = targetWidth / naturalAspectRatio;
            } else {
                targetHeight = Math.min(contentRect.height * 0.99, naturalHeight);
                targetWidth = targetHeight * naturalAspectRatio;
            }

            if (isRotated) {
                imageElement.style.width = `${targetHeight}px`;
                imageElement.style.height = `${targetWidth}px`;
                el.style.width = targetHeight > 0 ? `${targetHeight}px` : 'auto';
                el.style.height = targetWidth > 0 ? `${targetWidth}px` : 'auto';
            } else {
                imageElement.style.width = `${targetWidth}px`;
                imageElement.style.height = `${targetHeight}px`;
                el.style.width = targetWidth > 0 ? `${targetWidth}px` : 'auto';
                el.style.height = targetHeight > 0 ? `${targetHeight}px` : 'auto';
            }

            el.style.aspectRatio = `${naturalWidth / naturalHeight}`;
        },
        calculateConstraints(targetScale) {
            const el = this.itemEl;
            const contentEl = this.$pcGallery?.contentEl;

            if (!el || !contentEl) return { minX: 0, maxX: 0, minY: 0, maxY: 0 };

            const itemElement = el.firstElementChild;

            if (!itemElement) return { minX: 0, maxX: 0, minY: 0, maxY: 0 };

            const contentRect = contentEl.getBoundingClientRect();
            const scaleToUse = targetScale !== undefined ? targetScale : this.liveScale;

            let originalWidth = itemElement.offsetWidth;
            let originalHeight = itemElement.offsetHeight;

            const isRotated = Math.abs(this.rotation) % 180 === 90;

            if (isRotated) {
                [originalWidth, originalHeight] = [originalHeight, originalWidth];
            }

            const scaledWidth = originalWidth * scaleToUse;
            const scaledHeight = originalHeight * scaleToUse;

            const contentCenterX = contentRect.width / 2;
            const contentCenterY = contentRect.height / 2;

            const halfScaledWidth = scaledWidth / 2;
            const halfScaledHeight = scaledHeight / 2;

            const maxX = halfScaledWidth > contentCenterX ? halfScaledWidth - contentCenterX : 0;
            const minX = halfScaledWidth > contentCenterX ? -(halfScaledWidth - contentCenterX) : 0;
            const maxY = halfScaledHeight > contentCenterY ? halfScaledHeight - contentCenterY : 0;
            const minY = halfScaledHeight > contentCenterY ? -(halfScaledHeight - contentCenterY) : 0;

            return { minX, maxX, minY, maxY };
        },
        zoomIn() {
            this.scale = this.zoomedScale;

            const el = this.itemEl;

            if (el) el.style.cursor = 'zoom-out';
        },
        zoomOut() {
            this.scale = this.normalScale;
            this.position = { x: 0, y: 0 };

            const el = this.itemEl;

            if (el) el.style.cursor = 'zoom-in';
        },
        rotateLeft() {
            this.markRotating();
            this.rotation -= 90;
        },
        rotateRight() {
            this.markRotating();
            this.rotation += 90;
        },
        markRotating() {
            this.isRotating = true;

            const el = this.itemEl;

            if (el) el.style.transition = '';

            if (this.rotateTimer) clearTimeout(this.rotateTimer);

            this.rotateTimer = setTimeout(() => {
                this.isRotating = false;
                this.rotateTimer = null;
            }, 300);
        },
        flipX() {
            this.flip = { ...this.flip, x: Math.sign(this.flip.x) * -1 };
        },
        flipY() {
            this.flip = { ...this.flip, y: Math.sign(this.flip.y) * -1 };
        },
        download() {
            const el = this.itemEl;

            if (!el) return;

            const imageElement = el.querySelector('img');

            if (!imageElement || !imageElement.src) return;

            const link = document.createElement('a');

            link.href = imageElement.src;
            const urlParts = imageElement.src.split('/');

            link.download = urlParts[urlParts.length - 1] || 'image.jpg';
            link.target = '_blank';

            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        },
        onClick(e) {
            if (this.hasDragged) {
                this.hasDragged = false;

                return;
            }

            if (this.liveScale !== this.normalScale) {
                this.zoomOut();

                return;
            }

            const el = this.itemEl;

            if (el) {
                const itemRect = el.getBoundingClientRect();
                const itemCenterX = itemRect.width / 2;
                const itemCenterY = itemRect.height / 2;

                const clickX = e.clientX - itemRect.left;
                const clickY = e.clientY - itemRect.top;

                const offsetX = itemCenterX - clickX;
                const offsetY = itemCenterY - clickY;

                const zoomOffsetX = offsetX * (this.zoomedScale - 1);
                const zoomOffsetY = offsetY * (this.zoomedScale - 1);

                const constraints = this.calculateConstraints(this.zoomedScale);
                const constrainedX = Math.max(constraints.minX, Math.min(constraints.maxX, zoomOffsetX));
                const constrainedY = Math.max(constraints.minY, Math.min(constraints.maxY, zoomOffsetY));

                this.position = { x: constrainedX, y: constrainedY };
            }

            this.zoomIn();
        },
        onPointerDown(e) {
            e.currentTarget.setPointerCapture(e.pointerId);
            this.pointerData.set(e.pointerId, { x: e.clientX, y: e.clientY });
            this.pointerStart = { x: e.clientX, y: e.clientY };

            if (e.pointerType === 'touch' && this.pointerData.size >= 2) {
                this.isDragging = false;
                this.initialPinchDistance = 0;
                this.initialPinchScale = this.liveScale;
            } else if (this.liveScale > 1) {
                if (e.pointerType === 'mouse' || (e.pointerType === 'touch' && this.pointerData.size === 1)) {
                    this.isDragging = true;
                    this.dragStart = { x: e.clientX - this.livePosition.x, y: e.clientY - this.livePosition.y };
                    this.hasDragged = false;
                }
            }
        },
        onPointerMove(e) {
            if (!this.pointerData.has(e.pointerId)) return;

            const el = this.itemEl;

            if (!el) return;

            el.style.transition = 'none';

            if (e.pointerType === 'mouse' && !el.style.cursor) {
                el.style.cursor = this.liveScale > this.normalScale ? 'zoom-out' : 'zoom-in';
            }

            this.pointerData.set(e.pointerId, { x: e.clientX, y: e.clientY });

            const pointers = Array.from(this.pointerData.values());

            if (pointers.length === 2) {
                const [p1, p2] = pointers;
                const distance = Math.hypot(p2.x - p1.x, p2.y - p1.y);

                this.isPinching = true;
                this.hasDragged = true;

                if (this.initialPinchDistance === 0) {
                    this.initialPinchDistance = distance;
                    this.initialPinchScale = this.liveScale;
                } else {
                    const ratio = distance / this.initialPinchDistance;
                    const newScale = Math.max(this.normalScale, Math.min(this.zoomedScale, this.initialPinchScale * ratio));

                    const constraints = this.calculateConstraints(newScale);
                    const constrainedX = Math.max(constraints.minX, Math.min(constraints.maxX, this.livePosition.x));
                    const constrainedY = Math.max(constraints.minY, Math.min(constraints.maxY, this.livePosition.y));

                    this.liveScale = newScale;
                    this.livePosition = { x: constrainedX, y: constrainedY };

                    el.style.setProperty('--px-scale', `${newScale}`);
                    el.style.setProperty('--px-position-x', `${constrainedX}px`);
                    el.style.setProperty('--px-position-y', `${constrainedY}px`);
                }
            } else if (pointers.length === 1 && this.isDragging) {
                const pointer = pointers[0];
                const newX = pointer.x - this.dragStart.x;
                const newY = pointer.y - this.dragStart.y;
                const constraints = this.calculateConstraints(this.liveScale);

                const computedX = Math.max(constraints.minX, Math.min(constraints.maxX, newX));
                const computedY = Math.max(constraints.minY, Math.min(constraints.maxY, newY));

                this.livePosition = { x: computedX, y: computedY };

                el.style.setProperty('--px-position-x', `${computedX}px`);
                el.style.setProperty('--px-position-y', `${computedY}px`);

                const distFromStart = Math.hypot(pointer.x - this.pointerStart.x, pointer.y - this.pointerStart.y);

                if (distFromStart > 5) this.hasDragged = true;
            }
        },
        onPointerUp(e) {
            const el = this.itemEl;

            if (!el) return;

            el.style.transition = '';

            if (e.pointerType === 'mouse') {
                el.style.cursor = this.liveScale > this.normalScale ? 'zoom-out' : 'zoom-in';
            }

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

            this.pointerData.delete(e.pointerId);

            if (this.pointerData.size < 2) {
                this.initialPinchDistance = 0;
                this.initialPinchScale = 1;
                this.isPinching = false;
            }

            if (this.pointerData.size === 0) {
                this.isDragging = false;
                this.scale = this.liveScale;
                this.position = this.livePosition;
            }
        },
        bindWheelListener() {
            const el = this.itemEl;

            if (!el) return;

            this.wheelHandler = (e) => {
                if (!e.ctrlKey || !this.active) return;

                e.preventDefault();

                const itemEl = this.itemEl;

                if (!itemEl) return;

                itemEl.style.transition = 'none';

                const delta = -e.deltaY;
                const scaleFactor = 1 + delta * 0.01;
                const newScale = Math.max(this.normalScale, Math.min(this.zoomedScale, this.liveScale * scaleFactor));

                const constraints = this.calculateConstraints(newScale);
                const constrainedX = Math.max(constraints.minX, Math.min(constraints.maxX, this.livePosition.x));
                const constrainedY = Math.max(constraints.minY, Math.min(constraints.maxY, this.livePosition.y));

                this.liveScale = newScale;
                this.livePosition = { x: constrainedX, y: constrainedY };

                itemEl.style.setProperty('--px-scale', `${newScale}`);
                itemEl.style.setProperty('--px-position-x', `${constrainedX}px`);
                itemEl.style.setProperty('--px-position-y', `${constrainedY}px`);

                if (this.wheelSyncTimer) clearTimeout(this.wheelSyncTimer);

                this.wheelSyncTimer = setTimeout(() => {
                    const finalEl = this.itemEl;

                    if (finalEl) {
                        finalEl.style.transition = '';
                        finalEl.style.cursor = this.liveScale > this.normalScale ? 'zoom-out' : 'zoom-in';
                    }

                    this.scale = this.liveScale;
                    this.position = this.livePosition;
                    this.wheelSyncTimer = null;
                }, 150);
            };

            el.addEventListener('wheel', this.wheelHandler, { passive: false });
        },
        unbindWheelListener() {
            const el = this.itemEl;

            if (el && this.wheelHandler) el.removeEventListener('wheel', this.wheelHandler);

            this.wheelHandler = null;
        },
        bindResizeListener() {
            this.resizeHandler = () => {
                if (this.active) this.calculateItemSize();
            };

            window.addEventListener('resize', this.resizeHandler);
        },
        unbindResizeListener() {
            if (this.resizeHandler) {
                window.removeEventListener('resize', this.resizeHandler);
                this.resizeHandler = null;
            }
        }
    }
};
</script>
