/*******************************************************************************
 *
 * Copyright (c) 2004-2010 Guillaume Cottenceau
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2, as
 * published by the Free Software Foundation.
 *
 * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 ******************************************************************************/

/*
 * this file holds game operations: create, join, list etc.
 * it should be as far away as possible from network operations
 */

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <sys/socket.h>
#include <regex.h>

#include <glib.h>

#include "net.h"
#include "tools.h"
#include "log.h"
#include "game.h"

enum game_status { GAME_STATUS_OPEN, GAME_STATUS_CLOSED, GAME_STATUS_PLAYING };

#define MAX_PLAYERS_PER_GAME 5
struct game
{
        enum game_status status;
        int players_number;
        int players_conn[MAX_PLAYERS_PER_GAME];
        char* players_nick[MAX_PLAYERS_PER_GAME];
        int players_started[MAX_PLAYERS_PER_GAME];
};

static GList * games = NULL;
static GList * open_players = NULL;

static ssize_t amount_transmitted = 0;

static char ok_pong[] = "PONG";
static char ok_player_joined[] = "JOINED: %s";
static char ok_player_parted[] = "PARTED: %s";
static char ok_player_kicked[] = "KICKED: %s";
static char ok_talk[] = "TALK: %s";
static char ok_can_start[] = "GAME_CAN_START: %s";

static char wn_unknown_command[] = "UNKNOWN_COMMAND";
static char wn_missing_arguments[] = "MISSING_ARGUMENTS";
static char wn_nick_invalid[] = "INVALID_NICK";
static char wn_nick_in_use[] = "NICK_IN_USE";
static char wn_no_such_game[] = "NO_SUCH_GAME";
static char wn_game_full[] = "GAME_FULL";
static char wn_already_in_game[] = "ALREADY_IN_GAME";
static char wn_max_open_games[] = "ALREADY_MAX_OPEN_GAMES";
static char wn_not_started[] = "NOT_STARTED";
static char wn_already_ok_started[] = "ALREADY_OK_STARTED";
static char wn_not_in_game[] = "NOT_IN_GAME";
static char wn_alone_in_the_dark[] = "ALONE_IN_THE_DARK";
static char wn_not_creator[] = "NOT_CREATOR";
static char wn_no_such_player[] = "NO_SUCH_PLAYER";
static char wn_denied[] = "DENIED";
static char wn_flooding[] = "FLOODING";
static char wn_others_not_ready[] = "OTHERS_NOT_READY";

static char fl_line_unrecognized[] = "MISSING_FB_PROTOCOL_TAG";
static char fl_proto_mismatch[] = "INCOMPATIBLE_PROTOCOL";

char* nick[256];
char* geoloc[256];
char* IP[256];
int remote_proto_minor[256];
int admin_authorized[256];

// calculate the list of players for a given game
static char* list_game(const struct game * g)
{
        char list_game_str[8192] = "";
        int i;
        for (i = 0; i < g->players_number; i++) {
                strconcat(list_game_str, g->players_nick[i], sizeof(list_game_str));
                if (i < g->players_number - 1)
                        strconcat(list_game_str, ",", sizeof(list_game_str));
        }
        return memdup(list_game_str, strlen(list_game_str) + 1);
}

// calculate the list of players for a given game with geolocation
static char* list_game_with_geolocation(const struct game * g)
{
        char list_game_str[8192] = "";
        int i;
        char* n;
        for (i = 0; i < g->players_number; i++) {
                strconcat(list_game_str, g->players_nick[i], sizeof(list_game_str));
                n = geoloc[g->players_conn[i]];
                if (n != NULL) {
                        strconcat(list_game_str, ":", sizeof(list_game_str));
                        strconcat(list_game_str, n, sizeof(list_game_str));
                }
                if (i < g->players_number - 1)
                        strconcat(list_game_str, ",", sizeof(list_game_str));
        }
        return memdup(list_game_str, strlen(list_game_str) + 1);
}

