/**
 * Live 2-Channel Video Mixer Engine — v1.1
 * DJ-style mixing with crossfade, transitions, folder browsing,
 * image slideshow, BroadcastChannel output sync, transition registry,
 * drag & drop, VU meters, session save/load, BPM sync, per-deck FX,
 * playlist reorder, and light/dark theme.
 */

const VIDEO_MIXER_VERSION = '1.1';

// ============================================================
// Deck Class
// ============================================================
class Deck {
    constructor(id) {
        this.id = id;
        this.video = document.getElementById(`deck-${id}-video`);
        this.previewCanvas = document.getElementById(`deck-${id}-preview`);
        this.previewCtx = this.previewCanvas.getContext('2d');
        this.nowPlaying = document.getElementById(`deck-${id}-now-playing`);
        this.errorEl = document.getElementById(`deck-${id}-error`);

        this.playlist = [];
        this.currentIndex = -1;
        this.playing = false;
        this.loop = false;
        this.shuffle = false;
        this.volume = 1.0;
        this.speed = 1.0;
        // 0 = OFF (image stays on screen, no auto-advance). Slideshow mode = >0
        this.slideDuration = 0;

        this.currentImage = null;
        this.currentType = null;
        this.currentMediaUrl = '';
        this.imageTimer = null;
        this.imageLoadTime = 0;

        this.prevImage = null;
        this.imageFade = 1.0;
        this.imageFadeStart = 0;
        this.imageFading = false;

        this.browserPath = '';

        // Per-deck FX
        this.fx = { brightness: 100, contrast: 100, saturate: 100, hue: 0 };

        // Audio analysis for VU meter
        this._audioCtx = null;
        this._analyser = null;
        this._audioSource = null;
        this._audioData = null;

        // Track object URLs to revoke them and prevent memory leaks
        this._ownedObjectUrls = new Set();

        // Error retry counter to prevent infinite next() loops on broken playlists
        this._consecutiveErrors = 0;
        this._maxConsecutiveErrors = 5;
        this._errorTimer = null;

        this._setupVideoEvents();
        this._setupAudio();
    }

    _setupVideoEvents() {
        this.video.addEventListener('ended', () => {
            this._consecutiveErrors = 0; // healthy playthrough resets counter
            if (this.loop) {
                this.video.currentTime = 0;
                this.video.play().catch(() => {});
            } else {
                this.next();
            }
        });

        this.video.addEventListener('playing', () => {
            this._consecutiveErrors = 0; // healthy play resets counter
        });

        this.video.addEventListener('error', () => {
            this._showError();
            this._handleMediaError();
        });
    }

    _handleMediaError() {
        this._consecutiveErrors++;
        if (this._consecutiveErrors >= this._maxConsecutiveErrors) {
            // Stop trying — entire playlist may be broken
            this.pause();
            this.nowPlaying.textContent = 'Stopped — too many errors';
            this._consecutiveErrors = 0;
            return;
        }
        clearTimeout(this._errorTimer);
        this._errorTimer = setTimeout(() => this.next(), 1000);
    }

    /** Revoke any object URLs we created for this deck */
    _revokeOwnedUrls() {
        for (const url of this._ownedObjectUrls) {
            try { URL.revokeObjectURL(url); } catch (e) {}
        }
        this._ownedObjectUrls.clear();
    }

    /** Track a URL so we can revoke it later */
    _trackUrl(url) {
        if (typeof url === 'string' && url.startsWith('blob:')) {
            this._ownedObjectUrls.add(url);
        }
    }

    _setupAudio() {
        try {
            this._audioCtx = new (window.AudioContext || window.webkitAudioContext)();
            this._analyser = this._audioCtx.createAnalyser();
            this._analyser.fftSize = 256;
            this._audioSource = this._audioCtx.createMediaElementSource(this.video);
            this._audioSource.connect(this._analyser);
            this._analyser.connect(this._audioCtx.destination);
            this._audioData = new Uint8Array(this._analyser.frequencyBinCount);
        } catch (e) {
            // Audio analysis not available
        }
    }

    getAudioLevel() {
        if (!this._analyser || !this._audioData) return 0;
        this._analyser.getByteFrequencyData(this._audioData);
        let sum = 0;
        for (let i = 0; i < this._audioData.length; i++) {
            sum += this._audioData[i];
        }
        return sum / (this._audioData.length * 255);
    }

    _showError() {
        if (this.errorEl) {
            this.errorEl.classList.remove('hidden');
            setTimeout(() => this.errorEl.classList.add('hidden'), 4000);
        }
    }

    _hideError() {
        if (this.errorEl) this.errorEl.classList.add('hidden');
    }

    loadPlaylist(files) {
        // Revoke any previously-owned blob URLs to prevent memory leaks
        this._revokeOwnedUrls();
        this.playlist = files;
        this._consecutiveErrors = 0;
        // Track any new blob URLs in this playlist
        for (const f of files) {
            if (f && f._isObjectUrl) this._trackUrl(f.path);
        }
        if (files.length > 0) {
            this.loadItem(0);
        }
    }

    loadItem(index) {
        if (index < 0 || index >= this.playlist.length) return;
        this.currentIndex = index;
        const item = this.playlist[index];
        this.nowPlaying.textContent = item.name;
        this._hideError();

        this.video.pause();
        this.video.removeAttribute('src');
        try { this.video.load(); } catch (e) {}
        clearTimeout(this.imageTimer);
        clearTimeout(this._errorTimer);
        this.imageFading = false;
        // Release stale image reference
        if (this.imageFade >= 1) this.prevImage = null;

        const mediaUrl = `/api/media?path=${encodeURIComponent(item.path)}`;
        this.currentMediaUrl = mediaUrl;

        if (item.type === 'video') {
            this.currentType = 'video';
            this.currentImage = null;
            this.video.src = mediaUrl;
            this.video.volume = this.volume;
            this.video.playbackRate = this.speed;
            if (this.playing) {
                this.video.play().catch(() => {});
            }
        } else {
            this.currentType = 'image';
            this.prevImage = this.currentImage;
            this.imageLoadTime = performance.now();
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
                this.currentImage = img;
                if (this.prevImage) {
                    this.imageFading = true;
                    this.imageFadeStart = performance.now();
                    this.imageFade = 0;
                } else {
                    this.imageFade = 1;
                }
            };
            img.onerror = () => {
                this._showError();
                this._handleMediaError();
            };
            img.src = mediaUrl;

            if (this.playing) {
                this._startImageTimer();
            }
        }

