<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ face_name | default('Default') }}</title>
    <script src="{{ url_for('static', filename='js/socketio.min.js') }}"></script>
    <script src="{{ url_for('static', filename='js/canvasFace.js') }}"></script>
    <script src="{{ url_for('static', filename='js/helpers.js') }}"></script>
    
    <script>
        const name = document.title
        var socket = io();

        socket.on('face_control', function(msg){
            console.log('message to ' + msg.name + ' : ' + msg.action_type);
            if(msg.name != name){
                return
            }

            if (msg.action_type == 'say') {
                play_visemes(msg.visemes, 55, msg.times)
            }

            if (msg.action_type == 'stop_speech') {
                stop_visemes()
            }

            if (msg.action_type == 'look') {
                console.log('Calling lookat');
                lookat(msg.location[0], msg.location[1], msg.location[2], msg.time)
                looking = true //stops the idle movements
            }

            if (msg.action_type == 'release_gaze') {
                looking = false //starts the idle movements
            }

            if (msg.action_type == 'express') {
                time = msg.time
                for (var au_cmd in msg.aus){
                    au_number = parseInt(au_cmd.slice(2))
                    // if the last character is 'l' or 'r', it's a side command
                    // if it is a number, au() will assume both sides
                    side = au_cmd.slice(-1)
                    degree = parseFloat(msg.aus[au_cmd])
                    au(au_number, degree, side, time)
                }
            }

            if(msg.action_type == 'update_face'){

                Object.keys(params).forEach(key => {
                    if(msg.configuration.hasOwnProperty(key)){
                        params[key] = msg.configuration[key]
                    }
                });
            }

            if(msg.action_type == 'set_component_offsets'){
                const time = msg.time || 0;
                setComponentOffsets(msg.offsets, time);
            }

        });

        // Handle audio playback from server
        // --- Simple Audio Unlock (discard pre-unlock audio) --- //
        let audioUnlocked = false;
        let pylipsAudioEl = null;       // persistent audio element
        let currentObjectUrl = null;    // track for cleanup
        const SILENT_WAV = 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAIlYAAESsAAACABAAZGF0YQAAAAA='; // tiny silence

        function initPersistentAudio(){
            if(pylipsAudioEl) return;
            pylipsAudioEl = document.createElement('audio');
            pylipsAudioEl.id = 'pylips-audio';
            pylipsAudioEl.playsInline = true; // iOS inline playback
            pylipsAudioEl.preload = 'auto';
            pylipsAudioEl.style.display = 'none';
            // Warm with silence so element becomes eligible for later dynamic src swaps
            pylipsAudioEl.src = SILENT_WAV;
            document.body.appendChild(pylipsAudioEl);
        }

        async function warmAudioElement(){
            if(!pylipsAudioEl) initPersistentAudio();
            try {
                pylipsAudioEl.muted = true; // safe warm-up
                pylipsAudioEl.currentTime = 0;
                await pylipsAudioEl.play().catch(()=>{});
                pylipsAudioEl.pause();
                pylipsAudioEl.muted = false;
                pylipsAudioEl.currentTime = 0;
            } catch(e){
                console.debug('Warm audio element failed (may still proceed):', e);
            }
        }

        async function unlockAudio(){
            if(audioUnlocked) return;
            try {
                const Ctx = window.AudioContext || window.webkitAudioContext;
                if(Ctx){
                    const ctx = new Ctx();
                    if(ctx.state === 'suspended') await ctx.resume();
                    // Prime with a silent buffer (satisfies some mobile policies)
                    const buffer = ctx.createBuffer(1, 1, 22050);
                    const src = ctx.createBufferSource();
                    src.buffer = buffer;
                    src.connect(ctx.destination);
                    src.start(0);
                }
            } catch(e){
                console.debug('AudioContext priming failed (may still be okay):', e);
            }
            audioUnlocked = true;
            document.body.classList.add('audio-unlocked');
            const overlay = document.getElementById('audio-unlock-overlay');
            if(overlay){
                overlay.classList.add('fade-out');
                // Allow full opacity transition (.6s) before removal
                setTimeout(()=> overlay.remove(), 650);
            }
            // Initialize & warm persistent audio element AFTER gesture
            initPersistentAudio();
            await warmAudioElement();
        }

        // Incoming audio: simplified playback (persistent element). Uses data URI for small clips, Blob URL for large.
        socket.on('play_audio', function(msg){
            console.log('[Audio] Event for:', msg.name, 'file:', msg.filename);
            if(msg.name != name && msg.name != 'all') return;
            if(!audioUnlocked){
                console.log('[Audio] Discarded (locked). Filename:', msg.filename);
                return;
            }
            if(!msg.audio_data){
                console.warn('[Audio] No audio_data present; cannot play.');
                return;
            }

            if(!pylipsAudioEl) initPersistentAudio();

            const mime = msg.mime_type || 'audio/wav';
            const base64Len = msg.audio_data.length;
            console.log('[Audio] Base64 length:', base64Len);

            const SMALL_THRESHOLD = 600000; // ~450 KB raw audio equivalent; tweak as needed

            // Revoke any previous object URL before replacing
            if(currentObjectUrl){
                try { URL.revokeObjectURL(currentObjectUrl); } catch(_){}
                currentObjectUrl = null;
            }

            if(base64Len < SMALL_THRESHOLD){
                // Direct data URI assignment (fast, low overhead for small clips)
                pylipsAudioEl.src = `data:${mime};base64,${msg.audio_data}`;
            } else {
                // Large clip: use Blob to avoid huge inline data URI memory
                const blob = base64ToBlob(msg.audio_data, mime);
                currentObjectUrl = URL.createObjectURL(blob);
                pylipsAudioEl.src = currentObjectUrl;
            }

            pylipsAudioEl.currentTime = 0;
            // Some TV browsers benefit from explicit load() prior to play
            try { pylipsAudioEl.load(); } catch(_){}

            const attemptPlay = (label='primary') => {
                const p = pylipsAudioEl.play();
                if(p && typeof p.then === 'function'){
                    p.then(()=>{
                        console.log('[Audio] Playing (', label, '):', msg.filename);
                    }).catch(err => {
                        console.warn('[Audio] Play', label, 'failed:', err);
                        if(label === 'primary'){
                            // Retry once after a short delay (TV timing quirk)
                            setTimeout(()=>{
                                // For very stubborn cases, reassign src to force a fresh decode
                                if(base64Len < SMALL_THRESHOLD){
                                    pylipsAudioEl.src = `data:${mime};base64,${msg.audio_data}`;
                                } else if(currentObjectUrl){
                                    pylipsAudioEl.src = currentObjectUrl; // re-use same URL
                                }
                                pylipsAudioEl.currentTime = 0;
                                attemptPlay('retry');
                            }, 120);
                        }
                    });
                }
            };
            attemptPlay();
        });

        // Helper function to convert base64 to blob
        function base64ToBlob(base64Data, mimeType) {
            const byteCharacters = atob(base64Data);
            const byteNumbers = new Array(byteCharacters.length);
            for (let i = 0; i < byteCharacters.length; i++) {
                byteNumbers[i] = byteCharacters.charCodeAt(i);
            }
            const byteArray = new Uint8Array(byteNumbers);
            return new Blob([byteArray], {type: mimeType});
        }

        // Utility functions for external use
        window.playAudioFile = function(filename, targetName = name) {
            socket.emit('play_audio_file', {
                filename: filename,
                name: targetName
            });
        };

    </script>

