import config from './config.js';

const CURSOR_LERP_RATE = 1/100;
const CURSOR_FUNCHAR_RATE = 20;
const CURSOR_CHAR_MAX_LIFE = 5000;
const CURSOR_MAX_CHARS = 64;

/** @type HTMLCanvasElement */
let canvas;
/** @type CanvasRenderingContext2D */
let ctx;
/** @type Cursor */
let myCursor;
/** @type Map<number, Cursor> */
let cursors = new Map();

/** @type WebSocket */
let ws;

let running = false;
let lastUpdate = 0;

let cursorBoxHeight = 0;
let cursorBoxRadius = 0;
let cursorIDFontSize = 0;
let cursorCharFontSize = 0;

class Cursor {
    #funCharCooldown = CURSOR_FUNCHAR_RATE;

    /**
     * @param {string} id
     * @param {number} x
     * @param {number} y
     */
    constructor(id, x, y) {
        this.id = id;

        // real coordinates (canonical)
        this.x = x;
        this.y = y;
        // render coordinates (interpolated)
        this.rx = x;
        this.ry = y;

        this.msg = '';
        /** @type Array<FunChar> */
        this.funChars = new Array();
        this.colour = randomColour();
        this.click = false;
    }

    /**
     * @param {number} deltaTime
     */
    update(deltaTime) {
        this.rx += (this.x - this.rx) * CURSOR_LERP_RATE * deltaTime;
        this.ry += (this.y - this.ry) * CURSOR_LERP_RATE * deltaTime;

        if (this.#funCharCooldown > 0)
            this.#funCharCooldown -= deltaTime;

        const x = this.rx * innerWidth - scrollX;
        const y = this.ry * innerHeight - scrollY;
        const onBackground = ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--on-background');

        if (config.cursorFunMode === true) {
            if (this.msg.length > 0) {
                if (this.#funCharCooldown <= 0) {
                    this.#funCharCooldown = CURSOR_FUNCHAR_RATE;
                    if (this.funChars.length >= CURSOR_MAX_CHARS) {
                        this.funChars.shift();
                    }
                    const yOffset = -10 / innerHeight;
                    const accelMultiplier = 0.002;
                    this.funChars.push(new FunChar(
                        this.x, this.y + yOffset,
                        (this.x - this.rx) * accelMultiplier, (this.y - this.ry) * accelMultiplier,
                        this.msg));
                }
            }

            this.funChars.forEach(char => {
                if (char.life > CURSOR_CHAR_MAX_LIFE ||
                    char.y - scrollY > innerHeight ||
                    char.x < 0 ||
                    char.x * innerWidth - scrollX > innerWidth
                ) {
                    this.funChars = this.funChars.filter(c => c !== this);
                    return;
                }
                char.update(deltaTime);
            });
        } else if (this.msg.length > 0) {
            ctx.font = 'normal bold ' + cursorCharFontSize + 'px monospace';
            ctx.fillStyle = onBackground;
            ctx.fillText(
                this.msg,
                (x + 6) * devicePixelRatio,
                (y + -8) * devicePixelRatio);
        }

        const lightTheme = matchMedia && matchMedia('(prefers-color-scheme: light)').matches;

        if (lightTheme)
            ctx.filter = 'saturate(5) brightness(0.8)';

        const idText = '0x' + this.id.toString(16).padStart(8, '0');
        const colour = this.click ? onBackground : this.colour;

        ctx.beginPath();
        ctx.roundRect(
            (x) * devicePixelRatio,
            (y) * devicePixelRatio,
            (12 + 7.2 * idText.length) * devicePixelRatio,
            cursorBoxHeight,
            cursorBoxRadius);
        ctx.closePath();
        ctx.fillStyle = lightTheme ? '#fff8' : '#0008';
        ctx.fill();
        ctx.strokeStyle = colour;
        ctx.lineWidth = devicePixelRatio;
        ctx.stroke();

        ctx.font = cursorIDFontSize + 'px monospace';
        ctx.fillStyle = colour;
        ctx.fillText(
            idText,
            (x + 6) * devicePixelRatio,
            (y + 14) * devicePixelRatio);

        ctx.filter = '';
    }
}

class FunChar {
    /**
     * @param {number} x
     * @param {number} y
     * @param {number} xa
     * @param {number} ya
     * @param {string} text
     */
    constructor(x, y, xa, ya, text) {
        this.x = x;
        this.y = y;
        this.xa = xa + Math.random() * .0005 - .00025;
        this.ya = ya + Math.random() * -.00025;
        this.r = this.xa * 1000;
        this.ra = this.r * 0.01;
        this.text = text;
        this.life = 0;
    }