        this._highlightCurrent();
    }

    _startImageTimer() {
        clearTimeout(this.imageTimer);
        this.imageLoadTime = performance.now();
        // slideDuration of 0 = OFF — image stays on screen, no auto-advance
        if (this.slideDuration <= 0) return;
        this.imageTimer = setTimeout(() => {
            if (this.playing && this.currentType === 'image') {
                this.next();
            }
        }, this.slideDuration * 1000);
    }

    _highlightCurrent() {
        const list = document.querySelector(`.file-list[data-deck="${this.id}"]`);
        if (!list) return;
        list.querySelectorAll('li').forEach(li => li.classList.remove('active'));
        const activeLi = list.querySelector(`li[data-index="${this.currentIndex}"]`);
        if (activeLi) {
            activeLi.classList.add('active');
            activeLi.scrollIntoView({ block: 'nearest' });
        }
    }

    play() {
        this.playing = true;
        if (this._audioCtx && this._audioCtx.state === 'suspended') {
            this._audioCtx.resume();
        }
        if (this.currentType === 'video') {
            this.video.play().catch(() => {});
        } else if (this.currentType === 'image') {
            this._startImageTimer();
        }
        if (this.currentIndex === -1 && this.playlist.length > 0) {
            this.loadItem(0);
        }
    }

    pause() {
        this.playing = false;
        if (this.currentType === 'video') {
            this.video.pause();
        }
        clearTimeout(this.imageTimer);
    }

    togglePlay() {
        if (this.playing) this.pause();
        else this.play();
        return this.playing;
    }

    next() {
        if (this.playlist.length === 0) return;
        let nextIdx = this.shuffle
            ? Math.floor(Math.random() * this.playlist.length)
            : (this.currentIndex + 1) % this.playlist.length;
        this.loadItem(nextIdx);
    }

    prev() {
        if (this.playlist.length === 0) return;
        let prevIdx = this.shuffle
            ? Math.floor(Math.random() * this.playlist.length)
            : (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
        this.loadItem(prevIdx);
    }

    setVolume(val) {
        this.volume = val;
        this.video.volume = val;
        // Update volume display
        const volVal = document.querySelector(`.vol-val[data-deck="${this.id}"]`);
        if (volVal) volVal.textContent = Math.round(val * 100);
        const volSlider = document.querySelector(`.vol-slider[data-deck="${this.id}"]`);
        if (volSlider) volSlider.value = Math.round(val * 100);
    }

    setSpeed(val) { this.speed = val; this.video.playbackRate = val; }
    setSlideDuration(val) { this.slideDuration = val; }

    setFx(key, val) { this.fx[key] = val; }
    resetFx() {
        this.fx = { brightness: 100, contrast: 100, saturate: 100, hue: 0 };
    }

    getFxFilter() {
        const f = this.fx;
        return `brightness(${f.brightness}%) contrast(${f.contrast}%) saturate(${f.saturate}%) hue-rotate(${f.hue}deg)`;
    }

    seek(fraction) {
        if (this.currentType === 'video' && this.video.duration) {
            this.video.currentTime = fraction * this.video.duration;
        }
    }

    getProgress() {
        if (this.currentType === 'video' && this.video.duration) {
            return this.video.currentTime / this.video.duration;
        }
        return 0;
    }

    getImageProgress() {
        if (this.currentType === 'image' && this.playing && this.slideDuration > 0) {
            const elapsed = (performance.now() - this.imageLoadTime) / 1000;
            return Math.min(elapsed / this.slideDuration, 1);
        }
        return 0;
    }

    getTimeDisplay() {
        if (this.currentType === 'video' && this.video.duration) {
            return `${fmtTime(this.video.currentTime)} / ${fmtTime(this.video.duration)}`;
        }
        if (this.currentType === 'image') {
            // Slideshow OFF — just show image counter, no progress bar
            if (this.slideDuration <= 0) {
                return `<span>Img ${this.currentIndex + 1}/${this.playlist.length}</span>`;
            }
            const pct = Math.round(this.getImageProgress() * 100);
            return `<span>Img ${this.currentIndex + 1}/${this.playlist.length}</span><span class="slide-progress"><span class="slide-progress-fill" style="width:${pct}%"></span></span>`;
        }
        return '0:00 / 0:00';
    }

    getDrawable() {
        if (this.currentType === 'video') {
            if (this.video.readyState >= 2) return this.video;
            return null;
        }
        if (this.currentType === 'image' && this.currentImage) {
            return this.currentImage;
        }
        return null;
    }

    getState() {
        return {
            mediaUrl: this.currentMediaUrl,
            mediaType: this.currentType,
            playing: this.playing,
            currentTime: this.currentType === 'video' ? this.video.currentTime : 0,
            speed: this.speed,
            volume: this.volume,
            fx: { ...this.fx },
        };
    }

    /** Serializable session data */
    getSessionData() {
        return {
            playlist: this.playlist,
            currentIndex: this.currentIndex,
            playing: this.playing,
            loop: this.loop,
            shuffle: this.shuffle,
            volume: this.volume,
            speed: this.speed,
            slideDuration: this.slideDuration,
            fx: { ...this.fx },
            browserPath: this.browserPath,
        };
    }

    loadSessionData(data) {
        if (data.playlist && data.playlist.length > 0) {
            this.playlist = data.playlist;
            this.loop = data.loop || false;
            this.shuffle = data.shuffle || false;
            this.volume = data.volume != null ? data.volume : 1;
            this.speed = data.speed || 1;
            this.slideDuration = data.slideDuration != null ? data.slideDuration : 0;
            this.fx = data.fx || { brightness: 100, contrast: 100, saturate: 100, hue: 0 };
            this.browserPath = data.browserPath || '';
            this.setVolume(this.volume);
            this.setSpeed(this.speed);
            if (data.currentIndex >= 0) {
                this.loadItem(data.currentIndex);
            }
        }
    }

    drawPreview() {
        const c = this.previewCanvas;
        const ctx = this.previewCtx;
        const w = c.width = c.offsetWidth * window.devicePixelRatio;
        const h = c.height = c.offsetHeight * window.devicePixelRatio;

        ctx.clearRect(0, 0, w, h);
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, w, h);

        // Apply per-deck FX
        const hasFx = this.fx.brightness !== 100 || this.fx.contrast !== 100 ||
                       this.fx.saturate !== 100 || this.fx.hue !== 0;
        if (hasFx) ctx.filter = this.getFxFilter();

        if (this.currentType === 'image' && this.imageFading) {
            const elapsed = (performance.now() - this.imageFadeStart) / 1000;
            this.imageFade = Math.min(elapsed / 0.5, 1.0);
            if (this.imageFade >= 1.0) {
                this.imageFading = false;
                this.prevImage = null;
            }

            if (this.prevImage) {
                ctx.globalAlpha = 1 - this.imageFade;
                drawFit(ctx, this.prevImage, w, h);
            }
            if (this.currentImage) {
                ctx.globalAlpha = this.imageFade;
                drawFit(ctx, this.currentImage, w, h);
            }
            ctx.globalAlpha = 1;
        } else {
            const src = this.getDrawable();
            if (src) drawFit(ctx, src, w, h);
        }

        if (hasFx) ctx.filter = 'none';
    }
}


// ============================================================
// Mix Engine
// ============================================================
class MixEngine {
    constructor(deckA, deckB) {
        this.deckA = deckA;
        this.deckB = deckB;
        this.canvas = document.getElementById('master-output');
        this.ctx = this.canvas.getContext('2d');
        this.miniCanvas = document.getElementById('mini-preview');
        this.miniCtx = this.miniCanvas ? this.miniCanvas.getContext('2d') : null;
        this.crossfade = 0.5;
        this.transition = 'crossfade';
        this.fx = { brightness: 100, contrast: 100, saturate: 100, hue: 0 };
        this.outputW = 1920;
        this.outputH = 1080;

        this._autoMixing = false;
        this._autoMixStart = 0;
        this._autoMixFrom = 0;
        this._autoMixTo = 1;
        this._autoMixDuration = 2000;

        // Crossfader dead zones
        this._deadZone = 15; // out of 1000

        this._channel = new BroadcastChannel('mixer-sync');
        this._outputConnected = false;

        this._channel.onmessage = (e) => {
            if (e.data && e.data.type === 'output-heartbeat') {
                this._outputConnected = true;
                this._lastOutputHeartbeat = performance.now();
            }
        };
        this._lastOutputHeartbeat = 0;

        this._setResolution(1920, 1080);
        this._renderLoop();
    }

