#include "networking.h"

#include <assert.h>

#define ONVO_IDENTIFIER "c2438167-831b-4bf7-8bdc-abcdefabcd00"

static void listen_callback(
    const char *event,      /* "joined", "leaved", "game" */
    int64_t messsage_id,      /* sekventiellt meddelande‑ID (från host) */
    const char *client_id,   /* avsändarens klient‑ID (eller NULL) */
    json_t *data,           /* JSON‑objekt med godtycklig speldata */
    void *context         /* godtycklig pekare som skickas vidare */
) {
    Game_Networking *networking = (Game_Networking *)context;
    assert(networking != NULL);
    assert(networking->game_session != NULL);

    (void)data;

    printf("#%li :: Event: '%s' from '%s'.\n", messsage_id, event, client_id);

    assert(networking != NULL);

    if(strcmp(event, "joined") == 0) {
        // Create a new player in the session, get the id and store it as the value in the
        // 'client_id_to_player_index' hashmap using 'client_id' as the key.
        
        uint32_t *existing_id = map_get(&networking->client_id_to_player_index, client_id);
        assert(existing_id == NULL); // Something has gone terribly wrong if we get a 'joined' event for the same client-id with them still being in the map.
        
        uint16_t player_index = 0xFFFF;
        assert(game_session_create_player(networking->game_session, &player_index));
        assert(map_set(&networking->client_id_to_player_index, client_id, player_index) == 0); // 0 = OK.

        // TODO: SS - Send a 'game' message to the joinee with information about the session?

        printf("Added client-id '%s' (player-index: %u) to the map.\n", client_id, player_index);
    }
    else if(strcmp(event, "leaved") == 0) {
        // TODO: SS - Index the hashmap using 'client_id' as the key and attempt to get the
        // player-index for that client. Run 'game_session_destroy_player()'.

        uint32_t *existing_id = map_get(&networking->client_id_to_player_index, client_id);
        assert(existing_id != NULL); // Something has gone terribly wrong if we get a 'leaved' event for the a client-id that isn't in the map.
        
        uint16_t player_id_to_remove = *existing_id;
        
        game_session_destroy_player(networking->game_session, player_id_to_remove); // Returns void but crashes internally if id isn't OK.
        map_remove(&networking->client_id_to_player_index, client_id);

        printf("Removed client-id '%s' (player-index: %u) from the map.\n", client_id, player_id_to_remove);
    }
    else if(strcmp(event, "game") == 0) {
        // TODO: SS - Check the 'data' to see what type of game-event it is.
        // Possible game-events I can think of right now:
        // - Player-input.
        // - Match-state changed.
        // - Chat-message?
        // - Session-state?
        // - Maybe Player-Joined/Left. (seems like mp_api only sends 'joined'/'leaved' events to the host.)
    }
    else {
        printf("UNKNOWN EVENT-TYPE: '%s'.\n", event);
    }
}

static inline bool setup_api(Game_Networking *networking) {
    assert(networking != NULL);

    if(networking->api != NULL) {
        printf("Failed to set up API. Already active.\n");
        return false;
    }

    MultiplayerApi *mp_api = mp_api_create("kontoret.onvo.se", 9001, ONVO_IDENTIFIER);
    if(mp_api == NULL) {
        printf("Failed to set up API. Could not initialize.\n");
        return false;
    }

    networking->api = mp_api;
    return true;
}

static inline bool shutdown_api(Game_Networking *networking) {
    if(networking->api == NULL) {
        return false;
    }

    mp_api_destroy(networking->api);
    networking->api = NULL;

    return true;
}

static bool start_listening(Game_Networking *networking) {
    assert(networking != NULL);
    assert(networking->api != NULL);
    
    int listener_id = mp_api_listen(networking->api, listen_callback, (void *)networking);
    if(listener_id < 0) {
        return false;
    }

    networking->listener_id = (uint32_t)listener_id;

    return true;
}

static bool stop_listening(Game_Networking *networking) {
    assert(networking != NULL);
    assert(networking->api != NULL);

    mp_api_unlisten(networking->api, (int)networking->listener_id);
    return true;
}