static char list_games_str[16384] __attribute__((aligned(4096))) = "";
static char list_playing_geolocs_str[16384] __attribute__((aligned(4096))) = "";
static int players_in_game;
static int games_open;
static int games_running;
static void list_open_nicks_aux(gpointer data, gpointer user_data)
{
        char* n = nick[GPOINTER_TO_INT(data)];
        if (n == NULL)
                return;
        strconcat(list_games_str, n, sizeof(list_games_str));
        n = geoloc[GPOINTER_TO_INT(data)];
        if (n != NULL) {
                strconcat(list_games_str, ":", sizeof(list_games_str));
                strconcat(list_games_str, n, sizeof(list_games_str));
        }
        strconcat(list_games_str, ",", sizeof(list_games_str));
}
static void list_games_aux(gpointer data, gpointer user_data)
{
        const struct game* g = data;
        if (g->status == GAME_STATUS_OPEN) {
                char* game;
                games_open++;
                strconcat(list_games_str, "[", sizeof(list_games_str));
                game = list_game(g);
                strconcat(list_games_str, game, sizeof(list_games_str));
                free(game);
                strconcat(list_games_str, "]", sizeof(list_games_str));
        } else {
                int i;
                char* geo;
                players_in_game += g->players_number;
                games_running++;
                for (i = 0; i < g->players_number; i++) {
                        geo = geoloc[g->players_conn[i]];
                        if (geo != NULL) {
                                strconcat(list_playing_geolocs_str, geo, sizeof(list_playing_geolocs_str));
                                strconcat(list_playing_geolocs_str, ",", sizeof(list_playing_geolocs_str));
                        }
                }
                return;
        }
}
/* Game list is of the following scheme:
 * 1.1 protocol:
 * <list-of-open-players format="NICK|NICK:GEOLOC"> [<list-of-open-games format=<list-of-players format="NICK">>] free:%d games:%d playing:%d at:<list-of-playing-geolocs>
 * 1.0 protocol:
 * <list-of-open-players format="NICK|NICK:GEOLOC"> [<list-of-open-games format=<list-of-players format="NICK">>] free:%d games:%d playing:%d
 */
void calculate_list_games(void)
{
        char * free_players;
        list_games_str[0] = '\0';
        list_playing_geolocs_str[0] = '\0';
        players_in_game = 0;
        games_open = 0;
        games_running = 0;
        g_list_foreach(open_players, list_open_nicks_aux, NULL);
        strconcat(list_games_str, " ", sizeof(list_games_str));
        g_list_foreach(games, list_games_aux, NULL);
        free_players = asprintf_(" free:%d games:%d playing:%d at:%s", conns_nb() - players_in_game - 1, games_running, players_in_game, list_playing_geolocs_str);  // 1: don't count myself
        strconcat(list_games_str, free_players, sizeof(list_games_str));
        free(free_players);
}

static void create_game(int fd, char* nick)
{
        struct game * g = malloc_(sizeof(struct game));
        g->players_number = 1;
        g->players_conn[0] = fd;
        g->players_nick[0] = nick;
        g->status = GAME_STATUS_OPEN;
        games = g_list_append(games, g);
        open_players = g_list_remove(open_players, GINT_TO_POINTER(fd));
        calculate_list_games();
}

static int add_player(struct game * g, int fd, char* nick)
{
        char joined_msg[1000];
        int i;
        if (g->players_number < MAX_PLAYERS_PER_GAME) {
                /* inform other players */
                snprintf(joined_msg, sizeof(joined_msg), ok_player_joined, nick);
                for (i = 0; i < g->players_number; i++)
                        send_line_log_push(g->players_conn[i], joined_msg);

                g->players_conn[g->players_number] = fd;
                g->players_nick[g->players_number] = nick;
                g->players_number++;
                open_players = g_list_remove(open_players, GINT_TO_POINTER(fd));
                calculate_list_games();
                return 1;
        } else {
                free(nick);
                return 0;
        }
}

static int find_game_by_nick_aux(gconstpointer game, gconstpointer nick)
{
        const struct game * g = game;
        if (g->status == GAME_STATUS_OPEN
            && streq(g->players_nick[0], (char *) nick))
                return 0;
        else
                return 1;
}
static struct game* find_game_by_nick(char* nick)
{
        return GListp2data(g_list_find_custom(games, nick, find_game_by_nick_aux));
}

static int find_game_by_fd_aux(gconstpointer game, gconstpointer fd)
{
        const struct game* g = game;
        int fd_ = GPOINTER_TO_INT(fd);
        int i;
        for (i = 0; i < g->players_number; i++)
                if (g->players_conn[i] == fd_)
                        return 0;
        return 1;
}
static struct game* find_game_by_fd(int fd)
{
        return GListp2data(g_list_find_custom(games, GINT_TO_POINTER(fd), find_game_by_fd_aux));
}

