module.exports = class GameAgent {
    constructor (runtime) {
        this.runtime = runtime;
        this.targets = runtime.targets;
        this.addListeners();
        this.updateTargets();
    }

    addListeners () {
        this.runtime.on('targetWasCreated', () => this.updateTargets());
        this.runtime.on('targetWasRemoved', () => this.updateTargets());
        this.runtime.on(this.runtime.VARIABLE_UPDATE, (name, value) => this.sendMessage(name, value));
    }

    cleanup () {
        this.tLookup = null;
        this.targets = null;
        this.runtime.off('targetWasCreated', () => this.updateTargets());
        this.runtime.off('targetWasRemoved', () => this.updateTargets());
        this.runtime.removeAllListeners(this.runtime.VARIABLE_UPDATE);
    }

    updateTargets () {
        setTimeout(() => {
            this.tLookup = this.analyzeTargets(this.runtime.targets);
        }, 0);
    }

    sendMessage (name, value) {
        if (this.runtime.gameServer && this.runtime.gameServer.readyState !== WebSocket.OPEN) {
            // eslint-disable-next-line no-console
            return console.log(`can't send message ${name}:${value}, socket not open`);
        }
        this.runtime.gameServer.send(`var,${name}:${value}`);
    }

    handleMessage (data) {
        let command = null;
        const segments = data.split(',');
        return segments.map(s => {
            let arg = null;
            if (this.isCommand(s)) {
                command = s;
                return s;
            }
            switch (command) {
            case 'var':
            case 'prop':
                // eslint-disable-next-line no-console
                console.log('got command', command, s);
                arg = this.parseArgument(s, command);
                return this.processVariable(arg);
            default:
                // eslint-disable-next-line no-console
                console.log('unknown command', command, s);
                return 'ERRCMD';
            }
        }).join(',');
    }

    isCommand (text) {
        return !!text.match(/^(var|prop)$/g);
    }

    /**
     *
     * @param {*} arg Argument like `Hedgehog.cloud:200` for setting local variable (Sprite=Hedgehog),
     *                `cloud:200` for setting global variable. getting variable with `cloud` (without colon :)
     * @param {*} type 'var' or 'prop'
     * @return {object} Object bla
     */
    parseArgument (arg, type) {
        // example: var,a:100,b:400,prop,Balloons[0].x:45,Balloons[0].y
        const parts = arg.split(':');
        const sprite = parts[0].split('.');
        return {
            local: sprite.length > 1 ? sprite[0] : null,
            set: parts.length > 1,
            name: sprite[sprite.length - 1],
            value: parts[1],
            type
        };
    }

    stringifyArgument (arg) {
        const localText = arg.local ? `${arg.local}.` : '';
        return `${localText}${arg.name}:${arg.value}`;
    }

    processVariable (arg) {
        try {
            if (arg.set) return this.stringifyArgument(this.setValue(arg));
            return this.stringifyArgument(this.getValue(arg));
        } catch (error) {
            arg.value = 'ERRORVAR';
            return this.stringifyArgument(arg);
        }
    }

    setValue (arg) {
        if (arg.type === 'var') this.getVariable(arg.local, arg.name).value = arg.value;
        if (arg.type === 'prop') {
            const target = this.getTarget(arg.local);
            target[arg.name] = this.castValue(arg.name, arg.value);
            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;
            default:
                break;
            }
        }
        return arg;
    }

    getValue (arg) {
        if (arg.type === 'var') arg.value = this.getVariable(arg.local, arg.name).value;
        if (arg.type === 'prop') arg.value = this.getTarget(arg.local)[arg.name];
        return arg;
    }

    getVariable (local, variableName) {
        // if (arg.local) set local variable (Sprite variable)
        // else set global variable (Stage variable)
        return this.tLookup[local || 'Stage'].variables[variableName];
    }

    castValue (propertyName, value) {
        switch (propertyName) {
        case 'currentCostume':
        case 'direction':
        case 'size':
        case 'tempo':
        case 'videoTransparency':
        case 'volume':
        case 'x':
        case 'y':
            return Number(value);
        case 'draggable':
        case 'visible':
            return value === 'true' || value === '1' || value === 'yes';
        default:
            return value;
        }
    }

    getTarget (local) {
        // if (arg.local) get (local) Sprite
        // else get (global) Stage
        return this.tLookup[local || 'Stage'].target;
    }

    analyzeTargets (targets) {
        const targetNames = this.getTargetLookupNames(targets);

        return targetNames.map((name, i) => {
            const target = targets[i];
            return {
                index: i,
                id: targets[i],
                name: target.isStage ? 'Stage' : name,
                isStage: target.isStage,
                variables: this.getVariableLookupNames(target.variables),
                target
            };
        }).reduce((acc, t) => {
            acc[t.name] = t;
            return acc;
        }, {});
    }

    getTargetLookupNames (targets) {
        // get clones right: multiple targets with the same Name e.g. Hedgehog -> Hedgehog[0], Hedgehog[1] ...
        return targets.reduce((acc, t, i) => {
            const name = t.sprite.name;
            if (acc.lookups[name]) {
                const lookup = acc.lookups[name];
                const newCount = lookup.count + 1;
                if (lookup.count === 0) {
                    acc.targets[lookup.elementIndex] = `${acc.targets[lookup.elementIndex]}[${lookup.count}]`;
                }
                acc.targets.push(`${name}[${newCount}]`);
                acc.lookups[name] = {elementIndex: i, count: newCount};
            } else {
                acc.targets.push(name);
                acc.lookups[name] = {elementIndex: i, count: 0};
            }
            return acc;
        }, {targets: [], lookups: {}}).targets;
    }

    getVariableLookupNames (variables) {
        // lookup object by name e.g. { Hedgehog[0] : {...} }
        return Object.keys(variables).reduce((acc, key) => {
            const variable = variables[key];
            acc[variable.name] = variable;
            return acc;
        }, {});
    }
};
