/**
 * Output Window Renderer
 * Receives state from the main mixer window via BroadcastChannel
 * and renders the mixed output independently at full resolution.
 * Now with audio passthrough and per-deck FX support.
 */

const canvas = document.getElementById('output-canvas');
const ctx = canvas.getContext('2d');
const statusEl = document.getElementById('status');

let state = null;
let connected = false;
let lastStateTime = 0;

const decks = {
    a: {
        video: document.createElement('video'), image: null, visual: null,
        currentSrc: '', sourceRevision: null, cueRevision: null, type: null, lastState: null,
    },
    b: {
        video: document.createElement('video'), image: null, visual: null,
        currentSrc: '', sourceRevision: null, cueRevision: null, type: null, lastState: null,
    },
};

const deckFxBuffers = {
    a: document.createElement('canvas'),
    b: document.createElement('canvas'),
};

// Configure video elements — audio enabled for output window
for (const id of ['a', 'b']) {
    const v = decks[id].video;
    v.playsInline = true;
    v.preload = 'auto';
    v.crossOrigin = 'anonymous';
    // Start muted, unmute once connected
    v.muted = true;
}

// Audio unmute on first user interaction (browser policy)
let audioUnlocked = false;
function unlockAudio() {
    if (audioUnlocked) return;
    audioUnlocked = true;
    for (const id of ['a', 'b']) {
        decks[id].video.muted = false;
    }
    document.removeEventListener('click', unlockAudio);
    document.removeEventListener('keydown', unlockAudio);
}
document.addEventListener('click', unlockAudio);
document.addEventListener('keydown', unlockAudio);

const channel = new BroadcastChannel('mixer-sync');
channel.onmessage = (e) => {
    state = e.data;
    lastStateTime = performance.now();

    if (!connected) {
        connected = true;
        statusEl.textContent = 'Connected — click to enable audio';
        statusEl.classList.add('connected');
        setTimeout(() => { statusEl.style.opacity = '0'; }, 4000);
    }

    syncDeck('a', state.deckA);
    syncDeck('b', state.deckB);
};

// Send heartbeat
setInterval(() => {
    channel.postMessage({ type: 'output-heartbeat' });
}, 1000);


function syncDeck(id, deckState) {
    if (!deckState) return;
    const d = decks[id];
    d.lastState = deckState;
    const mediaUrl = deckState.mediaUrl || '';
    const sourceRevision = deckState.sourceRevision ?? null;
    const cueRevision = deckState.cueRevision ?? null;

    if (!mediaUrl) {
        const hadSource = Boolean(d.currentSrc || d.type || d.image || d.visual
            || d.video.hasAttribute('src'));
        if (hadSource) {
            d.video.pause();
            d.video.removeAttribute('src');
            try { d.video.load(); } catch (e) {}
        }
        d.currentSrc = '';
        d.sourceRevision = sourceRevision;
        d.cueRevision = cueRevision;
        d.type = null;
        d.image = null;
        d.visual = null;
        return;
    }

    const sourceChanged = mediaUrl !== d.currentSrc
        || deckState.mediaType !== d.type
        || sourceRevision !== d.sourceRevision
        || cueRevision !== d.cueRevision;

    if (sourceChanged) {
        d.currentSrc = mediaUrl;
        d.sourceRevision = sourceRevision;
        d.cueRevision = cueRevision;
        d.type = deckState.mediaType;

        if (deckState.mediaType === 'video') {
            d.image = null;
            d.visual = null;
            d.video.src = mediaUrl;
            if (deckState.playing) {
                d.video.play().catch(() => {});
            }
        } else if (deckState.mediaType === 'visual') {
            // Legacy generative visual from an older saved session — render locally at high res
            d.image = null;
            d.video.pause();
            d.video.removeAttribute('src');
            const visualId = mediaUrl.slice('visual:'.length);
            const vw = (state && state.outputW) || 1280;
            const vh = (state && state.outputH) || 720;
            d.visual = window.createVisualInstance ? createVisualInstance(visualId, vw, vh) : null;
        } else {
            d.visual = null;
            d.video.pause();
            d.video.removeAttribute('src');
            const img = new Image();
            img.crossOrigin = 'anonymous';
            img.onload = () => {
                if (d.currentSrc === mediaUrl
                    && d.sourceRevision === sourceRevision
                    && d.cueRevision === cueRevision) d.image = img;
            };
            img.onerror = () => {
                if (d.currentSrc === mediaUrl
                    && d.sourceRevision === sourceRevision
                    && d.cueRevision === cueRevision) d.image = null;
            };
            img.src = mediaUrl;
        }
    }

    if (deckState.mediaType === 'visual' && d.visual && state) {
        const vw = state.outputW || 1280;
        const vh = state.outputH || 720;
        if (d.visual.canvas.width !== vw || d.visual.canvas.height !== vh) {
            const visualId = mediaUrl.slice('visual:'.length);
            d.visual = createVisualInstance(visualId, vw, vh);
        }
    }

    if (deckState.mediaType === 'video') {
        if (deckState.playing && d.video.paused) {
            d.video.play().catch(() => {});
        } else if (!deckState.playing && !d.video.paused) {
            d.video.pause();
        }

        // Sync time if drifted > 0.5s
        if (deckState.currentTime !== undefined && d.video.duration) {
            const drift = Math.abs(d.video.currentTime - deckState.currentTime);
            if (drift > 0.5) {
                d.video.currentTime = deckState.currentTime;
            }
        }

        if (deckState.speed && d.video.playbackRate !== deckState.speed) {
            d.video.playbackRate = deckState.speed;
        }

        // Sync volume based on crossfader position
        if (state) {
            const cf = state.crossfade || 0;
            const vol = deckState.volume || 1;
            if (id === 'a') {
                d.video.volume = vol * (1 - cf);
            } else {
                d.video.volume = vol * cf;
            }
        }
    }
}


