/*
 * tngp/client.c
 *
 * Copyright (C) 2022 bzt
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * @brief Networking client functions
 *
 */

#ifdef __EMSCRIPTEN__
/* yeah, no chance that Wasm can support raw TCP or UDP... */
int client_eqchange = 0;
void client_init() {}
void client_free() {}
int client_ctrl() { return 0; }
void client_view() {}
int client_register(char *user, char *pass, int *opts, int *attrs) { (void)user; (void)pass; (void)opts; (void)attrs; return -1; }
int client_login(char *user, char *pass) { (void)user; (void)pass; return -1; }
int client_send(int type, unsigned char *buf, int len) { (void)type; (void)buf; (void)len; return -1; }
void client_recv() { return; }
#else
#include <errno.h>
#ifdef __WIN32__
#include <ws2tcpip.h>
#else
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#endif

#include "mbedtls/common.h"
#include "mbedtls/sha256.h"
#include "mbedtls/platform.h"
#include "mbedtls/platform_util.h"
#include "mbedtls/entropy.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/ssl.h"
#include "mbedtls/net_sockets.h"

#define NETQ_IMPLEMENTATION
#define NETQ_SEND client_udp_sendto
#define NETQ_NO_PTHREAD
#include "tngp.h"

int nanosleep(const struct timespec *rqtp, struct timespec *rmtp);

FILE *client_f = NULL;
char *client_url = NULL;
int client_step, client_udpsock = 0, client_addrlen = 0, client_map, client_x, client_y, client_d, client_a, client_eqchange = 0;
struct sockaddr_storage client_addr;
uint8_t client_enc[256];
mbedtls_entropy_context client_entropy;
mbedtls_ctr_drbg_context client_ctr_drbg;
mbedtls_net_context client_fd;
mbedtls_ssl_context client_ssl;
mbedtls_ssl_config client_conf;
netq_t client_nq;
uint32_t client_last;

int client_connect();
void client_disconnect();

typedef struct {
    int len;        /* remaining amount of data to receive */
    uint8_t *buf;   /* buffer */
} client_cache_t;
client_cache_t *client_cache = NULL;

/**
 * Initialize
 */
void client_init()
{
    mbedtls_sha256_context sha;
    unsigned char hash[32];
    char *s;
    int i;
    FILE *f;
    uint64_t fsiz;

    client_step = 0;
    if(!main_fn || !*main_fn || memcmp(main_fn, "tng://", 6)) {
        main_switchtab(TAB_LAUNCHER);
        return;
    }
    if(!files_cache || !*files_cache) {
        error_reason = ERR_NOCACHE;
        main_switchtab(TAB_ERROR);
        return;
    }
    mbedtls_sha256_init(&sha);
    mbedtls_sha256_starts_ret(&sha, 0);
    mbedtls_sha256_update_ret(&sha, (uint8_t*)main_fn, strlen(main_fn));
    mbedtls_sha256_finish_ret(&sha, hash);
    mbedtls_sha256_free(&sha);
    if(client_url) free(client_url);
    client_url = main_fn;
    main_fn = (char*)main_alloc(strlen(files_cache) + 70);
    strcpy(main_fn, files_cache);
    s = main_fn + strlen(main_fn);
    s += sprintf(s, SEP "%02x", hash[0]);
    files_mkdir(main_fn);
    *s++ = SEP[0];
    for(i = 1; i < 32; i++) s += sprintf(s, "%02x", hash[i]);
    memset(&world.hdr, 0, sizeof(world.hdr));
    f = files_open(main_fn, "rb");
    if(f) { i = fread(&world.hdr, 1, sizeof(world.hdr), f); fclose(f); }
    fsiz = files_size(main_fn) - 64; memcpy(&world.hdr.crc, &fsiz, 8);
    scr_w = win_w; scr_h = 42;
    screen = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, scr_w, scr_h);
    if(screen) {
        SDL_SetTextureBlendMode(screen, SDL_BLENDMODE_BLEND);
        SDL_LockTexture(screen, NULL, (void**)&scr_data, &scr_p);
        for(i = 0; i < scr_w * scr_h; i++) scr_data[i] = 0;
        ssfn_dst.fg = 0xff404040;
        main_text(screen, (scr_w - main_textwidth(lang[CLIENT_CONNECT])) / 2, 13, lang[CLIENT_CONNECT]);
        SDL_UnlockTexture(screen);
    }
}

/**
 * Free resources
 */