    _setResolution(w, h) {
        this.outputW = w;
        this.outputH = h;
        this.canvas.width = w;
        this.canvas.height = h;
    }

    setCrossfade(val) {
        // Apply dead zones at extremes
        if (val * 1000 <= this._deadZone) val = 0;
        else if (val * 1000 >= 1000 - this._deadZone) val = 1;
        this.crossfade = val;
    }

    setTransition(id) { this.transition = id; }
    setFx(key, val) { this.fx[key] = val; }
    resetFx() { this.fx = { brightness: 100, contrast: 100, saturate: 100, hue: 0 }; }

    startAutoMix(duration) {
        this._autoMixDuration = duration;
        this._autoMixFrom = this.crossfade;
        this._autoMixTo = this.crossfade < 0.5 ? 1.0 : 0.0;
        this._autoMixStart = performance.now();
        this._autoMixing = true;
    }

    _updateAutoMix() {
        if (!this._autoMixing) return;
        const elapsed = performance.now() - this._autoMixStart;
        const t = Math.min(elapsed / this._autoMixDuration, 1.0);
        const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
        this.crossfade = this._autoMixFrom + (this._autoMixTo - this._autoMixFrom) * eased;
        document.getElementById('crossfader').value = this.crossfade * 1000;
        if (t >= 1.0) {
            this._autoMixing = false;
            document.getElementById('auto-mix-btn').classList.remove('mixing');
        }
    }

    _broadcastState() {
        if (performance.now() - this._lastOutputHeartbeat > 3000) {
            this._outputConnected = false;
        }
        updateOutputStatus(this._outputConnected);

        this._channel.postMessage({
            crossfade: this.crossfade,
            transition: this.transition,
            fx: { ...this.fx },
            deckA: this.deckA.getState(),
            deckB: this.deckB.getState(),
            sendAudio: true,
        });
    }

    _renderLoop() {
        this._updateAutoMix();
        this.deckA.drawPreview();
        this.deckB.drawPreview();
        this._renderMaster();
        this._renderMiniPreview();
        this._broadcastState();
        this._updateVU();
        requestAnimationFrame(() => this._renderLoop());
    }

    _updateVU() {
        const levelA = this.deckA.getAudioLevel();
        const levelB = this.deckB.getAudioLevel();
        const fillA = document.getElementById('vu-fill-a');
        const fillB = document.getElementById('vu-fill-b');
        if (fillA) {
            fillA.style.width = (levelA * 100) + '%';
            fillA.classList.toggle('hot', levelA > 0.8);
        }
        if (fillB) {
            fillB.style.width = (levelB * 100) + '%';
            fillB.classList.toggle('hot', levelB > 0.8);
        }
    }

    _renderMiniPreview() {
        if (!this.miniCanvas || !this.miniCtx) return;
        const mc = this.miniCanvas;
        const mctx = this.miniCtx;
        const dpr = window.devicePixelRatio || 1;
        mc.width = mc.offsetWidth * dpr;
        mc.height = mc.offsetHeight * dpr;
        if (this.canvas.width > 0 && this.canvas.height > 0) {
            mctx.drawImage(this.canvas, 0, 0, mc.width, mc.height);
        }
    }

    _renderMaster() {
        const ctx = this.ctx;
        const w = this.outputW;
        const h = this.outputH;
        const cf = this.crossfade;

        // Get drawables with per-deck FX baked in
        const srcA = this.deckA.getDrawable();
        const srcB = this.deckB.getDrawable();

        const filterStr = `brightness(${this.fx.brightness}%) contrast(${this.fx.contrast}%) saturate(${this.fx.saturate}%) hue-rotate(${this.fx.hue}deg)`;
        ctx.filter = filterStr;

        ctx.clearRect(0, 0, w, h);
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, w, h);

        const transition = window.TransitionRegistry.get(this.transition);
        transition.render(ctx, srcA, srcB, cf, w, h, drawFit);

        ctx.filter = 'none';
        ctx.globalAlpha = 1;
        ctx.globalCompositeOperation = 'source-over';
    }

    getSessionData() {
        return {
            crossfade: this.crossfade,
            transition: this.transition,
            fx: { ...this.fx },
            outputW: this.outputW,
            outputH: this.outputH,
        };
    }

    loadSessionData(data) {
        if (data.crossfade != null) {
            this.crossfade = data.crossfade;
            document.getElementById('crossfader').value = data.crossfade * 1000;
        }
        if (data.transition) this.transition = data.transition;
        if (data.fx) this.fx = data.fx;
        if (data.outputW && data.outputH) this._setResolution(data.outputW, data.outputH);
    }
}


// ============================================================
// Utility Functions
// ============================================================

function drawFit(ctx, src, canvasW, canvasH) {
    const srcW = src.videoWidth || src.naturalWidth || src.width;
    const srcH = src.videoHeight || src.naturalHeight || src.height;
    if (!srcW || !srcH) return;
    const scale = Math.max(canvasW / srcW, canvasH / srcH);
    const drawW = srcW * scale;
    const drawH = srcH * scale;
    const x = (canvasW - drawW) / 2;
    const y = (canvasH - drawH) / 2;
    ctx.drawImage(src, x, y, drawW, drawH);
}

function fmtTime(seconds) {
    const m = Math.floor(seconds / 60);
    const s = Math.floor(seconds % 60);
    return `${m}:${s.toString().padStart(2, '0')}`;
}

function escHtml(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
}


// ============================================================
// File Browser
// ============================================================
class FileBrowser {
    constructor(deckId, deck) {
        this.deckId = deckId;
        this.deck = deck;
        this.currentPath = '';
        this.currentFiles = [];

        this.pathDisplay = document.querySelector(`.browser-path[data-deck="${deckId}"]`);
        this.fileList = document.querySelector(`.file-list[data-deck="${deckId}"]`);
        this.pathInput = document.querySelector(`.path-input[data-deck="${deckId}"]`);

        this._setupEvents();
        this._loadHome();
    }