int find_player_number(struct game *g, int fd)
{
        int i;
        for (i = 0; i < g->players_number; i++)
                if (g->players_conn[i] == fd)
                        return i;
        l0(OUTPUT_TYPE_ERROR, "Internal error");
        exit(EXIT_FAILURE);
}

static void real_start_game(struct game* g)
{
        int i;
        char mapping_str[4096] = "";
        char can_start_msg[1000];
        for (i = 0; i < g->players_number; i++) {
                int len = strlen(mapping_str);
                if (len >= sizeof(mapping_str)-1)
                        return;
                mapping_str[len] = g->players_conn[i];
                mapping_str[len+1] = '\0';
                strconcat(mapping_str, g->players_nick[i], sizeof(mapping_str));
                if (i < g->players_number - 1)
                        strconcat(mapping_str, ",", sizeof(mapping_str));
        }
        snprintf(can_start_msg, sizeof(can_start_msg), ok_can_start, mapping_str);
        for (i = 0; i < g->players_number; i++) {
                send_line_log_push_binary(g->players_conn[i], can_start_msg, ok_can_start);
                g->players_started[i] = 0;
        }
        g->status = GAME_STATUS_PLAYING;
}

static void start_game(int fd)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                if (g->players_conn[0] == fd) {
                        if (g->players_number == 1) {
                                send_line_log(fd, wn_alone_in_the_dark, "START");
                                return;
                        }
                        send_ok(fd, "START");
                        real_start_game(g);
                        calculate_list_games();
                        l2(OUTPUT_TYPE_INFO, "running games increments to: %d (%d players)", games_running, players_in_game);
                } else {
                        send_line_log(fd, wn_not_creator, "START");
                }

        } else {
                l0(OUTPUT_TYPE_ERROR, "Internal error");
                exit(EXIT_FAILURE);
        }
}

static void close_game(int fd)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                if (g->players_conn[0] == fd) {
                        if (g->players_number == 1) {
                                send_line_log(fd, wn_alone_in_the_dark, "CLOSE");
                                return;
                        }
                        send_ok(fd, "CLOSE");
                        g->status = GAME_STATUS_CLOSED;
                        calculate_list_games();
                } else {
                        send_line_log(fd, wn_not_creator, "CLOSE");
                }

        } else {
                l0(OUTPUT_TYPE_ERROR, "Internal error");
                exit(EXIT_FAILURE);
        }
}

static int min_protocol_level(struct game* g)
{
        int i;
        int minor = remote_proto_minor[g->players_conn[0]];
        for (i = 1; i < g->players_number; i++)
                minor = MIN(minor, remote_proto_minor[g->players_conn[i]]);
        return minor;
}

static void setoptions(int fd, char* options)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                if (g->players_conn[0] == fd) {
                        int i;
                        char* msg;
                        send_ok(fd, "SETOPTIONS");
                        msg = asprintf_("OPTIONS: %s,PROTOCOLLEVEL:%d", options, min_protocol_level(g));
                        for (i = 0; i < g->players_number; i++)
                                if (remote_proto_minor[g->players_conn[i]] >= 1)
                                        send_line_log_push(g->players_conn[i], msg);
                        free(msg);
                } else {
                        send_line_log(fd, wn_not_creator, "SETOPTIONS");
                }

        } else {
                l0(OUTPUT_TYPE_ERROR, "Internal error");
                exit(EXIT_FAILURE);
        }
}

static void leader_check_game_start(int fd)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                if (g->status == GAME_STATUS_PLAYING) {
                        int i;
                        for (i = 0; i < g->players_number; i++) {
                                if (fd != g->players_conn[i]) {
                                        if (!g->players_started[i]) {
                                                send_line_log(fd, wn_others_not_ready, "LEADER_CHECK_GAME_START");
                                                return;
                                        }
                                }
                        }
                        send_ok(fd, "LEADER_CHECK_GAME_START");
                } else {
                        send_line_log(fd, wn_not_started, "LEADER_CHECK_GAME_START");
                }
        } else {
                l0(OUTPUT_TYPE_ERROR, "Internal error");
                exit(EXIT_FAILURE);
        }
}

