const GameServerMessageProtocol = require('./gameserver-message-protocol');

class ScratchMessage extends GameServerMessageProtocol {
    constructor (opts) {
        super(opts);
        this.agent = opts.agent;
    }

    getVar (arg) {
        arg.value = this.agent.getVariable(arg).value;
        return arg;
    }

    setVar (arg) {
        this.agent.getVariable(arg).value = arg.value;
        return arg;
    }

    getProp (arg) {
        switch (arg.name) {
        case 'url':
            arg.value = encodeURIComponent(window.location.href);
            break;
        default:
            arg.value = this.agent.getTarget(arg)[arg.name];
            break;
        }
        return arg;
    }

    setProp (arg) {
        const runtime = this.agent.runtime;
        let target;
        try {
            target = this.agent.getTarget(arg);
            target[arg.name] = this.castValue(arg.name, arg.value);
        } catch (error) { /**/ }

        switch (arg.name) {
        case 'x':
        case 'y':
            target.setXY(target.x, target.y);
            break;
        case 'size':
            target.setSize(target.size);
            break;
        case 'direction':
            target.setDirection(target.direction);
            break;
        case 'url':
            window.location.href = decodeURIComponent(arg.value);
            break;
        case 'start':
            runtime.start();
            runtime.greenFlag();
            break;
        case 'reload':
            window.location.reload(true);
            break;
        default:
            break;
        }
        return arg;
    }
}

module.exports = class GameAgent {
    constructor (opts) {
        this.runtime = opts.runtime;
        this.targets = opts.runtime.targets;
        this.messaging = new ScratchMessage({agent: this});
        this.broadcastVars = [
            'gameState',
            'controlled',
            'calibration',
            'playerName',
            'load',
            'save',
            'score',
            'fire',
            'collected',
            'crashed',
            'gotShot',
            'power',
            'powerall',
            'turbo',
            'wantsBoost',
            'boosts',
            'lap',
            'lapTime',
            'bestLap',
            'bestLapTime']
            .reduce((memo, v) => {
                memo[v] = true;
                return memo;
            }, {});
        this.initialize();
        this.addListeners();
    }

    addListeners () {
        this.runtime.on('PROJECT_START', () => this.initialize());
        this.runtime.on('VARIABLE_UPDATE', (target, variable) => this.sendMessage(target, variable));
        this.runtime.on('targetWasRemoved', target => this.unmemoizeTarget(target));
    }

    initialize () {
        this.memoizedTargets = {
            // <id>:<nameWithIndex> // no index for target.isOriginal=true
            id2name: {},
            // <nameWithIndex:target> // no index for target.isOriginal=true
            name2target: {},
            // used when no entry yet exists for a new target with target.isOriginal=false
            // <name:lastIndex>
            nameCount: {}
        };
        this.memoizedVariables = {
            // <nameWithIndex>:<variable>
        };
    }

    connect (ws) {
        this.connection = ws;
    }

    sendMessage (target, variable) {
        // only send broadcast variables to the server
        if (!this.broadcastVars[variable.name]) return;
        const {variableName, changed} = this.memoize(target, variable);
        if (!changed) return;
        const formattedMessage = this.messaging.formatMessage(variableName, variable.value);
        if (!this.connection) {
            // eslint-disable-next-line no-console
            return console.log(`can't send message ${formattedMessage}, no connection, call connect() first.`);
        } else if (this.connection.readyState !== WebSocket.OPEN) {
            // eslint-disable-next-line no-console
            return console.log(`can't send message ${formattedMessage}, socket not open`);
        }
        this.connection.send(formattedMessage);
    }

    handleMessage (data) {
        return this.messaging.handleMessage(data);
    }

    getVariable (arg) {
        const localArg = {...arg, local: arg.local || 'Stage', path: arg.path || 'Stage'};
        this.memoizeArg(localArg);
        // if (arg.local) set local variable (Sprite variable)
        // else set global variable (Stage variable)
        return this.memoizedVariables[`${localArg.local}.${localArg.name}`].original;
    }

    getTarget (arg) {
        const localArg = {...arg, local: arg.local || 'Stage', path: arg.path || 'Stage'};
        this.memoizeArg(localArg);
        // if (arg.local) get (local) Sprite
        // else get (global) Stage
        return this.memoizedTargets.name2target[localArg.local];
    }

    memoizeArg (arg) {
        // get target
        let target;
        if (arg.i) {
            target = this.runtime.targets.filter(it => it.sprite.name === arg.path)[arg.i];
        } else {
            target = this.runtime.targets.find(it => it.isOriginal && it.sprite.name === arg.path);
        }
        if (!target || !target.variables) {
            // eslint-disable-next-line no-console
            return console.log(`cant get target for arg: ${arg.name}`);
        }
        // get variable
        const variable = Object.values(target.variables).find(it => it.name === arg.name);
        if (!variable) {
            // eslint-disable-next-line no-console
            return console.log(`cant get variable for arg: ${arg.name}`);
        }
        this.memoize(target, variable);
    }

    memoize (target, variable) {
        const memoizedTarget = this.memoizeTarget(target, variable);
        const targetName = this.getTargetName(memoizedTarget);
        const variableName = `${targetName}.${variable.name}`;
        const changed = this.memoizeVariable(variableName, variable);
        return {memoizedTarget, targetName, variableName, changed};
    }

    memoizeTarget (sourceTarget, variable) {
        const target = this.getStageOrSpriteTarget(sourceTarget, variable);
        if (this.memoizedTargets.id2name[target.id]) return target;
        const spriteName = target.sprite.name;
        if (target.isOriginal) {
            this.memoizedTargets.id2name[target.id] = spriteName;
            this.memoizedTargets.name2target[spriteName] = target;
        } else {
            let newIndex = 0;
            this.runtime.targets.find(it => {
                if (!it.isOriginal && it.sprite.name === target.sprite.name) newIndex += 1;
                return it.id === target.id;
            });
            const newName = `${spriteName}[${newIndex}]`;
            this.memoizedTargets.id2name[target.id] = newName;
            this.memoizedTargets.name2target[newName] = target;
        }
        return target;
    }

    unmemoizeTarget (target) {
        const memoizedName = this.memoizedTargets.id2name[target.id];
        if (!memoizedName) return;
        const spriteName = memoizedName.split('[')[0];
        Object.entries(this.memoizedTargets.name2target)
            .filter(([, t]) => t.sprite.name === spriteName)
            .forEach(([name, t]) => {
                delete this.memoizedTargets.id2name[t.id];
                delete this.memoizedTargets.name2target[name];
                Object.keys(this.memoizedVariables)
                    .filter(varName => varName.split('.')[0] === name)
                    .forEach(varName => {
                        delete this.memoizedVariables[varName];
                    });
            });
    }

    getTargetName (target) {
        return this.memoizedTargets.id2name[target.id];
    }

    memoizeVariable (key, variable) {
        const memo = this.memoizedVariables;
        if (typeof memo[key] !== 'undefined' && memo[key].lastValue === variable.value) return false;
        memo[key] = {
            original: variable,
            lastValue: variable.value
        };
        return true;
    }

    getStageOrSpriteTarget (target, variable) {
        const stageTarget = this.runtime.targets.find(it => it.isStage);
        if (stageTarget && stageTarget.variables && stageTarget.variables[variable.id]) {
            return stageTarget;
        }
        return target;
    }

};