    _setupEvents() {
        document.querySelectorAll(`.nav-btn[data-deck="${this.deckId}"]`).forEach(btn => {
            btn.addEventListener('click', () => {
                const nav = btn.dataset.nav;
                if (nav === 'up' && this.currentPath) {
                    this.browse(this.currentPath.split('/').slice(0, -1).join('/') || '/');
                } else if (nav === 'home') {
                    this._loadHome();
                } else if (nav === 'go') {
                    const val = this.pathInput.value.trim();
                    if (val) this.browse(val);
                }
            });
        });

        this.pathInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                const val = this.pathInput.value.trim();
                if (val) this.browse(val);
            }
        });

        document.querySelector(`.load-folder-btn[data-deck="${this.deckId}"]`).addEventListener('click', () => {
            if (this.currentFiles.length > 0) {
                this.deck.loadPlaylist(this.currentFiles);
                this._renderFileList();
            }
        });
    }

    async _loadHome() {
        try {
            const resp = await fetch('/api/home');
            const data = await resp.json();
            this.browse(data.path);
        } catch (e) {
            this.browse('/');
        }
    }

    async browse(path) {
        try {
            const resp = await fetch(`/api/browse?path=${encodeURIComponent(path)}`);
            if (!resp.ok) return;
            const data = await resp.json();
            this.currentPath = data.current;
            this.deck.browserPath = data.current;
            this.pathDisplay.textContent = data.current;
            this.pathInput.value = data.current;
            this.currentFiles = data.files;
            this._renderBrowser(data);
        } catch (e) {
            console.error('Browse error:', e);
        }
    }

    _renderBrowser(data) {
        this.fileList.innerHTML = '';

        if (data.parent && data.parent !== data.current) {
            const li = document.createElement('li');
            li.innerHTML = '<span class="icon dir-icon">&#x1F4C1;</span> ..';
            li.addEventListener('click', () => this.browse(data.parent));
            this.fileList.appendChild(li);
        }

        data.dirs.forEach(dir => {
            const li = document.createElement('li');
            li.innerHTML = `<span class="icon dir-icon">&#x1F4C1;</span> ${escHtml(dir.name)}`;
            li.addEventListener('click', () => this.browse(dir.path));
            this.fileList.appendChild(li);
        });

        data.files.forEach((file, idx) => {
            const li = document.createElement('li');
            const iconClass = file.type === 'video' ? 'vid-icon' : 'img-icon';
            const icon = file.type === 'video' ? '&#x1F3AC;' : '&#x1F5BC;';
            li.innerHTML = `<span class="icon ${iconClass}">${icon}</span> ${escHtml(file.name)}`;
            li.dataset.index = idx;

            if (this.deck.playlist.length > 0 &&
                this.deck.currentIndex >= 0 &&
                this.deck.playlist[this.deck.currentIndex]?.path === file.path) {
                li.classList.add('active');
            }

            li.addEventListener('dblclick', () => {
                this.deck.loadPlaylist(data.files);
                this.deck.loadItem(idx);
                this.deck.play();
                updatePlayButton(this.deckId, true);
            });

            // Drag reorder within playlist
            li.draggable = true;
            li.addEventListener('dragstart', (e) => {
                e.dataTransfer.setData('text/plain', idx.toString());
                e.dataTransfer.effectAllowed = 'move';
                li.classList.add('dragging');
            });
            li.addEventListener('dragend', () => li.classList.remove('dragging'));
            li.addEventListener('dragover', (e) => {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                li.classList.add('drag-over');
            });
            li.addEventListener('dragleave', () => li.classList.remove('drag-over'));
            li.addEventListener('drop', (e) => {
                e.preventDefault();
                li.classList.remove('drag-over');
                const fromIdx = parseInt(e.dataTransfer.getData('text/plain'));
                const toIdx = idx;
                if (fromIdx !== toIdx && this.deck.playlist.length > 0) {
                    const items = [...this.deck.playlist];
                    const [moved] = items.splice(fromIdx, 1);
                    items.splice(toIdx, 0, moved);
                    this.deck.playlist = items;
                    // Adjust current index
                    if (this.deck.currentIndex === fromIdx) {
                        this.deck.currentIndex = toIdx;
                    } else if (fromIdx < this.deck.currentIndex && toIdx >= this.deck.currentIndex) {
                        this.deck.currentIndex--;
                    } else if (fromIdx > this.deck.currentIndex && toIdx <= this.deck.currentIndex) {
                        this.deck.currentIndex++;
                    }
                    this._renderBrowser(data);
                }
            });

            this.fileList.appendChild(li);
        });
    }

    _renderFileList() {
        if (this.currentPath) this.browse(this.currentPath);
    }
}


// ============================================================
// Transition Picker UI
// ============================================================

function buildTransitionPicker() {
    const picker = document.getElementById('transition-picker');
    const currentName = document.getElementById('transition-current-name');
    const categories = window.TransitionRegistry.getByCategory();

    picker.innerHTML = '';

    for (const [catName, transitions] of Object.entries(categories)) {
        const catDiv = document.createElement('div');
        catDiv.className = 'trans-category';

        const catLabel = document.createElement('div');
        catLabel.className = 'trans-cat-label';
        catLabel.textContent = catName;
        catDiv.appendChild(catLabel);

        const catItems = document.createElement('div');
        catItems.className = 'trans-cat-items';

        for (const t of transitions) {
            const btn = document.createElement('button');
            btn.className = 'trans-btn';
            btn.textContent = t.name;
            btn.dataset.transId = t.id;
            btn.title = t.name;
            if (t.id === 'crossfade') btn.classList.add('active');

            btn.addEventListener('click', () => {
                picker.querySelectorAll('.trans-btn').forEach(b => b.classList.remove('active'));
                btn.classList.add('active');
                mixer.setTransition(t.id);
                currentName.textContent = t.name;
            });
            catItems.appendChild(btn);
        }

        catDiv.appendChild(catItems);
        picker.appendChild(catDiv);
    }
}

async function loadCustomTransitions() {
    try {
        const resp = await fetch('/api/transitions');
        const data = await resp.json();
        const customList = document.getElementById('custom-list');
        customList.innerHTML = '';

        // Remove old custom transition <script> tags to prevent pile-up
        document.querySelectorAll('script[data-custom-transition]').forEach(s => s.remove());

        if (data.transitions.length === 0) {
            customList.innerHTML = '<span class="no-custom">No custom transitions. Add .js files to ~/VideoMixer/transitions/</span>';
            return;
        }

        for (const t of data.transitions) {
            try {
                const script = document.createElement('script');
                script.src = `/api/transitions/${encodeURIComponent(t.name)}?t=${Date.now()}`;
                script.dataset.customTransition = t.name;
                document.head.appendChild(script);

                const tag = document.createElement('span');
                tag.className = 'custom-tag';
                tag.textContent = t.name;
                customList.appendChild(tag);
            } catch (e) {
                console.error(`Failed to load custom transition: ${t.name}`, e);
            }
        }

        setTimeout(() => buildTransitionPicker(), 500);
    } catch (e) {
        console.error('Failed to load custom transitions:', e);
    }
}


// ============================================================
// Output Window Management
// ============================================================

function updateOutputStatus(connected) {
    const el = document.getElementById('output-status');
    if (connected) {
        el.textContent = 'Output: Connected';
        el.className = 'output-connected';
    } else {
        el.textContent = 'Output: --';
        el.className = 'output-disconnected';
    }
}

function openOutputWindow() {
    if (window.pywebview && window.pywebview.api) {
        window.pywebview.api.open_output_window();
    } else {
        window.open(
            '/output',
            'VideoMixerOutput',
            'width=1280,height=720,resizable=yes,menubar=no,toolbar=no,location=no,status=no'
        );
    }
}


// ============================================================
// BPM / Beat Sync
// ============================================================
class BPMSync {
    constructor() {
        this.bpm = 0;
        this.taps = [];
        this.syncing = false;
        this._beatTimer = null;
    }

    tap() {
        const now = performance.now();
        this.taps.push(now);

        // Keep only last 8 taps
        if (this.taps.length > 8) this.taps.shift();

        // Reset if gap > 3 seconds
        if (this.taps.length >= 2) {
            const gap = now - this.taps[this.taps.length - 2];
            if (gap > 3000) {
                this.taps = [now];
                this.bpm = 0;
                this._updateDisplay();
                return;
            }
        }

        if (this.taps.length >= 2) {
            let totalGap = 0;
            for (let i = 1; i < this.taps.length; i++) {
                totalGap += this.taps[i] - this.taps[i - 1];
            }
            const avgGap = totalGap / (this.taps.length - 1);
            this.bpm = Math.round(60000 / avgGap);
        }

        this._updateDisplay();

        // Flash the button
        const btn = document.getElementById('tap-tempo-btn');
        btn.classList.add('flash');
        setTimeout(() => btn.classList.remove('flash'), 100);
    }