bool networking_try_host(Game_Networking *networking, Game_Session *session, Game_Session_Settings settings, uint16_t *out_host_player_id) {
    assert(networking != NULL);
    assert(session != NULL);

    if(!settings.online) {
        printf("Failed to host; Game_Session_Settings.online == false, expected it to be true.\n");
        return false;
    }

    if(!setup_api(networking)) {
        printf("Failed to host; API is already set up.\n");
        return false;
    }

    char *session_id = NULL;
    char *local_client_id = NULL;
    json_t *response_data = NULL;
    int host_result = mp_api_host(
        networking->api,
        NULL, // TODO: SS - Send data to server that contains the game-session's settings.
        &session_id,
        &local_client_id,
        &response_data
    );
    if(host_result != MP_API_OK) {
        printf("Failed to host; Got result: %i.\n", host_result);
        shutdown_api(networking);
        return false;
    }
    
    printf("Started hosting session '%s', local_client_id is '%s'.\n", session_id, local_client_id);
    networking->is_host = true;
    networking->game_session = session;
    networking->session_id = session_id;
    networking->local_client_id = local_client_id;

    map_init(&networking->client_id_to_player_index);

    assert(start_listening(networking));

    // Set up session.
    game_session_init(
        session,
        settings
    );

    // Add the host to the game.
    uint16_t host_player_index = 0xFFFF;
    assert(game_session_create_player(networking->game_session, &host_player_index));
    map_set(&networking->client_id_to_player_index, local_client_id, host_player_index);
    *out_host_player_id = host_player_index;
    printf("Host-player created (client-id: '%s'). Player-id: %u.\n", local_client_id, host_player_index);
    
    if(response_data != NULL) {
        // TODO: SS - Handle JSON-response.
        json_decref(response_data);
    }

    return true;
}

bool networking_try_join(Game_Networking *networking, Game_Session *session, const char *session_id) {
    assert(networking != NULL);
    assert(session != NULL);

    if(strlen(session_id) == 0) {
        return false;
    }
    
    if(!setup_api(networking)) {
        printf("Failed to join; API is already set up.\n");
        return false;
    }
    
    printf("Trying to join session using code: '%s' ...\n", session_id);

    char *result_session_id;
    char *local_client_id;
    json_t *received_json;
    int join_result = mp_api_join(
        networking->api, // API.
        session_id, // Session-code.
        NULL, // JSON-data to send. // TODO: SS - Send information about you as a player (username, ..).
        &result_session_id, // Received Session-id. I assume it should be the same as 'session_id'. // TODO: SS - Ask Robin why mp_api_join() returns a session_id. Is it to support 'abcdef' => 'ABCDEF' and/or proxies?
        &local_client_id, // Received client-id.
        &received_json // Received JSON-data.
    );
    if(join_result != MP_API_OK) {
        shutdown_api(networking);
        printf("Failed to join; Got result: %i.\n", join_result);
        return false;
    }
    assert(strcmp(result_session_id, session_id) == 0);

    printf("Joined session '%s', local_client_id is '%s'.\n", result_session_id, local_client_id);
    networking->is_host = true;
    networking->game_session = session;
    networking->session_id = result_session_id;
    networking->local_client_id = local_client_id;

    map_init(&networking->client_id_to_player_index);

    // TODO: SS - Decide how to network this..
    // Should we just receive the session's settings as well as a long list of commands of what has happened throughout the match (so far) - at least one per tick?
    // Or, should this client receive the current state of the session?

    if(received_json != NULL) {
        // TODO: SS - Read this json. It should contain valuable information about the session.
        
        json_decref(received_json);
    }
    
    assert(start_listening(networking));

    // TODO: SS - Set up session based on what we got in the json.
    // game_session_init(
    //     session,
    //     settings
    // );

    return true;
}

bool networking_stop(Game_Networking *networking) {
    assert(networking != NULL);
    if(networking->api == NULL) {
        printf("Failed to stop networking; API is not set up.\n");
        return false;
    }
    
    // TODO: SS - MP_API (preferably) should send a 'hey the server has shut down' message to all connected clients.
    // If not, we might need to do it by having the host send a Game-message to all clients.
    
    map_deinit(&networking->client_id_to_player_index);
    
    assert(stop_listening(networking));
    networking->listener_id = 0;
    
    shutdown_api(networking);
    networking->game_session = NULL;
    networking->is_host = false;

    if(networking->session_id != NULL) {
        free(networking->session_id);
        networking->session_id = NULL;
    }
    if(networking->local_client_id != NULL) {
        free(networking->local_client_id);
        networking->local_client_id = NULL;
    }
    
    printf("Stopped networking.\n");
    return true;
}