const servers = {};
const inDvar = 'sv_iw4madmin_in';
const outDvar = 'sv_iw4madmin_out';
const integrationEnabledDvar = 'sv_iw4madmin_integration_enabled';
const groupSeparatorChar = '\x1d';
const recordSeparatorChar = '\x1e';
const unitSeparatorChar = '\x1f';

let busFileIn = '';
let busFileOut = '';
let busMode = 'rcon';
let busDir = '';

const init = (registerNotify, serviceResolver, config, scriptHelper) => {
    registerNotify('IManagementEventSubscriptions.ClientStateInitialized', (clientEvent, _) => plugin.onClientEnteredMatch(clientEvent));
    registerNotify('IGameServerEventSubscriptions.ServerValueReceived', (serverValueEvent, _) => plugin.onServerValueReceived(serverValueEvent));
    registerNotify('IGameServerEventSubscriptions.ServerValueSetCompleted', (serverValueEvent, _) => plugin.onServerValueSetCompleted(serverValueEvent));
    registerNotify('IGameServerEventSubscriptions.MonitoringStarted', (monitorStartEvent, _) => plugin.onServerMonitoringStart(monitorStartEvent));
    registerNotify('IGameEventSubscriptions.MatchStarted', (matchStartEvent, _) => plugin.onMatchStart(matchStartEvent));
    registerNotify('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => plugin.onPenalty(penaltyEvent));

    plugin.onLoad(serviceResolver, config, scriptHelper);
    return plugin;
};

const plugin = {
    author: 'RaidMax',
    version: '2.1',
    name: 'Game Interface',
    serviceResolver: null,
    eventManager: null,
    logger: null,
    commands: null,
    scriptHelper: null,
    configWrapper: null,
    config: {
        pollingRate: 300
    },

    onLoad: function (serviceResolver, configWrapper, scriptHelper) {
        this.serviceResolver = serviceResolver;
        this.eventManager = serviceResolver.resolveService('IManager');
        this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
        this.commands = commands;
        this.configWrapper = configWrapper;
        this.scriptHelper = scriptHelper;

        const storedConfig = this.configWrapper.getValue('config', newConfig => {
            if (newConfig) {
                plugin.logger.logInformation('{Name} config reloaded.', plugin.name);
                plugin.config = newConfig;
            }
        });

        if (storedConfig != null) {
            this.config = storedConfig
        } else {
            this.configWrapper.setValue('config', this.config);
        }
    },

    onClientEnteredMatch: function (clientEvent) {
        const serverState = servers[clientEvent.client.currentServer.id];

        if (serverState === undefined || serverState == null) {
            this.initializeServer(clientEvent.client.currentServer);
        } else if (!serverState.running && !serverState.initializationInProgress) {
            serverState.running = true;
            this.requestGetDvar(inDvar, clientEvent.client.currentServer);
        }
    },

    onPenalty: function (penaltyEvent) {
        const warning = 1;
        if (penaltyEvent.penalty.type !== warning || !penaltyEvent.client.isIngame) {
            return;
        }

        sendScriptCommand(penaltyEvent.client.currentServer, 'Alert', penaltyEvent.penalty.punisher, penaltyEvent.client, {
            alertType: this.translations('GLOBAL_WARNING') + '!',
            message: penaltyEvent.penalty.offense
        });
    },

    onServerValueReceived: function (serverValueEvent) {
        const name = serverValueEvent.response.name;
        if (name === integrationEnabledDvar) {
            this.handleInitializeServerData(serverValueEvent);
        } else if (name === inDvar) {
            this.handleIncomingServerData(serverValueEvent);
        }
    },

    onServerValueSetCompleted: async function (serverValueEvent) {
        this.logger.logDebug('Set {dvarName}={dvarValue} success={success} from {server}', serverValueEvent.valueName,
            serverValueEvent.value, serverValueEvent.success, serverValueEvent.server.id);
        
        if (serverValueEvent.valueName !== inDvar && serverValueEvent.valueName !== outDvar) {
            this.logger.logDebug('Ignoring set complete of {name}', serverValueEvent.valueName);
            return;
        }

        const serverState = servers[serverValueEvent.server.id];
        serverState.outQueue.shift();

        this.logger.logDebug('outQueue len = {outLen}, inQueue len = {inLen}', serverState.outQueue.length, serverState.inQueue.length);

        // if it didn't succeed, we need to retry
        if (!serverValueEvent.success && !this.eventManager.cancellationToken.isCancellationRequested) {
            this.logger.logDebug('Set of server value failed... retrying');
            this.requestSetDvar(serverValueEvent.valueName, serverValueEvent.value, serverValueEvent.server);
            return;
        }

        // we informed the server that we received the event
        if (serverState.inQueue.length > 0 && serverValueEvent.valueName === inDvar) {
            const input = serverState.inQueue.shift();

            // if we queued an event then the next loop will be at the value set complete
            await this.processEventMessage(input, serverValueEvent.server);
        }

        this.logger.logDebug('loop complete');
        // loop restarts
        this.requestGetDvar(inDvar, serverValueEvent.server);
    },
    
    onServerMonitoringStart: function (monitorStartEvent) {
        this.initializeServer(monitorStartEvent.server);
    },

    onMatchStart: function (matchStartEvent) {
        busMode = 'rcon';
        this.sendEventMessage(matchStartEvent.server, true, 'GetBusModeRequested', null, null, null, {});
    },

    initializeServer: function (server) {
        servers[server.id] = {
            enabled: false,
            running: false,
            initializationInProgress: true,
            queuedMessages: [],
            inQueue: [],
            outQueue: [],
            commandQueue: []
        };

        this.logger.logDebug('Initializing game interface for {serverId}', server.id);
        this.requestGetDvar(integrationEnabledDvar, server);
    },

    handleInitializeServerData: function (responseEvent) {
        this.logger.logInformation('GSC integration enabled = {integrationValue} for {server}',
            responseEvent.response.value, responseEvent.server.id);

        if (responseEvent.response.value !== '1') {
            return;
        }

        const serverState = servers[responseEvent.server.id];
        serverState.outQueue.shift();
        serverState.enabled = true;
        serverState.running = true;
        serverState.initializationInProgress = false;
        
        // todo: this might not work for all games
        responseEvent.server.rconParser.configuration.floodProtectInterval = 150;

        this.sendEventMessage(responseEvent.server, true, 'GetBusModeRequested', null, null, null, {});
        this.sendEventMessage(responseEvent.server, true, 'GetCommandsRequested', null, null, null, {});
        this.requestGetDvar(inDvar, responseEvent.server);
    },

    handleIncomingServerData: function (responseEvent) {
        this.logger.logDebug('Received {dvarName}={dvarValue} success={success} from {server}', responseEvent.response.name,
            responseEvent.response.value, responseEvent.success, responseEvent.server.id);

        const serverState = servers[responseEvent.server.id];
        serverState.outQueue.shift();

        const utilities = importNamespace('SharedLibraryCore.Utilities');

        if (responseEvent.server.connectedClients.count === 0 && !utilities.isDevelopment) {
            // no clients connected so we don't need to query
            serverState.running = false;
            return;
        }

        // read failed, so let's retry
        if (!responseEvent.success && !this.eventManager.cancellationToken.isCancellationRequested) {
            this.logger.logDebug('Get of server value failed... retrying');
            this.requestGetDvar(responseEvent.response.name, responseEvent.server);
            return;
        }

        let input = responseEvent.response.value;
        const server = responseEvent.server;

        if (this.eventManager.cancellationToken.isCancellationRequested) {
            return;
        }

        // no data available so we poll again or send any outgoing messages
        if (isEmpty(input)) {
            this.logger.logDebug('No data to process from server');
            if (serverState.commandQueue.length > 0) {
                this.logger.logDebug('Sending next out message');
                const nextMessage = serverState.commandQueue.shift();
                this.requestSetDvar(outDvar, nextMessage, server);
            } else {
                this.requestGetDvar(inDvar, server);
            }
            return;
        }

        serverState.inQueue.push(input);

        // let server know that we received the data
        this.requestSetDvar(inDvar, '', server);
    },

    processEventMessage: async function (input, server) {
        let messageQueued = false;
        const event = parseEvent(input);

        this.logger.logDebug('Processing input... {eventType} {subType} {@data} {clientNumber}', event.eventType,
            event.subType, event.data, event.clientNumber);

        const metaService = this.serviceResolver.ResolveService('IMetaServiceV2');
        const threading = importNamespace('System.Threading');
        const tokenSource = new threading.CancellationTokenSource();
        const token = tokenSource.token;

        // todo: refactor to mapping if possible
        if (event.eventType === 'ClientDataRequested') {
            const client = server.getClientByNumber(event.clientNumber);

            if (client != null) {
                this.logger.logDebug('Found client {name}', client.name);

                let data = [];

                const metaService = this.serviceResolver.resolveService('IMetaServiceV2');

                if (event.subType === 'Meta') {
                    const meta = (await metaService.getPersistentMeta(event.data, client.clientId, token)).result;
                    data[event.data] = meta === null ? '' : meta.Value;
                    this.logger.logDebug('event data is {data}', event.data);
                } else {
                    const clientStats = getClientStats(client, server);
                    const tagMeta = (await metaService.getPersistentMetaByLookup('ClientTagV2', 'ClientTagNameV2', client.clientId, token)).result;
                    data = {
                        level: client.level,
                        clientId: client.clientId,
                        lastConnection: client.timeSinceLastConnectionString,
                        tag: tagMeta?.value ?? '',
                        performance: clientStats?.performance ?? 200.0
                    };
                }

                this.sendEventMessage(server, false, 'ClientDataReceived', event.subType, client, undefined, data);
                messageQueued = true;
            } else {
                this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}', event.clientNumber, event.eventType);
                this.sendEventMessage(server, false, 'ClientDataReceived', 'Fail', event.clientNumber, undefined, {
                    ClientNumber: event.clientNumber
                });
                messageQueued = true;
            }
        }

        if (event.eventType === 'SetClientDataRequested') {
            let client = server.getClientByNumber(event.clientNumber);
            let clientId;

            if (client != null) {
                clientId = client.clientId;
            } else {
                clientId = parseInt(event.data['clientId']);
            }

            this.logger.logDebug('ClientId={clientId}', clientId);

            if (clientId == null || isNaN(clientId)) {
                this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}: {EventData}', event.clientNumber, event.eventType, event.data);
                this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
                    ClientNumber: event.clientNumber
                }, undefined, {
                    status: 'Fail'
                });
                messageQueued = true;
            } else {
                if (event.subType === 'Meta') {
                    try {
                        if (event.data['value'] != null && event.data['key'] != null) {
                            this.logger.logDebug('Key={key}, Value={value}, Direction={direction} {token}', event.data['key'], event.data['value'], event.data['direction'], token);
                            if (event.data['direction'] != null) {
                                const parsedValue = parseInt(event.data['value']);
                                const key = event.data['key'].toString();
                                if (!isNaN(parsedValue)) {
                                    event.data['direction'] = 'up' ?
                                        (await metaService.incrementPersistentMeta(key, parsedValue, clientId, token)).result :
                                        (await metaService.decrementPersistentMeta(key, parsedValue, clientId, token)).result;
                                }
                            } else {
                                const _ = (await metaService.setPersistentMeta(event.data['key'], event.data['value'], clientId, token)).result;
                            }

                            if (event.data['key'] === 'PersistentClientGuid') {
                                const serverEvents = importNamespace('SharedLibraryCore.Events.Management');
                                const persistentIdEvent = new serverEvents.ClientPersistentIdReceiveEvent(client, event.data['value']);
                                this.eventManager.queueEvent(persistentIdEvent);
                            }
                        }
                        this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
                            ClientNumber: event.clientNumber
                        }, undefined, {
                            status: 'Complete'
                        });
                        messageQueued = true;
                    } catch (error) {
                        this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
                            ClientNumber: event.clientNumber
                        }, undefined, {
                            status: 'Fail'
                        });
                        this.logger.logError('Could not persist client meta {Key}={Value} {error} for {Client}', event.data['key'], event.data['value'], error.toString(), clientId);
                        messageQueued = true;
                    }
                }
            }
        }

        if (event.eventType === 'UrlRequested') {
            const urlRequest = this.parseUrlRequest(event);

            this.logger.logDebug('Making gamescript web request {@Request}', urlRequest);

            this.scriptHelper.requestUrl(urlRequest, response => {
                this.logger.logDebug('Got response for gamescript web request - {Response}', response);

                if (typeof response !== 'string' && !(response instanceof String)) {
                    response = JSON.stringify(response);
                }
                
                const max = 10;
                this.logger.logDebug(`response length ${response.length}`);
                
                let quoteReplace = '\\"';
                // todo: may be more than just T6
                if (server.gameCode === 'T6') {
                    quoteReplace = '\\\\"';
                }
                
                let chunks = chunkString(response.replace(/"/gm, quoteReplace).replace(/[\n|\t]/gm, ''), 800);
                if (chunks.length > max) {
                    this.logger.logWarning(`Response chunks greater than max (${max}). Data truncated!`);
                    chunks = chunks.slice(0, max);
                }
                this.logger.logDebug(`chunk size ${chunks.length}`);
                
                for (let i = 0; i < chunks.length; i++) {
                    this.sendEventMessage(server, false, 'UrlRequestCompleted', null, null,
                        null, { entity: event.data.entity, remaining: chunks.length - (i + 1), response: chunks[i]});
                }
            });
        }
        
        if (event.eventType === 'RegisterCommandRequested') {
            this.registerDynamicCommand(event);
        }
        
        if (event.eventType === 'GetBusModeRequested') {
            if (event.data?.directory && event.data?.mode) {
                busMode = event.data.mode;
                busDir = event.data.directory.replace('\'', '').replace('"', '');
                if (event.data?.inLocation && event.data?.outLocation) {
                    busFileIn = event.data?.inLocation;
                    busFileOut = event.data?.outLocation;
                }
                this.logger.logDebug('Setting bus mode to {mode} {dir}', busMode, busDir);
            }
        }

        tokenSource.dispose();
        return messageQueued;
    },

    sendEventMessage: function (server, responseExpected, event, subtype, origin, target, data) {
        let targetClientNumber = -1;
        let originClientNumber = -1;

        if (target != null) {
            targetClientNumber = target.clientNumber;
        }

        if (origin != null) {
            originClientNumber = origin.clientNumber
        }

        const output = `${responseExpected ? '1' : '0'}${groupSeparatorChar}${event}${groupSeparatorChar}${subtype}${groupSeparatorChar}${originClientNumber}${groupSeparatorChar}${targetClientNumber}${groupSeparatorChar}${buildDataString(data)}`;
        this.logger.logDebug('Queuing output for server {output}', output);

        servers[server.id].commandQueue.push(output);
    },

    requestGetDvar: function (dvarName, server) {
        const serverState = servers[server.id];

        if (dvarName !== integrationEnabledDvar && busMode === 'file') {
            this.scriptHelper.requestNotifyAfterDelay(250, () => {
                const io = importNamespace('System.IO');
                serverState.outQueue.push({});
                try {
                    const content = io.File.ReadAllText(`${busDir}/${fileForDvar(dvarName)}`);
                    plugin.onServerValueReceived({
                        server: server,
                        source: server,
                        success: true,
                        response: {
                            name: dvarName,
                            value: content
                        }
                    });
                } catch (e) {
                    plugin.logger.logError('Could not get bus data {exception}', e.toString());
                    plugin.onServerValueReceived({
                        server: server,
                        success: false,
                        response: {
                            name: dvarName
                        }
                    });
                }
            });
            
            return;
        }
        
        const serverEvents = importNamespace('SharedLibraryCore.Events.Server');
        const requestEvent = new serverEvents.ServerValueRequestEvent(dvarName, server);
        requestEvent.delayMs = this.config.pollingRate;
        requestEvent.timeoutMs = 2000;
        requestEvent.source = this.name;

        if (server.matchEndTime !== null) {
            const extraDelay = 15000;
            const end = new Date(server.matchEndTime.toString());
            const diff = new Date().getTime() - end.getTime();

            if (diff < extraDelay) {
                requestEvent.delayMs = (extraDelay - diff) + this.config.pollingRate;
                this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs);
            }
        }

        this.logger.logDebug('requesting {dvar}', dvarName);

        serverState.outQueue.push(requestEvent);

        if (serverState.outQueue.length <= 1) {
            this.eventManager.queueEvent(requestEvent);
        } else {
            this.logger.logError('[requestGetDvar] Queue is full!');
        }
    },

    requestSetDvar: function (dvarName, dvarValue, server) {
        const serverState = servers[server.id];
        
        if ( busMode === 'file' ) {
            this.scriptHelper.requestNotifyAfterDelay(250, async () => {
                const io = importNamespace('System.IO');
                try {
                    const path = `${busDir}/${fileForDvar(dvarName)}`;
                    plugin.logger.logDebug('writing {value} to {file}', dvarValue, path);
                    io.File.WriteAllText(path, dvarValue);
                    serverState.outQueue.push({});
                    await plugin.onServerValueSetCompleted({
                        server: server,
                        source: server,
                        success: true,
                        value: dvarValue,
                        valueName: dvarName,
                    });
                } catch (e) {
                    plugin.logger.logError('Could not set bus data {exception}', e.toString());
                    await plugin.onServerValueSetCompleted({
                        server: server,
                        success: false,
                        valueName: dvarName,
                        value: dvarValue
                    });
                }
            })
            
            return;
        }

        const serverEvents = importNamespace('SharedLibraryCore.Events.Server');
        const requestEvent = new serverEvents.ServerValueSetRequestEvent(dvarName, dvarValue, server);
        requestEvent.delayMs = this.config.pollingRate;
        requestEvent.timeoutMs = 2000;
        requestEvent.source = this.name;

        if (server.matchEndTime !== null) {
            const extraDelay = 15000;
            const end = new Date(server.matchEndTime.toString());
            const diff = new Date().getTime() - end.getTime();

            if (diff < extraDelay) {
                requestEvent.delayMs = (extraDelay - diff) + this.config.pollingRate;
                this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs);
            }
        }

        serverState.outQueue.push(requestEvent);

        this.logger.logDebug('outQueue size = {length}', serverState.outQueue.length);

        // if this is the only item in the out-queue we can send it immediately
        if (serverState.outQueue.length === 1) {
            this.eventManager.queueEvent(requestEvent);
        } else {
            this.logger.logError('[requestSetDvar] Queue is full!');
        }
    },

    parseUrlRequest: function(event) {
        const url = event.data?.url;

        if (url === undefined) {
            this.logger.logWarning('No url provided for gamescript web request - {Event}', event);
            return;
        }

        const body = event.data?.body;
        const method = event.data?.method || 'GET';
        const contentType = event.data?.contentType || 'text/plain';
        const headers = event.data?.headers;

        const dictionary = System.Collections.Generic.Dictionary(System.String, System.String);
        const headerDict = new dictionary();

        if (headers) {
            const eachHeader = headers.split(',');

            for (let eachKeyValue of eachHeader) {
                const keyValueSplit = eachKeyValue.split(':');
                if (keyValueSplit.length === 2) {
                    headerDict.add(keyValueSplit[0], keyValueSplit[1]);
                }
            }
        }

        const script = importNamespace('IW4MAdmin.Application.Plugin.Script');
        return new script.ScriptPluginWebRequest(url, body, method, contentType, headerDict);
    },
    
    registerDynamicCommand: function(event) {
        const commandWrapper = {
            commands: [{
                name: event.data['name'] || 'DEFAULT',
                description: event.data['description'] || 'DEFAULT',
                alias: event.data['alias'] || 'DEFAULT',
                permission: event.data['minPermission'] || 'DEFAULT',
                targetRequired: (event.data['targetRequired'] || '0') === '1',
                supportedGames: (event.data['supportedGames'] || '').split(','),

                execute: (gameEvent) => {
                    if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                        return;
                    }
                    
                    if (gameEvent.data === '--reload' && gameEvent.origin.level === 'Owner') {
                        this.sendEventMessage(gameEvent.owner, true, 'GetCommandsRequested', null, null, null, { name: gameEvent.extra.name });
                    } else {
                        sendScriptCommand(gameEvent.owner, `${event.data['eventKey']}Execute`, gameEvent.origin, gameEvent.target, {
                            args: gameEvent.data
                        });
                    }
                }
            }]
        }
        
        this.scriptHelper.registerDynamicCommand(commandWrapper);
    }
};

