const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const STV_GLOBAL_URL = () => "https://api.7tv.app/v2/emotes/global";
const STV_CHANNEL_URL = (user) => "https://api.7tv.app/v2/users/" + user + "/emotes";
const FFZ_GLOBAL_URL = () => "https://api.frankerfacez.com/v1/set/global";
const FFZ_CHANNEL_URL = (user) => "https://api.frankerfacez.com/v1/room/" + user;
const BTT_GLOBAL_URL = () => "https://api.betterttv.net/3/cached/emotes/global";
const BTT_CHANNEL_URL = (id) => "https://api.betterttv.net/3/cached/users/twitch/" + id;

function getData(ajaxurl) { 
    return $.ajax({
      url: ajaxurl,
      type: 'GET',
    });
  };

const get_g7TV = async() => {
    const req = await getData(STV_GLOBAL_URL())
    return req.map((v,i)=> ({str:v.name, url: (v.urls['2']||v.urls['2']||v.urls['1'])[1]}));
}
const get_p7TV = async(user) => {
    try{
        const req = await getData(STV_CHANNEL_URL(user))
        return req.map((v,i)=> ({str:v.name, url: (v.urls['2']||v.urls['2']||v.urls['1'])[1]}));
    }catch(e){return []}
}
const get_gFFZ = async() => {
    const req = await getData(FFZ_GLOBAL_URL())
    return req.sets['3'].emoticons.map((v,i)=> ({str:v.name, url: 'https:'+(v.urls['2']||v.urls['2']||v.urls['1'])}))
}
const get_pFFZ = async(user) => {
    try{
        const req = await getData(FFZ_CHANNEL_URL(user))
        return req.sets[req.room.set].emoticons.map((v,i)=> ({str:v.name, url: 'https:'+(v.urls['2']||v.urls['2']||v.urls['1'])}))
    }catch(e){return []}
}

const get_gBTT = async() => {
    const req = await getData(BTT_GLOBAL_URL())
    return req.map((v,i) => ({str:v.code, url: 'https://cdn.betterttv.net/emote/'+v.id+'/2x'}))
}
const get_pBTT = async(id) => {
    try{
        id = '486183840'
        const req = await getData(BTT_CHANNEL_URL(id))
        return req.sharedEmotes.map((v,i) => ({str:v.code, url: 'https://cdn.betterttv.net/emote/'+v.id+'/2x'}))
    }catch(e){return []}
}

const get_emotes = async(username, userid) => {
    return [].concat(await get_g7TV(), await get_p7TV(username),
              await get_gFFZ(), await get_pFFZ(username),
              await get_gBTT(), await get_pBTT(userid))
}

const canvasSizing = (cnv) => {
    const resizeCanvas = () => {
        cnv.width = window.innerWidth;
        cnv.height = window.innerHeight;
    }
    window.addEventListener('resize', resizeCanvas, false);
    resizeCanvas();
}

function waitFor(conditionFunction) {
    const poll = resolve => {
        if(conditionFunction()) resolve();
        else setTimeout(_ => poll(resolve), 400);
    }
    return new Promise(poll);
}