static void ok_start_game(int fd)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                if (g->status == GAME_STATUS_PLAYING) {
                        int i;
                        for (i = 0; i < g->players_number; i++) {
                                if (g->players_conn[i] == fd) {
                                        if (!g->players_started[i]) {
                                                if (remote_proto_minor[g->players_conn[i]] >= 1)
                                                        send_ok(fd, "OK_GAME_START");
                                                g->players_started[i] = 1;
                                                l1(OUTPUT_TYPE_DEBUG, "[%d] entering prio mode", g->players_conn[i]);
                                                add_prio(g->players_conn[i]);
                                        } else {
                                                send_line_log(fd, wn_already_ok_started, "OK_GAME_START");
                                        }
                                }
                        }
                } else {
                        send_line_log(fd, wn_not_started, "OK_GAME_START");
                }
        } else {
                l0(OUTPUT_TYPE_ERROR, "Internal error");
                exit(EXIT_FAILURE);
        }
}

static void kick_player(int fd, struct game * g, char * nick)
{
        int i;
        for (i = 0; i < g->players_number; i++) {
                if (g->players_conn[i] != fd && streq(g->players_nick[i], nick)) {
                        send_ok(fd, "KICK");
                        send_line_log_push(g->players_conn[i], "KICKED");
                        player_part_game_(g->players_conn[i], ok_player_kicked);
                        return;
                }
        }
        send_line_log(fd, wn_no_such_player, "KICK");
}

void player_connects(int fd)
{
        open_players = g_list_append(open_players, GINT_TO_POINTER(fd));
}

void player_disconnects(int fd)
{
        open_players = g_list_remove(open_players, GINT_TO_POINTER(fd));
}

static void talk_serverwide_aux(gpointer data, gpointer user_data)
{
        send_line_log_push(GPOINTER_TO_INT(data), user_data);
}

static gboolean check_match_alert_words(gconstpointer data, gconstpointer user_data)
{
        const regex_t* preg = data;
        const char* msg = user_data;
        if (regexec(preg, msg, 0, NULL, 0) == 0) {
                return TRUE;
        }
        return FALSE;
}

static void talk(int fd, char* msg)
{
        struct game * g = find_game_by_fd(fd);
        char talk_msg[1000];

        if (g_list_any(alert_words, check_match_alert_words, msg))
                l2(OUTPUT_TYPE_INFO, "message '%s' from %s matches alert words!", msg, IP[fd]);

        amount_talk_flood[fd]++;
        if (amount_talk_flood[fd] == 15) {
                l1(OUTPUT_TYPE_INFO, "'%s' is flooding!", IP[fd]);
                send_line_log(fd, wn_flooding, msg);
                conn_terminated(fd, "flooding");
                return;
        }

        snprintf(talk_msg, sizeof(talk_msg), ok_talk, msg);
        if (g) {
                // player is in a game, it's a game-only chat
                int i;
                for (i = 0; i < g->players_number; i++)
                        send_line_log_push(g->players_conn[i], talk_msg);
        } else {
                // player is not in a game, it's a server-wide chat
                g_list_foreach(open_players, talk_serverwide_aux, talk_msg);
        }
}

static void status(int fd, char* msg)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                char* game = list_game(g);
                send_line_log(fd, game, msg);
                free(game);
        } else {
                send_line_log(fd, wn_not_in_game, msg);
        }
}

static void status_geo(int fd, char* msg)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                char* game = list_game_with_geolocation(g);
                send_line_log(fd, game, msg);
                free(game);
        } else {
                send_line_log(fd, wn_not_in_game, msg);
        }
}

static void protocol_level(int fd, char* msg)
{
        // Find the smallest minor protocol level among players in game
        struct game * g = find_game_by_fd(fd);
        if (g) {
                char* response;
                int level = min_protocol_level(g);
                response = asprintf_("%d", level);
                send_line_log(fd, response, msg);
                free(response);
        } else {
                send_line_log(fd, wn_not_in_game, msg);
        }
}

static gboolean nick_available_aux(gconstpointer data, gconstpointer user_data)
{
        const struct game* g = data;
        const char* nick = user_data;
        int i;
        for (i = 0; i < g->players_number; i++)
                if (streq(g->players_nick[i], nick))
                        return TRUE;
        return FALSE;
}
static int nick_available(char* nick)
{
        return !g_list_any(games, nick_available_aux, nick);
}

