import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

// Register GSAP plugin with error boundary
try {
    gsap.registerPlugin(ScrollTrigger);
} catch (error) {
    console.error('Failed to register GSAP ScrollTrigger plugin:', error);
    throw new Error('GSAP ScrollTrigger plugin registration failed. Horizontal pin scroll will not work.', { cause: error });
}

export interface HorizontalPinScrollConfig {
    pinTarget: string | HTMLElement;
    trackTarget: string | HTMLElement;
    /** Element whose scroll position decides when the pin engages/releases.
     *  Defaults to pinTarget. Pass a different element (e.g. one that sits
     *  earlier in the document flow) to engage the pin sooner than the
     *  pinned element's own arrival at the trigger position. */
    triggerTarget?: string | HTMLElement;
    scrub?: number | boolean;
    respectReducedMotion?: boolean;
}

/**
 * Pins `pinTarget` in the viewport and scrubs `trackTarget` horizontally as
 * the user keeps scrolling, releasing the pin once the track has been fully
 * revealed — used for the home page's mountain sections.
 */
export class HorizontalPinScroll {
    private tween: gsap.core.Tween | null = null;
    private scrollTrigger: ScrollTrigger | null = null;
    private config: HorizontalPinScrollConfig;

    constructor(config: HorizontalPinScrollConfig) {
        this.config = {
            scrub: 1,
            respectReducedMotion: true,
            ...config
        };
    }

    public play(): void {
        if (this.config.respectReducedMotion && this.shouldReduceMotion()) {
            console.log('[HorizontalPinScroll] Skipped - prefers-reduced-motion');
            return;
        }

        const pinEl = this.resolve(this.config.pinTarget);
        const track = this.resolve(this.config.trackTarget);
        const triggerEl = this.config.triggerTarget ? this.resolve(this.config.triggerTarget) : pinEl;

        if (!pinEl || !track || !triggerEl) {
            console.warn('[HorizontalPinScroll] pinTarget, trackTarget or triggerTarget not found');
            return;
        }

        // Distance is measured against the pinned viewport's width, not the
        // track's own — the track is `width: max-content` so it has no
        // internal overflow of its own (scrollWidth === clientWidth there).
        const distance = (): number => Math.max(0, track.scrollWidth - pinEl.clientWidth);

        // Header clearance: CSS `scroll-margin-top` on the trigger element
        // has NO effect on ScrollTrigger's start calculation — that property
        // only applies to native `scrollIntoView`/scroll-snap, not to
        // getBoundingClientRect-based trigger math. Without an explicit
        // offset here, the pin engages the instant triggerEl's top hits the
        // literal viewport top, i.e. while it's still hidden behind the
        // fixed header — leaving only its lower portion visible once pinned.
        // Read the live header height (it changes at a breakpoint, see
        // --header-height-css in _variables.scss) so the trigger's top
        // aligns with the space just below the header instead.
        const headerHeight = (): number =>
            parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height-css')) || 0;

        // Same recipe as joa.fr's working gallery pin (pin the viewport,
        // scrub the track's x). fromTo (not .to()) so the start value is always the literal 0,
        // not whatever track.x happens to be at the moment GSAP recomputes
        // it — with invalidateOnRefresh + the ScrollTrigger.refresh() calls
        // in home.ts (fired on window load / fonts.ready), a plain .to()
        // would re-capture a mid-scroll x as its new "start", permanently
        // losing the left gutter on first descent until scrolling back past
        // the trigger reset progress to 0.
        this.tween = gsap.fromTo(track, { x: 0 }, {
            x: () => -distance(),
            ease: 'none',
            force3D: true, // explicit compositing layer — Firefox is pickier than Chrome about promoting pinned/scrubbed transforms
            scrollTrigger: {
                trigger: triggerEl,
                start: () => `top top+=${headerHeight()}`,
                end: () => `+=${distance()}`,
                scrub: this.config.scrub,
                pin: pinEl,
                pinSpacing: true,
                invalidateOnRefresh: true,
            },
        });

        this.scrollTrigger = this.tween?.scrollTrigger ?? null;
        pinEl.classList.add('is-horizontal-pinned');
    }

    public destroy(): void {
        this.scrollTrigger?.kill();
        this.tween?.kill();
        this.tween = null;
        this.scrollTrigger = null;
    }

    private resolve(target: string | HTMLElement): HTMLElement | null {
        return typeof target === 'string'
            ? document.querySelector<HTMLElement>(target)
            : target;
    }

    private shouldReduceMotion(): boolean {
        return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    }
}