    /**
     * @param {number} deltaTime
     */
    update(deltaTime) {
        this.life += deltaTime;

        this.x += this.xa * deltaTime;
        this.y += this.ya * deltaTime;
        this.r += this.ra * deltaTime;
        this.ya = Math.min(this.ya + 0.000001 * deltaTime, 10);

        const x = this.x * innerWidth - scrollX;
        const y = this.y * innerHeight - scrollY;

        const translateOffset = {
            x: (x + 7.2) * devicePixelRatio,
            y: (y - 7.2) * devicePixelRatio,
        };
        ctx.translate(translateOffset.x, translateOffset.y);
        ctx.rotate(this.r);
        ctx.translate(-translateOffset.x, -translateOffset.y);

        ctx.font = 'normal bold ' + cursorCharFontSize + 'px monospace';
        ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--on-background');
        ctx.fillText(
            this.text,
            x * devicePixelRatio,
            y * devicePixelRatio);

        ctx.resetTransform();
    }
}

/**
 * @returns string
 */
function randomColour() {
    const min = 128;
    const range = 100;
    const red   = Math.round((min + Math.random() * range)).toString(16);
    const green = Math.round((min + Math.random() * range)).toString(16);
    const blue  = Math.round((min + Math.random() * range)).toString(16);

    return '#' + red + green + blue;
}

/**
 * @param {MouseEvent} event 
 */
let mouseMoveLock = false;
const mouseMoveCooldown = 1000/30;
function handleMouseMove(event) {
    if (!myCursor) return;

    const x = event.pageX / innerWidth;
    const y = event.pageY / innerHeight;
    const f = 10000; // four digit floating-point precision

    if (!mouseMoveLock) {
        mouseMoveLock = true;
        if (ws && ws.readyState == WebSocket.OPEN)
            ws.send(`pos:${Math.round(x * f) / f}:${Math.round(y * f) / f}`);
        setTimeout(() => {
            mouseMoveLock = false;
        }, mouseMoveCooldown);
    }

    myCursor.x = x;
    myCursor.y = y;
}

function handleMouseDown() {
    myCursor.click = true;
    if (ws && ws.readyState == WebSocket.OPEN)
        ws.send('click:1');
}
function handleMouseUp() {
    myCursor.click = false;
    if (ws && ws.readyState == WebSocket.OPEN)
        ws.send('click:0');
}

/**
 * @param {KeyboardEvent} event
 */
function handleKeyPress(event) {
    if (event.key.length > 1) return;
    if (event.metaKey || event.ctrlKey) return;
    if (myCursor.msg === event.key) return;
    if (ws && ws.readyState == WebSocket.OPEN)
        ws.send(`char:${event.key}`);
    myCursor.msg = event.key;
}

function handleKeyUp() {
    if (ws && ws.readyState == WebSocket.OPEN)
        ws.send(`nochar`);
    myCursor.msg = '';
}

/**
 * @param {number} timestamp
 */
function update(timestamp) {
    if (!running) return;

    const deltaTime = timestamp - lastUpdate;
    lastUpdate = timestamp;

    ctx.clearRect(0, 0, canvas.width, canvas.height);

    cursors.forEach(cursor => {
        cursor.update(deltaTime);
    });

    requestAnimationFrame(update);
}

function handleWindowResize() {
    canvas.width = innerWidth * devicePixelRatio;
    canvas.height = innerHeight * devicePixelRatio;
    cursorBoxHeight = 20 * devicePixelRatio;
    cursorBoxRadius = 4 * devicePixelRatio;
    cursorIDFontSize = 12 * devicePixelRatio;
    cursorCharFontSize = 20 * devicePixelRatio;
}

function cursorSetup() {
    if (running) throw new Error('Only one instance of Cursor can run at a time.');
    running = true;

    canvas = document.createElement('canvas');
    canvas.id = 'cursors';
    handleWindowResize();
    document.body.appendChild(canvas);

    ctx = canvas.getContext('2d');

    myCursor = new Cursor('You!', innerWidth / 2, innerHeight / 2);
    cursors.set(0, myCursor);

    addEventListener('resize', handleWindowResize);
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mousedown', handleMouseDown);
    document.addEventListener('mouseup', handleMouseUp);
    document.addEventListener('keypress', handleKeyPress);
    document.addEventListener('keyup', handleKeyUp);

    requestAnimationFrame(update);

    ws = new WebSocket('/cursor-ws');
    ws.addEventListener('open', () => {
        console.log('Cursor connected to server successfully.');

        ws.send(`loc:${location.pathname}`);
    });
    ws.addEventListener('error', error => {
        console.error('Cursor WebSocket error:', error);
    });
    ws.addEventListener('close', () => {
        console.log('Cursor connection closed.');
    });
    ws.addEventListener('message', event => {
        const args = String(event.data).split(':');
        if (args.length == 0) return;

        let id = 0;
        /** @type Cursor */
        let cursor;
        if (args.length > 1) {
            id = Number(args[1]);
            cursor = cursors.get(id);
        }

        switch (args[0]) {
            case 'id': {
                myCursor.id = id;
                break;
            }
            case 'join': {
                if (id === myCursor.id) break;
                cursors.set(id, new Cursor(id, 0, 0));
                break;
            }
            case 'leave': {
                if (!cursor || cursor === myCursor) break;
                cursors.delete(id);
                break;
            }
            case 'char': {
                if (!cursor || cursor === myCursor) break;
                cursor.msg = args[2];
                break;
            }
            case 'nochar': {
                if (!cursor || cursor === myCursor) break;
                cursor.msg = '';
                break;
            }
            case 'click': {
                if (!cursor || cursor === myCursor) break;
                cursor.click = args[2] == '1';
                break;
            }
            case 'pos': {
                if (!cursor || cursor === myCursor) break;
                cursor.x = Number(args[2]);
                cursor.y = Number(args[3]);
                break;
            }
            default: {
                console.warn('Cursor: Unknown command received from server:', args[0]);
                break;
            }
        }
    });

    console.log(`Cursor tracking @ ${location.pathname}`);
}

function cursorDestroy() {
    if (!running) return;

    removeEventListener('resize', handleWindowResize);
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('mousedown', handleMouseDown);
    document.removeEventListener('mouseup', handleMouseUp);
    document.removeEventListener('keypress', handleKeyPress);
    document.removeEventListener('keyup', handleKeyUp);

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    cursors.clear();
    myCursor = null;

    console.log(`Cursor no longer tracking.`);
    running = false;
}

if (config.cursor === true) {
    cursorSetup();
}

config.addListener('cursor', enabled => {
    if (enabled === true)
        cursorSetup();
    else
        cursorDestroy();
});