    toggleSync() {
        this.syncing = !this.syncing;
        const btn = document.getElementById('beat-sync-btn');
        btn.classList.toggle('active', this.syncing);

        if (this.syncing && this.bpm > 0) {
            this._startBeatSync();
        } else {
            clearInterval(this._beatTimer);
            this._beatTimer = null;
        }
    }

    _startBeatSync() {
        clearInterval(this._beatTimer);
        if (this.bpm <= 0) return;

        const interval = 60000 / this.bpm;
        // Trigger auto-mix on every 4th beat
        let beatCount = 0;
        this._beatTimer = setInterval(() => {
            if (!this.syncing) {
                clearInterval(this._beatTimer);
                return;
            }
            beatCount++;
            if (beatCount % 4 === 0) {
                const dur = parseInt(document.getElementById('auto-mix-duration').value);
                mixer.startAutoMix(dur);
                document.getElementById('auto-mix-btn').classList.add('mixing');
            }
        }, interval);
    }

    _updateDisplay() {
        const el = document.getElementById('bpm-display');
        if (el) el.textContent = this.bpm > 0 ? `${this.bpm} BPM` : '-- BPM';
    }
}


// ============================================================
// Session Save/Load
// ============================================================

function saveSession() {
    const session = {
        version: 2,
        timestamp: Date.now(),
        mixer: mixer.getSessionData(),
        deckA: deckA.getSessionData(),
        deckB: deckB.getSessionData(),
        autopilot: {
            holdTime: autopilot.holdTime,
            transitionTime: autopilot.transitionTime,
        },
        bpm: bpmSync.bpm,
        theme: document.documentElement.dataset.theme || 'dark',
    };

    const json = JSON.stringify(session, null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `video-mixer-session-${new Date().toISOString().slice(0, 10)}.json`;
    a.click();
    URL.revokeObjectURL(url);
}

function loadSession() {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';
    input.addEventListener('change', (e) => {
        const file = e.target.files[0];
        if (!file) return;
        const reader = new FileReader();
        reader.onload = (ev) => {
            try {
                const session = JSON.parse(ev.target.result);
                if (session.mixer) mixer.loadSessionData(session.mixer);
                if (session.deckA) deckA.loadSessionData(session.deckA);
                if (session.deckB) deckB.loadSessionData(session.deckB);
                if (session.autopilot) {
                    autopilot.setHoldTime(session.autopilot.holdTime);
                    autopilot.setTransitionTime(session.autopilot.transitionTime);
                    document.getElementById('autopilot-hold').value = session.autopilot.holdTime;
                    document.getElementById('autopilot-hold-val').textContent = session.autopilot.holdTime + 's';
                    document.getElementById('autopilot-trans').value = session.autopilot.transitionTime;
                    document.getElementById('autopilot-trans-val').textContent = session.autopilot.transitionTime + 's';
                }
                if (session.theme) {
                    document.documentElement.dataset.theme = session.theme;
                    localStorage.setItem('mixer-theme', session.theme);
                }
                // Restore UI sliders
                restoreUIFromDecks();
            } catch (err) {
                alert('Failed to load session: ' + err.message);
            }
        };
        reader.readAsText(file);
    });
    input.click();
}

function restoreUIFromDecks() {
    ['a', 'b'].forEach(id => {
        const deck = id === 'a' ? deckA : deckB;
        // Volume
        const volSlider = document.querySelector(`.vol-slider[data-deck="${id}"]`);
        if (volSlider) volSlider.value = Math.round(deck.volume * 100);
        const volVal = document.querySelector(`.vol-val[data-deck="${id}"]`);
        if (volVal) volVal.textContent = Math.round(deck.volume * 100);
        // Speed
        const speedSlider = document.querySelector(`.speed-slider[data-deck="${id}"]`);
        if (speedSlider) speedSlider.value = Math.round(deck.speed * 100);
        const speedVal = document.querySelector(`.speed-val[data-deck="${id}"]`);
        if (speedVal) speedVal.textContent = deck.speed.toFixed(1) + 'x';
        // Slide duration
        const slideSlider = document.querySelector(`.slide-dur-slider[data-deck="${id}"]`);
        if (slideSlider) slideSlider.value = deck.slideDuration;
        const slideVal = document.querySelector(`.slide-dur-val[data-deck="${id}"]`);
        if (slideVal) slideVal.textContent = deck.slideDuration === 0 ? 'OFF' : deck.slideDuration + 's';
        // Loop/shuffle buttons
        const loopBtn = document.querySelector(`.transport-btn[data-deck="${id}"][data-action="loop"]`);
        if (loopBtn) loopBtn.classList.toggle('active', deck.loop);
        const shuffleBtn = document.querySelector(`.transport-btn[data-deck="${id}"][data-action="shuffle"]`);
        if (shuffleBtn) shuffleBtn.classList.toggle('active', deck.shuffle);
        // Per-deck FX
        ['brightness', 'contrast', 'saturate', 'hue'].forEach(key => {
            const sl = document.querySelector(`.dfx-slider[data-deck="${id}"][data-fx="${key}"]`);
            if (sl) sl.value = deck.fx[key];
        });
        // Play button
        updatePlayButton(id, deck.playing);
    });
    // Master FX
    document.getElementById('fx-brightness').value = mixer.fx.brightness;
    document.getElementById('fx-contrast').value = mixer.fx.contrast;
    document.getElementById('fx-saturate').value = mixer.fx.saturate;
    document.getElementById('fx-hue').value = mixer.fx.hue;
}


// ============================================================
// Drag & Drop
// ============================================================

// Media file extensions
const MEDIA_EXTS = new Set(['.mp4', '.mov', '.webm', '.m4v', '.ogv', '.mkv',
                            '.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff', '.tif']);
const VIDEO_EXTS = new Set(['.mp4', '.mov', '.webm', '.m4v', '.ogv', '.mkv']);

function getExt(name) {
    const dot = name.lastIndexOf('.');
    return dot >= 0 ? name.slice(dot).toLowerCase() : '';
}

function isMediaFile(name) {
    return MEDIA_EXTS.has(getExt(name));
}

/**
 * Recursively read all files from a FileSystemDirectoryEntry.
 * Returns a Promise<File[]>.
 */
function readDirectoryRecursive(dirEntry) {
    return new Promise((resolve) => {
        const allFiles = [];
        let pending = 0;

        function readEntries(reader, done) {
            reader.readEntries((entries) => {
                if (entries.length === 0) { done(); return; }
                entries.forEach(entry => processEntry(entry));
                // readEntries may not return all at once — keep reading
                readEntries(reader, done);
            }, () => done());
        }

        function processEntry(entry) {
            pending++;
            if (entry.isFile) {
                entry.file((file) => {
                    // Preserve the relative path from the entry
                    if (isMediaFile(file.name)) {
                        allFiles.push(file);
                    }
                    pending--;
                    if (pending === 0) resolve(allFiles);
                }, () => { pending--; if (pending === 0) resolve(allFiles); });
            } else if (entry.isDirectory) {
                const reader = entry.createReader();
                readEntries(reader, () => {
                    pending--;
                    if (pending === 0) resolve(allFiles);
                });
            } else {
                pending--;
                if (pending === 0) resolve(allFiles);
            }
        }

        const reader = dirEntry.createReader();
        readEntries(reader, () => {
            if (pending === 0) resolve(allFiles);
        });
    });
}

/**
 * Process a drop event — handles both individual files and full folders.
 * Uses webkitGetAsEntry() to detect directories and recursively enumerate.
 */
async function processDroppedItems(dataTransfer) {
    const items = dataTransfer.items;
    const allFiles = [];

    if (items && items.length > 0 && items[0].webkitGetAsEntry) {
        // Modern approach: use entries API to handle folders
        const entryPromises = [];
        for (let i = 0; i < items.length; i++) {
            const entry = items[i].webkitGetAsEntry();
            if (!entry) continue;
            if (entry.isDirectory) {
                entryPromises.push(readDirectoryRecursive(entry));
            } else if (entry.isFile) {
                entryPromises.push(new Promise((resolve) => {
                    entry.file((f) => resolve(isMediaFile(f.name) ? [f] : []),
                               () => resolve([]));
                }));
            }
        }
        const results = await Promise.all(entryPromises);
        results.forEach(files => allFiles.push(...files));
    } else {
        // Fallback: plain file list (no folder support)
        Array.from(dataTransfer.files).forEach(file => {
            if (isMediaFile(file.name)) allFiles.push(file);
        });
    }

    // Sort files alphabetically by name
    allFiles.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));

    // Convert to playlist items with object URLs
    return allFiles.map(file => {
        const ext = getExt(file.name);
        return {
            name: file.name,
            path: URL.createObjectURL(file),
            type: VIDEO_EXTS.has(ext) ? 'video' : 'image',
            ext: ext,
            _isObjectUrl: true,
        };
    });
}

