const playlist = [
    { url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/01.%20PINK%20FLIGHT%20ATTENDANT.mp3', name: 'TRACK 1 - PINK FLIGHT ATTENDANT' },
    { url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/02.%20NOW.mp3', name: 'TRACK 2 - NOW' },
    { url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/03.%20MAZES.mp3', name: 'TRACK 3 - MAZES' },
    { url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/04.%20FAMILY%20MAN.mp3', name: 'TRACK 4 - FAMILY MAN' },
    { url: 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MP3%20BOUNCE/05.%20TOLLBOOTH%20SAINTS.mp3', name: 'TRACK 5 - TOLLBOOTH SAINTS' }
];

let currentTrack = 0;
const videoUrl = 'https://feed.falsefinish.club/Echo%20Reality/PINK%20FLIGHT/MAZES%20HB.mp4';

const audio = document.getElementById('audio');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const stopBtn = document.getElementById('stopBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const volumeSlider = document.getElementById('volumeSlider');
const timeDisplay = document.getElementById('timeDisplay');
const trackName = document.getElementById('trackName');
const trackNameInner = document.getElementById('trackNameInner');
const reelLeft = document.getElementById('reelLeft');
const reelRight = document.getElementById('reelRight');
const tapeLeft = document.getElementById('tapeLeft');
const tapeRight = document.getElementById('tapeRight');
const reelContainerLeft = document.getElementById('reelContainerLeft');
const reelContainerRight = document.getElementById('reelContainerRight');
const ejectBtn = document.getElementById('ejectBtn');
const lightningBtn = document.getElementById('lightningBtn');
const videoOverlay = document.getElementById('videoOverlay');
const videoPlayer = document.getElementById('videoPlayer');
const closeVideo = document.getElementById('closeVideo');

// Alby Lightning panel elements
const albyOverlay = document.getElementById('albyOverlay');
const albyPanel = document.getElementById('albyPanel');
const albyCloseBtn = document.getElementById('albyCloseBtn');
const albyAmount = document.getElementById('albyAmount');
const albyMemo = document.getElementById('albyMemo');
const albyIncrementBtn = document.getElementById('albyIncrementBtn');
const albyDecrementBtn = document.getElementById('albyDecrementBtn');
const albyBoostBtn = document.getElementById('albyBoostBtn');
const albyDisplayAmount = document.getElementById('albyDisplayAmount');
const albyCharCount = document.getElementById('albyCharCount');
const albySimpleBoost = document.getElementById('albySimpleBoost');
const albyTrackSection = document.getElementById('albyTrackSection');
const albyTrackName = document.getElementById('albyTrackName');
const albyIncludeTrack = document.getElementById('albyIncludeTrack');

// Tape size constants (in pixels)
const TAPE_MIN_SIZE = 70;  // Minimum tape size (just larger than reel-inner)
const TAPE_MAX_SIZE = 180;  // Maximum tape size (fills most of reel)

// ========================================
// CONTINUOUS TAPE MODEL - STATE
// Tracks are treated as sequential positions on a single tape
// ========================================
let trackDurations = [];        // Duration of each track in seconds
let trackStartPositions = [];   // Cumulative start position of each track on tape
let totalTapeDuration = 0;      // Total length of entire tape
let durationsLoaded = false;    // Flag indicating when all durations are known

/**
 * Load metadata for all tracks to get their durations
 * Uses a temporary audio element to fetch duration without full download
 * Falls back to 240 seconds (4 min) if metadata can't be loaded
 */
async function loadAllDurations() {
    console.log('Loading track durations...');
    trackDurations = [];
    
    for (let i = 0; i < playlist.length; i++) {
        try {
            // For track 0, use the main audio element since loadTrack(0) already loads it
            if (i === 0 && currentTrack === 0) {
                // Wait for main audio to load metadata if not already
                if (audio.duration && !isNaN(audio.duration)) {
                    trackDurations[i] = audio.duration;
                } else {
                    // Wait for main audio metadata
                    trackDurations[i] = await new Promise((resolve) => {
                        if (audio.duration && !isNaN(audio.duration)) {
                            resolve(audio.duration);
                        } else {
                            audio.addEventListener('loadedmetadata', function onMeta() {
                                audio.removeEventListener('loadedmetadata', onMeta);
                                resolve(audio.duration);
                            });
                        }
                    });
                }
            } else {
                // For other tracks, use temp audio element
                trackDurations[i] = await getTrackDuration(playlist[i].url);
            }
            console.log(`Track ${i + 1} duration: ${formatTime(trackDurations[i])}`);
        } catch (e) {
            console.warn(`Failed to get duration for track ${i + 1}, using fallback`);
            trackDurations[i] = 240; // 4 minute fallback
        }
    }
    
    // Calculate cumulative start positions
    trackStartPositions = [0];
    for (let i = 1; i < trackDurations.length; i++) {
        trackStartPositions[i] = trackStartPositions[i - 1] + trackDurations[i - 1];
    }
    
    // Calculate total tape duration
    totalTapeDuration = trackStartPositions[trackDurations.length - 1] + trackDurations[trackDurations.length - 1];
    
    durationsLoaded = true;
    console.log(`Total tape duration: ${formatTime(totalTapeDuration)}`);
    console.log('Track start positions:', trackStartPositions.map(formatTime));
    
    // Update tape sizes now that we have durations
    updateTapeSizes();
}

/**
 * Get duration of a single track using a temporary audio element
 * @param {string} url - URL of the audio track
 * @returns {Promise<number>} Duration in seconds
 */
function getTrackDuration(url) {
    return new Promise((resolve, reject) => {
        const tempAudio = new Audio();
        tempAudio.preload = 'metadata'; // Only fetch headers, not entire file
        
        // Cleanup function to properly abort any ongoing request
        function cleanup() {
            tempAudio.removeEventListener('loadedmetadata', onMetadata);
            tempAudio.removeEventListener('error', onError);
            tempAudio.src = '';
            tempAudio.load(); // Force abort of any pending request
        }
        
        // Set timeout for slow loads
        const timeout = setTimeout(() => {
            cleanup();
            reject(new Error('Timeout loading metadata'));
        }, 10000); // 10 second timeout
        
        function onMetadata() {
            clearTimeout(timeout);
            const duration = tempAudio.duration;
            cleanup();
            resolve(duration);
        }
        
        function onError(e) {
            clearTimeout(timeout);
            cleanup();
            reject(e);
        }
        
        tempAudio.addEventListener('loadedmetadata', onMetadata);
        tempAudio.addEventListener('error', onError);
        
        // Direct URL - browser handles caching via HTTP headers
        tempAudio.src = url;
    });
}

// ========================================
// TAPE POSITION HELPER FUNCTIONS
// Convert between track-local and global tape positions
// ========================================

/**
 * Get the current position on the entire tape (global position)
 * @returns {number} Position in seconds from tape start
 */
function getCurrentTapePosition() {
    if (!durationsLoaded) return 0;
    return trackStartPositions[currentTrack] + (audio.currentTime || 0);
}

/**
 * Get the current tape progress as a value from 0 to 1
 * @returns {number} Progress through entire tape (0 = start, 1 = end)
 */
function getTapeProgress() {
    if (!durationsLoaded || !totalTapeDuration) return 0;
    return getCurrentTapePosition() / totalTapeDuration;
}

/**
 * Find which track contains a given global tape position
 * @param {number} tapePosition - Position in seconds from tape start
 * @returns {Object} { trackIndex, positionInTrack }
 */
function findTrackAtPosition(tapePosition) {
    if (!durationsLoaded) return { trackIndex: 0, positionInTrack: 0 };
    
    // Clamp to valid range
    tapePosition = Math.max(0, Math.min(totalTapeDuration, tapePosition));
    
    // Find the track that contains this position
    for (let i = trackStartPositions.length - 1; i >= 0; i--) {
        if (tapePosition >= trackStartPositions[i]) {
            const positionInTrack = tapePosition - trackStartPositions[i];
            // Clamp position within track duration
            const clampedPosition = Math.min(positionInTrack, trackDurations[i] - 0.01);
            return {
                trackIndex: i,
                positionInTrack: Math.max(0, clampedPosition)
            };
        }
    }
    
    // Fallback to start
    return { trackIndex: 0, positionInTrack: 0 };
}

/**
 * Get the global tape position for the start of a specific track
 * @param {number} trackIndex - Index of the track
 * @returns {number} Position in seconds from tape start
 */
function getTrackStartPosition(trackIndex) {
    if (!durationsLoaded) return 0;
    return trackStartPositions[trackIndex] || 0;
}

// ========================================
// SOUND EFFECTS MODULE - START
// Web Audio API synthesized sounds for tactile feedback
// ========================================
const SoundEffects = {
    ctx: null,
    
    // Initialize AudioContext (must be called after user gesture)
    init() {
        if (!this.ctx) {
            this.ctx = new (window.AudioContext || window.webkitAudioContext)();
        }
        // Resume if suspended (browser autoplay policy)
        if (this.ctx.state === 'suspended') {
            this.ctx.resume();
        }
    },
    
    // Play a mechanical button click sound
    playButtonClick() {
        this.init();
        const now = this.ctx.currentTime;
        
        // Create nodes for the click sound
        const clickOsc = this.ctx.createOscillator();
        const clickGain = this.ctx.createGain();
        const noiseGain = this.ctx.createGain();
        
        // Low frequency "thunk" component (80-150Hz)
        clickOsc.type = 'sine';
        clickOsc.frequency.setValueAtTime(150, now);
        clickOsc.frequency.exponentialRampToValueAtTime(80, now + 0.05);
        
        // Gain envelope for the thunk - fast attack, quick decay
        clickGain.gain.setValueAtTime(0.6, now);
        clickGain.gain.exponentialRampToValueAtTime(0.01, now + 0.08);
        
        clickOsc.connect(clickGain);
        clickGain.connect(this.ctx.destination);
        
        // High frequency "click" transient using noise
        const bufferSize = this.ctx.sampleRate * 0.02; // 20ms of noise
        const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
        const output = noiseBuffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) {
            output[i] = Math.random() * 2 - 1;
        }
        
        const noise = this.ctx.createBufferSource();
        noise.buffer = noiseBuffer;
        
        // Bandpass filter to shape the noise into a click
        const filter = this.ctx.createBiquadFilter();
        filter.type = 'bandpass';
        filter.frequency.value = 2000;
        filter.Q.value = 1;
        
        noiseGain.gain.setValueAtTime(0.35, now);
        noiseGain.gain.exponentialRampToValueAtTime(0.01, now + 0.03);
        
        noise.connect(filter);
        filter.connect(noiseGain);
        noiseGain.connect(this.ctx.destination);
        
        // Start and stop
        clickOsc.start(now);
        clickOsc.stop(now + 0.1);
        noise.start(now);
        noise.stop(now + 0.03);
    },
    
    // Start continuous tape wind sound (for reel dragging)
    // Uses looping noise buffer for seamless continuous playback
    startTapeWindLoop() {
        this.init();
        
        // Stop any existing loop
        this.stopTapeWindLoop();
        
        const now = this.ctx.currentTime;
        const sampleRate = this.ctx.sampleRate;
        
        // Create 1 second of loopable noise
        const bufferSize = sampleRate * 1;
        const noiseBuffer = this.ctx.createBuffer(1, bufferSize, sampleRate);
        const output = noiseBuffer.getChannelData(0);
        
        // Generate noise with amplitude modulation
        for (let i = 0; i < bufferSize; i++) {
            const noise = Math.random() * 2 - 1;
            const wobble = 1 + 0.1 * Math.sin(i / sampleRate * 20 * Math.PI * 2);
            output[i] = noise * wobble;
        }
        
        this.tapeWindSource = this.ctx.createBufferSource();
        this.tapeWindSource.buffer = noiseBuffer;
        this.tapeWindSource.loop = true;
        
        // Bandpass filter for tape character
        const filter = this.ctx.createBiquadFilter();
        filter.type = 'bandpass';
        filter.frequency.value = 1200;
        filter.Q.value = 0.5;
        
        // High shelf to reduce harshness
        const highShelf = this.ctx.createBiquadFilter();
        highShelf.type = 'highshelf';
        highShelf.frequency.value = 3000;
        highShelf.gain.value = -6;
        
        // Gain with fade in
        this.tapeWindGain = this.ctx.createGain();
        this.tapeWindGain.gain.setValueAtTime(0, now);
        this.tapeWindGain.gain.linearRampToValueAtTime(0.07, now + 0.05);
        
        // Connect chain
        this.tapeWindSource.connect(filter);
        filter.connect(highShelf);
        highShelf.connect(this.tapeWindGain);
        this.tapeWindGain.connect(this.ctx.destination);
        
        this.tapeWindSource.start(now);
    },
    
    // Stop the continuous tape wind sound
    stopTapeWindLoop() {
        if (this.tapeWindGain && this.ctx) {
            const now = this.ctx.currentTime;
            this.tapeWindGain.gain.linearRampToValueAtTime(0, now + 0.1);
            
            // Clean up after fade
            const sourceToStop = this.tapeWindSource;
            setTimeout(() => {
                if (sourceToStop) {
                    try {
                        sourceToStop.stop();
                    } catch(e) {}
                }
            }, 150);
            
            this.tapeWindSource = null;
            this.tapeWindGain = null;
        }
    }
};
// ========================================
// SOUND EFFECTS MODULE - END
// ========================================

// ========================================
// DISPLAY GLITCH MODULE - START
// Random visual glitches for dystopian CRT effect
// ========================================
const DisplayGlitch = {
    display: null,
    vhsTracking: null,
    isEnabled: true,
    
    // Timing ranges (in milliseconds)
    RGB_GLITCH_MIN_INTERVAL: 5000,    // 5 seconds
    RGB_GLITCH_MAX_INTERVAL: 15000,   // 15 seconds
    RGB_GLITCH_DURATION_MIN: 80,      // 80ms
    RGB_GLITCH_DURATION_MAX: 250,     // 250ms
    
    BLACKOUT_MIN_INTERVAL: 30000,     // 30 seconds
    BLACKOUT_MAX_INTERVAL: 90000,     // 90 seconds
    BLACKOUT_DURATION: 60,            // 60ms per flicker
    
    VHS_MIN_INTERVAL: 10000,          // 10 seconds
    VHS_MAX_INTERVAL: 30000,          // 30 seconds
    VHS_DURATION: 400,                // 400ms
    
    /**
     * Initialize the glitch module
     * Gets DOM references and starts random glitch intervals
     */
    init() {
        this.display = document.getElementById('display');
        this.vhsTracking = document.getElementById('vhsTracking');
        
        if (!this.display || !this.vhsTracking) {
            console.warn('DisplayGlitch: Could not find display elements');
            return;
        }
        
        // Start the random glitch schedulers
        this.scheduleNext('rgb');
        this.scheduleNext('blackout');
        this.scheduleNext('vhs');
        
        console.log('DisplayGlitch: Initialized');
    },
    
    /**
     * Get a random value between min and max
     */
    randomBetween(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    },
    
    /**
     * Trigger RGB chromatic aberration glitch
     * Adds class that causes RGB channel separation animation
     */
    triggerRgbGlitch() {
        if (!this.isEnabled || !this.display) return;
        
        const duration = this.randomBetween(
            this.RGB_GLITCH_DURATION_MIN,
            this.RGB_GLITCH_DURATION_MAX
        );
        
        this.display.classList.add('glitch-rgb');
        
        setTimeout(() => {
            this.display.classList.remove('glitch-rgb');
        }, duration);
    },
    
    /**
     * Trigger intermittent display failure (blackout)
     * Creates 1-3 quick flickers for realistic CRT failure
     */
    triggerBlackout() {
        if (!this.isEnabled || !this.display) return;
        
        // Random number of flickers (1-3)
        const flickerCount = this.randomBetween(1, 3);
        let currentFlicker = 0;
        
        const doFlicker = () => {
            this.display.classList.add('blackout');
            
            setTimeout(() => {
                this.display.classList.remove('blackout');
                currentFlicker++;
                
                // If more flickers needed, do them after a short gap
                if (currentFlicker < flickerCount) {
                    setTimeout(doFlicker, this.randomBetween(30, 80));
                }
            }, this.BLACKOUT_DURATION);
        };
        
        doFlicker();
    },
    
    /**
     * Trigger VHS tracking lines
     * Randomly shows vertical (scrolling up) or horizontal (scrolling left) tracking lines
     */
    triggerVhsTracking() {
        if (!this.isEnabled || !this.vhsTracking) return;
        
        // Remove all classes to reset
        this.vhsTracking.classList.remove('active', 'vertical', 'horizontal');
        
        // Force reflow to restart animation
        void this.vhsTracking.offsetWidth;
        
        // Randomly choose direction (50/50 chance)
        const direction = Math.random() < 0.5 ? 'vertical' : 'horizontal';
        
        this.vhsTracking.classList.add(direction);
        this.vhsTracking.classList.add('active');
        
        // Remove classes after animation completes
        setTimeout(() => {
            this.vhsTracking.classList.remove('active', 'vertical', 'horizontal');
            
            // 30% chance of a second burst shortly after
            if (Math.random() < 0.3) {
                setTimeout(() => {
                    this.triggerVhsTracking();
                }, this.randomBetween(100, 300));
            }
        }, this.VHS_DURATION);
    },
    
    /**
     * Schedule the next occurrence of a glitch type
     * @param {string} type - 'rgb', 'blackout', or 'vhs'
     */
    scheduleNext(type) {
        let minInterval, maxInterval, triggerFn;
        
        switch (type) {
            case 'rgb':
                minInterval = this.RGB_GLITCH_MIN_INTERVAL;
                maxInterval = this.RGB_GLITCH_MAX_INTERVAL;
                triggerFn = () => this.triggerRgbGlitch();
                break;
            case 'blackout':
                minInterval = this.BLACKOUT_MIN_INTERVAL;
                maxInterval = this.BLACKOUT_MAX_INTERVAL;
                triggerFn = () => this.triggerBlackout();
                break;
            case 'vhs':
                minInterval = this.VHS_MIN_INTERVAL;
                maxInterval = this.VHS_MAX_INTERVAL;
                triggerFn = () => this.triggerVhsTracking();
                break;
            default:
                return;
        }
        
        const delay = this.randomBetween(minInterval, maxInterval);
        
        setTimeout(() => {
            triggerFn();
            this.scheduleNext(type); // Schedule next occurrence
        }, delay);
    },
    
    /**
     * Enable or disable glitch effects
     * @param {boolean} enabled
     */
    setEnabled(enabled) {
        this.isEnabled = enabled;
    }
};

// Initialize glitch effects after a short delay
setTimeout(() => DisplayGlitch.init(), 1000);
// ========================================
// DISPLAY GLITCH MODULE - END
// ========================================

// ========================================
// REEL ANIMATION HELPERS - START
// Consolidated functions for controlling reel/tape spinning
// ========================================

/**
 * Start spinning animation on all reels and tape elements
 */
function startReelAnimation() {
    reelLeft.classList.add('spinning');
    reelRight.classList.add('spinning');
    tapeLeft.classList.add('spinning');
    tapeRight.classList.add('spinning');
}

/**
 * Stop spinning animation on all reels and tape elements
 */
function stopReelAnimation() {
    reelLeft.classList.remove('spinning');
    reelRight.classList.remove('spinning');
    tapeLeft.classList.remove('spinning');
    tapeRight.classList.remove('spinning');
}

/**
 * Set animation speed for all reel elements
 * @param {string} duration - CSS duration value (e.g., '0.3s') or empty string for default
 */
function setReelAnimationSpeed(duration = '') {
    reelLeft.style.animationDuration = duration;
    reelRight.style.animationDuration = duration;
    tapeLeft.style.animationDuration = duration;
    tapeRight.style.animationDuration = duration;
}

// ========================================
// REEL ANIMATION HELPERS - END
// ========================================

// ========================================
// BUTTON CLICK SOUNDS - Add to all buttons
// Uses mousedown for immediate tactile feedback
// ========================================
[playBtn, pauseBtn, stopBtn, prevBtn, nextBtn, ejectBtn, lightningBtn].forEach(btn => {
    btn.addEventListener('mousedown', () => {
        SoundEffects.playButtonClick();
    });
});

// Lightning button - opens Alby Lightning panel
// (toggleAlbyPanel function defined in ALBY PANEL FUNCTIONALITY section below)
lightningBtn.addEventListener('click', () => {
    toggleAlbyPanel();
});

// ========================================
// TITLE SCROLL ANIMATION (JavaScript controlled bounce)
// Moves 1px at a time, bounces at edges, pauses with player
// ========================================
let titleScrollPosition = 0;
let titleScrollDirection = 1; // 1 = moving left (text shifts left), -1 = moving right
let titleScrollInterval = null;
const SCROLL_SPEED = 333; // milliseconds between 1px moves (1 second per ~3 pixels)

function startTitleScroll() {
    if (titleScrollInterval) return; // Already scrolling
    titleScrollInterval = setInterval(updateTitleScroll, SCROLL_SPEED);
}

function stopTitleScroll() {
    if (titleScrollInterval) {
        clearInterval(titleScrollInterval);
        titleScrollInterval = null;
    }
}

function resetTitleScroll() {
    stopTitleScroll();
    titleScrollPosition = 0;
    titleScrollDirection = 1;
    trackNameInner.style.left = '0px';
}

function updateTitleScroll() {
    const containerWidth = trackName.offsetWidth;
    const textWidth = trackNameInner.offsetWidth;
    
    // Calculate max scroll - text scrolls until its right edge hits container's right edge
    // If text is shorter than container, scroll until text's left edge hits container's right edge
    let maxScroll;
    if (textWidth > containerWidth) {
        // Text is longer - scroll until right edge of text reaches right edge of container
        maxScroll = textWidth - containerWidth;
    } else {
        // Text is shorter - scroll across the container width but keep text visible
        // Text starts at left (0), scrolls right until left edge is at (containerWidth - textWidth)
        maxScroll = containerWidth - textWidth;
    }
    
    // Move 1px in current direction
    titleScrollPosition += titleScrollDirection;
    
    // Bounce at edges
    if (titleScrollPosition >= maxScroll) {
        titleScrollPosition = maxScroll;
        titleScrollDirection = -1; // Start moving back
    } else if (titleScrollPosition <= 0) {
        titleScrollPosition = 0;
        titleScrollDirection = 1; // Start moving forward
    }
    
    // Apply position
    // If text is longer: shift text LEFT (negative value) so we see the end
    // If text is shorter: shift text RIGHT (positive value) to scroll across display
    if (textWidth > containerWidth) {
        trackNameInner.style.left = -titleScrollPosition + 'px';
    } else {
        trackNameInner.style.left = titleScrollPosition + 'px';
    }
}

// ========================================
// AUDIO LOADING HELPERS - START
// Unified helpers for loading tracks and waiting for audio ready
// ========================================

/**
 * Wait for audio element to be ready
 * @param {string} event - Event to wait for ('loadedmetadata' or 'canplay')
 * @returns {Promise} Resolves when audio is ready
 */
function waitForAudioReady(event = 'loadedmetadata') {
    return new Promise(resolve => {
        // If already ready, resolve immediately
        if (event === 'loadedmetadata' && audio.readyState >= 1) {
            resolve();
            return;
        }
        if (event === 'canplay' && audio.readyState >= 3) {
            resolve();
            return;
        }
        
        function onReady() {
            audio.removeEventListener(event, onReady);
            resolve();
        }
        audio.addEventListener(event, onReady);
        audio.load();
    });
}

/**
 * Load a track with options for seeking, auto-play, and waiting
 * @param {number} index - Track index to load
 * @param {Object} options - Loading options
 * @param {number} [options.seekTo] - Position in seconds to seek to after loading
 * @param {boolean} [options.autoPlay=false] - Whether to auto-play after loading
 * @param {string} [options.waitFor='none'] - Event to wait for ('none', 'loadedmetadata', 'canplay')
 * @returns {Promise} Resolves when track is loaded (and ready if waitFor specified)
 */
async function loadTrackAsync(index, options = {}) {
    const { seekTo, autoPlay = false, waitFor = 'none' } = options;
    
    currentTrack = index;
    trackNameInner.textContent = playlist[index].name;
    resetTitleScroll();
    
    // Direct URL - browser handles caching via HTTP headers
    audio.src = playlist[index].url;
    
    // Wait for audio ready if requested
    if (waitFor !== 'none') {
        await waitForAudioReady(waitFor);
    }
    
    // Seek if specified
    if (typeof seekTo === 'number') {
        audio.currentTime = seekTo;
    }
    
    // Auto-play if requested
    if (autoPlay) {
        try {
            await audio.play();
            startReelAnimation();
            startTitleScroll();
        } catch (e) {
            console.log('Auto-play failed:', e);
        }
    }
}

// ========================================
// AUDIO LOADING HELPERS - END
// ========================================

// Initial track load (async, no await needed for initial load)
loadTrackAsync(0);

// Load all track durations for continuous tape model
loadAllDurations();

// Set initial volume
audio.volume = 0.7;

// Play button
playBtn.addEventListener('click', () => {
    audio.play();
    startReelAnimation();
    startTitleScroll();
});

// Pause button - keeps title at current position
pauseBtn.addEventListener('click', () => {
    audio.pause();
    stopReelAnimation();
    stopTitleScroll();
});

// Stop button - rewinds tape to the beginning (Track 1, position 0) with animation
stopBtn.addEventListener('click', async () => {
    audio.pause();
    
    // Check if we need to rewind (not already at beginning)
    const needsRewind = currentTrack !== 0 || audio.currentTime > 0.5;
    
    if (needsRewind && durationsLoaded) {
        // Animate rewind to beginning
        await animateRewindToStart();
    }
    
    // Load track 0 if not already on it
    if (currentTrack !== 0) {
        await loadTrackAsync(0, { waitFor: 'loadedmetadata', seekTo: 0 });
    } else {
        audio.currentTime = 0;
    }
    
    stopReelAnimation();
    resetTapeSizes(); // Ensure tape visuals are at beginning
    
    // Manually update time display since timeupdate doesn't fire when paused
    const duration = formatTime(audio.duration);
    timeDisplay.textContent = `00:00 / ${duration}`;
});

// Previous track (prev button with bar)
// Uses animated seek to smoothly wind tape to previous track position
prevBtn.addEventListener('click', () => {
    console.log('Prev button clicked');
    const wasPlaying = !audio.paused;
    const targetTrack = (currentTrack - 1 + playlist.length) % playlist.length;
    animatedSeekToTrack(targetTrack, wasPlaying);
});

// Next track (next button with bar)
// Uses animated seek to smoothly wind tape to next track position
nextBtn.addEventListener('click', () => {
    console.log('Next button clicked');
    const wasPlaying = !audio.paused;
    const targetTrack = (currentTrack + 1) % playlist.length;
    animatedSeekToTrack(targetTrack, wasPlaying);
});

// Drag on reels to scrub audio (supports cross-track scrubbing)
let isDragging = false;
let startY = 0;
let startTapePosition = 0;  // Global tape position at drag start
let wasPlayingBeforeDrag = false;
let lastDragY = 0;
let tapeWindPlaying = false;
let scrubTrackChangeInProgress = false; // Prevents multiple track loads during scrub

function startDrag(e) {
    // Allow drag if we have duration info (either from track or global tape)
    if (durationsLoaded || audio.duration) {
        e.preventDefault(); // Prevent text selection during drag
        isDragging = true;
        startY = e.clientY || e.touches[0].clientY;
        lastDragY = startY;
        startTapePosition = getCurrentTapePosition();
        wasPlayingBeforeDrag = !audio.paused;
        tapeWindPlaying = false;
        scrubTrackChangeInProgress = false;
        // Don't pause - let audio continue playing while scrubbing
    }
}

function drag(e) {
    if (!isDragging) return;
    if (scrubTrackChangeInProgress) return; // Wait for track change to complete
    
    const currentY = e.clientY || e.touches[0].clientY;
    const deltaY = startY - currentY;
    const instantDeltaY = lastDragY - currentY;
    lastDragY = currentY;
    
    // Calculate scrub speed for audio effects
    const scrubSpeed = Math.min(Math.abs(instantDeltaY) / 20, 1); // Normalize to 0-1
    
    // Start tape wind sound only when actually moving
    if (scrubSpeed > 0.1 && !tapeWindPlaying) {
        SoundEffects.startTapeWindLoop();
        tapeWindPlaying = true;
    } else if (scrubSpeed <= 0.1 && tapeWindPlaying) {
        SoundEffects.stopTapeWindLoop();
        tapeWindPlaying = false;
    }
    
    // Calculate target tape position based on drag distance
    // Sensitivity: dragging 100px moves 10% of total tape (or current track if durations not loaded)
    let targetTapePosition;
    if (durationsLoaded && totalTapeDuration > 0) {
        // Cross-track scrubbing mode
        const scrubAmount = (deltaY / 100) * totalTapeDuration * 0.05;
        targetTapePosition = Math.max(0, Math.min(totalTapeDuration - 0.01, startTapePosition + scrubAmount));
        
        // Find which track this position falls in
        const target = findTrackAtPosition(targetTapePosition);
        
        if (target.trackIndex !== currentTrack) {
            // Need to switch tracks
            scrubTrackChangeInProgress = true;
            loadTrackForScrub(target.trackIndex, target.positionInTrack).then(() => {
                scrubTrackChangeInProgress = false;
                updateTapeSizes();
            });
        } else {
            // Same track, just seek
            audio.currentTime = target.positionInTrack;
        }
    } else if (audio.duration) {
        // Fallback to single-track scrubbing
        const scrubAmount = (deltaY / 100) * audio.duration * 0.1;
        const newTime = Math.max(0, Math.min(audio.duration, audio.currentTime + scrubAmount));
        audio.currentTime = newTime;
    }
    
    // Adjust playback rate for sped-up audio effect
    if (wasPlayingBeforeDrag && scrubSpeed > 0.1) {
        audio.playbackRate = 1 + (scrubSpeed * 3); // 1x to 4x
    } else if (wasPlayingBeforeDrag) {
        audio.playbackRate = 1;
    }
    
    // Keep playing during scrub
    if (audio.paused && wasPlayingBeforeDrag) {
        audio.play();
    }
    
    // Update tape visual
    updateTapeSizes();
}

/**
 * Load a track during scrubbing and seek to position
 * @param {number} trackIndex - Index of track to load
 * @param {number} seekPosition - Position within track to seek to
 */
async function loadTrackForScrub(trackIndex, seekPosition) {
    await loadTrackAsync(trackIndex, { 
        waitFor: 'loadedmetadata', 
        seekTo: seekPosition,
        autoPlay: wasPlayingBeforeDrag 
    });
}

function endDrag() {
    if (isDragging) {
        isDragging = false;
        scrubTrackChangeInProgress = false;
        
        // Stop tape wind sound and reset playback rate
        if (tapeWindPlaying) {
            SoundEffects.stopTapeWindLoop();
            tapeWindPlaying = false;
        }
        audio.playbackRate = 1;
        
        // Continue playing if it was playing before
        if (wasPlayingBeforeDrag && audio.paused) {
            audio.play();
            startReelAnimation();
        }
    }
}

// ========================================
// ANIMATED SEEK TO TRACK
// Smoothly animates tape reels when changing tracks via Next/Prev
// ========================================

let animatedSeekInProgress = false;

/**
 * Easing function for smooth animation
 */
function easeInOutQuad(t) {
    return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
}

/**
 * Animate tape rewind to the beginning of the tape
 * Used by stop button and end-of-tape auto-rewind
 * @returns {Promise} Resolves when animation completes
 */
async function animateRewindToStart() {
    const animationDuration = 500;
    const startProgress = getTapeProgress();
    const endProgress = 0;
    
    SoundEffects.startTapeWindLoop();
    setReelAnimationSpeed('0.3s');
    startReelAnimation();
    
    const startTime = performance.now();
    await new Promise(resolve => {
        function animate(time) {
            const elapsed = time - startTime;
            const t = Math.min(elapsed / animationDuration, 1);
            const easedT = easeInOutQuad(t);
            const currentProgress = startProgress + (endProgress - startProgress) * easedT;
            setTapeSizesAtProgress(currentProgress);
            
            if (t < 1) {
                requestAnimationFrame(animate);
            } else {
                resolve();
            }
        }
        requestAnimationFrame(animate);
    });
    
    SoundEffects.stopTapeWindLoop();
    setReelAnimationSpeed('');
}

/**
 * Animate tape reels to a target track with visual fast-forward/rewind effect
 * @param {number} targetTrackIndex - Index of the track to seek to
 * @param {boolean} wasPlaying - Whether audio was playing before seek
 * @returns {Promise} Resolves when animation and track load complete
 */
async function animatedSeekToTrack(targetTrackIndex, wasPlaying) {
    if (animatedSeekInProgress) return;
    animatedSeekInProgress = true;
    
    const animationDuration = 500; // ms
    
    // Calculate start and end tape progress
    const startProgress = durationsLoaded ? getTapeProgress() : 0;
    const endProgress = durationsLoaded ? getTrackStartPosition(targetTrackIndex) / totalTapeDuration : 0;
    
    // Start tape wind sound
    SoundEffects.startTapeWindLoop();
    
    // Speed up reel animation during transition
    setReelAnimationSpeed('0.3s');
    startReelAnimation();
    
    // Speed up audio during transition if playing
    if (wasPlaying) {
        audio.playbackRate = 4;
    }
    
    // Animate tape sizes from current to target position
    const startTime = performance.now();
    
    await new Promise(resolve => {
        function animate(time) {
            const elapsed = time - startTime;
            const t = Math.min(elapsed / animationDuration, 1);
            const easedT = easeInOutQuad(t);
            
            // Interpolate progress
            const currentProgress = startProgress + (endProgress - startProgress) * easedT;
            
            // Update tape sizes
            setTapeSizesAtProgress(currentProgress);
            
            if (t < 1) {
                requestAnimationFrame(animate);
            } else {
                resolve();
            }
        }
        requestAnimationFrame(animate);
    });
    
    // Stop tape wind sound
    SoundEffects.stopTapeWindLoop();
    
    // Reset animation speed
    setReelAnimationSpeed('');
    audio.playbackRate = 1;
    
    // Load the new track using unified helper
    await loadTrackAsync(targetTrackIndex, { 
        waitFor: 'canplay', 
        autoPlay: wasPlaying 
    });
    
    // Handle animation state after track loads
    if (!wasPlaying) {
        stopReelAnimation();
    }
    animatedSeekInProgress = false;
}

// Add drag listeners to all reel elements (containers, tape, and inner spools)
[reelLeft, reelRight, tapeLeft, tapeRight, reelContainerLeft, reelContainerRight].forEach(el => {
    el.addEventListener('mousedown', startDrag);
    el.addEventListener('touchstart', startDrag, { passive: false });
});

document.addEventListener('mousemove', drag);
document.addEventListener('touchmove', drag);
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);

// Auto-play next track when current ends
audio.addEventListener('ended', async () => {
    const wasLastTrack = currentTrack === playlist.length - 1;
    
    // End of tape - rewind to beginning with sound, then continue playing
    if (wasLastTrack) {
        if (durationsLoaded) {
            // Animate rewind to beginning with sound
            await animateRewindToStart();
        }
        
        // Load track 0 and seek to start
        await loadTrackAsync(0, { waitFor: 'loadedmetadata', seekTo: 0 });
        resetTapeSizes();
    } else {
        // Normal track transition - advance to next track
        await loadTrackAsync((currentTrack + 1) % playlist.length);
    }
    
    // Auto-play the next track
    audio.play();
    startReelAnimation();
    startTitleScroll();
});

// Volume control
volumeSlider.addEventListener('input', (e) => {
    audio.volume = e.target.value / 100;
});

// Update tape wound sizes based on global tape position
function updateTapeSizes() {
    // Use global tape progress if durations are loaded, otherwise fall back to current track
    let progress;
    if (durationsLoaded && totalTapeDuration > 0) {
        progress = getTapeProgress();
    } else if (audio.duration) {
        // Fallback to per-track progress while durations are loading
        progress = audio.currentTime / audio.duration;
    } else {
        return;
    }
    
    setTapeSizesAtProgress(progress);
}

// Initialize tape sizes - rewind to beginning of tape
function resetTapeSizes() {
    tapeLeft.style.setProperty('--tape-size', TAPE_MAX_SIZE + 'px');
    tapeRight.style.setProperty('--tape-size', TAPE_MIN_SIZE + 'px');
}

// Set tape sizes to a specific progress value (0 to 1)
function setTapeSizesAtProgress(progress) {
    const tapeRange = TAPE_MAX_SIZE - TAPE_MIN_SIZE;
    const leftSize = TAPE_MAX_SIZE - (progress * tapeRange);
    const rightSize = TAPE_MIN_SIZE + (progress * tapeRange);
    tapeLeft.style.setProperty('--tape-size', leftSize + 'px');
    tapeRight.style.setProperty('--tape-size', rightSize + 'px');
}

resetTapeSizes();

// Update time display and tape sizes
audio.addEventListener('timeupdate', () => {
    const current = formatTime(audio.currentTime);
    const duration = formatTime(audio.duration);
    timeDisplay.textContent = `${current} / ${duration}`;
    updateTapeSizes();
});

// Format time helper
function formatTime(seconds) {
    if (isNaN(seconds)) return '00:00';
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}

// Stop animations when audio pauses (title stays at current position)
audio.addEventListener('pause', () => {
    stopReelAnimation();
    stopTitleScroll();
});

// Eject button - opens fullscreen video
ejectBtn.addEventListener('click', () => {
    audio.pause();
    stopReelAnimation();
    videoPlayer.src = videoUrl;
    videoOverlay.classList.add('active');
    videoPlayer.play();
});

// Close video
closeVideo.addEventListener('click', () => {
    videoPlayer.pause();
    videoPlayer.src = '';
    videoOverlay.classList.remove('active');
});

// Unified Escape key handler for all overlays/panels
document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') {
        if (videoOverlay.classList.contains('active')) {
            closeVideo.click();
        } else if (albyPanel.classList.contains('active')) {
            toggleAlbyPanel();
        }
    }
});

// ========================================
// ALBY LIGHTNING PANEL FUNCTIONALITY - START
// Handles panel open/close, amount controls, and boost button
// ========================================

/**
 * Toggle the Alby Lightning panel open/closed
 * Also handles the backdrop overlay visibility
 * Shows current track info when audio is loaded (playing or paused, not stopped)
 */
function toggleAlbyPanel() {
    const isActive = albyPanel.classList.contains('active');
    if (isActive) {
        // Close panel
        albyPanel.classList.remove('active');
        albyOverlay.classList.remove('active');
    } else {
        // Open panel
        albyPanel.classList.add('active');
        albyOverlay.classList.add('active');
        
        // Check if audio is loaded (has duration and currentTime > 0, meaning not at start/stopped)
        // Show track section if audio has been played or is playing
        const audioIsLoaded = audio.src && (audio.currentTime > 0 || !audio.paused);
        
        if (audioIsLoaded) {
            // Show track section and populate with current track name
            albyTrackSection.classList.add('visible');
            albyTrackName.textContent = playlist[currentTrack].name;
            albyIncludeTrack.checked = true;
        } else {
            // Hide track section if no audio loaded/played
            albyTrackSection.classList.remove('visible');
            albyIncludeTrack.checked = false;
        }
    }
}

/**
 * Build the final memo including track info if checkbox is checked
 * Returns the complete memo string to be sent with the boost
 */
function buildFinalMemo() {
    let finalMemo = albyMemo.value;
    
    // Append track info if checkbox is checked and track section is visible
    if (albyIncludeTrack.checked && albyTrackSection.classList.contains('visible')) {
        const trackInfo = ` | Now Playing: ${playlist[currentTrack].name}`;
        finalMemo = finalMemo + trackInfo;
    }
    
    return finalMemo;
}

/**
 * Update the simple-boost component attributes and display amount
 * Called whenever amount or memo values change
 */
function updateAlbyBoostButton() {
    const amount = parseFloat(albyAmount.value).toFixed(1);
    const finalMemo = buildFinalMemo();
    
    // Update simple-boost component attributes
    albySimpleBoost.setAttribute('amount', amount);
    albySimpleBoost.setAttribute('memo', finalMemo);
    
    // Update displayed amount on our custom button
    albyDisplayAmount.textContent = parseFloat(amount).toFixed(2);
}

/**
 * Update the memo character count display
 */
function updateAlbyCharCount() {
    albyCharCount.textContent = albyMemo.value.length;
}

// Close button click handler
albyCloseBtn.addEventListener('click', toggleAlbyPanel);

// Overlay click handler - close panel when clicking backdrop
albyOverlay.addEventListener('click', toggleAlbyPanel);

// Increment amount button (+$1.00)
albyIncrementBtn.addEventListener('click', () => {
    const currentValue = parseFloat(albyAmount.value);
    albyAmount.value = (currentValue + 1.0).toFixed(1);
    updateAlbyBoostButton();
});

// Decrement amount button (-$1.00, minimum $0.10)
albyDecrementBtn.addEventListener('click', () => {
    const currentValue = parseFloat(albyAmount.value);
    const newValue = Math.max(0.1, currentValue - 1.0);
    albyAmount.value = newValue.toFixed(1);
    updateAlbyBoostButton();
});

// Amount input change handler
albyAmount.addEventListener('input', () => {
    if (albyAmount.value && albyAmount.value >= 0.1) {
        updateAlbyBoostButton();
    }
});

// Amount input blur handler - validate minimum value
albyAmount.addEventListener('blur', () => {
    if (!albyAmount.value || albyAmount.value < 0.1) {
        albyAmount.value = '0.1';
    }
    updateAlbyBoostButton();
});

// Memo textarea input handler
albyMemo.addEventListener('input', () => {
    updateAlbyCharCount();
    updateAlbyBoostButton();
});

// Include track checkbox change handler
albyIncludeTrack.addEventListener('change', () => {
    updateAlbyBoostButton();
});

// Boost button click - trigger the simple-boost component
albyBoostBtn.addEventListener('click', () => {
    // Build final memo and validate length
    const finalMemo = buildFinalMemo();
    
    // Check if total memo exceeds 400 characters
    if (finalMemo.length > 400) {
        alert(`Memo too long! Your message with track info is ${finalMemo.length} characters. Maximum is 400.\n\nPlease shorten your message or uncheck "Include current track?"`);
        return; // Don't send the boost
    }
    
    // Ensure latest form values are synced to simple-boost before triggering
    updateAlbyBoostButton();
    // Click the inner button div inside the shadow DOM
    const innerButton = albySimpleBoost.shadowRoot?.querySelector('.simple-boost-button');
    if (innerButton) {
        innerButton.click();
    }
});

// Listen for successful payment, reset form fields, and close modal
albySimpleBoost.addEventListener('success', (e) => {
    // Reset form to default values
    albyAmount.value = '1.0';
    albyMemo.value = '';
    // Reset track checkbox (will be re-evaluated when panel reopens)
    albyIncludeTrack.checked = true;
    albyTrackSection.classList.remove('visible');
    updateAlbyCharCount();
    updateAlbyBoostButton();
    // Close the modal after successful boost
    toggleAlbyPanel();
});

// Initialize character count and sync simple-boost attributes on page load
updateAlbyCharCount();
updateAlbyBoostButton();

// ========================================
// ALBY LIGHTNING PANEL FUNCTIONALITY - END
// ========================================