static gboolean already_in_game_aux(gconstpointer data, gconstpointer user_data)
{
        const struct game* g = data;
        int fd = GPOINTER_TO_INT(user_data);
        int i;
        for (i = 0; i < g->players_number; i++)
                if (g->players_conn[i] == fd)
                        return TRUE;
        return FALSE;
}
static int already_in_game(int fd)
{
        return g_list_any(games, already_in_game_aux, GINT_TO_POINTER(fd));
}

static int is_nick_ok(char* nick)
{
        int i;
        if (strlen(nick) > 10)
                return 0;
        for (i = 0; i < strlen(nick); i++) {
                if (!((nick[i] >= 'a' && nick[i] <= 'z')
                      || (nick[i] >= 'A' && nick[i] <= 'Z')
                      || (nick[i] >= '0' && nick[i] <= '9')
                      || nick[i] == '-' || nick[i] == '_')) {
                        return 0;
                }
        }
        return 1;
}

/* true return value indicates that connection must be closed */
int process_msg(int fd, char* msg)
{
        int client_proto_major;
        int client_proto_minor;
        char * args;
        char * ptr, * ptr2;
        char * msg_orig;

        /* check for leading protocol tag */
        if (!str_begins_static_str(msg, "FB/")
            || strlen(msg) < 8) {  // 8 stands for "FB/M.m f"(oo)
                send_line_log(fd, fl_line_unrecognized, msg);
                return 1;
        }
    
        /* check if client protocol is compatible; for simplicity, we don't support client protocol more recent
         * than server protocol, we suppose that our servers are upgraded when a new release appears (but of
         * course client protocol older is supported within the major protocol) */
        client_proto_major = charstar_to_int(msg + 3);
        client_proto_minor = charstar_to_int(msg + 5);
        if (client_proto_major != proto_major
            || client_proto_minor > proto_minor) {
                send_line_log(fd, fl_proto_mismatch, msg);
                return 1;
        }

        if (remote_proto_minor[fd] == -1)
                remote_proto_minor[fd] = client_proto_minor;

        msg_orig = strdup(msg);

        /* after protocol, first word is command, then possible args */
        current_command = msg + 7; // 7 stands for "FB/M.m "
        if ((ptr = strchr(current_command, ' '))) {
                *ptr = '\0';
                args = current_command + strlen(current_command) + 1;
        } else
                args = NULL;

        if (streq(current_command, "PING")) {
                send_line_log(fd, ok_pong, msg_orig);
        } else if (streq(current_command, "NICK")) {
                if (!args) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else {
                        if ((ptr = strchr(args, ' ')))
                                *ptr = '\0';
                        if (strlen(args) > 10)
                                args[10] = '\0';
                        if (!is_nick_ok(args)) {
                                send_line_log(fd, wn_nick_invalid, msg_orig);
                        } else {
                                if (nick[fd] != NULL) {
                                        free(nick[fd]);
                                }
                                nick[fd] = strdup(args);
                                calculate_list_games();
                                send_ok(fd, msg_orig);
                        }
                }
        } else if (streq(current_command, "GEOLOC")) {
                if (!args) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else {
                        if ((ptr = strchr(args, ' ')))
                                *ptr = '\0';
                        if (strlen(args) > 13)  // sign, 4 digits, dot, colon, sign, 4 digits, dot
                                args[13] = '\0';
                        if (geoloc[fd] != NULL) {
                                free(geoloc[fd]);
                        }
                        geoloc[fd] = strdup(args);
                        calculate_list_games();
                        send_ok(fd, msg_orig);
                }
        } else if (streq(current_command, "CREATE")) {
                if (!args) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else {
                        if ((ptr = strchr(args, ' ')))
                                *ptr = '\0';
                        if (strlen(args) > 10)
                                args[10] = '\0';
                        if (!is_nick_ok(args)) {
                                send_line_log(fd, wn_nick_invalid, msg_orig);
                        } else if (!nick_available(args)) {
                                send_line_log(fd, wn_nick_in_use, msg_orig);
                        } else if (already_in_game(fd)) {
                                send_line_log(fd, wn_already_in_game, msg_orig);
                        } else if (games_open == 16) {  // FB client can display 16 max
                                send_line_log(fd, wn_max_open_games, msg_orig);
                        } else {
                                create_game(fd, strdup(args));
                                send_ok(fd, msg_orig);
                        }
                }
        } else if (streq(current_command, "JOIN")) {
                if (!args || !(ptr = strchr(args, ' '))) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else {
                        struct game * g;
                        char* nick = ptr + 1;
                        *ptr = '\0';
                        if ((ptr2 = strchr(ptr, ' ')))
                                *ptr2 = '\0';
                        if (strlen(nick) > 10)
                                nick[10] = '\0';
                        if (!is_nick_ok(nick)) {
                                send_line_log(fd, wn_nick_invalid, msg_orig);
                        } else if (!nick_available(nick)) {
                                send_line_log(fd, wn_nick_in_use, msg_orig);
                        } else if (already_in_game(fd)) {
                                send_line_log(fd, wn_already_in_game, msg_orig);
                        } else if (!(g = find_game_by_nick(args))) {
                                send_line_log(fd, wn_no_such_game, msg_orig);
                        } else {
                                if (add_player(g, fd, strdup(nick)))
                                        send_ok(fd, msg_orig);
                                else
                                        send_line_log(fd, wn_game_full, msg_orig);
                        }
                }
        } else if (streq(current_command, "KICK")) {
                if (!args) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else {
                        if ((ptr = strchr(args, ' ')))
                                *ptr = '\0';
                        if (strlen(args) > 10)
                                args[10] = '\0';
                        if (!already_in_game(fd)) {
                                send_line_log(fd, wn_not_in_game, msg_orig);
                        } else {
                                struct game * g = find_game_by_fd(fd);
                                if (g->players_conn[0] != fd) {
                                        send_line_log(fd, wn_not_creator, msg_orig);
                                } else {
                                        kick_player(fd, g, args);
                                }
                        }
                }
        } else if (streq(current_command, "PART")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        player_part_game(fd);
                        send_ok(fd, msg_orig);
                }
        } else if (streq(current_command, "LIST")) {
                send_line_log(fd, list_games_str, msg_orig);
        } else if (streq(current_command, "STATUS")) {  // 1.0 command
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        status(fd, msg_orig);
                }
        } else if (streq(current_command, "STATUSGEO")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        status_geo(fd, msg_orig);
                }
        } else if (streq(current_command, "PROTOCOL_LEVEL")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        protocol_level(fd, msg_orig);
                }
        } else if (streq(current_command, "TALK")) {
                if (!args) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else {
                        talk(fd, args);
                }
        } else if (streq(current_command, "START")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        start_game(fd);
                }
        } else if (streq(current_command, "CLOSE")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        close_game(fd);
                }
        } else if (streq(current_command, "SETOPTIONS")) {
                if (!args) {
                        send_line_log(fd, wn_missing_arguments, msg_orig);
                } else if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        setoptions(fd, args);
                }
        } else if (streq(current_command, "LEADER_CHECK_GAME_START")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        leader_check_game_start(fd);
                }
        } else if (streq(current_command, "OK_GAME_START")) {
                if (!already_in_game(fd)) {
                        send_line_log(fd, wn_not_in_game, msg_orig);
                } else {
                        ok_start_game(fd);
                }
        } else if (streq(current_command, "ADMIN_REREAD")) {
                if (!admin_authorized[fd]) {
                        send_line_log(fd, wn_denied, msg_orig);
                } else {
                        reread();
                        send_ok(fd, "ADMIN_REREAD");
                }
        } else {
                send_line_log(fd, wn_unknown_command, msg);
        }

        free(msg_orig);
        current_command = NULL;

        return 0;
}