$(async function () {
    const webSocket = window.socket;
    
    var img_map = {}, static_emotes = {};
    var lastframe = 0;
    var preloaded = false, initialized = false;
    var particles = [];

    const canvas = document.getElementById('wall');
    const ctx = canvas.getContext('2d');
    canvasSizing(canvas);

    const loadImages = (emotes) => {
        let loadcount = 0, loadtotal = emotes.length;
        preloaded = false;
        var loadedimages = {};
        for (let i=0; i<emotes.length; i++) {
            const image = new Image();
            image.onload = function () {
                loadcount++;
                if (loadcount == loadtotal) preloaded = true;
            };
            image.src = emotes[i].url;
            loadedimages[emotes[i].str] = image;
        }
        return loadedimages;
    }


    const loadImagesTwitch = async(emotesId) => {
        const promiseArray = []; 
        for (let i=0; i<emotesId.length; i++) {
            promiseArray.push(new Promise(resolve => {
                const image = new Image();
                image.onload = () => resolve();
                image.src = "https://static-cdn.jtvnw.net/emoticons/v2/" + emotesId[i] + "/default/dark/2.0";
                img_map[emotesId[i]] = image;
            }));
        }
        await Promise.all(promiseArray);
        return true;
    }

    const randEmote = (set) => {
        if(!Array.isArray(set))  
            set = Object.keys(img_map) || []
        return set[parseInt(set.length* Math.random())]
    }

    const username = "soraefir"
    const userid = "486204587"
    static_emotes = await get_emotes(username,userid)
    img_map = loadImages(static_emotes)

    const part_speed = 50;
    const part_bounce_el = 0.9
    const fdt = 1;
    const drt = 8;
    const tWave = (t) => (t<0 || t>drt )? 0 : (t<fdt ? t/fdt : (t>(drt-fdt) ? (drt-t)/fdt : 1))
    const sWave = (t, s) => (t<0 || t>drt) ? 1 : (t<fdt ? t*s/fdt : (t>(drt-fdt) ? (drt-t)*s/fdt : s))
    const randRange = (min,max) => (Math.random()* (max-min) + min)

    class Particle {
        constructor(emote,data){
            this.size = 56;
            this.data = data;
            this.setPhysics(randRange(0,canvas.width),
                            randRange(0,canvas.height),
                            0,0,0,0);
            this.sete = emote ? emote:[]
            this.emote = Array.isArray(emote)? randEmote(this.sete) : emote
            this.time = 0;
        }

        setPhysics(x,y,vx,vy,ax,ay){
            this.x = x || this.x || 0;
            this.y = y || this.y || 0;
            this.vx = vx || this.vx || 0;
            this.vy = vy || this.vy || 0;
            this.ax = ax || this.ax || 0;
            this.ay = ay || this.ay || 0;
        }

        draw(){
            ctx.globalAlpha = tWave(this.time);
            if(img_map[this.emote]){
                let img = img_map[this.emote]
                let factor =  img.naturalWidth / img.naturalHeight;
                ctx.drawImage(img, this.x, this.y, 
                              sWave(this.time,this.size*factor), sWave(this.time,this.size));
            
            }
        }

        update(tick){
            this.time += tick;
            this.vx += tick * this.ax;
            this.vy += tick * this.ay;
            this.x += tick * this.vx * part_speed;
            this.y += tick * this.vy * part_speed;
        }
    }


    class RainP extends Particle {
        constructor(emote,data) {
            super(emote,data)
            this.setPhysics(randRange(0, canvas.width),
                            randRange(0, -this.size),
                            0,randRange(10,15),
                            0,0)
            this.size = randRange(this.size/2,this.size)
            this.time = 5.5+this.y/canvas.height*2;
        }

        draw(){
            ctx.globalAlpha = tWave(this.time);
            if(img_map[this.emote]){
                let img = img_map[this.emote]
                let factor =  img.naturalWidth / img.naturalHeight;
                ctx.drawImage(img, this.x, this.y, 
                              this.size*factor, this.size);
            }
        }
    }

    class BounceP extends Particle {
        constructor(emote,data) {
            super(emote,data)
            let speed = randRange(5,10);
            let ang = randRange(0, 2*Math.PI);
            this.setPhysics(randRange(0, canvas.width),
                            randRange(0, canvas.height),
                            Math.cos(ang)*speed,
                            Math.sin(ang)*speed,
                            0,0)
        }

        update(tick){
            super.update(tick);
            if (this.x < 0) {
                this.vx = this.vx < 0 ? -this.vx * part_bounce_el : this.vx;
                this.x = 0;
            } else if (this.x + this.size > canvas.width) {
                this.vx = this.vx > 0 ? -this.vx * part_bounce_el : this.vx;
                this.x = canvas.width - this.size;
            }
            if (this.y <0) {
                this.vy =  this.vy < 0 ? -this.vy * part_bounce_el : this.vy;
                this.y = 0;
            } else if (this.y + this.size > canvas.height) {
                this.vy =  this.vy > 0 ? -this.vy * part_bounce_el : this.vy;
                this.y = canvas.height - this.size;
            }
        }
    }

    class BounceGP extends BounceP {
        constructor(emote,data) {
            super(emote,data)
            let speed = randRange(5,10);
            let ang = randRange(0,2*Math.PI);
            this.setPhysics(randRange(0,canvas.width),
                            randRange(0,canvas.height),
                            Math.cos(ang)*speed,
                            Math.sin(ang)*speed,
                            0,10);
        }
    }

    class ExplosionP extends RainP {
        constructor(emote,data,args){
            super(emote,data)
            let speed = randRange(5,15);
            let fang = 2*Math.PI* (args.angle/360)
            let ang = randRange(-fang/2, +fang/2)+Math.PI/2;
            this.setPhysics(args.x,args.y,
                            Math.cos(ang)*speed,
                            -Math.sin(ang)*speed,
                            0,10);
            this.time = drt - fdt*2;
            this.size = randRange(this.size/2,this.size)
        }
        
    }

    class FireworkP extends Particle {
        constructor(emote,data) {
            super(emote,data)
            this.setPhysics(canvas.width/2,canvas.height,
                            0,-15,
                            0,0);
            this.time=fdt;
        }

        update(tick){
            super.update(tick);
            if(this.y <= canvas.height/4 && this.time < drt){
                this.time = drt;
                genExplosion(this.x,this.y,360, this.sete)
            }
        }
    }

    class BombP extends Particle {
        constructor(emote,data) {
            super(emote,data)
            this.setPhysics(randRange(0, canvas.width-this.size),
                            -this.size,
                            0,15,
                            0,0)
            this.time=fdt;
        }

        update(tick){
            super.update(tick);
            if(this.y >= canvas.height - this.size && this.time < drt){
                this.time = drt;
                genExplosion(this.x,this.y,60, this.sete)
            }
        }
    }


    const createParticule = (partP, emote, data, args) => {
        let a =  new partP(emote,data,args)
        particles.push(a);
        setTimeout(()=>{particles.shift()}, drt*1000+2000);
    }

    const genExplosion = (x,y,ang,em) =>{
        for(let i=0; i<100; ++i) createParticule(ExplosionP, em? em:randEmote(), {},{x:x,y:y,angle:ang});
    }

    const genRain = (em) => {
        let xpos = randRange(0, canvas.width);
        let time = 0;
        const interval = setInterval(()=>{
            for(let i=0; i<10; ++i) createParticule(RainP, em? em:randEmote(), {},{});
            ++time;
            if(time > 50) clearInterval(interval);
          }, 100);
    }

    const genVolcano = (em) => {
        let xpos = randRange(0, canvas.width);
        let time = 0;
        const interval = setInterval(()=>{
            for(let i=0; i<10; ++i) createParticule(ExplosionP, em? em:randEmote(), {},{x:xpos,y:canvas.height,angle:30});
            ++time;
            if(time > 50) clearInterval(interval);
          }, 100);
    }

    function main(tframe) {
        window.requestAnimationFrame(main);
        
        var dt = (tframe - lastframe) / 1000;
        lastframe = tframe;

        ctx.clearRect(0, 0, canvas.width, canvas.height);
        if (!initialized) {
            if (preloaded)  setTimeout(function(){initialized = true;}, 1000);
        } else {
            particles.forEach(p => p.update(dt));
            particles.forEach(p => p.draw());
        }
    }

    main(0);


    const handleEmoteData = async (d) =>{
        let te = d.twitch, ce = d.custom.map(e=>e.replace(/\s/g, '')).filter(e=> img_map[e]);
        if(!te || te.length == 0){
            return ce;
        }else{
            let cleanTE = te.map(e => e.split(":")[0]).filter(e=> e.length>0);
            await loadImagesTwitch(cleanTE)
            return ce.concat(cleanTE)
        }
    }

    const handleSocketMessage = async (e)=>{
        try {
            let rawMessage = e.data,
                message = JSON.parse(rawMessage);

            if(!message.hasOwnProperty('eventFamily') || message.eventFamily != 'emote' || 
                !message.hasOwnProperty('eventType') || !message.hasOwnProperty('data'))
                return;

            let emote = await handleEmoteData(message.data)
            if(message.eventType == 'bounce') {
                for(let em of emote)
                    createParticule(BounceP, em, {}, {})
            }else if(message.eventType == 'rain') {
                genRain(emote)
            }else if(message.eventType == 'firework') {
                createParticule(FireworkP, emote, {}, {})
            }else if(message.eventType == 'volcano') {
                genVolcano(emote)
            }else if(message.eventType == 'bomb') {
                createParticule(BombP, emote, {}, {})
            }else if(message.eventType == 'explosion') {
                createParticule(ExplosionP, emote, {}, {})
                //genExplosion(message.data)
            }else {
                console.log("unhandled:", message.eventType)
            }
        } catch (ex) {
            console.log(ex)
        }
    };


    jQuery(async ()=>{
        
        try{
            socket.addFamilyHandler("emote", handleSocketMessage);
            if(socket){
                while(socket.getReadyState() === 0){
                    await new Promise(r => setTimeout(r, 500));
                }
            }
        }catch(e) {console.log(e)}
    })

});