function setupDragDrop() {
    // Enable drop on each entire deck panel (not just preview)
    document.querySelectorAll('.deck').forEach(deckEl => {
        const deckId = deckEl.id.replace('deck-', '');
        const overlay = deckEl.querySelector('.drop-overlay');

        deckEl.addEventListener('dragover', (e) => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'copy';
            if (overlay) overlay.classList.remove('hidden');
        });

        deckEl.addEventListener('dragleave', (e) => {
            if (!deckEl.contains(e.relatedTarget)) {
                if (overlay) overlay.classList.add('hidden');
            }
        });

        deckEl.addEventListener('drop', async (e) => {
            e.preventDefault();
            if (overlay) overlay.classList.add('hidden');

            // Show loading state
            const deck = deckId === 'a' ? deckA : deckB;
            deck.nowPlaying.textContent = 'Loading...';

            const playlist = await processDroppedItems(e.dataTransfer);

            if (playlist.length > 0) {
                deck.playlist = playlist;
                deck.loadItemDirect(0);
                deck.play();
                updatePlayButton(deckId, true);
                deck.nowPlaying.textContent = playlist[0].name;
            } else {
                deck.nowPlaying.textContent = 'No media files found';
                setTimeout(() => {
                    if (deck.nowPlaying.textContent === 'No media files found') {
                        deck.nowPlaying.textContent = 'No media loaded';
                    }
                }, 3000);
            }
        });
    });
}


// ============================================================
// Theme Toggle
// ============================================================

function initTheme() {
    const saved = localStorage.getItem('mixer-theme');
    if (saved) {
        document.documentElement.dataset.theme = saved;
    }
    // No auto day/night for the mixer — user controls it
}

function toggleTheme() {
    const html = document.documentElement;
    const next = html.dataset.theme === 'light' ? 'dark' : 'light';
    html.dataset.theme = next;
    localStorage.setItem('mixer-theme', next);
}


// ============================================================
// Autopilot
// ============================================================
class Autopilot {
    constructor() {
        this.active = false;
        this.holdTime = 8;
        this.transitionTime = 3;
        this._timer = null;
        this._liveDeck = 'a';
        this._state = 'holding';
    }

    start() {
        if (this.active) return;
        this.active = true;

        if (!deckA.playing && deckA.playlist.length > 0) {
            deckA.play();
            updatePlayButton('a', true);
        }
        if (!deckB.playing && deckB.playlist.length > 0) {
            deckB.play();
            updatePlayButton('b', true);
        }

        this._liveDeck = mixer.crossfade < 0.5 ? 'a' : 'b';
        this._state = 'holding';

        this._scheduleNext();
        this._updateUI();
    }

    stop() {
        this.active = false;
        clearTimeout(this._timer);
        this._timer = null;
        this._updateUI();
    }

    toggle() {
        if (this.active) this.stop();
        else this.start();
    }

    setHoldTime(val) { this.holdTime = val; }
    setTransitionTime(val) { this.transitionTime = val; }

    _scheduleNext() {
        if (!this.active) return;
        clearTimeout(this._timer);

        if (this._state === 'holding') {
            let holdMs = this.holdTime * 1000;
            const liveDeckObj = this._liveDeck === 'a' ? deckA : deckB;

            if (liveDeckObj.currentType === 'video' && liveDeckObj.video.duration) {
                const remaining = (liveDeckObj.video.duration - liveDeckObj.video.currentTime) * 1000;
                if (remaining > 0 && remaining < holdMs) {
                    holdMs = Math.max(remaining - this.transitionTime * 1000, 500);
                }
            }

            this._timer = setTimeout(() => {
                if (!this.active) return;
                this._startTransition();
            }, holdMs);
        }
    }

    _startTransition() {
        if (!this.active) return;
        this._state = 'transitioning';

        const nextDeck = this._liveDeck === 'a' ? 'b' : 'a';
        const nextDeckObj = nextDeck === 'a' ? deckA : deckB;

        nextDeckObj.next();
        if (!nextDeckObj.playing) {
            nextDeckObj.play();
            updatePlayButton(nextDeck, true);
        }

        const allTransitions = window.TransitionRegistry.getAll();
        const randomTrans = allTransitions[Math.floor(Math.random() * allTransitions.length)];
        mixer.setTransition(randomTrans.id);

        const picker = document.getElementById('transition-picker');
        if (picker) {
            picker.querySelectorAll('.trans-btn').forEach(b => b.classList.remove('active'));
            const activeBtn = picker.querySelector(`[data-trans-id="${randomTrans.id}"]`);
            if (activeBtn) activeBtn.classList.add('active');
        }
        const nameEl = document.getElementById('transition-current-name');
        if (nameEl) nameEl.textContent = randomTrans.name;

        mixer.startAutoMix(this.transitionTime * 1000);

        this._timer = setTimeout(() => {
            if (!this.active) return;
            this._liveDeck = nextDeck;
            this._state = 'holding';
            this._scheduleNext();
        }, this.transitionTime * 1000 + 200);
    }

    _updateUI() {
        const btn = document.getElementById('autopilot-btn');
        if (!btn) return;
        if (this.active) {
            btn.textContent = 'AUTOPILOT ON';
            btn.classList.add('active');
        } else {
            btn.textContent = 'Autopilot';
            btn.classList.remove('active');
        }
    }
}


// ============================================================
// UI Controller
// ============================================================

let deckA, deckB, mixer, browserA, browserB, autopilot, bpmSync;

