/**
 * AnimationScheduler - Coordinates animations, spinners, progress bars, and elapsed time
 * Provides frame-based animation updates and smooth transitions
 */
import { EventEmitter } from 'events';
import { performance } from 'perf_hooks';
export class AnimationScheduler extends EventEmitter {
    animations = new Map();
    animationLoop = null;
    targetFPS = 30;
    lastFrameTime = 0;
    frameInterval;
    isRunning = false;
    disposed = false;
    // Predefined easing functions
    static Easing = {
        linear: (t) => t,
        easeInQuad: (t) => t * t,
        easeOutQuad: (t) => t * (2 - t),
        easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
        easeInCubic: (t) => t * t * t,
        easeOutCubic: (t) => --t * t * t + 1,
        easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
        easeOutElastic: (t) => {
            const p = 0.3;
            return Math.pow(2, -10 * t) * Math.sin((t - p / 4) * (2 * Math.PI) / p) + 1;
        },
    };
    // Predefined spinner frames
    static SpinnerFrames = {
        dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
        dots2: ['⣾', '⣽', '⣻', '⢿', '⡿', '⣟', '⣯', '⣷'],
        dots3: ['⠋', '⠙', '⠚', '⠞', '⠖', '⠦', '⠴', '⠲', '⠳', '⠓'],
        line: ['-', '\\', '|', '/'],
        pipe: ['┤', '┘', '┴', '└', '├', '┌', '┬', '┐'],
        simpleDots: ['.  ', '.. ', '...', '   '],
        simpleDotsScrolling: ['.  ', '.. ', '...', ' ..', '  .', '   '],
        star: ['✶', '✸', '✹', '✺', '✹', '✷'],
        hamburger: ['☱', '☲', '☴'],
        growVertical: ['▁', '▃', '▄', '▅', '▆', '▇', '▆', '▅', '▄', '▃'],
        growHorizontal: ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '▊', '▋', '▌', '▍', '▎'],
        balloon: [' ', '.', 'o', 'O', '@', '*', ' '],
        noise: ['▓', '▒', '░'],
        bounce: ['⠁', '⠂', '⠄', '⠂'],
        boxBounce: ['▖', '▘', '▝', '▗'],
        circle: ['◜', '◠', '◝', '◞', '◡', '◟'],
        arc: ['◜', '◠', '◝', '◞', '◡', '◟'],
        arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
        bouncingBar: [
            '[    ]',
            '[=   ]',
            '[==  ]',
            '[=== ]',
            '[ ===]',
            '[  ==]',
            '[   =]',
            '[    ]',
            '[   =]',
            '[  ==]',
            '[ ===]',
            '[====]',
            '[=== ]',
            '[==  ]',
            '[=   ]',
        ],
    };
    constructor(targetFPS = 30) {
        super();
        this.targetFPS = targetFPS;
        this.frameInterval = 1000 / targetFPS;
    }
    /**
     * Create and register a spinner animation
     */
    createSpinner(id, message, frames = AnimationScheduler.SpinnerFrames.dots) {
        const animation = {
            id,
            type: 'spinner',
            startTime: performance.now(),
            fps: 10, // Spinners typically run at 10 FPS
            frameCount: frames.length,
            currentFrame: 0,
            data: {
                frames,
                message,
            },
        };
        this.register(animation);
        return animation;
    }
    /**
     * Create and register a progress animation
     */
    createProgress(id, current, total, duration = 500) {
        const animation = {
            id,
            type: 'progress',
            startTime: performance.now(),
            duration,
            fps: 60, // Smooth progress animations at 60 FPS
            frameCount: Math.ceil((duration / 1000) * 60),
            currentFrame: 0,
            data: {
                current,
                target: current,
                total,
            },
            easing: AnimationScheduler.Easing.easeOutQuad,
        };
        this.register(animation);
        return animation;
    }
    /**
     * Update progress animation target
     */
    updateProgress(id, newTarget) {
        const animation = this.animations.get(id);
        if (!animation || animation.type !== 'progress')
            return;
        animation.data.current = this.getCurrentProgressValue(animation);
        animation.data.target = newTarget;
        animation.startTime = performance.now();
        animation.currentFrame = 0;
    }
    /**
     * Create and register an elapsed time animation
     */
    createElapsed(id, startTime = Date.now()) {
        const animation = {
            id,
            type: 'elapsed',
            startTime: performance.now(),
            fps: 1, // Update once per second
            frameCount: Infinity,
            currentFrame: 0,
            data: {
                startTime,
                format: this.formatElapsedTime,
            },
        };
        this.register(animation);
        return animation;
    }
    /**
     * Create and register a transition animation
     */
    createTransition(id, from, to, property, duration = 300, easing) {
        const animation = {
            id,
            type: 'transition',
            startTime: performance.now(),
            duration,
            fps: 60,
            frameCount: Math.ceil((duration / 1000) * 60),
            currentFrame: 0,
            data: {
                from,
                to,
                property,
            },
            easing: easing || AnimationScheduler.Easing.easeInOutQuad,
        };
        this.register(animation);
        return animation;
    }
    /**
     * Register an animation
     */
    register(animation) {
        this.animations.set(animation.id, animation);
        // Start the animation loop if not running
        if (!this.isRunning && this.animations.size > 0) {
            this.start();
        }
        this.emit('animation:registered', animation);
    }
    /**
     * Unregister an animation
     */
    unregister(id) {
        const animation = this.animations.get(id);
        if (animation) {
            this.animations.delete(id);
            this.emit('animation:unregistered', animation);
            // Stop the loop if no animations remain
            if (this.animations.size === 0) {
                this.stop();
            }
        }
    }
    /**
     * Start the animation loop
     */
    start() {
        if (this.isRunning || this.disposed)
            return;
        this.isRunning = true;
        this.lastFrameTime = performance.now();
        this.animationLoop = setInterval(() => this.tick(), this.frameInterval);
        this.emit('scheduler:start');
    }
    /**
     * Stop the animation loop
     */
    stop() {
        if (!this.isRunning)
            return;
        if (this.animationLoop) {
            clearInterval(this.animationLoop);
            this.animationLoop = null;
        }
        this.isRunning = false;
        this.emit('scheduler:stop');
    }
    /**
     * Main animation tick
     */
    tick() {
        const now = performance.now();
        const deltaTime = now - this.lastFrameTime;
        // Process each animation
        for (const [, animation] of this.animations) {
            // Check if animation should update based on its FPS
            const animationInterval = 1000 / animation.fps;
            const elapsedTime = now - animation.startTime;
            const expectedFrame = Math.floor(elapsedTime / animationInterval);
            if (expectedFrame > animation.currentFrame) {
                animation.currentFrame = expectedFrame;
                // Check if animation is complete
                if (animation.duration &&
                    elapsedTime >= animation.duration &&
                    animation.frameCount !== Infinity) {
                    this.completeAnimation(animation);
                    continue;
                }
                // Update animation
                this.updateAnimation(animation, elapsedTime);
                // Call frame callback
                if (animation.onFrame) {
                    animation.onFrame(animation);
                }
                this.emit('animation:frame', animation);
            }
        }
        this.lastFrameTime = now;
        this.emit('scheduler:tick', { deltaTime, animationCount: this.animations.size });
    }
    /**
     * Update animation based on type
     */
    updateAnimation(animation, elapsedTime) {
        switch (animation.type) {
            case 'spinner':
                this.updateSpinner(animation);
                break;
            case 'progress':
                this.updateProgressAnimation(animation, elapsedTime);
                break;
            case 'elapsed':
                this.updateElapsed(animation);
                break;
            case 'transition':
                this.updateTransition(animation, elapsedTime);
                break;
        }
    }
    /**
     * Update spinner animation
     */
    updateSpinner(animation) {
        const frameIndex = animation.currentFrame % animation.data.frames.length;
        this.emit('spinner:frame', {
            id: animation.id,
            frame: animation.data.frames[frameIndex],
            message: animation.data.message,
        });
    }
    /**
     * Update progress animation
     */
    updateProgressAnimation(animation, elapsedTime) {
        if (!animation.duration)
            return;
        const progress = Math.min(elapsedTime / animation.duration, 1);
        const easedProgress = animation.easing ? animation.easing(progress) : progress;
        const { current, target, total } = animation.data;
        const newValue = current + (target - current) * easedProgress;
        const percentage = Math.round((newValue / total) * 100);
        this.emit('progress:update', {
            id: animation.id,
            value: newValue,
            percentage,
            total,
            formatted: animation.data.format
                ? animation.data.format(newValue, total)
                : `${percentage}%`,
        });
    }
    /**
     * Update elapsed time animation
     */
    updateElapsed(animation) {
        const elapsed = Date.now() - animation.data.startTime;
        const formatted = animation.data.format
            ? animation.data.format(elapsed)
            : this.formatElapsedTime(elapsed);
        this.emit('elapsed:update', {
            id: animation.id,
            elapsed,
            formatted,
        });
    }
    /**
     * Update transition animation
     */
    updateTransition(animation, elapsedTime) {
        if (!animation.duration)
            return;
        const progress = Math.min(elapsedTime / animation.duration, 1);
        const easedProgress = animation.easing ? animation.easing(progress) : progress;
        const { from, to } = animation.data;
        let value;
        if (typeof from === 'number' && typeof to === 'number') {
            value = from + (to - from) * easedProgress;
        }
        else {
            // For non-numeric values, switch at 50% progress
            value = easedProgress < 0.5 ? from : to;
        }
        this.emit('transition:update', {
            id: animation.id,
            property: animation.data.property,
            value,
            progress: easedProgress,
        });
    }
    /**
     * Complete an animation
     */
    completeAnimation(animation) {
        if (animation.onComplete) {
            animation.onComplete(animation);
        }
        this.emit('animation:complete', animation);
        this.unregister(animation.id);
    }
    /**
     * Get current progress value with easing
     */
    getCurrentProgressValue(animation) {
        const elapsedTime = performance.now() - animation.startTime;
        if (!animation.duration)
            return animation.data.target;
        const progress = Math.min(elapsedTime / animation.duration, 1);
        const easedProgress = animation.easing ? animation.easing(progress) : progress;
        const { current, target } = animation.data;
        return current + (target - current) * easedProgress;
    }
    /**
     * Format elapsed time
     */
    formatElapsedTime(elapsedMs) {
        const seconds = Math.floor(elapsedMs / 1000);
        const minutes = Math.floor(seconds / 60);
        const hours = Math.floor(minutes / 60);
        if (hours > 0) {
            return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
        }
        else if (minutes > 0) {
            return `${minutes}m ${seconds % 60}s`;
        }
        else {
            return `${seconds}s`;
        }
    }
    /**
     * Get animation by ID
     */
    getAnimation(id) {
        return this.animations.get(id);
    }
    /**
     * Get all active animations
     */
    getActiveAnimations() {
        return Array.from(this.animations.values());
    }
    /**
     * Check if scheduler is running
     */
    isActive() {
        return this.isRunning;
    }
    /**
     * Get target FPS
     */
    getTargetFPS() {
        return this.targetFPS;
    }
    /**
     * Set target FPS
     */
    setTargetFPS(fps) {
        this.targetFPS = fps;
        this.frameInterval = 1000 / fps;
        // Restart the loop with new interval if running
        if (this.isRunning) {
            this.stop();
            this.start();
        }
    }
    /**
     * Clear all animations
     */
    clearAll() {
        for (const animation of this.animations.values()) {
            if (animation.onComplete) {
                animation.onComplete(animation);
            }
        }
        this.animations.clear();
        this.stop();
        this.emit('scheduler:cleared');
    }
    /**
     * Dispose of the scheduler
     */
    dispose() {
        this.disposed = true;
        this.clearAll();
        this.removeAllListeners();
    }
    /**
     * Check if the scheduler has been disposed
     */
    isDisposed() {
        return this.disposed;
    }
}
//# sourceMappingURL=AnimationScheduler.js.map