#include "BanchoProtocol.h"

#include <stdlib.h>
#include <string.h>

#include "Bancho.h"

// Null array for returning empty structures when trying to read more data out of a Packet than expected.
u8 NULL_ARRAY[NULL_ARRAY_SIZE] = {0};

// Amount of slots inside a multi lobby
const int MAX_SLOTS = 16;

Room::Room() {
    // 0-initialized room means we're not in multiplayer at the moment
}

Room::Room(Packet *packet) {
    id = read<u16>(packet);
    in_progress = read<u8>(packet);
    match_type = read<u8>(packet);
    mods = read<u32>(packet);
    name = read_string(packet);

    has_password = read<u8>(packet) > 0;
    if(has_password) {
        // Discard password. It should be an empty string, but just in case, read it properly.
        packet->pos--;
        read_string(packet);
    }

    map_name = read_string(packet);
    map_id = read<u32>(packet);

    auto hash_str = read_string(packet);
    map_md5 = hash_str.toUtf8();

    nb_players = 0;
    for(int i = 0; i < MAX_SLOTS; i++) {
        slots[i].status = read<u8>(packet);
    }
    for(int i = 0; i < MAX_SLOTS; i++) {
        slots[i].team = read<u8>(packet);
    }
    for(int s = 0; s < MAX_SLOTS; s++) {
        if(!slots[s].is_locked()) {
            nb_open_slots++;
        }

        if(slots[s].has_player()) {
            slots[s].player_id = read<u32>(packet);
            nb_players++;
        }
    }

    host_id = read<u32>(packet);
    mode = read<u8>(packet);
    win_condition = read<u8>(packet);
    team_type = read<u8>(packet);
    freemods = read<u8>(packet);
    if(freemods) {
        for(int i = 0; i < MAX_SLOTS; i++) {
            slots[i].mods = read<u32>(packet);
        }
    }

    seed = read<u32>(packet);
}

void Room::pack(Packet *packet) {
    write<u16>(packet, id);
    write<u8>(packet, in_progress);
    write<u8>(packet, match_type);
    write<u32>(packet, mods);
    write_string(packet, name.toUtf8());
    write_string(packet, password.toUtf8());
    write_string(packet, map_name.toUtf8());
    write<u32>(packet, map_id);
    write_string(packet, map_md5.toUtf8());
    for(int i = 0; i < MAX_SLOTS; i++) {
        write<u8>(packet, slots[i].status);
    }
    for(int i = 0; i < MAX_SLOTS; i++) {
        write<u8>(packet, slots[i].team);
    }
    for(int s = 0; s < MAX_SLOTS; s++) {
        if(slots[s].has_player()) {
            write<u32>(packet, slots[s].player_id);
        }
    }

    write<u32>(packet, host_id);
    write<u8>(packet, mode);
    write<u8>(packet, win_condition);
    write<u8>(packet, team_type);
    write<u8>(packet, freemods);
    if(freemods) {
        for(int i = 0; i < MAX_SLOTS; i++) {
            write<u32>(packet, slots[i].mods);
        }
    }

    write<u32>(packet, seed);
}

bool Room::is_host() { return host_id == bancho.user_id; }

void read_bytes(Packet *packet, u8 *bytes, size_t n) {
    if(packet->pos + n > packet->size) {
        packet->pos = packet->size + 1;
    } else {
        memcpy(bytes, packet->memory + packet->pos, n);
        packet->pos += n;
    }
}

u32 read_uleb128(Packet *packet) {
    u32 result = 0;
    u32 shift = 0;
    u8 byte = 0;

    do {
        byte = read<u8>(packet);
        result |= (byte & 0x7f) << shift;
        shift += 7;
    } while(byte & 0x80);

    return result;
}

UString read_string(Packet *packet) {
    u8 empty_check = read<u8>(packet);
    if(empty_check == 0) return UString("");

    u32 len = read_uleb128(packet);
    u8 *str = new u8[len + 1];
    read_bytes(packet, str, len);
    str[len] = '\0';

    auto ustr = UString((const char *)str);
    delete[] str;

    return ustr;
}

std::string read_stdstring(Packet *packet) {
    u8 empty_check = read<u8>(packet);
    if(empty_check == 0) return std::string();

    u32 len = read_uleb128(packet);
    u8 *str = new u8[len + 1];
    read_bytes(packet, str, len);

    std::string str_out((const char *)str, len);
    delete[] str;

    return str_out;
}

MD5Hash read_hash(Packet *packet) {
    MD5Hash hash;

    u8 empty_check = read<u8>(packet);
    if(empty_check == 0) return hash;

    u32 len = read_uleb128(packet);
    if(len > 32) {
        len = 32;
    }

    read_bytes(packet, (u8 *)hash.hash, len);
    hash.hash[len] = '\0';
    return hash;
}

void skip_string(Packet *packet) {
    u8 empty_check = read<u8>(packet);
    if(empty_check == 0) {
        return;
    }

    u32 len = read_uleb128(packet);
    packet->pos += len;
}

void write_bytes(Packet *packet, u8 *bytes, size_t n) {
    if(packet->pos + n > packet->size) {
        packet->memory = (unsigned char *)realloc(packet->memory, packet->size + n + 4096);
        packet->size += n + 4096;
        if(!packet->memory) return;
    }

    memcpy(packet->memory + packet->pos, bytes, n);
    packet->pos += n;
}

void write_uleb128(Packet *packet, u32 num) {
    if(num == 0) {
        u8 zero = 0;
        write<u8>(packet, zero);
        return;
    }

    while(num != 0) {
        u8 next = num & 0x7F;
        num >>= 7;
        if(num != 0) {
            next |= 0x80;
        }
        write<u8>(packet, next);
    }
}

void write_string(Packet *packet, const char *str) {
    if(!str || str[0] == '\0') {
        u8 zero = 0;
        write<u8>(packet, zero);
        return;
    }

    u8 empty_check = 11;
    write<u8>(packet, empty_check);

    u32 len = strlen(str);
    write_uleb128(packet, len);
    write_bytes(packet, (u8 *)str, len);
}

void write_hash(Packet *packet, MD5Hash hash) {
    write<u8>(packet, 0x0B);
    write<u8>(packet, 0x20);
    write_bytes(packet, (u8 *)hash.hash, 32);
}
