const BeansAPI = Object.freeze((() => {
    const pointerNuke = pointer => {
        if(pointer.element instanceof Element)
            document.body.removeChild(pointer.element);

        pointer.element = undefined;
        pointer.elementHand = undefined;
        pointer.elementGlow = undefined;
        pointer.elementNumber = undefined;
    };

    const pointerDraw = pointer => {
        pointer.fadeOutStart = false;

        if(!pointer.visible) {
            pointerNuke(pointer);
            return;
        }

        if(!pointer.element) {
            pointer.element = document.createElement('div');
            pointer.element.style.width = '64px';
            pointer.element.style.height = '64px';
            pointer.element.style.pointerEvents = 'none';
            pointer.element.style.borderWidth = '0';
            pointer.element.style.boxStyle = 'border-box';
            pointer.element.style.position = 'fixed';
            pointer.element.style.zIndex = pointer.self ? '2147483647' : (10000000 + (pointer.connId ?? 0)).toString();
            pointer.element.style.transform = 'scale(.5) rotate(-20deg)';
            pointer.element.style.transformOrigin = '-18px 10px';

            pointer.elementNumber = document.createElement('div');
            pointer.elementNumber.style.position = 'absolute';
            pointer.elementNumber.style.top = '28px';
            pointer.elementNumber.style.left = '0';
            pointer.elementNumber.style.zIndex = '30';
            pointer.elementNumber.style.color = 'var(--pointer-colour, #00f)';
            pointer.elementNumber.style.fontFamily = 'sans-serif';
            pointer.elementNumber.style.fontSize = '24px';
            pointer.elementNumber.style.fontWeight = '700';
            pointer.elementNumber.style.letterSpacing = '-1px';
            pointer.elementNumber.style.textAlign = 'center';
            pointer.elementNumber.style.width = pointer.element.style.width;
            pointer.elementNumber.style.height = pointer.element.style.height;
            pointer.element.appendChild(pointer.elementNumber);

            pointer.elementGlow = document.createElement('div');
            pointer.elementGlow.style.position = 'absolute';
            pointer.elementGlow.style.top = '0';
            pointer.elementGlow.style.left = '0';
            pointer.elementGlow.style.zIndex = '20';
            pointer.elementGlow.style.backgroundColor = 'var(--pointer-colour, #00f)';
            pointer.elementGlow.style.width = pointer.element.style.width;
            pointer.elementGlow.style.height = pointer.element.style.height;
            pointer.elementGlow.style.maskImage = 'url(//static.flash.moe/images/pointer.png)';
            pointer.elementGlow.style.webkitMaskImage = 'url(//static.flash.moe/images/pointer.png)';
            pointer.elementGlow.style.maskType = 'alpha';
            pointer.elementGlow.style.webkitMaskType = 'alpha';
            pointer.element.appendChild(pointer.elementGlow);

            pointer.elementHand = document.createElement('div');
            pointer.elementHand.style.position = 'absolute';
            pointer.elementHand.style.top = '0';
            pointer.elementHand.style.left = '0';
            pointer.elementHand.style.zIndex = '10';
            pointer.elementHand.style.width = pointer.element.style.width;
            pointer.elementHand.style.height = pointer.element.style.height;
            pointer.elementHand.style.backgroundColor = '#fff';
            pointer.elementHand.style.maskImage = 'url(//static.flash.moe/images/pointer.png)';
            pointer.elementHand.style.webkitMaskImage = 'url(//static.flash.moe/images/pointer.png)';
            pointer.elementHand.style.maskType = 'alpha';
            pointer.elementHand.style.webkitMaskType = 'alpha';
            pointer.element.appendChild(pointer.elementHand);

            document.body.appendChild(pointer.element);
        }

        const opacity = pointer.self ? .5 : 1;
        pointer.elementNumber.textContent = pointer.userId ?? pointer.connId ?? 0;
        pointer.element.style.setProperty('--pointer-colour', '#' + (pointer.colour ?? 0).toString(16).padStart(6, '0'));
        pointer.element.style.opacity = opacity.toString();
        pointer.element.style.left = Math.round(window.innerWidth * ((pointer.xPos ?? 0) / 0xFFFF)).toString() + 'px';
        pointer.element.style.top = Math.round(window.innerHeight * ((pointer.yPos ?? 0) / 0xFFFF)).toString() + 'px';

        if(pointer.click) {
            pointer.elementHand.style.maskPosition = '-64px 0';
            pointer.elementHand.style.webkitMaskPosition = '-64px 0';
            pointer.elementGlow.style.maskPosition = '-64px -64px';
            pointer.elementGlow.style.webkitMaskPosition = '-64px -64px';
        } else {
            pointer.elementHand.style.maskPosition = '0 0';
            pointer.elementHand.style.webkitMaskPosition = '0 0';
            pointer.elementGlow.style.maskPosition = '0 -64px';
            pointer.elementGlow.style.webkitMaskPosition = '0 -64px';
        }

        pointer.fadeOutStart = undefined;
        const fadeOut = time => {
            if(pointer.fadeOutStart === false || !pointer.element) {
                pointer.fadeOutStart = undefined;
                return;
            }

            if(pointer.fadeOutStart === undefined)
                pointer.fadeOutStart = time;

            const elapsed = time - pointer.fadeOutStart;
            const completion = Math.min(1, Math.max(0, elapsed / 30000));
            const eased = completion === 0 ? 0 : Math.pow(2, 10 * completion - 10);

            pointer.element.style.opacity = (opacity - (opacity * eased)).toString();

            if(completion < 1)
                requestAnimationFrame(fadeOut);
            else
                pointerNuke(pointer);
        };
        requestAnimationFrame(fadeOut);
    };

    const pointers = new Map;
    let selfPointer;
    let sock;

    const pub = {};
    let connected = false;
    const runWhenConnected = [];

    const connect = () => {
        sock = new WebSocket(`${location.protocol.replace('http', 'ws')}//beans.flashii.net`);
        sock.binaryType = 'arraybuffer';
        sock.addEventListener('close', ev => {
            connected = false;
            sock = undefined;
            setTimeout(() => { connect(); }, 1000);
        });
        sock.addEventListener('message', ev => {
            if(!(ev.data instanceof ArrayBuffer))
                return;

            const view = new DataView(ev.data);
            let offset = 0;

            let connId = offset < view.byteLength ? view.getUint8(offset++) : 0;
            connId <<= 8;
            connId |= offset < view.byteLength ? view.getUint8(offset++) : 0;
            connId <<= 8;
            connId |= offset < view.byteLength ? view.getUint8(offset++) : 0;
            connId <<= 8;
            connId |= offset < view.byteLength ? view.getUint8(offset++) : 0;

            const isSelf = !!(connId & 0x80000000);
            connId &= 0x7FFFFFFF;

            let pointer;
            if(pointers.has(connId))
                pointer = pointers.get(connId);
            else
                pointers.set(connId, pointer = { connId, visible: false, xPos: -1000, yPos: -1000, });

            if(isSelf) {
                if(selfPointer)
                    selfPointer.self = false;

                selfPointer = pointer;
                selfPointer.self = true;

                connected = true;
                for(const callback of runWhenConnected)
                    callback(pub);
            }

            const oflags = offset < view.byteLength ? view.getUint8(offset++) : 0;
            pointer.visible = !!(oflags & 0x10);
            pointer.click = !!(oflags & 0x20);

            if(oflags & 0x01) {
                pointer.userId = offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.userId <<= 8;
                pointer.userId |= offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.userId <<= 8;
                pointer.userId |= offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.userId <<= 8;
                pointer.userId |= offset < view.byteLength ? view.getUint8(offset++) : 0;
            }

            if(oflags & 0x02) {
                pointer.xPos = offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.xPos <<= 8;
                pointer.xPos |= offset < view.byteLength ? view.getUint8(offset++) : 0;
            }

            if(oflags & 0x04) {
                pointer.yPos = offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.yPos <<= 8;
                pointer.yPos |= offset < view.byteLength ? view.getUint8(offset++) : 0;
            }

            if(oflags & 0x08) {
                pointer.colour = offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.colour <<= 8;
                pointer.colour |= offset < view.byteLength ? view.getUint8(offset++) : 0;
                pointer.colour <<= 8;
                pointer.colour |= offset < view.byteLength ? view.getUint8(offset++) : 0;
            }

            pointerDraw(pointer);
        });
    };

    const pointerUpdate = ev => {
        if(!connected)
            return;

        let iflags = 0;
        let oflags = 0;
        const buffer = [];
        const xPos = Math.round((ev.clientX / window.innerWidth) * 0xFFFF);
        const yPos = Math.round((ev.clientY / window.innerHeight) * 0xFFFF);
        const click = !!(ev.buttons & 1);

        if(!click !== !selfPointer.click) {
            selfPointer.click = click;
            iflags |= 0x20;
            if(click) oflags |= 0x20;
            buffer.push(click ? 0x78 : 0x58);
        }

        if(xPos !== selfPointer.xPos) {
            selfPointer.xPos = xPos;
            iflags |= 0x02;
            buffer.push((xPos & 0xFF00) >> 8);
            buffer.push(xPos & 0xFF);
        }

        if(yPos !== selfPointer.yPos) {
            selfPointer.yPos = yPos;
            iflags |= 0x04;
            buffer.push((yPos & 0xFF00) >> 8);
            buffer.push(yPos & 0xFF);
        }

        if(iflags) {
            if(iflags & 0x01)
                buffer.unshift(oflags);
            buffer.unshift(iflags);
        }

        if(buffer.length > 0) {
            sock.send(new Uint8Array(buffer));
            pointerDraw(selfPointer);
        }
    };

    window.addEventListener('pointermove', pointerUpdate);
    window.addEventListener('pointerup', pointerUpdate);
    window.addEventListener('pointerdown', pointerUpdate);

    connect();

    pub.runWhenConnected = callback => {
        if(typeof callback !== 'function')
            return;

        runWhenConnected.push(callback);
        if(connected)
            callback(pub);
    };

    pub.setUserInfo = (id, colour) => {
        if(!connected)
            return;

        if(typeof id === 'string')
            id = parseInt(id);
        if(typeof id !== 'number' || !Number.isInteger(id))
            id = undefined;

        if(typeof colour === 'string')
            colour = colour.startsWith('#') ? parseInt(colour.substring(1), 16) : parseInt(colour);
        if(typeof colour !== 'number' || !Number.isInteger(colour))
            colour = undefined;

        let iflags = 0x10;
        const buffer = [0x10];
        selfPointer.visible = true;

        if(id !== undefined && id !== selfPointer.userId) {
            selfPointer.userId = id;
            iflags |= 0x01;
            buffer.push((id & 0xFF000000) >> 24);
            buffer.push((id & 0xFF0000) >> 16);
            buffer.push((id & 0xFF00) >> 8);
            buffer.push(id & 0xFF);
        }

        if(colour !== undefined && colour !== selfPointer.colour) {
            selfPointer.colour = colour;
            iflags |= 0x08;
            buffer.push((colour & 0xFF0000) >> 16);
            buffer.push((colour & 0xFF00) >> 8);
            buffer.push(colour & 0xFF);
        }

        if(iflags)
            buffer.unshift(iflags);

        if(buffer.length > 0) {
            sock.send(new Uint8Array(buffer));
            pointerDraw(selfPointer);
        }
    };

    return pub;
})());