import gsap from 'gsap';

interface TrailImage {
    url: string;
    alt: string;
}

interface TrailEffectOptions {
    container: HTMLElement;
    images: TrailImage[];
    // Element to listen for mouse/touch move on, when different from
    // `container`. `container` typically has `pointer-events: none` (so the
    // spawned images don't block clicks on real content behind them), which
    // also means it can never be an event target itself — listen on an
    // interactive ancestor instead and pass it here. Defaults to `container`.
    listenTarget?: HTMLElement;
    maxImages?: number;
    fadeInDuration?: number;
    fadeOutDuration?: number;
    fadeOutDelay?: number;
    imageSize?: number;
    // Minimum cursor travel (px) between spawns — without this, a single
    // mousemove burst spawns overlapping images on top of each other.
    // Defaults to `imageSize`, i.e. images spawn edge-to-edge, never
    // overlapping.
    minSpawnDistance?: number;
}

/**
 * TrailEffect - Creates an interactive trail of images following mouse/touch
 *
 * Images appear at cursor position with fadeIn animation and disappear
 * after a delay with fadeOut. Optimized for performance using GSAP
 * and transform-based positioning.
 *
 * @example
 * const trail = new TrailEffect({
 *   container: document.querySelector('.trail-container'),
 *   images: [{url: '/img1.jpg', alt: 'Image 1'}],
 *   maxImages: 8
 * });
 * trail.start();
 */
export class TrailEffect {
    private container: HTMLElement;
    private listenTarget: HTMLElement;
    private images: TrailImage[];
    private maxImages: number;
    private fadeInDuration: number;
    private fadeOutDuration: number;
    private fadeOutDelay: number;
    private imageSize: number;
    private minSpawnDistance: number;
    private lastSpawnPos: { x: number; y: number } | null = null;
    private activeImages: HTMLImageElement[] = [];
    private imagePool: HTMLImageElement[] = [];
    private currentImageIndex = 0;
    private isActive = false;
    private boundHandleMove: (e: MouseEvent | TouchEvent) => void;

    constructor(options: TrailEffectOptions) {
        this.container = options.container;
        this.listenTarget = options.listenTarget ?? options.container;
        this.images = options.images;
        this.maxImages = options.maxImages ?? 8;
        this.fadeInDuration = options.fadeInDuration ?? 0.3;
        this.fadeOutDuration = options.fadeOutDuration ?? 0.5;
        this.fadeOutDelay = options.fadeOutDelay ?? 1.2;
        this.imageSize = options.imageSize ?? 200;
        this.minSpawnDistance = options.minSpawnDistance ?? this.imageSize;

        this.boundHandleMove = this.handleMove.bind(this);

        // Pre-create image pool for performance
        this.createImagePool();
    }

    /**
     * Pre-create image elements to avoid DOM creation during interaction
     */
    private createImagePool(): void {
        if (this.images.length === 0) return;

        for (let i = 0; i < this.maxImages; i++) {
            const img = document.createElement('img');
            img.className = 'trail-image';
            img.style.cssText = `
                position: absolute;
                pointer-events: none;
                opacity: 0;
                will-change: transform, opacity;
                max-width: ${this.imageSize}px;
                max-height: ${this.imageSize}px;
                object-fit: cover;
            `;
            img.setAttribute('aria-hidden', 'true');
            this.imagePool.push(img);
        }
    }

    /**
     * Get next image from pool, reusing existing elements
     */
    private getImageFromPool(): HTMLImageElement | null {
        if (this.images.length === 0) return null;

        // Find unused image in pool
        let img = this.imagePool.find(i => !this.activeImages.includes(i));

        // If all images active, reuse oldest one
        if (!img && this.activeImages.length > 0) {
            img = this.activeImages.shift()!;
            gsap.killTweensOf(img);
        }

        if (!img) return null;

        // Set image source (cycle through available images)
        const imageData = this.images[this.currentImageIndex % this.images.length];
        if (!imageData) return null;

        img.src = imageData.url;
        img.alt = imageData.alt;
        this.currentImageIndex++;

        return img;
    }