</head>
<body data-overlay-disabled="{{ 'true' if disable_audio_overlay else 'false' }}">
    {% if not disable_audio_overlay %}
    <!-- Audio Unlock Overlay (Removed after first interaction) -->
    <div id="audio-unlock-overlay">
        <div class="unlock-card">
            <h2>Start PyLips</h2>
            <p id="unlock-hint">Press the button below<br>to enable audio in<br>your web browser.</p>
            <button id="unlock-btn" type="button" aria-label="Enable audio now">Enable</button>
        </div>
    </div>
    {% endif %}

    <link rel="stylesheet" href="{{ url_for('static', filename='css/overlay_styles.css') }}">

    <script>
        // Use a data-flag injected by server instead of Jinja control flow inside JS.
    // Determine overlay disabled state via body data attribute to avoid inline Jinja in JS.
    const overlayDisabled = (document.body && document.body.dataset && document.body.dataset.overlayDisabled === 'true');
        if(!overlayDisabled){
            const _btn = document.getElementById('unlock-btn');
            if(_btn){
                _btn.addEventListener('click', unlockAudio, { once:true });
                setTimeout(()=>{ try{ _btn.focus(); }catch(e){} }, 50);
            }
        } else {
            document.addEventListener('DOMContentLoaded', () => {
                unlockAudio();
            });
        }
    </script>

</body>
</html>