/*
milestone release
therefore it is not nuxted or similar, but shall
nov 2025
@todo: typescript, nuxt
*/

import { FilesetResolver, HandLandmarker, GestureRecognizer } from './vision_bundle.js';
import { createApp, nextTick } from './vue.esm-browser.js';
import { getBoundingBoxes, getGridSection } from './spatial.js';

const { querySelector } = document;
const { parse, stringify } = JSON;
const { log, table } = console;
const HOLD_THRESHOLD = 3000;
const gridConfig = [2, 1];

// Enabled gestures for app
const GESTURES = ['THUMB_UP', 'THUMB_DOWN', 'VICTORY', 'OPEN_PALM', 'POINTING_UP'];
const GESTURES_NARROWED = {
    'thumbs': ['THUMB_UP', 'THUMB_DOWN'],
    'numbers': ['POINTING_UP', 'VICTORY'],
    'pointing': ['POINTING_UP'],
    'submit': ['OPEN_PALM']
};
const GESTURE_SUBMIT = 'OPEN_PALM';

let video = document.querySelector('video'),
    prevGestureName,
    possibleGestureName,
    gestureCompleted,
    holdStarts = 0,
    holdCurrent = 0,
    lastTime,
    gestureRecon,
    result,
    animId;

const app = createApp({
    data: (() => ({
        hands: 0,
        landmarks: [],
        msg: '',
        gesturesBundleLoaded: false,
        debug: false,
        ui: {
            progress: 0,
            camera: {
                progress: 0
            },
            typing: false,
            mode: 'mode-prompt'
        },
        step: 0,
        statusMsg: '',
        surveyStatus: '',
        survey: {
            data: [],
            progress: 0,
            status: '',
            possible: ''
        },
        prompt: {
            text: [],
            options: [],
            images: [],
            narrow: ''
        },
        
        answers: [],
        currentItem: {}
    })),
    
    methods: {
        log(msg) {
            this.msg += msg;
            window.scrollTo({
                left: 0,
                top: document.querySelector('.card').clientHeight,
                behaviour: 'smooth'
            })
        },
        async promptify() {
            const regex = /(https:\/\/[a-z\/0-9\/\.]+\.(jpg|png))/ig;
            const str = this.currentItem.title.replace(regex, '');
            
            const imgUrls = this.currentItem.title.match(regex);
            const INTVL_TYPING = 350;
            let parts = str.split(''),
                currentIntvl = 0,
                intvl;
            
            
            this.prompt.text = [];
            
            for (let p = 0; p < parts.length; p++) {
                this.prompt.text.push({
                    letter: parts[p],
                    hidden: true
                })
            }
            
            // typing-when-printing effect
            
            this.ui.typing = true;
            this.prompt.text.forEach((item, k) => {
                
                this.prompt.text[k].hidden = false;
                
                
            });
            
            setTimeout(() => {
                
                if (imgUrls != null) {
                    this.prompt.images = imgUrls;
                }
                this.ui.typing = false;
            }, 1250);
        },
        startCamera: startCamera,
        stopCamera: stopCamera,
        surveyProgress(percent) {
            this.survey.progress = percent
        },
        async fetchQuestions() {
            
            let res = navigator.onLine ? await fetch('https://ws.devignia.site/api/survey') :
                await fetch('ws/survey.json');
            
            this.survey.data = await res.json();
            this.step += 1;
        },
        answer(answer, landmarks) {
            this.answers.push({ survey_statement_id: this.survey.data[this.step - 1].id, answer: answer, gesture: landmarks })
            if (this.answers.length === this.survey.data.length) {
                this.surveyStatus = 'ended';
            } else {
                this.step += 1;
            }
        },
        async submitAnswers() {
            let body = new FormData();
            body.append('answers', stringify(this.answers))
            
            let res = await fetch('https://ws.devignia.site/api/answers', { method: 'POST', body })
            let payload = await res.json();
            this.statusMsg = payload.message;
            this.surveyStatus = 'sent';
        },
        possibleGesture(name = '') {
            this.survey.possible = name.toUpperCase()
            // options.querySelector('svg').classList.add('gauging')
            
        },
        toggleUIMode() {
            
            document.body.classList.toggle('mode-prompt')
            document.body.classList.toggle('mode-facetime')
            localStorage.setItem('uimode', document.body.className)
            if (this.ui.mode == 'mode-prompt') {
                video.height = '98%'
            }
            else {
                video.width = window.innerWidth * 0.32;
                video.height = window.innerHeight * 0.32;
                
            }
        }
    },
    
    created() {
        
        document.body.className = localStorage.getItem('uimode') || 'mode-prompt'
        this.ui.mode = document.body.className
    },
    mounted() {
        
        (async () => {
            // Load gestures g
            
            gestureRecon = await loadRecognisers();
            this.ui.progress = 50;
            // g ended
            
            // Load surveys s
            await this.fetchQuestions();
            this.ui.progress = 100;
            // s ended
        })();
        lucide.createIcons({ attrs: { class: 'inline mr-2' } })
    },
    updated() {
        lucide.createIcons()
    },
    watch: {
        surveyStatus(val, prev) {
            switch (val) {
                case 'sent':
                    this.stopCamera();
                    break;
            }
        },
        step(val, prev) {
            this.prompt.images = [];
            this.currentItem = this.survey.data[--val]
            this.promptify()
        }
    },
    computed: {
        ready() {
            return this.gesturesBundleLoaded && this.survey.length > 0;
        }
    },
    
    
}).mount('main')