document.addEventListener('DOMContentLoaded', () => {
    initTheme();

    deckA = new Deck('a');
    deckB = new Deck('b');

    // Add direct load method for drag-dropped files (object URLs)
    Deck.prototype.loadItemDirect = function(index) {
        if (index < 0 || index >= this.playlist.length) return;
        this.currentIndex = index;
        const item = this.playlist[index];
        this.nowPlaying.textContent = item.name;
        this._hideError();

        this.video.pause();
        this.video.removeAttribute('src');
        clearTimeout(this.imageTimer);
        this.imageFading = false;

        this.currentMediaUrl = item.path; // Object URL

        if (item.type === 'video') {
            this.currentType = 'video';
            this.currentImage = null;
            this.video.src = item.path;
            this.video.volume = this.volume;
            this.video.playbackRate = this.speed;
            if (this.playing) this.video.play().catch(() => {});
        } else {
            this.currentType = 'image';
            this.prevImage = this.currentImage;
            this.imageLoadTime = performance.now();
            const img = new Image();
            img.onload = () => {
                this.currentImage = img;
                if (this.prevImage) {
                    this.imageFading = true;
                    this.imageFadeStart = performance.now();
                    this.imageFade = 0;
                } else {
                    this.imageFade = 1;
                }
            };
            img.onerror = () => {
                this._showError();
                this._handleMediaError();
            };
            img.src = item.path;
            if (this.playing) this._startImageTimer();
        }
    };

    mixer = new MixEngine(deckA, deckB);
    browserA = new FileBrowser('a', deckA);
    browserB = new FileBrowser('b', deckB);

    autopilot = new Autopilot();
    bpmSync = new BPMSync();

    buildTransitionPicker();
    loadCustomTransitions();

    setupTransportButtons();
    setupSliders();
    setupCrossfader();
    setupAutoMix();
    setupAutopilot();
    setupFx();
    setupDeckFx();
    setupOutputResolution();
    setupPopOut();
    setupKeyboard();
    setupProgressUpdater();
    setupDragDrop();
    setupBPM();
    setupSession();
    setupFullscreen();
    setupResumeAudio();
    setupThemeToggle();

    if (window.pywebview && window.pywebview.api) {
        setTimeout(() => openOutputWindow(), 1500);
    }

    // Auto-save session to localStorage — only mixer state and FX, NOT playlists
    // (Saving blob: URLs is useless and grows storage indefinitely.)
    setInterval(() => {
        try {
            // Strip playlist data — paths may be blob URLs that won't survive a reload
            const dA = deckA.getSessionData();
            const dB = deckB.getSessionData();
            delete dA.playlist;
            delete dB.playlist;
            const session = {
                mixer: mixer.getSessionData(),
                deckA: dA,
                deckB: dB,
                theme: document.documentElement.dataset.theme,
            };
            localStorage.setItem('mixer-autosave', JSON.stringify(session));
        } catch (e) {}
    }, 10000);

    // Auto-restore from localStorage
    try {
        const saved = localStorage.getItem('mixer-autosave');
        if (saved) {
            const session = JSON.parse(saved);
            if (session.mixer) mixer.loadSessionData(session.mixer);
            if (session.theme) document.documentElement.dataset.theme = session.theme;
            // Don't auto-restore playlists (paths may have changed)
        }
    } catch (e) {}
});


function setupTransportButtons() {
    document.querySelectorAll('.transport-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            const deckId = btn.dataset.deck;
            const action = btn.dataset.action;
            const deck = deckId === 'a' ? deckA : deckB;

            switch (action) {
                case 'play': {
                    const playing = deck.togglePlay();
                    updatePlayButton(deckId, playing);
                    break;
                }
                case 'next': deck.next(); break;
                case 'prev': deck.prev(); break;
                case 'loop':
                    deck.loop = !deck.loop;
                    btn.classList.toggle('active', deck.loop);
                    break;
                case 'shuffle':
                    deck.shuffle = !deck.shuffle;
                    btn.classList.toggle('active', deck.shuffle);
                    break;
            }
        });
    });
}

function updatePlayButton(deckId, playing) {
    const btn = document.querySelector(`.play-btn[data-deck="${deckId}"]`);
    if (playing) {
        btn.innerHTML = '&#x23F8;';
        btn.classList.add('active');
    } else {
        btn.innerHTML = '&#x25B6;';
        btn.classList.remove('active');
    }
}

function setupSliders() {
    document.querySelectorAll('.vol-slider').forEach(slider => {
        slider.addEventListener('input', () => {
            const deck = slider.dataset.deck === 'a' ? deckA : deckB;
            deck.setVolume(slider.value / 100);
        });
    });

    document.querySelectorAll('.speed-slider').forEach(slider => {
        slider.addEventListener('input', () => {
            const deck = slider.dataset.deck === 'a' ? deckA : deckB;
            const speed = slider.value / 100;
            deck.setSpeed(speed);
            document.querySelector(`.speed-val[data-deck="${slider.dataset.deck}"]`).textContent = speed.toFixed(1) + 'x';
        });
    });

    document.querySelectorAll('.slide-dur-slider').forEach(slider => {
        slider.addEventListener('input', () => {
            const deck = slider.dataset.deck === 'a' ? deckA : deckB;
            const val = parseInt(slider.value);
            deck.setSlideDuration(val);
            const display = document.querySelector(`.slide-dur-val[data-deck="${slider.dataset.deck}"]`);
            display.textContent = val === 0 ? 'OFF' : val + 's';
            // If turning slideshow ON while an image is playing, start the timer now
            if (val > 0 && deck.currentType === 'image' && deck.playing) {
                deck._startImageTimer();
            } else if (val === 0) {
                clearTimeout(deck.imageTimer);
            }
        });
    });

    document.querySelectorAll('.progress-slider').forEach(slider => {
        slider.addEventListener('input', () => {
            const deck = slider.dataset.deck === 'a' ? deckA : deckB;
            deck.seek(slider.value / 1000);
        });
    });
}

function setupCrossfader() {
    const cf = document.getElementById('crossfader');
    cf.addEventListener('input', () => {
        mixer.setCrossfade(cf.value / 1000);
    });
}

function setupAutoMix() {
    const durSlider = document.getElementById('auto-mix-duration');
    const durVal = document.getElementById('auto-mix-dur-val');
    durSlider.addEventListener('input', () => {
        durVal.textContent = (durSlider.value / 1000).toFixed(1) + 's';
    });

    document.getElementById('auto-mix-btn').addEventListener('click', () => {
        const dur = parseInt(document.getElementById('auto-mix-duration').value);
        mixer.startAutoMix(dur);
        document.getElementById('auto-mix-btn').classList.add('mixing');
    });
}

function setupAutopilot() {
    const btn = document.getElementById('autopilot-btn');
    if (btn) btn.addEventListener('click', () => autopilot.toggle());

    const holdSlider = document.getElementById('autopilot-hold');
    const holdVal = document.getElementById('autopilot-hold-val');
    if (holdSlider) {
        holdSlider.addEventListener('input', () => {
            autopilot.setHoldTime(parseInt(holdSlider.value));
            holdVal.textContent = holdSlider.value + 's';
        });
    }

    const transSlider = document.getElementById('autopilot-trans');
    const transVal = document.getElementById('autopilot-trans-val');
    if (transSlider) {
        transSlider.addEventListener('input', () => {
            autopilot.setTransitionTime(parseFloat(transSlider.value));
            transVal.textContent = transSlider.value + 's';
        });
    }
}

function setupFx() {
    ['brightness', 'contrast', 'saturate', 'hue'].forEach(key => {
        const el = document.getElementById(`fx-${key}`);
        el.addEventListener('input', () => {
            mixer.setFx(key, parseInt(el.value));
        });
    });

    document.getElementById('fx-reset').addEventListener('click', () => {
        mixer.resetFx();
        document.getElementById('fx-brightness').value = 100;
        document.getElementById('fx-contrast').value = 100;
        document.getElementById('fx-saturate').value = 100;
        document.getElementById('fx-hue').value = 0;
    });
}