const commands = [{
    name: 'giveweapon',
    description: 'gives specified weapon',
    alias: 'gw',
    permission: 'SeniorAdmin',
    targetRequired: true,
    arguments: [{
        name: 'player',
        required: true
    },
        {
            name: 'weapon name',
            required: true
        }
    ],
    supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
    execute: (gameEvent) => {
        if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
            return;
        }
        sendScriptCommand(gameEvent.owner, 'GiveWeapon', gameEvent.origin, gameEvent.target, {
            weaponName: gameEvent.data
        });
    }
},
    {
        name: 'takeweapons',
        description: 'take all weapons from specified player',
        alias: 'tw',
        permission: 'SeniorAdmin',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'TakeWeapons', gameEvent.origin, gameEvent.target, undefined);
        }
    },
    {
        name: 'switchteam',
        description: 'switches specified player to the opposite team',
        alias: 'st',
        permission: 'Administrator',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'SwitchTeams', gameEvent.origin, gameEvent.target, undefined);
        }
    },
    {
        name: 'lockcontrols',
        description: 'locks target player\'s controls',
        alias: 'lc',
        permission: 'Administrator',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'LockControls', gameEvent.origin, gameEvent.target, undefined);
        }
    },
    {
        name: 'noclip',
        description: 'enable noclip on yourself ingame',
        alias: 'nc',
        permission: 'SeniorAdmin',
        targetRequired: false,
        arguments: [],
        supportedGames: ['IW4', 'IW5'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'NoClip', gameEvent.origin, gameEvent.origin, undefined);
        }
    },
    {
        name: 'hide',
        description: 'hide yourself ingame',
        alias: 'hi',
        permission: 'SeniorAdmin',
        targetRequired: false,
        arguments: [],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'Hide', gameEvent.origin, gameEvent.origin, undefined);
        }
    },
    {
        name: 'alert',
        description: 'alert a player',
        alias: 'alr',
        permission: 'SeniorAdmin',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        },
            {
                name: 'message',
                required: true
            }
        ],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.Owner, 'Alert', gameEvent.origin, gameEvent.target, {
                alertType: 'Alert',
                message: gameEvent.Data
            });
        }
    },
    {
        name: 'gotoplayer',
        description: 'teleport to a player',
        alias: 'g2p',
        permission: 'SeniorAdmin',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'Goto', gameEvent.origin, gameEvent.target, undefined);
        }
    },
    {
        name: 'playertome',
        description: 'teleport a player to you',
        alias: 'p2m',
        permission: 'SeniorAdmin',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'PlayerToMe', gameEvent.origin, gameEvent.target, undefined);
        }
    },
    {
        name: 'goto',
        description: 'teleport to a position',
        alias: 'g2',
        permission: 'SeniorAdmin',
        targetRequired: false,
        arguments: [{
            name: 'x',
            required: true
        },
            {
                name: 'y',
                required: true
            },
            {
                name: 'z',
                required: true
            }
        ],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }

            const args = String(gameEvent.Data).split(' ');
            sendScriptCommand(gameEvent.owner, 'Goto', gameEvent.origin, gameEvent.target, {
                x: args[0],
                y: args[1],
                z: args[2]
            });
        }
    },
    {
        name: 'kill',
        description: 'kill a player',
        alias: 'kpl',
        permission: 'SeniorAdmin',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'Kill', gameEvent.origin, gameEvent.target, undefined);
        }
    },
    {
        name: 'setspectator',
        description: 'sets a player as spectator',
        alias: 'spec',
        permission: 'Administrator',
        targetRequired: true,
        arguments: [{
            name: 'player',
            required: true
        }],
        supportedGames: ['IW4', 'IW5', 'T5', 'T6'],
        execute: (gameEvent) => {
            if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
                return;
            }
            sendScriptCommand(gameEvent.owner, 'SetSpectator', gameEvent.origin, gameEvent.target, undefined);
        }
    }
];