void client_free()
{
    if(screen) { SDL_DestroyTexture(screen); screen = NULL; }
    client_disconnect();
}

/**
 * View
 */
void client_view()
{
    unsigned char req[2] = { (2 << 5) | 1, 0 }, tmp[16384];
    uint64_t curr;
    uint32_t crc;
    int i, j, k;
    SDL_Rect rect;

    main_splash();
    rect.x = 0; rect.w = scr_w;
    rect.y = win_h - scr_h; rect.h = scr_h;

    /* this is hackish, should not all of these be in a view, but I was aiming for simplicity here
     * view is the function that's guaranteed to be called from the main_loop */
    switch(client_step) {
        case 0: client_step = 1; break;
        case 1:
            switch(client_connect()) {
                case 0:
                    if(verbose) printf("tngp: assets cache up-to-date\r\n");
                    main_switchtab(TAB_SPLASH);
                break;
                case 1:
                    if(verbose) printf("tngp: downloading assets (%lu bytes)\r\n", (unsigned long int)world.asiz);
                    client_f = files_open(main_fn, "wb+");
                    if(!client_f) {
                        error_reason = ERR_NOCACHE;
                        main_switchtab(TAB_ERROR);
                    } else {
                        /* send request assets */
                        while( ( i = mbedtls_ssl_write( &client_ssl, req, 2 ) ) <= 0 &&
                            (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
                        i = fwrite(&world.hdr, 1, sizeof(world.hdr), client_f);
                        if(screen) {
                            SDL_LockTexture(screen, NULL, (void**)&scr_data, &scr_p);
                            for(i = 0; i < scr_w * (scr_h - 8); i++) scr_data[i] = 0;
                            for(i = (scr_h - 8) * scr_w; i < scr_w * scr_h; i++) scr_data[i] = 0xff040404;
                            ssfn_dst.fg = 0xff404040;
                            main_text(screen, (scr_w - main_textwidth(lang[CLIENT_DOWNLOAD])) / 2, 13, lang[CLIENT_DOWNLOAD]);
                            SDL_UnlockTexture(screen);
                        }
                        client_step = 2;
                    }
                break;
                case -1:
                    error_reason = ERR_CONNECT;
                    main_switchtab(TAB_ERROR);
                break;
                case -2:
                    error_reason = ERR_COMPAT;
                    main_switchtab(TAB_ERROR);
                break;
            }
        break;
        case 2:
            /* we could just copy one block on each view call, but that would result in poor downloading speed
             * so instead we refresh the screen after each received block here, no matter the required fps */
            for(curr = crc = 0; curr < world.asiz;) {
                while( ( i = mbedtls_ssl_read( &client_ssl, tmp,
                  world.asiz - curr > sizeof(tmp) ? sizeof(tmp) : world.asiz - curr) ) < 0 &&
                    (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
                if(i <= 0) break;
                crc = crc32(crc, tmp, i);
                fwrite(tmp, 1, i, client_f);
                curr += i;
                if(screen) {
                    SDL_LockTexture(screen, NULL, (void**)&scr_data, &scr_p);
                    j = (scr_h - 8) * scr_p/4; k = scr_w * curr / world.asiz;
                    for(i = 0; i < k ; i++)
                        scr_data[j + i] =
                        scr_data[j + scr_p/4 + i] =
                        scr_data[j + 2*(scr_p/4) + i] =
                        scr_data[j + 3*(scr_p/4) + i] =
                        scr_data[j + 4*(scr_p/4) + i] =
                        scr_data[j + 5*(scr_p/4) + i] =
                        scr_data[j + 6*(scr_p/4) + i] =
                        scr_data[j + 7*(scr_p/4) + i] = 0xff600000;
                    SDL_UnlockTexture(screen);
                    SDL_RenderCopy(renderer, screen, NULL, &rect);
                    SDL_RenderPresent(renderer);
                }
            }
            fclose(client_f); client_f = NULL;
            if(curr && curr == world.asiz)
                while( ( i = mbedtls_ssl_read( &client_ssl, tmp, 4) ) < 0 &&
                    (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
            if(!curr || curr != world.asiz || memcmp(tmp, &crc, 4)) {
                if(screen) {
                    SDL_LockTexture(screen, NULL, (void**)&scr_data, &scr_p);
                    for(i = 0; i < scr_w * scr_h; i++) scr_data[i] = 0xff000040;
                    ssfn_dst.fg = 0xffffffff;
                    main_text(screen, (scr_w - main_textwidth(lang[ERR_DOWNLOAD])) / 2, 13, lang[ERR_DOWNLOAD]);
                    SDL_UnlockTexture(screen);
                }
                client_step = -1;
            } else
                main_switchtab(TAB_SPLASH);
        break;
    }
    if(screen) SDL_RenderCopy(renderer, screen, NULL, &rect);
}

/**
 * Close connection with server and free resources
 */
void client_close()
{
    int i;

    if(client_udpsock) {
        close(client_udpsock);
        client_udpsock = 0;
    }
    if(client_cache) {
        for(i = 0; i < world.nummap; i++)
            if(client_cache[i].buf) free(client_cache[i].buf);
        free(client_cache); client_cache = NULL;
    }
#ifdef __WIN32__
    WSACleanup();
#endif
}

/**
 * Connect to TirNanoG Server
 * returns:
 * 0 - ok, assets cached
 * 1 - ok, assets not cached or not uptodate
 * -1 - connection error
 * -2 - compatibility error
 */
int client_connect()
{
    char *s, *e, *server_ip = NULL, *server_port = "4433";
    uint8_t tmp[64];
    int ret, fam;

    memset(&client_addr, 0, sizeof(client_addr)); client_addrlen = 0;
    mbedtls_entropy_init( &client_entropy );
    mbedtls_ctr_drbg_init( &client_ctr_drbg );
    mbedtls_net_init( &client_fd );
    mbedtls_ssl_init( &client_ssl );
    mbedtls_ssl_config_init( &client_conf );
    if(!client_url || memcmp(client_url, "tng://", 6) || mbedtls_ssl_config_defaults( &client_conf, MBEDTLS_SSL_IS_CLIENT,
      MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT ) != 0 ||
      mbedtls_ctr_drbg_seed( &client_ctr_drbg, mbedtls_entropy_func, &client_entropy, (const unsigned char *)"tngp", 4) != 0)
        goto err;
    server_ip = (char*)malloc(strlen(client_url) + 1);
    if(!server_ip) goto err;
    s = client_url + 6;
    if(*s == '[') for(e = s++; *e && *e != ']'; e++);
    else for(e = s; *e && *e != ':'; e++);
    memcpy(server_ip, s, e - s); server_ip[e - s] = 0; s = e;
    for(; *e && *e != ':'; e++);
    if(*e == ':') server_port = e + 1;
    mbedtls_ssl_conf_authmode( &client_conf, MBEDTLS_SSL_VERIFY_OPTIONAL );
    mbedtls_ssl_conf_rng( &client_conf, mbedtls_ctr_drbg_random, &client_ctr_drbg );
    if(mbedtls_ssl_setup( &client_ssl, &client_conf ) != 0 ) {
        if(verbose) printf("tngp: ssl setup error\r\n");
        /* do not call client_disconnect, because close notify might get into an infinite loop with invalid connection */
err:    mbedtls_net_free( &client_fd );
        mbedtls_ssl_free( &client_ssl );
        mbedtls_ssl_config_free( &client_conf );
        mbedtls_ctr_drbg_free( &client_ctr_drbg );
        mbedtls_entropy_free( &client_entropy );
        if(server_ip) free(server_ip);
        return -1;
    }
    if(mbedtls_net_connect( &client_fd, server_ip, server_port, MBEDTLS_NET_PROTO_TCP, &client_addr, &client_addrlen,
      &fam ) != 0) {
        if(verbose) printf("tngp: connection error %s\r\n", client_url);
        goto err;
    }
    free(server_ip); server_ip = NULL;
    mbedtls_ssl_set_bio( &client_ssl, &client_fd, mbedtls_net_send, mbedtls_net_recv, NULL );
    while( ( ret = mbedtls_ssl_handshake( &client_ssl ) ) != 0 )
        if( ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE ) {
            if(verbose) printf("tngp: handshake error %s\r\n", client_url);
            goto err;
        }
    if(fam == AF_INET)
        ((struct sockaddr_in*)&client_addr)->sin_port = htons(atoi(server_port));
    else
        ((struct sockaddr_in6*)&client_addr)->sin6_port = htons(atoi(server_port));

    /* say hi */
    memset(tmp, 0, sizeof(tmp)); tmp[1] = 1; tmp[4] = 'T'; tmp[5] = 'N'; tmp[6] = 'G'; tmp[7] = 'P';
    while( ( ret = mbedtls_ssl_write( &client_ssl, tmp, 8 ) ) <= 0 &&
        (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));
    /* read response */
    memset( tmp, 0, sizeof( tmp ) );
    while( ( ret = mbedtls_ssl_read( &client_ssl, tmp, 64 ) ) < 0 &&
        (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE));
    if(ret != 64 || tmp[1] != 8 || tmp[4] != 'T' || tmp[5] != 'N' || tmp[6] != 'G' || tmp[7] != 'S') {
        if(verbose) printf("tngp: communication error %s\r\n", client_url);
        goto err;
    }
    memcpy(&world.asiz, &tmp[56], 8);
    if(!tng_compat((tng_hdr_t *)&tmp)) return -2;
    /* is our cache up-to-date? */
    if(!memcmp(tmp, &world.hdr, 64)) return 0;
    memcpy(&world.hdr, tmp, 64);
    return 1;
}

/**
 * Disconnect from server
 */
void client_disconnect()
{
    mbedtls_ssl_close_notify( &client_ssl );
    mbedtls_net_free( &client_fd );
    mbedtls_ssl_free( &client_ssl );
    mbedtls_ssl_config_free( &client_conf );
    mbedtls_ctr_drbg_free( &client_ctr_drbg );
    mbedtls_entropy_free( &client_entropy );
}

/**
 * Parse response to login or register
 */
int client_enterthematrix()
{
    int i, l, n = 1, port;
    uint8_t *msg, tmp[270];
    struct timespec tv;
    struct sockaddr_storage peer;
    socklen_t addrlen;
#ifdef __WIN32__
    DWORD nb;
    WSADATA WsaData;
    WSAStartup(MAKEWORD(2,2), &WsaData);
#endif

    memset(&client_nq, 0, sizeof(client_nq));
    client_map = client_x = client_y = client_d = -1; client_a = 0;
    client_last = 0;
    memset(tmp, 0, sizeof(tmp));
    while( ( i = mbedtls_ssl_read( &client_ssl, tmp, 4 ) ) < 0 &&
        (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
    if(i != 4 || ((tmp[1] << 8) | tmp[0]) != ((270 << 5) | 5)) { client_disconnect(); return tmp[2] ? -1 : 0; }
    while( ( i = mbedtls_ssl_read( &client_ssl, tmp + 4, 270 - 4 ) ) < 0 &&
        (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
    if(i != 270 - 4) { client_disconnect(); return 0; }
    memcpy(client_enc, tmp + 2, sizeof(client_enc));
    memcpy(&l, tmp + 266, 4); l = SDL_SwapLE32(l);
    if(l > 0) {
        msg = (uint8_t*)malloc(l);
        if(!msg) {
            client_disconnect();
            return 0;
        }
        memset(msg, 0, l);
        while( ( i = mbedtls_ssl_read( &client_ssl, msg, l ) ) < 0 &&
            (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
        client_disconnect();
        world_flush();
        if(i != l) { free(msg); return 0; }
        i = world_deserializestate(msg, l);
        if(verbose) printf("tngp: received initial game state (%u bytes), numplayers %u\r\n", l, world.numplayers);
        free(msg);
    }
    if(i) {
        /* send UDP hello to server */
        memset(&peer, 0, sizeof(peer));
        peer.ss_family = client_addr.ss_family;
        if(client_addr.ss_family == AF_INET) {
            ((struct sockaddr_in*)&peer)->sin_addr.s_addr = INADDR_ANY;
            msg = (uint8_t*)&((struct sockaddr_in*)&client_addr)->sin_addr;
            port = ntohs(((struct sockaddr_in*)&client_addr)->sin_port);
            l = 4;
        } else {
            ((struct sockaddr_in6*)&peer)->sin6_addr = in6addr_any;
            msg = (uint8_t*)&((struct sockaddr_in6*)&client_addr)->sin6_addr;
            port = ntohs(((struct sockaddr_in6*)&client_addr)->sin6_port);
            l = 16;
        }
        if(verbose > 1) {
            printf("tngp: sending UDP Hello to server %s", client_addr.ss_family == AF_INET ? "IPv4" : "IPv6");
            for(i = 0; i < l; i++) printf(" %02x", msg[i]);
            printf(" port %d\r\n", port);
        }
        client_udpsock = socket(peer.ss_family, SOCK_DGRAM, IPPROTO_UDP);
        if(client_udpsock <= 0) {
            if(verbose) printf("tngp: socket failed %d errno %d\r\n", client_udpsock, errno);
            client_udpsock = 0; return 0;
        }
        if(setsockopt(client_udpsock, SOL_SOCKET, SO_REUSEADDR, (const char *)&n, sizeof(n)) ||
#ifdef __WIN32__
          ioctlsocket(client_udpsock, FIONBIO, &nb) ||
#else
          fcntl(client_udpsock, F_SETFL, O_NONBLOCK, 1) ||
#endif
          bind(client_udpsock, (struct sockaddr *)&peer, client_addrlen)) {
            if(verbose) printf("tngp: bind failed errno %d\r\n", errno);
            client_close(); return 0;
        }
        memset(tmp, 0, 32);
        *((uint16_t*)&tmp[6]) = (10 << 5) | 6; tmp[0] = 'H'; tmp[1] = 'i'; memcpy(tmp + 8, tmp + 258, 8);
        /* here we don't have reliable UDP yet, so send it multiple times to be sure */
        for(n = 10; n; n--) {
            i = sendto(client_udpsock, (const char*)tmp, 16, 0, (struct sockaddr*)&client_addr, client_addrlen);
            if(verbose && i != 16) printf("tngp: sendto failed errno %d\r\n", errno);
            for(l = 0; i == 16 && l < 3; l++) {
                /* Wait a little bit before attempting to receive */
                tv.tv_sec = 0; tv.tv_nsec = 1000000;
                nanosleep(&tv, NULL);
                addrlen = client_addrlen;
                if(recvfrom(client_udpsock, (char*)tmp + 16, 16, 0, (struct sockaddr*)&peer, &addrlen) == 16 &&
                  !memcmp(tmp, tmp + 16, 16)) {
                    if(verbose) printf("tngp: world server connection established\r\n");
                    return 1;
                }
            }
            if(verbose) printf("tngp: recvfrom failed errno %d\r\n", errno);
            /* send or receive failed. Wait for a bit more and retry sending too */
            tv.tv_sec = 0; tv.tv_nsec = 100000000;
            nanosleep(&tv, NULL);
        }
        client_close();
        return -1;
    }
    return 0;
}

/**
 * Register character on server
 * opts: (option value << 8 | option palette) x world.numchar
 * attrs: attribute values x world.numchrattrs
 */
int client_register(char *user, char *pass, int32_t *opts, int32_t *attrs)
{
    int i, l;
    uint8_t *msg;

    if(!user || !*user || !pass || !*pass) return 0;
    for(i = 0; user[i]; i++) if(user[i] < ' ' || user[i] == '\"') return 0;
    l = 36 + strlen(user) + 1 + (world.numchar + world.numchrattrs) * sizeof(int32_t);
    msg = (uint8_t*)malloc(l);
    if(!msg) return 0;
    memset(msg, 0, l);
    if(l > 2048) l = 2048;  /* must not be bigger than tngs.c / main_ssl_con() / buf[] */
    /* construct message */
    *((uint16_t*)msg) = (l << 5) | 2;
    msg[2] = 'e'; msg[3] = 'n';
    if(world.lang && world.langidx >= 0 && world.langidx < world.numlang) {
        msg[2] = world.lang[world.langidx].id[0];
        msg[3] = world.lang[world.langidx].id[1];
    }
    world_hashpass(pass, msg + 4);
    i = strlen(user) + 1;
    memcpy(msg + 36, user, i);
    if(opts) memcpy(msg + 36 + i, opts, world.numchar * sizeof(int32_t));
    if(attrs) memcpy(msg + 36 + i + world.numchar * sizeof(int32_t), attrs, world.numchrattrs * sizeof(int32_t));
    if(verbose) printf("tngp: sending register (%u bytes)\r\n", l);

    switch(client_connect()) {
        case 0:
            while( ( i = mbedtls_ssl_write( &client_ssl, msg, l ) ) <= 0 &&
                (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
            free(msg);
            return client_enterthematrix();
        break;
        case 1: client_disconnect(); break;
    }
    free(msg);
    return -1;
}

/**
 * Log in to server
 */
int client_login(char *user, char *pass)
{
    int i, l;
    uint8_t *msg;

    if(!user || !*user || !pass || !*pass) return 0;
    for(i = 0; user[i]; i++) if(user[i] < ' ' || user[i] == '\"') return 0;
    l = 36 + strlen(user) + 1;
    msg = (uint8_t*)malloc(l);
    if(!msg) return 0;
    memset(msg, 0, l);
    /* construct message */
    *((uint16_t*)msg) = (l << 5) | 3;
    msg[2] = 'e'; msg[3] = 'n';
    if(world.lang && world.langidx >= 0 && world.langidx < world.numlang) {
        msg[2] = world.lang[world.langidx].id[0];
        msg[3] = world.lang[world.langidx].id[1];
    }
    world_hashpass(pass, msg + 4);
    i = strlen(user) + 1;
    memcpy(msg + 36, user, i);
    if(verbose) printf("tngp: sending login (%u bytes)\r\n", l);

    switch(client_connect()) {
        case 0:
            while( ( i = mbedtls_ssl_write( &client_ssl, msg, l ) ) <= 0 &&
                (i == MBEDTLS_ERR_SSL_WANT_READ || i == MBEDTLS_ERR_SSL_WANT_WRITE));
            free(msg);
            return client_enterthematrix();
        break;
        case 1: client_disconnect(); break;
    }
    free(msg);
    return -1;
}

/**
 * We have received a map from server, parse it
 */
void client_gotmap(int idx, uint8_t *buf, int len)
{
    world_map_t **maps = cutscn_idx != -1 ? cutscn_scene.maps : game_scene.maps;
    int id, i;

    if(idx < 0 || idx >= world.nummap || !buf || len < 1) return;
    /* if there's no main map yet, we must have got that */
    if(!maps[MAP_MAIN]) maps[MAP_MAIN] = world_deserializemap(buf, len, idx);
    if(!maps[MAP_MAIN]) return;
    /* load neighboring maps for main map */
    if(maps[MAP_MAIN]->id == idx) {
        for(i = 0; i < world.numngh; i++)
            if(maps[MAP_MAIN]->neightbors[i] != -1) {
                id = SDL_SwapLE32(maps[MAP_MAIN]->neightbors[i]); client_send(10, (uint8_t*)&id, 4);
            }
        if(cutscn_idx == -1) game_setmap();
        else cutscn_setmap();
    } else
    switch(world.type) {
        case TNG_TYPE_ORTHO: case TNG_TYPE_ISO:
            /* for these, we load the neightbors in two steps: first S, W, N, E, second SW, SE and NW, NE */
            if(maps[MAP_MAIN]->neightbors[0] == idx) {
                if(!maps[MAP_S]) maps[MAP_S] = world_deserializemap(buf, len, idx);
                /* for South, also load South-West and South-East neightbor maps */
                if(maps[MAP_S]) {
                    if(!maps[MAP_SW] || maps[MAP_S]->neightbors[1] != maps[MAP_SW]->id) {
                        world_freemap(maps[MAP_SW]); maps[MAP_SW] = NULL;
                        id = SDL_SwapLE32(maps[MAP_S]->neightbors[1]); client_send(10, (uint8_t*)&id, 4);
                    }
                    if(!maps[MAP_SE] || maps[MAP_S]->neightbors[3] != maps[MAP_SE]->id) {
                        world_freemap(maps[MAP_SE]); maps[MAP_SE] = NULL;
                        id = SDL_SwapLE32(maps[MAP_S]->neightbors[3]); client_send(10, (uint8_t*)&id, 4);
                    }
                }
            } else
            if(maps[MAP_MAIN]->neightbors[1] == idx && !maps[MAP_W]) maps[MAP_W] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[2] == idx) {
                if(!maps[MAP_N]) maps[MAP_N] = world_deserializemap(buf, len, idx);
                /* for North, also load North-West and North-East neightbor maps */
                if(maps[MAP_N]) {
                    if(!maps[MAP_NW] || maps[MAP_N]->neightbors[1] != maps[MAP_NW]->id) {
                        world_freemap(maps[MAP_NW]); maps[MAP_NW] = NULL;
                        id = SDL_SwapLE32(maps[MAP_N]->neightbors[1]); client_send(10, (uint8_t*)&id, 4);
                    }
                    if(!maps[MAP_NE] || maps[MAP_N]->neightbors[3] != maps[MAP_NE]->id) {
                        world_freemap(maps[MAP_NE]); maps[MAP_NE] = NULL;
                        id = SDL_SwapLE32(maps[MAP_N]->neightbors[3]); client_send(10, (uint8_t*)&id, 4);
                    }
                }
            } else
            if(maps[MAP_MAIN]->neightbors[3] == idx && !maps[MAP_E]) maps[MAP_E] = world_deserializemap(buf, len, idx); else
            if(!maps[MAP_NW] && maps[MAP_N] && maps[MAP_N]->neightbors[1] == idx)
                maps[MAP_NW] = world_deserializemap(buf, len, idx);
            else
            if(!maps[MAP_NE] && maps[MAP_N] && maps[MAP_N]->neightbors[3] == idx)
                maps[MAP_NE] = world_deserializemap(buf, len, idx);
            else
            if(!maps[MAP_SW] && maps[MAP_S] && maps[MAP_S]->neightbors[1] == idx)
                maps[MAP_SW] = world_deserializemap(buf, len, idx);
            else
            if(!maps[MAP_SE] && maps[MAP_S] && maps[MAP_S]->neightbors[3] == idx)
                maps[MAP_SE] = world_deserializemap(buf, len, idx);
        break;
        /* for hexagonal grids, we got all the neighbors in one step */
        case TNG_TYPE_HEXV:
            if(maps[MAP_MAIN]->neightbors[0] == idx && !maps[MAP_SW]) maps[MAP_SW] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[1] == idx && !maps[MAP_W]) maps[MAP_W] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[2] == idx && !maps[MAP_NW]) maps[MAP_NW] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[3] == idx && !maps[MAP_NE]) maps[MAP_NE] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[4] == idx && !maps[MAP_E]) maps[MAP_E] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[5] == idx && !maps[MAP_SE]) maps[MAP_SE] = world_deserializemap(buf, len, idx);
        break;
        case TNG_TYPE_HEXH:
            if(maps[MAP_MAIN]->neightbors[0] == idx && !maps[MAP_S]) maps[MAP_S] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[1] == idx && !maps[MAP_SW]) maps[MAP_SW] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[2] == idx && !maps[MAP_NW]) maps[MAP_NW] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[3] == idx && !maps[MAP_N]) maps[MAP_N] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[4] == idx && !maps[MAP_NE]) maps[MAP_NE] = world_deserializemap(buf, len, idx); else
            if(maps[MAP_MAIN]->neightbors[5] == idx && !maps[MAP_SE]) maps[MAP_SE] = world_deserializemap(buf, len, idx);
        break;
    }
}

/**
 * Low level UDP sender. DO NOT CALL DIRECTLY, use client_send instead
 */
int client_udp_sendto(void *ctx, const void *msg, int len)
{
    int i;
    uint8_t *data = (uint8_t*)msg;

    (void)ctx;
    for(i = 0; i < len; i++)
        data[i] ^= client_enc[i & 0xff];
    /* on Linux sendto has "void*", but on Winfos "const char*" */
    return sendto(client_udpsock, (const char*)data, len, 0, (struct sockaddr*)&client_addr, client_addrlen);
}

/**
 * Send an UDP message
 * Returns negative number on error.
 */
int client_send(int type, uint8_t *buf, int len)
{
    uint8_t data[NETQ_MTU];

    if(type < 8 || len < 0 || len > NETQ_MTU - 6 || !client_udpsock || !client_addrlen) return -1;
    memset(data, 0, sizeof(data));
    *((uint16_t*)data) = (len << 5) | (type & 31);
    if(buf && len) memcpy(data + 2, buf, len);
    if(verbose > 2) printf("tngp: sending packet %u (%u bytes)\r\n", type, len);
    return netq_send(&client_nq, data, len + 2, NULL);
}

/**
 * Receive an UDP message
 */
void client_recv()
{
    int ret, len, i, j, arg1, arg2;
    struct sockaddr_storage peer;
    socklen_t addrlen;
    uint8_t buf[NETQ_MTU], *ptr, *end;

    if(!client_udpsock) return;
    /* if we have received a message, push it to the queue */
    addrlen = client_addrlen;
    ret = recvfrom(client_udpsock, (char*)buf, sizeof(buf), 0, (struct sockaddr*)&peer, &addrlen);
    if(ret >= 8) {
        for(i = 0; i < ret; i++)
            buf[i] ^= client_enc[i & 0xff];
        netq_push(&client_nq, buf, ret, NULL);
    }
    /* get next, possibly re-ordered message to parse */
    memset(buf, 0, sizeof(buf));
    len = netq_pop(&client_nq, buf, sizeof(buf));
    if(len < 2) return;
    if(verbose > 2) printf("tngp: received packet %u (%u bytes)\r\n", buf[0] & 31, len);

    /*** parse UDP message ***/
    switch(buf[0] & 31) {
        /* Status Update */
        case 7:
            ptr = buf + 2; end = buf + len;
            while(ptr < end) {
                ret = *ptr++;
                memcpy(&arg1, ptr, 4); arg1 = SDL_SwapLE32(arg1); ptr += 4;
                switch(ret) {
                    case 0:
                        /* update client coordinates */
                        memcpy(&arg2, ptr, 4); arg2 = SDL_SwapLE32(arg2);
                        ptr += 9;
                    break;
                    case 1:
                        /* logout, remove client */
                    break;
                    case 2:
                        /* sprites */
                    break;
                    case 3:
                        /* inventory */
                        ptr = end;
                    break;
                }
            }
        break;

        /* Server Sending Map */
        case 11:
            if(len < 12) break;
            len -= 12;
            memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1);
            if(arg1 >= 0 && arg1 < world.nummap) {
                i = (buf[8] << 16) | (buf[7] << 8) | buf[6];
                if(i > len) {
                    if(!client_cache) {
                        client_cache = (client_cache_t*)malloc(world.nummap * sizeof(client_cache_t));
                        if(!client_cache) break;
                        memset(client_cache, 0, world.nummap * sizeof(client_cache_t));
                    }
                    if(!client_cache[arg1].buf) {
                        client_cache[arg1].buf = (uint8_t*)malloc(i);
                        if(!client_cache[arg1].buf) break;
                        client_cache[arg1].len = i;
                    }
                    memcpy(client_cache[arg1].buf + ((buf[11] << 16) | (buf[10] << 8) | buf[9]), buf + 12, len);
                    client_cache[arg1].len -= len;
                    if(client_cache[arg1].len <= 0) {
                        client_gotmap(arg1, client_cache[arg1].buf, i);
                        free(client_cache[arg1].buf); client_cache[arg1].buf = NULL;
                        client_cache[arg1].len = 0;
                    }
                } else
                    client_gotmap(arg1, buf + 12, len);
            }
        break;

        /* Dialog */
        case 12:
            memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1);
        break;

        /* Audio Chat */
        case 13:
            if(buf[7] >> 4) {
                ptr = (uint8_t*)stbi_zlib_decode_malloc_guesssize_headerflag((const char*)buf + 8, len - 8, 4096, &j, 1);
                media_playaud(buf[6], buf[7] & 0xf, ptr, j);
                free(ptr);
            } else
                media_playaud(buf[6], buf[7] & 0xf, buf + 8, len - 8);
        break;

        /* Quest */
        case 14:
            if(world.players && world.numplayers > 0) {
                memcpy(&arg1, buf + 2, 4); arg1 = SDL_SwapLE32(arg1); arg2 = buf[6];
                if(arg2 > 1) {
                    /* remove accepted quest if cancelled */
                    for(j = 0; j < world.players[0].data.numqst && world.players[0].data.quests[j * 2 + 1] != arg1; j++);
                    if(j < world.players[0].data.numqst) {
                        memcpy(&world.players[0].data.quests[j * 2], &world.players[0].data.quests[(j + 1) * 2],
                            (world.players[0].data.numqst - j) * 2 * sizeof(int));
                        world.players[0].data.numqst--;
                        if(!world.players[0].data.numqst) {
                            free(world.players[0].data.quests); world.players[0].data.quests = NULL;
                        }
                    }
                } else
                    /* quests accepted or completed recorded on server */
                    world_quest(&world.players[0].data, arg1, arg2, 1);
            }
        break;
    }
}

/**
 * Send update to server
 */
void client_sendupdate()
{
    int i, l = 10;
    uint8_t buf[1536];

    if(client_udpsock && world.players && world.numplayers > 0 && (world.players[0].data.map != client_map ||
      world.players[0].data.x != client_x || world.players[0].data.y != client_y || world.players[0].data.d != client_d ||
      world.players[0].data.altitude != client_a || client_eqchange || client_last + 1000 < main_tick)) {
        client_last = main_tick;
        client_map = world.players[0].data.map;
        client_x = world.players[0].data.x;
        client_y = world.players[0].data.y;
        client_d = world.players[0].data.d;
        client_a = world.players[0].data.altitude;
        i = SDL_SwapLE32(client_map);
        memcpy(buf, &i, 4);
        buf[4] = client_x & 0xff; buf[5] = (client_x >> 8) & 0xff;
        buf[6] = client_y & 0xff; buf[7] = (client_y >> 8) & 0xff;
        buf[8] = client_d; buf[9] = client_a;
        if(client_eqchange) {
            buf[10] = 4;
            memcpy(buf + 11, world.players[0].equip, 64);
            memcpy(buf + 75, world.players[0].belt, 64);
            l += 129;
            client_eqchange = 0;
        }
        client_send(8, buf, l);
    }
}

#endif /* if not emscripten */
