import { SnapPointDetector } from './SnapPointDetector.js';
import { PerformanceMonitor } from '../../../../utils/performanceMonitor.js';
import { normalizeWheelDelta, getMaxScroll, lerp, clampScroll, calculateVelocity, calculateScrollProgress, detectScrollDirection } from '../../../../utils/smoothScrollUtils.js';
import { applyRubberBandEffect, applyElasticEdgeEffect, calculateSpringForce } from './effects.js';
import { easeOutQuart, smoothDecel } from './easings.js';

/**
 * Default configuration for smooth scrolling
 */
const DEFAULT_CONFIG = {
    lerp: 0.1,
    easing: smoothDecel,
    respectSnapPoints: true,
    snapEasing: easeOutQuart,
    snapVelocityThreshold: 0.8, // Lower = more sensitive (snaps sooner)
    snapLerp: 0.15, // Snap animation speed (higher = faster)
    elasticEdges: false,
    elasticStrength: 0.3,
    rubberBand: false,
    rubberBandStrength: 0.5,
    wheelMultiplier: 1,
    touchpadMultiplier: 0.4,
    targetFPS: 60,
    adaptiveQuality: true,
};
/**
 * Core smooth scroll engine
 * Manages RAF loop, interpolation, snap detection, and effects
 */
class SmoothScrollEngine {
    constructor(element, config = {}, orientation = 'vertical') {
        this.snapDetector = null;
        this.rafId = null;
        this.lastTime = 0;
        this.isDestroyed = false;
        this.previousPosition = 0;
        this.idleFrames = 0;
        // Programmatic scroll animation
        this.animationStartTime = 0;
        this.animationDuration = 0;
        this.animationStartPosition = 0;
        this.animationTargetPosition = 0;
        this.animationEasing = null;
        this.animationOnComplete = null;
        /**
         * Handle wheel event
         */
        this.handleWheel = (event) => {
            if (this.isDestroyed || !this.state.isRunning)
                return;
            event.preventDefault();
            const { delta, isTouchpad } = normalizeWheelDelta(event, this.orientation);
            const multiplier = isTouchpad
                ? this.config.touchpadMultiplier
                : this.config.wheelMultiplier;
            let adjustedDelta = delta * multiplier;
            // Apply rubber-band effect if enabled
            if (this.config.rubberBand) {
                const max = getMaxScroll(this.element, this.orientation);
                adjustedDelta = applyRubberBandEffect(adjustedDelta, this.state.target, 0, max, this.config.rubberBandStrength);
            }
            this.state.target += adjustedDelta;
            // Cancel snap animation if user scrolls
            if (this.state.isSnapping) {
                this.state.isSnapping = false;
                this.state.targetSnapPoint = null;
            }
            // Cancel programmatic animation
            this.animationOnComplete = null;
            this.wake();
        };
        this.touchStartY = 0;
        this.touchStartX = 0;
        /**
         * Handle touch start
         */
        this.handleTouchStart = (event) => {
            this.touchStartY = event.touches[0].clientY;
            this.touchStartX = event.touches[0].clientX;
        };
        /**
         * Handle touch move
         */
        this.handleTouchMove = (event) => {
            if (this.isDestroyed || !this.state.isRunning)
                return;
            const touchY = event.touches[0].clientY;
            const touchX = event.touches[0].clientX;
            const deltaY = this.touchStartY - touchY;
            const deltaX = this.touchStartX - touchX;
            const delta = this.orientation === 'horizontal' ? deltaX : deltaY;
            // Only prevent default if we're actually scrolling in our direction
            if (Math.abs(delta) > 5) {
                event.preventDefault();
            }
            let adjustedDelta = delta;
            // Apply rubber-band effect if enabled
            if (this.config.rubberBand) {
                const max = getMaxScroll(this.element, this.orientation);
                adjustedDelta = applyRubberBandEffect(adjustedDelta, this.state.target, 0, max, this.config.rubberBandStrength);
            }
            this.state.target += adjustedDelta;
            this.touchStartY = touchY;
            this.touchStartX = touchX;
            // Cancel snap animation
            if (this.state.isSnapping) {
                this.state.isSnapping = false;
                this.state.targetSnapPoint = null;
            }
            this.wake();
        };
        /**
         * Main RAF tick function
         */
        this.tick = (time) => {
            if (!this.state.isRunning || this.isDestroyed)
                return;
            const deltaTime = time - this.lastTime;
            this.lastTime = time;
            // Record frame for performance monitoring
            this.performanceMonitor.recordFrame(time);
            // Handle programmatic animation
            if (this.animationOnComplete) {
                this.animateProgrammaticScroll(time);
            }
            // Handle snap animation
            else if (this.state.isSnapping) {
                this.animateSnap(time, deltaTime);
            }
            // Normal smooth scrolling
            else {
                this.interpolate(deltaTime);
                this.checkForSnap();
            }
            // Apply effects and sync to DOM
            this.applyEffects();
            this.syncToDOM();
            this.updateCallbacks();
            // Detect idle state: stop the loop when settled to save CPU
            const isSettled = Math.abs(this.state.current - this.state.target) < 0.5 &&
                Math.abs(this.state.velocity.current) < 0.1 &&
                !this.state.isSnapping &&
                !this.animationOnComplete;
            if (isSettled) {
                this.idleFrames++;
                if (this.idleFrames >= SmoothScrollEngine.IDLE_THRESHOLD) {
                    // Snap to exact target to avoid sub-pixel drift
                    this.state.current = this.state.target;
                    this.syncToDOM();
                    this.rafId = null; // Mark loop as idle so wake() can restart it
                    return; // Don't schedule next frame; wake() will restart
                }
            }
            else {
                this.idleFrames = 0;
            }
            // Continue loop
            this.rafId = requestAnimationFrame(this.tick);
        };
        this.element = element;
        this.orientation = orientation;
        this.config = { ...DEFAULT_CONFIG, ...config };
        // Initialize state
        const currentScroll = orientation === 'horizontal' ? element.scrollLeft : element.scrollTop;
        this.previousPosition = currentScroll;
        this.state = {
            current: currentScroll,
            target: currentScroll,
            velocity: {
                current: 0,
                previous: 0,
                timestamp: performance.now(),
            },
            isRunning: false,
            isSnapping: false,
            snapProgress: 0,
            snapStartPosition: 0,
            targetSnapPoint: null,
            snapStartTime: 0,
        };
        // Initialize snap detector if enabled
        if (this.config.respectSnapPoints) {
            this.snapDetector = new SnapPointDetector(element, orientation);
        }
        // Initialize performance monitor
        this.performanceMonitor = new PerformanceMonitor(this.config.targetFPS);
        if (this.config.adaptiveQuality) {
            this.performanceMonitor.setOnUpdate((metrics) => {
                if (process.env.NODE_ENV !== 'production') {
                    console.log('[SmoothScroll] Performance:', metrics);
                }
                // Reduce lerp if performance is poor (makes animation less demanding)
                if (metrics.isBelowTarget && this.config.lerp > 0.05) {
                    this.config.lerp = Math.max(0.05, this.config.lerp * 0.9);
                }
            });
        }
        this.bindEvents();
    }
    /**
     * Bind wheel and touch events
     */
    bindEvents() {
        this.element.addEventListener('wheel', this.handleWheel, { passive: false });
        this.element.addEventListener('touchstart', this.handleTouchStart, {
            passive: true,
        });
        this.element.addEventListener('touchmove', this.handleTouchMove, {
            passive: false,
        });
    }
    /**
     * Unbind events
     */
    unbindEvents() {
        this.element.removeEventListener('wheel', this.handleWheel);
        this.element.removeEventListener('touchstart', this.handleTouchStart);
        this.element.removeEventListener('touchmove', this.handleTouchMove);
    }
    /**
     * Wake the RAF loop if it went idle.
     * Called from input handlers so the engine resumes on new input.
     */
    wake() {
        if (this.isDestroyed || !this.state.isRunning)
            return;
        // If the RAF loop has gone idle (no pending frame), restart it
        if (this.rafId === null) {
            this.idleFrames = 0;
            this.lastTime = performance.now();
            this.rafId = requestAnimationFrame(this.tick);
        }
    }
    /**
     * Start the RAF loop
     */
    start() {
        if (this.state.isRunning || this.isDestroyed)
            return;
        this.state.isRunning = true;
        this.lastTime = performance.now();
        this.tick(this.lastTime);
    }
    /**
     * Stop the RAF loop
     */
    stop() {
        this.state.isRunning = false;
        if (this.rafId !== null) {
            cancelAnimationFrame(this.rafId);
            this.rafId = null;
        }
    }
    /**
     * Animate programmatic scrollTo
     */
    animateProgrammaticScroll(time) {
        const elapsed = time - this.animationStartTime;
        const progress = Math.min(1, elapsed / this.animationDuration);
        const easedProgress = this.animationEasing
            ? this.animationEasing(progress)
            : progress;
        this.state.current =
            this.animationStartPosition +
                (this.animationTargetPosition - this.animationStartPosition) *
                    easedProgress;
        this.state.target = this.state.current;
        if (progress >= 1) {
            // Animation complete
            const callback = this.animationOnComplete;
            this.animationOnComplete = null;
            if (callback)
                callback();
        }
    }
    /**
     * Animate to snap point using velocity-based smoothing
     */
    animateSnap(time, deltaTime) {
        if (!this.state.targetSnapPoint) {
            this.state.isSnapping = false;
            return;
        }
        const targetPosition = this.state.targetSnapPoint.position;
        // Use smooth lerp-based animation instead of time-based
        // This feels more natural and less jerky
        this.state.target = targetPosition;
        this.state.current = lerp(this.state.current, targetPosition, this.config.snapLerp);
        // Calculate distance to target
        const distance = Math.abs(this.state.current - targetPosition);
        // Snap complete when very close (within 1px)
        if (distance < 1) {
            const snapPoint = this.state.targetSnapPoint;
            this.state.isSnapping = false;
            this.state.targetSnapPoint = null;
            this.state.current = snapPoint.position;
            this.state.target = snapPoint.position;
            if (this.config.onSnapPointReached) {
                this.config.onSnapPointReached(snapPoint);
            }
        }
    }
    /**
     * Interpolate scroll position
     */
    interpolate(deltaTime) {
        const max = getMaxScroll(this.element, this.orientation);
        // Clamp target (allow overscroll if elastic/rubber-band enabled)
        if (!this.config.elasticEdges && !this.config.rubberBand) {
            this.state.target = clampScroll(this.state.target, 0, max);
        }
        // Interpolate (lerp controls speed directly, easing is for animations only)
        this.state.current = lerp(this.state.current, this.state.target, this.config.lerp);
        // Calculate velocity from actual position change (not from old velocity)
        const newVelocity = calculateVelocity(this.state.current, this.previousPosition, deltaTime);
        this.previousPosition = this.state.current;
        this.state.velocity.previous = this.state.velocity.current;
        this.state.velocity.current = newVelocity;
        this.state.velocity.timestamp = performance.now();
        // Notify velocity changes
        if (this.config.onVelocityChange &&
            Math.abs(newVelocity - this.state.velocity.previous) > 1) {
            this.config.onVelocityChange(newVelocity);
        }
    }
    /**
     * Check if should snap to nearest point
     */
    checkForSnap() {
        if (!this.config.respectSnapPoints ||
            !this.snapDetector ||
            this.state.isSnapping) {
            return;
        }
        // Check if velocity is low enough to snap (user stopped scrolling)
        const isSlowingDown = Math.abs(this.state.velocity.current) < this.config.snapVelocityThreshold;
        // Also check if current and target are close (finished scrolling)
        const isSettled = Math.abs(this.state.current - this.state.target) < 2;
        if (isSlowingDown && isSettled) {
            const nearest = this.snapDetector.findNearestSnapPoint(this.state.current);
            const distanceToSnap = nearest ? Math.abs(nearest.position - this.state.current) : 0;
            // Snap if we're not already on the snap point (0.5px tolerance for float rounding)
            if (nearest && distanceToSnap > 0.5) {
                // For very small distances, jump directly instead of animating
                if (distanceToSnap < 5) {
                    this.state.current = nearest.position;
                    this.state.target = nearest.position;
                    if (this.config.onSnapPointReached) {
                        this.config.onSnapPointReached(nearest);
                    }
                }
                else {
                    // Start snap animation
                    this.state.isSnapping = true;
                    this.state.targetSnapPoint = nearest;
                    this.state.snapStartPosition = this.state.current;
                    this.state.snapStartTime = performance.now();
                }
            }
        }
    }
    /**
     * Apply visual effects (elastic, rubber-band)
     */
    applyEffects() {
        const max = getMaxScroll(this.element, this.orientation);
        // Elastic edges - allow overscroll with resistance
        if (this.config.elasticEdges) {
            // Check if we're out of bounds
            const isOutOfBounds = this.state.current < 0 || this.state.current > max;
            if (isOutOfBounds) {
                // Apply elastic effect to current position
                this.state.current = applyElasticEdgeEffect(this.state.current, 0, max, this.config.elasticStrength);
                // Apply strong spring force to pull back
                const springForce = calculateSpringForce(this.state.current, 0, max, 0.25);
                if (Math.abs(springForce) > 0.1) {
                    this.state.target += springForce;
                }
            }
        }
        else {
            // Hard clamp
            this.state.current = clampScroll(this.state.current, 0, max);
            this.state.target = clampScroll(this.state.target, 0, max);
        }
    }
    /**
     * Sync interpolated position to DOM
     */
    syncToDOM() {
        if (this.orientation === 'horizontal') {
            this.element.scrollLeft = this.state.current;
        }
        else {
            this.element.scrollTop = this.state.current;
        }
    }
    /**
     * Update callbacks with current progress
     */
    updateCallbacks() {
        if (this.config.onScroll) {
            this.config.onScroll(this.getProgress());
        }
    }
    /**
     * Programmatic scroll to position
     */
    scrollTo(position, options = {}) {
        var _a, _b, _c;
        const max = getMaxScroll(this.element, this.orientation);
        const clampedPosition = clampScroll(position, 0, max);
        const duration = (_a = options.duration) !== null && _a !== void 0 ? _a : 800;
        const easing = (_b = options.easing) !== null && _b !== void 0 ? _b : easeOutQuart;
        // Cancel any ongoing animations
        this.state.isSnapping = false;
        this.state.targetSnapPoint = null;
        // Setup animation
        this.animationStartTime = performance.now();
        this.animationDuration = duration;
        this.animationStartPosition = this.state.current;
        this.animationTargetPosition = clampedPosition;
        this.animationEasing = easing;
        this.animationOnComplete = (_c = options.onComplete) !== null && _c !== void 0 ? _c : null;
        // Start if not running, or wake if idle
        if (!this.state.isRunning) {
            this.start();
        }
        else {
            this.wake();
        }
    }
    /**
     * Scroll to element
     */
    scrollToElement(element, options = {}) {
        const containerRect = this.element.getBoundingClientRect();
        const elementRect = element.getBoundingClientRect();
        let position;
        if (this.orientation === 'horizontal') {
            const currentScroll = this.element.scrollLeft;
            position = elementRect.left - containerRect.left + currentScroll;
        }
        else {
            const currentScroll = this.element.scrollTop;
            position = elementRect.top - containerRect.top + currentScroll;
        }
        this.scrollTo(position, options);
    }
    /**
     * Get current velocity
     */
    getVelocity() {
        return this.state.velocity.current;
    }
    /**
     * Get current scroll progress
     */
    getProgress() {
        const max = getMaxScroll(this.element, this.orientation);
        const progress = calculateScrollProgress(this.state.current, max);
        const direction = detectScrollDirection(this.state.velocity.current);
        return {
            current: this.state.current,
            target: this.state.target,
            max,
            progress,
            velocity: this.state.velocity.current,
            direction,
            isAtTop: this.state.current <= 0,
            isAtBottom: this.state.current >= max,
        };
    }
    /**
     * Update configuration
     */
    updateConfig(config) {
        this.config = { ...this.config, ...config };
        // Update snap detector if needed
        if (config.respectSnapPoints !== undefined) {
            if (config.respectSnapPoints && !this.snapDetector) {
                this.snapDetector = new SnapPointDetector(this.element, this.orientation);
            }
            else if (!config.respectSnapPoints && this.snapDetector) {
                this.snapDetector = null;
            }
        }
        // Update performance monitor
        if (config.targetFPS !== undefined) {
            this.performanceMonitor.setTargetFPS(config.targetFPS);
        }
    }
    /**
     * Refresh snap points (call when DOM changes)
     */
    refreshSnapPoints() {
        var _a;
        (_a = this.snapDetector) === null || _a === void 0 ? void 0 : _a.refresh();
    }
    /**
     * Destroy the engine
     */
    destroy() {
        this.isDestroyed = true;
        this.stop();
        this.unbindEvents();
        this.performanceMonitor.destroy();
        this.snapDetector = null;
    }
}
SmoothScrollEngine.IDLE_THRESHOLD = 10; // Stop after N settled frames

export { SmoothScrollEngine };