ssize_t get_reset_amount_transmitted(void)
{
        ssize_t ret = amount_transmitted;
        amount_transmitted = 0;
        return ret;
}

static void conn_to_terminate_helper(gpointer data, gpointer user_data)
{
        conn_terminated(GPOINTER_TO_INT(data), "system error on send (probably peer shutdown or try again)");
}

void process_msg_prio_(int fd, char* msg, ssize_t len, struct game* g)
{
        GList * conn_to_terminate = NULL;
        if (!g)
                g = find_game_by_fd(fd);
        if (g) {
                int i;
                for (i = 0; i < g->players_number; i++) {
                        // Pings are for the server only. Don't broadcast them to save bandwidth.
                        if (len == 3 && msg[1] == 'p') {
                                // nada

                        // Emitter wants to receive synchro message as well
                        } else if (g->players_conn[i] == fd && len > 2 && msg[1] == '!') {
                                char synchro4self[] = "?!\n";
                                ssize_t retval;
                                synchro4self[0] = fd;
                                l1(OUTPUT_TYPE_DEBUG, "[%d] sending self synchro", g->players_conn[i]);
                                retval = send(g->players_conn[i], synchro4self, sizeof(synchro4self) - 1, MSG_NOSIGNAL|MSG_DONTWAIT);
                                if (retval != sizeof(synchro4self) - 1) {
                                        if (retval != -1) {
                                                l4(OUTPUT_TYPE_INFO, "[%d] short send of %zd instead of %zd bytes from %d - destination is not reading data "
                                                                     "(illegal FB client) or our upload bandwidth is saturated - sorry, cannot continue serving "
                                                                     "this client in this situation, closing connection",
                                                                     g->players_conn[i], retval, sizeof(synchro4self) - 1, fd);
                                        }
                                        conn_to_terminate = g_list_append(conn_to_terminate, GINT_TO_POINTER(g->players_conn[i]));
                                }

                        } else if (g->players_conn[i] != fd) {
                                ssize_t retval;
                                l3(OUTPUT_TYPE_DEBUG, "[%d] sending %zd bytes to %d", fd, len, g->players_conn[i]);
                                retval = send(g->players_conn[i], msg, len, MSG_NOSIGNAL|MSG_DONTWAIT);
                                if (retval != len) {
                                        if (retval != -1) {
                                                l4(OUTPUT_TYPE_INFO, "[%d] short send of %zd instead of %zd bytes from %d - destination is not reading data "
                                                                     "(illegal FB client) or our upload bandwidth is saturated - sorry, cannot continue serving "
                                                                     "this client in this situation, closing connection",
                                                                     g->players_conn[i], retval, len, fd);
                                        }
                                        conn_to_terminate = g_list_append(conn_to_terminate, GINT_TO_POINTER(g->players_conn[i]));
                                }
                        }
                }
                if (conn_to_terminate) {
                        g_list_foreach(conn_to_terminate, conn_to_terminate_helper, NULL);
                        g_list_free(conn_to_terminate);
                }
        } else {
                l1(OUTPUT_TYPE_ERROR, "Internal error: could not find game by fd: %d", fd);
                exit(EXIT_FAILURE);
        }
}