function setupDeckFx() {
    // Toggle per-deck FX panels
    document.querySelectorAll('.deck-fx-toggle').forEach(btn => {
        btn.addEventListener('click', () => {
            const deckId = btn.dataset.deck;
            const controls = document.querySelector(`.deck-fx-controls[data-deck="${deckId}"]`);
            controls.classList.toggle('hidden');
            btn.innerHTML = controls.classList.contains('hidden') ? 'FX &#x25BC;' : 'FX &#x25B2;';
        });
    });

    // Per-deck FX sliders
    document.querySelectorAll('.dfx-slider').forEach(slider => {
        slider.addEventListener('input', () => {
            const deck = slider.dataset.deck === 'a' ? deckA : deckB;
            deck.setFx(slider.dataset.fx, parseInt(slider.value));
        });
    });

    // Reset per-deck FX
    document.querySelectorAll('.deck-fx-reset').forEach(btn => {
        btn.addEventListener('click', () => {
            const deckId = btn.dataset.deck;
            const deck = deckId === 'a' ? deckA : deckB;
            deck.resetFx();
            document.querySelectorAll(`.dfx-slider[data-deck="${deckId}"]`).forEach(sl => {
                sl.value = sl.dataset.fx === 'hue' ? 0 : 100;
            });
        });
    });
}

function setupOutputResolution() {
    document.getElementById('output-resolution').addEventListener('change', (e) => {
        const [w, h] = e.target.value.split('x').map(Number);
        mixer._setResolution(w, h);
    });
}

function setupPopOut() {
    document.getElementById('popout-btn').addEventListener('click', openOutputWindow);
    document.getElementById('load-custom-btn').addEventListener('click', () => {
        loadCustomTransitions();
    });
    const shortcutsBtn = document.getElementById('shortcuts-btn');
    if (shortcutsBtn) {
        shortcutsBtn.addEventListener('click', () => {
            document.getElementById('shortcuts-overlay').classList.toggle('hidden');
        });
    }
}

function setupBPM() {
    document.getElementById('tap-tempo-btn').addEventListener('click', () => bpmSync.tap());
    document.getElementById('beat-sync-btn').addEventListener('click', () => bpmSync.toggleSync());
}

function setupSession() {
    document.getElementById('save-session-btn').addEventListener('click', saveSession);
    document.getElementById('load-session-btn').addEventListener('click', loadSession);
}

function setupFullscreen() {
    document.getElementById('fullscreen-btn').addEventListener('click', () => {
        if (document.fullscreenElement) {
            document.exitFullscreen();
        } else {
            document.documentElement.requestFullscreen();
        }
    });
}

/**
 * Resume any suspended AudioContexts on focus / visibility change.
 * Browsers auto-suspend AudioContexts after periods of inactivity which
 * can cause audio to "die" after the app has been running a while.
 */
function setupResumeAudio() {
    const resumeAll = () => {
        [deckA, deckB].forEach(d => {
            try {
                if (d._audioCtx && d._audioCtx.state === 'suspended') {
                    d._audioCtx.resume().catch(() => {});
                }
            } catch (e) {}
        });
    };
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) resumeAll();
    });
    window.addEventListener('focus', resumeAll);
    window.addEventListener('click', resumeAll, { once: false });
    // Periodic resume guard
    setInterval(resumeAll, 30000);
}

/**
 * Detect a sluggish render loop and try to recover. If frames stop
 * coming through (e.g. after sleep/wake), kick the loop back on.
 */
function setupRenderHealthCheck() {
    let lastTick = performance.now();
    function tick() {
        lastTick = performance.now();
        requestAnimationFrame(tick);
    }
    requestAnimationFrame(tick);

    setInterval(() => {
        const lag = performance.now() - lastTick;
        if (lag > 5000) {
            // Render loop stalled — kick it
            console.warn('Render loop stalled, kicking');
            requestAnimationFrame(tick);
        }
    }, 5000);
}

function setupThemeToggle() {
    // Add theme toggle button dynamically to toolbar
    const controls = document.getElementById('toolbar-controls');
    const btn = document.createElement('button');
    btn.id = 'theme-toggle-btn';
    btn.title = 'Toggle Light/Dark Mode';
    btn.textContent = document.documentElement.dataset.theme === 'dark' ? '☀' : '☾';
    btn.addEventListener('click', () => {
        toggleTheme();
        btn.textContent = document.documentElement.dataset.theme === 'dark' ? '☀' : '☾';
    });
    // Insert before the ? button
    const shortcutsBtn = document.getElementById('shortcuts-btn');
    controls.insertBefore(btn, shortcutsBtn);
}

function setupKeyboard() {
    const shortcutsOverlay = document.getElementById('shortcuts-overlay');
    document.getElementById('close-shortcuts').addEventListener('click', () => {
        shortcutsOverlay.classList.add('hidden');
    });

    document.addEventListener('keydown', (e) => {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;

        switch (e.key.toLowerCase()) {
            case 'q': {
                const playing = deckA.togglePlay();
                updatePlayButton('a', playing);
                break;
            }
            case 'w': {
                const playing = deckB.togglePlay();
                updatePlayButton('b', playing);
                break;
            }
            case 'a': deckA.prev(); break;
            case 's':
                if (e.ctrlKey || e.metaKey) {
                    e.preventDefault();
                    saveSession();
                } else {
                    deckA.next();
                }
                break;
            case 'k': deckB.prev(); break;
            case 'l': deckB.next(); break;
            case 'arrowleft': {
                e.preventDefault();
                const cf = document.getElementById('crossfader');
                cf.value = Math.max(0, parseInt(cf.value) - 50);
                mixer.setCrossfade(cf.value / 1000);
                break;
            }
            case 'arrowright': {
                e.preventDefault();
                const cf = document.getElementById('crossfader');
                cf.value = Math.min(1000, parseInt(cf.value) + 50);
                mixer.setCrossfade(cf.value / 1000);
                break;
            }
            case ' ':
                e.preventDefault();
                document.getElementById('auto-mix-btn').click();
                break;
            case 'o':
                openOutputWindow();
                break;
            case 'p':
                autopilot.toggle();
                break;
            case 'f':
                if (!e.ctrlKey && !e.metaKey) {
                    if (document.fullscreenElement) document.exitFullscreen();
                    else document.documentElement.requestFullscreen();
                }
                break;
            case 't':
                bpmSync.tap();
                break;
            case '1': // Vol down deck A
                deckA.setVolume(Math.max(0, deckA.volume - 0.05));
                break;
            case '3': // Vol up deck A
                deckA.setVolume(Math.min(1, deckA.volume + 0.05));
                break;
            case '2': // Vol down deck B
                deckB.setVolume(Math.max(0, deckB.volume - 0.05));
                break;
            case '4': // Vol up deck B
                deckB.setVolume(Math.min(1, deckB.volume + 0.05));
                break;
            case '?':
                shortcutsOverlay.classList.toggle('hidden');
                break;
        }
    });
}

function setupProgressUpdater() {
    setInterval(() => {
        ['a', 'b'].forEach(id => {
            const deck = id === 'a' ? deckA : deckB;
            const slider = document.querySelector(`.progress-slider[data-deck="${id}"]`);
            const display = document.querySelector(`.time-display[data-deck="${id}"]`);
            slider.value = deck.getProgress() * 1000;
            display.innerHTML = deck.getTimeDisplay();
        });
    }, 250);
}