function saveWhenCompletes(gesture, videoTime = 0) {
    
    const gestureName = gesture.toUpperCase();
    if (
        (!GESTURES_NARROWED[app.currentItem.narrow].includes(gestureName.toUpperCase()) &&
            !GESTURES_NARROWED['submit'].includes(gestureName.toUpperCase())) ||
        !['', gestureName].includes(prevGestureName)
    )
    {
        holdStarts = 0;
        holdCurrent = 0;
        prevGestureName = '';
        possibleGestureName = '';
        app.surveyProgress(0)
        app.possibleGesture('');
        return;
    }
    
    if (holdStarts == 0)
        holdStarts = videoTime;
    
    
    
    // Currents
    holdCurrent = videoTime;
    prevGestureName = gestureName;
    
    
    app.surveyProgress((((holdCurrent - holdStarts) * 1000) / HOLD_THRESHOLD) * 100)
    
    if (holdCurrent > 0 && possibleGestureName == '') {
        possibleGestureName = gestureName;
        app.log(`Possible ${possibleGestureName}\n`)
        app.possibleGesture(possibleGestureName)
    }
    if (((holdCurrent - holdStarts) * 1000) >= HOLD_THRESHOLD) {
        holdStarts = 0;
        app.surveyProgress(0)
        app.possibleGesture('');
        return gestureName;
    }
    
    return;
}

async function loadRecognisers() {
    const vision = await FilesetResolver.forVisionTasks(
        "./wasm"
    );
    app.ui.progress = 20;
    const recon = await GestureRecognizer.createFromOptions(
        vision,
        {
            baseOptions: {
                modelAssetPath: "./models/gesture_recognizer.task",
                delegate: 'GPU'
            },
            numHands: 2,
            runningMode: 'VIDEO'
        }
    );
    app.ui.progress = 50;
    app.gesturesBundleLoaded = true;
    return recon;
}


async function renderAndDetect() {
    
    let startTime = performance.now()
    if (lastTime !== video.currentTime) {
        lastTime = video.currentTime;
        result = gestureRecon.recognizeForVideo(video, startTime);
    }
    
    if (result.gestures.length > 0) {
        
        gestureCompleted = saveWhenCompletes(result.gestures[0][0].categoryName, video.currentTime)
        if (gestureCompleted) {
            
            app.possibleGesture(gestureCompleted)
            
            if (app.surveyStatus != 'ended') {
                
                
                if (gestureCompleted == 'POINTING_UP' && app.currentItem.narrow == 'numbers') {
                    let sections = getGridSection(result.landmarks[0][8])
                    if (sections.length)
                        gestureCompleted = `POINTING_UP:${sections[0].at}`
                }
                result.currentTime = video.currentTime;
                app.answer(gestureCompleted, result)
                
                
                
                
                
            }
            else {
                
                if (gestureCompleted.toUpperCase() == GESTURE_SUBMIT)
                    app.possibleGesture(gestureCompleted)
                
                app.submitAnswers();
            }
            
            
            
            
        }
    } else {
        app.surveyProgress(0)
        app.possibleGesture()
    }
    
    animId = requestAnimationFrame(renderAndDetect)
}

function stopCamera() {
    video.pause();
    video.removeAttribute('src')
    video.width = video.dataset.previousWidth;
    video.height = video.dataset.previousHeight;
    cancelAnimationFrame(animId)
}

function startCamera() {
    
    let videoWidth = window.innerWidth * 0.32,
        videoHeight = window.innerHeight * 0.32;
    
    // Width and Height be inverted in constraints to optimal proportion (?)
    const videoConstraints = {
        video: {
            facingMode: 'user',
        }
    };
    
    app.ui.camera.progress = 25;
    navigator.mediaDevices.getUserMedia(videoConstraints).then((stream) => {
        video.removeAttribute('hidden')
        video.dataset.previousWidth = video.width;
        video.dataset.previousHeight = video.height;
        video.width = videoWidth;
        video.height = videoHeight;
        
        
        // after ease css transition for optimal rendering 
        setTimeout(() => {
            
            video.srcObject = stream;
            app.surveyStatus = 'started';
            app.ui.camera.progress = 50;
        }, 325)
        video.addEventListener('loadeddata', renderAndDetect)
    })
    
    video.addEventListener('loadedmetadata', () => {
        video.dataset.isPlaying = true;
        app.ui.camera.progress = 75;
        video.play()
        
        app.ui.camera.progress = 100;
    })
    
    // toggle webcam play/stop
    video.addEventListener('click', () => {
        if (video.dataset.isPlaying)
            video.pause();
        else
            video.play();
        
        video.dataset.isPlaying = !video.dataset.isPlaying;
        
    });
    
}

// handy.js, nov 2025