void process_msg_prio(int fd, char* msg, ssize_t len)
{
        process_msg_prio_(fd, msg, len, NULL);
}

void player_part_game(int fd)
{
        player_part_game_(fd, NULL);
}

void player_part_game_(int fd, char* reason)
{
        struct game * g = find_game_by_fd(fd);
        if (g) {
                char * save_nick;
                int j;
                int i = find_player_number(g, fd);

                // remove parting player from game
                save_nick = g->players_nick[i];
                for (j = i; j < g->players_number - 1; j++) {
                        g->players_conn[j] = g->players_conn[j + 1];
                        g->players_nick[j] = g->players_nick[j + 1];
                        g->players_started[j] = g->players_started[j + 1];
                }
                g->players_number--;
                
                // completely remove game if empty
                if (g->players_number == 0) {
                        int was_running = g->status == GAME_STATUS_PLAYING;
                        games = g_list_remove(games, g);
                        free(g);
                        calculate_list_games();
                        if (was_running)
                                l2(OUTPUT_TYPE_INFO, "running games decrements to: %d (%d players)", games_running, players_in_game);

                } else {
                        if (g->status == GAME_STATUS_PLAYING) {
                                // inform other players, playing state
                                char leave_player_prio_msg[] = "?l\n";
                                leave_player_prio_msg[0] = fd;
                                process_msg_prio_(fd, leave_player_prio_msg, strlen(leave_player_prio_msg), g);
                        } else {
                                char parted_msg[1000];
                                // inform other players, non-playing state
                                snprintf(parted_msg, sizeof(parted_msg), reason ? reason : ok_player_parted, save_nick);
                                for (j = 0; j < g->players_number; j++)
                                        send_line_log_push(g->players_conn[j], parted_msg);
                        }
                        calculate_list_games();
                }
                free(save_nick);

                open_players = g_list_append(open_players, GINT_TO_POINTER(fd));
        }
}