function getDrawable(id) {
    const d = decks[id];
    if (d.type === 'video' && d.video.readyState >= 2) return d.video;
    if (d.type === 'image' && d.image) return d.image;
    if (d.type === 'visual' && d.visual) return d.visual.canvas;
    return null;
}

function getProcessedDrawable(id, w, h) {
    const src = getDrawable(id);
    if (!src) return null;
    const d = decks[id];
    const fx = d.lastState && d.lastState.fx;
    if (!fx || (fx.brightness === 100 && fx.contrast === 100
        && fx.saturate === 100 && fx.hue === 0)) return src;

    const buffer = deckFxBuffers[id];
    if (buffer.width !== w || buffer.height !== h) {
        buffer.width = w;
        buffer.height = h;
    }
    const bctx = buffer.getContext('2d');
    bctx.clearRect(0, 0, w, h);
    bctx.save();
    bctx.filter = `brightness(${fx.brightness}%) contrast(${fx.contrast}%) saturate(${fx.saturate}%) hue-rotate(${fx.hue}deg)`;
    drawFit(bctx, src, w, h);
    bctx.restore();
    return buffer;
}

function tickVisuals(now) {
    for (const id of ['a', 'b']) {
        const d = decks[id];
        if (d.type !== 'visual' || !d.visual || !d.lastState) continue;
        if (!d.lastState.playing) {
            d.visual._last = now; // freeze without a time jump on resume
            continue;
        }
        d.visual.tick(now, d.lastState.speed || 1, (state && state.bpm) || 0, 0);
    }
}


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 render() {
    const dpr = window.devicePixelRatio || 1;
    const w = Math.max(1, Math.round(window.innerWidth * dpr));
    const h = Math.max(1, Math.round(window.innerHeight * dpr));
    if (canvas.width !== w || canvas.height !== h) {
        canvas.width = w;
        canvas.height = h;
    }

    tickVisuals(performance.now());

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

    if (state) {
        const srcA = getProcessedDrawable('a', w, h);
        const srcB = getProcessedDrawable('b', w, h);
        const cf = state.crossfade || 0;

        // Apply master FX
        if (state.fx) {
            const fx = state.fx;
            ctx.filter = `brightness(${fx.brightness}%) contrast(${fx.contrast}%) saturate(${fx.saturate}%) hue-rotate(${fx.hue}deg)`;
        }

        const transId = state.transition || 'crossfade';
        const transition = window.TransitionRegistry.get(transId);

        transition.render(ctx, srcA, srcB, cf, w, h, drawFit);

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

    // Check for disconnect
    if (connected && performance.now() - lastStateTime > 3000) {
        connected = false;
        statusEl.textContent = 'Disconnected';
        statusEl.classList.remove('connected');
        statusEl.style.opacity = '1';
    }

    requestAnimationFrame(render);
}

// Auto-hide cursor
let cursorTimeout;
document.addEventListener('mousemove', () => {
    document.body.classList.add('show-cursor');
    clearTimeout(cursorTimeout);
    cursorTimeout = setTimeout(() => {
        document.body.classList.remove('show-cursor');
    }, 3000);
});

// Fullscreen on double-click
canvas.addEventListener('dblclick', () => {
    if (document.fullscreenElement) {
        document.exitFullscreen();
    } else {
        document.documentElement.requestFullscreen();
    }
});

render();