    /**
     * Handle mouse/touch move events
     */
    private handleMove(e: MouseEvent | TouchEvent): void {
        if (!this.isActive) return;

        // Get cursor position. `TouchEvent` isn't defined at all on some
        // browsers (e.g. desktop Safari without touch support) — checking
        // `'touches' in e` avoids referencing the global.
        let x: number, y: number;
        if ('touches' in e) {
            const touch = e.touches[0];
            if (!touch) return;
            x = touch.clientX;
            y = touch.clientY;
        } else {
            x = e.clientX;
            y = e.clientY;
        }

        // Throttle by distance travelled, not by event frequency — spawns
        // stay evenly spaced along the cursor path instead of clumping
        // (and overlapping) on fast mousemove bursts.
        if (this.lastSpawnPos) {
            const dx = x - this.lastSpawnPos.x;
            const dy = y - this.lastSpawnPos.y;
            if (Math.sqrt(dx * dx + dy * dy) < this.minSpawnDistance) {
                return;
            }
        }
        this.lastSpawnPos = { x, y };

        this.createTrailImage(x, y);
    }

    /**
     * Create and animate a trail image at specified position
     */
    private createTrailImage(x: number, y: number): void {
        const img = this.getImageFromPool();
        if (!img) return;

        // x/y are viewport-relative (clientX/clientY); convert to a position
        // relative to the (absolutely-positioned) container, which isn't
        // necessarily viewport-aligned itself (e.g. a container further down
        // a scrolled, non-fixed section).
        const containerRect = this.container.getBoundingClientRect();

        // Position image centered on cursor, with a slight random tilt for a
        // hand-placed, non-mechanical feel.
        gsap.set(img, {
            x: x - containerRect.left - this.imageSize / 2,
            y: y - containerRect.top - this.imageSize / 2,
            rotation: gsap.utils.random(-5, 5),
            opacity: 0,
            scale: 0.8,
        });

        // Add to container if not already present
        if (!img.parentElement) {
            this.container.appendChild(img);
        }

        this.activeImages.push(img);

        // Fade in animation
        gsap.to(img, {
            opacity: 1,
            scale: 1,
            duration: this.fadeInDuration,
            ease: 'power2.out',
        });

        // Fade out animation (delayed)
        gsap.to(img, {
            opacity: 0,
            scale: 0.9,
            duration: this.fadeOutDuration,
            delay: this.fadeInDuration + this.fadeOutDelay,
            ease: 'power2.in',
            onComplete: () => {
                // Remove from active list when animation completes
                const index = this.activeImages.indexOf(img);
                if (index > -1) {
                    this.activeImages.splice(index, 1);
                }
            },
        });
    }

    /**
     * Start trail effect (listen to mouse/touch events)
     */
    public start(): void {
        if (this.isActive || this.images.length === 0) return;

        this.isActive = true;
        this.listenTarget.addEventListener('mousemove', this.boundHandleMove);
        this.listenTarget.addEventListener('touchmove', this.boundHandleMove, { passive: true });
    }

    /**
     * Stop trail effect and cleanup
     */
    public stop(): void {
        if (!this.isActive) return;

        this.isActive = false;
        this.listenTarget.removeEventListener('mousemove', this.boundHandleMove);
        this.listenTarget.removeEventListener('touchmove', this.boundHandleMove);

        // Fade out all active images
        this.activeImages.forEach(img => {
            gsap.to(img, {
                opacity: 0,
                duration: this.fadeOutDuration,
                ease: 'power2.in',
            });
        });

        this.activeImages = [];
    }

    /**
     * Destroy trail effect and remove all images from DOM
     */
    public destroy(): void {
        this.stop();

        // Kill all animations
        this.imagePool.forEach(img => {
            gsap.killTweensOf(img);
            img.remove();
        });

        this.imagePool = [];
        this.activeImages = [];
    }
}
