const Core = require('./core');

class Gamespy3 extends Core {
    constructor() {
        super();
        this.sessionId = 1;
        this.encoding = 'latin1';
        this.byteorder = 'be';
        this.useOnlySingleSplit = false;
        this.isJc2mp = false;
    }

    async run(state) {
        const buffer = await this.sendPacket(9, false, false, false);
        const reader = this.reader(buffer);
        let challenge = parseInt(reader.string());
        this.debugLog("Received challenge key: " + challenge);
        if (challenge === 0) {
            // Some servers send us a 0 if they don't want a challenge key used
            // BF2 does this.
            challenge = null;
        }

        let requestPayload;
        if(this.isJc2mp) {
            // they completely alter the protocol. because why not.
            requestPayload = Buffer.from([0xff,0xff,0xff,0x02]);
        } else {
            requestPayload = Buffer.from([0xff,0xff,0xff,0x01]);
        }
        /** @type Buffer[] */
        const packets = await this.sendPacket(0,challenge,requestPayload,true);

        // iterate over the received packets
        // the first packet will start off with k/v pairs, followed with data fields
        // the following packets will only have data fields
        state.raw.playerTeamInfo = {};

        for(let iPacket = 0; iPacket < packets.length; iPacket++) {
            const packet = packets[iPacket];
            const reader = this.reader(packet);

            this.debugLog("Parsing packet #" + iPacket);
            this.debugLog(packet);

            // Parse raw server key/values

            if(iPacket === 0) {
                while(!reader.done()) {
                    const key = reader.string();
                    if(!key) break;

                    let value = reader.string();
                    while(value.match(/^p[0-9]+$/)) {
                        // fix a weird ut3 bug where some keys don't have values
                        value = reader.string();
                    }

                    state.raw[key] = value;
                    this.debugLog(key + " = " + value);
                }
            }

            // Parse player, team, item array state

            if(this.isJc2mp) {
                state.raw.numPlayers2 = reader.uint(2);
                while(!reader.done()) {
                    const player = {};
                    player.name = reader.string();
                    player.steamid = reader.string();
                    player.ping = reader.uint(2);
                    state.players.push(player);
                }
            } else {
                let firstMode = true;
                while(!reader.done()) {
                    if (reader.uint(1) <= 2) continue;
                    reader.skip(-1);
                    let fieldId = reader.string();
                    if(!fieldId) continue;
                    const fieldIdSplit = fieldId.split('_');
                    const fieldName = fieldIdSplit[0];
                    const itemType = fieldIdSplit.length > 1 ? fieldIdSplit[1] : 'no_';

                    if(!(itemType in state.raw.playerTeamInfo)) {
                        state.raw.playerTeamInfo[itemType] = [];
                    }
                    const items = state.raw.playerTeamInfo[itemType];

                    let offset = reader.uint(1);
                    firstMode = false;

                    this.debugLog(() => "Parsing new field: itemType=" + itemType + " fieldName=" + fieldName + " startOffset=" + offset);

                    while(!reader.done()) {
                        const item = reader.string();
                        if(!item) break;

                        while(items.length <= offset) { items.push({}); }
                        items[offset][fieldName] = item;
                        this.debugLog("* " + item);
                        offset++;
                    }
                }
            }
        }

        // Turn all that raw state into something useful

        if ('hostname' in state.raw) state.name = state.raw.hostname;
        else if('servername' in state.raw) state.name = state.raw.servername;
        if ('mapname' in state.raw) state.map = state.raw.mapname;
        if (state.raw.password === '1') state.password = true;
        if ('maxplayers' in state.raw) state.maxplayers = parseInt(state.raw.maxplayers);
        if ('hostport' in state.raw) state.gamePort = parseInt(state.raw.hostport);

        if('' in state.raw.playerTeamInfo) {
            for (const playerInfo of state.raw.playerTeamInfo['']) {
                const player = {};
                for(const from of Object.keys(playerInfo)) {
                    let key = from;
                    let value = playerInfo[from];

                    if(key === 'player') key = 'name';
                    if(key === 'score' || key === 'ping' || key === 'team' || key === 'deaths' || key === 'pid') value = parseInt(value);
                    player[key] = value;
                }
                state.players.push(player);
            }
        }
    }

    async sendPacket(type,challenge,payload,assemble) {
        const challengeLength = challenge === null ? 0 : 4;
        const payloadLength = payload ? payload.length : 0;

        const b = Buffer.alloc(7 + challengeLength + payloadLength);
        b.writeUInt8(0xFE, 0);
        b.writeUInt8(0xFD, 1);
        b.writeUInt8(type, 2);
        b.writeUInt32BE(this.sessionId, 3);
        if(challengeLength) b.writeInt32BE(challenge, 7);
        if(payloadLength) payload.copy(b, 7+challengeLength);

        let numPackets = 0;
        const packets = {};
        return await this.udpSend(b,(buffer) => {
            const reader = this.reader(buffer);
            const iType = reader.uint(1);
            if(iType !== type) {
                this.logger.debug('Skipping packet, type mismatch');
                return;
            }
            const iSessionId = reader.uint(4);
            if(iSessionId !== this.sessionId) {
                this.logger.debug('Skipping packet, session id mismatch');
                return;
            }

            if(!assemble) {
                return reader.rest();
            }
            if(this.useOnlySingleSplit) {
                // has split headers, but they are worthless and only one packet is used
                reader.skip(11);
                return [reader.rest()];
            }

            reader.skip(9); // filler data -- usually set to 'splitnum\0'
            let id = reader.uint(1);
            const last = (id & 0x80);
            id = id & 0x7f;
            if(last) numPackets = id+1;

            reader.skip(1); // "another 'packet number' byte, but isn't understood."

            packets[id] = reader.rest();
            if(this.debug) {
                this.debugLog("Received packet #"+id + (last ? " (last)" : ""));
            }

            if(!numPackets || Object.keys(packets).length !== numPackets) return;

            // assemble the parts
            const list = [];
            for(let i = 0; i < numPackets; i++) {
                if(!(i in packets)) {
                    throw new Error('Missing packet #'+i);
                }
                list.push(packets[i]);
            }
            return list;
        });
    }
}

module.exports = Gamespy3;