const sendScriptCommand = (server, command, origin, target, data) => {
    const serverState = servers[server.id];
    if (serverState === undefined || !serverState.enabled) {
        return;
    }
    plugin.sendEventMessage(server, false, 'ExecuteCommandRequested', command, origin, target, data);
};

const getClientStats = (client, server) => {
    const contextFactory = plugin.serviceResolver.ResolveService('IDatabaseContextFactory');
    const context = contextFactory.createContext(false);
    const stats = context.clientStatistics.getClientsStatData([client.ClientId], server.legacyDatabaseId);
    context.dispose();

    return stats.length > 0 ? stats[0] : undefined;
};

const parseEvent = (input) => {
    if (input === undefined) {
        return {};
    }

    const eventInfo = input.split(groupSeparatorChar);

    return {
        eventType: eventInfo[1],
        subType: eventInfo[2],
        clientNumber: eventInfo[3],
        data: eventInfo.length > 4 ? parseDataString(eventInfo[4]) : undefined
    };
};

const buildDataString = data => {
    if (data === undefined) {
        return '';
    }

    let formattedData = '';

    for (let [key, value] of Object.entries(data)) {
        formattedData += `${key}${unitSeparatorChar}${value}${recordSeparatorChar}`;
    }

    return formattedData.slice(0, -1);
};

const parseDataString = data => {
    if (data === undefined) {
        return '';
    }

    const dict = {};
    const split = data.split(recordSeparatorChar);

    for (let i = 0; i < split.length; i++) {
        const segment = split[i];
        const keyValue = segment.split(unitSeparatorChar);
        if (keyValue.length !== 2) {
            continue;
        }
        dict[keyValue[0]] = keyValue[1];
    }

    return Object.keys(dict).length === 0 ? data : dict;
};

const validateEnabled = (server, origin) => {
    const enabled = servers[server.id] != null && servers[server.id].enabled;
    if (!enabled) {
        origin.tell('Game interface is not enabled on this server');
    }
    return enabled;
};

const isEmpty = (value) => {
    return value == null || false || value === '' || value === 'null';
};

const chunkString = (str, chunkSize) => {
    const result = [];
    for (let i = 0; i < str.length; i += chunkSize) {
        result.push(str.slice(i, i + chunkSize));
    }

    return result;
}

const fileForDvar = (dvar) => {
    if (dvar === inDvar) {
        return busFileIn;
    }
    
    return busFileOut;
}