#include <stdio.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h> // read(), write(), close()
#include <arpa/inet.h>
#include "truco_server_config.h"
#include "truco.h"

#define MAX 80
#define SA struct sockaddr

#define INVALID_FD -1
#define JUGADOR_INVALIDO_SERVER MAX_JUGADORES_SERVER+1
#define MESA_INVALIDA MAX_MESAS+1
#define SILLA_INVALIDA MAX_JUGADORES_MESA+1

typedef int socket_t;

#define DEBUG 1

#if DEBUG == 1
    #define DEBUG_PRINT(line) do { line; } while(0)
#else
    #define DEBUG_PRINT(line) do { } while(0)
#endif

/* MACROS NEEDED FOR BUFFERS */
#define STR(x) #x
#define XSTR(x) STR(x)
#define DIGITS(x) (sizeof(XSTR(x)) - 1)
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
#define STRLEN_CT(s) (sizeof(s) - 1)

#define SOCKET_IO_ERROR_CHECK(x) ((x) <= 0)
#define SOCKET_ERROR_CHECK(x) ((x) < 0)

#define COMMON_PACKET_BYTES_EXTENSION 4 ///<PP \0
#define SUM_COMMON_EXT(a) ((size_t)((a) + (COMMON_PACKET_BYTES_EXTENSION))) ///<Apply COMMON_PACKET_BYTES_EXTENSION to (a)

typedef struct{
    socket_t fd;
    char ip[INET_ADDRSTRLEN];
} Conn;

typedef struct{
    char apodo[MAX_LONGITUD_APODO];
    Conn *conn;
    uint8_t id_mesa;
} Jugador;

typedef struct{
    uint16_t jugadores[MAX_JUGADORES_MESA];
    EstadoPartida *estado_partida;
} Mesa;

Conn conexiones[MAX_CONEXIONES];
Jugador jugadores[MAX_JUGADORES_SERVER];
Mesa mesas[MAX_MESAS];

Conn* getConnByFd(socket_t fd) {
    for (uint16_t i = 0; i < MAX_CONEXIONES; i++) if (conexiones[i].fd == fd) return &conexiones[i];
    return NULL;
}

void closeAndClearFd(socket_t fd){
    close(fd);
    // FD_CLR(fd, master);

    Conn *c = getConnByFd(fd);
    c->fd = INVALID_FD;
    c->ip[0] = '\0';
}

/*PACKET SEND LOGIC*/

/**Sends *paquete to connectionId (fd/socket) */
void sendToClient(const char *paquete, socket_t sockfd){
    DEBUG_PRINT(printf("ENVIANDO %s A %d\n", paquete, sockfd));

    // if(WS_LISTEN_PORT > 0 && getConnByFd(connectionId)->flags & CONN_IS_WS_FLAG){
    //     websocket_send_text(connectionId, paquete);
    //     return;
    // }
    int n = send(sockfd, paquete, strlen(paquete), MSG_NOSIGNAL);
    if (SOCKET_IO_ERROR_CHECK(n)) perror("send");
}

/**Sends *paquete to a player of playerId (players[i]) as long as he is not leaving. */
void enviarPaquete(const char *paquete, uint8_t jugadorId){
    if (jugadorId >= MAX_JUGADORES_SERVER) return;
    socket_t fd = jugadores[jugadorId].conn->fd;
    // if (SOCKET_ERROR_CHECK(fd) || (players[playerId].flags & PLAYER_LEAVING_FLAG)) return;
    sendToClient(paquete, fd);
}

/**Sends *paquete to area of areaId (areaList[i]) as long as they joined the server and are not leaving. */
void enviarPaqueteAMesa(char *paquete, uint8_t mesaId){
    for (uint8_t i = 0; i < MAX_JUGADORES_MESA; i++){
        uint16_t jugador = mesas[mesaId].jugadores[i];
        
        if(jugador == JUGADOR_INVALIDO_SERVER) continue;

        enviarPaquete(paquete, mesas[mesaId].jugadores[i]);
    } 
}

/**Use memcpy to copy *src to *dst and make sure there is a \0 at the end (dst_size)*/
void safe_strcpy(char *dst, const char *src, size_t dst_size){
    size_t len = strlen(src);

    if (len >= dst_size)
        len = dst_size - 1;

    memcpy(dst, src, len);
    dst[len] = '\0';
}


/**
    Make an 16 bit unsigned int out of a string. Loops through it and 
    returns 0 if/when no numbers are found. 
*/
uint16_t parseInt(const char *s)
{
    uint16_t value = 0;
    while (*s >= '0' && *s <= '9') value = value * 10 + (*s++ - '0');
    return value;
}

typedef struct {
    char **args;
    uint16_t tamano_paquete;
    Conn *conn;
    Jugador *jugador;
    uint8_t jugador_id;
    uint8_t numero_args;
} ContextoPaquete;

typedef void (*HandlerFunc)(ContextoPaquete *ctx);

/**
  Common Packet Handler Structure

  This is what goes in commonPacketDispatchTable[]. 
  Adding a new paquete is just creating this struct and its handler.

  \param packetName
    Name of the paquete to match when comparing the first argument received in the paquete at processPacket()
  \param handler
    HandlerFunc that receives the ContextoPaquete to process the paquete.
  \param requiredArgs
    Amount of required args to validate before shooting the handler.
*/
typedef struct {
    const char *packetName;
    HandlerFunc handler;
    uint8_t requiredArgs;
} CommonPacketHandler;

void handle_UM(ContextoPaquete *ctx){
    uint16_t id_mesa = parseInt(ctx->args[0]);
    if(id_mesa > MAX_MESAS || ctx->jugador->id_mesa == id_mesa) return;

    uint8_t silla_encontrada = SILLA_INVALIDA;


    for(uint8_t i = 0; i < MAX_JUGADORES_MESA; i++){
        if(mesas[id_mesa].jugadores[i] != JUGADOR_INVALIDO_SERVER) continue;

        char njBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_SERVER) + 1 + MAX_LONGITUD_APODO)];

        sprintf(njBuffer, "NJ %u %s\n", i, ctx->jugador->apodo);

        enviarPaqueteAMesa(njBuffer, id_mesa);

        //anoto el jugador en la mesa y la mesa en el jugador
        mesas[id_mesa].jugadores[i] = ctx->jugador_id;
        ctx->jugador->id_mesa = id_mesa;
        silla_encontrada = i;
        break;
    }

    if(silla_encontrada == SILLA_INVALIDA) return;

    char umBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_SERVER))];

    sprintf(umBuffer, "UM %u\n", id_mesa);

    enviarPaquete(umBuffer, ctx->jugador_id);
}

void handle_CA(ContextoPaquete *ctx){
    uint16_t id_mesa = parseInt(ctx->args[0]);
    if(id_mesa > MAX_MESAS || ctx->jugador->id_mesa == id_mesa) return;

    uint8_t silla_encontrada = SILLA_INVALIDA;

    for(uint8_t i = 0; i < MAX_JUGADORES_MESA; i++){
        if(mesas[id_mesa].jugadores[i] != JUGADOR_INVALIDO_SERVER) continue;

        //anoto el jugador en la mesa y la mesa en el jugador
        mesas[id_mesa].jugadores[i] = ctx->jugador_id;
        ctx->jugador->id_mesa = id_mesa;
        silla_encontrada = i;
        break;
    }

    if(silla_encontrada == SILLA_INVALIDA) return;
}

CommonPacketHandler commonPacketDispatchTable[] = {
    {"UM", handle_UM, 1}
}; /**< This is where most packets are declared. Refer to the official network protocol,
CommonPacketHandler (to understand how to implement a new package) and ContextoPaquete*/

CommonPacketHandler paquetesMesaTablaDespacho[] = {
    {"CA", handle_CA, 1}
}; /**< This is where most packets are declared. Refer to the official network protocol,
CommonPacketHandler (to understand how to implement a new package) and ContextoPaquete*/

void recorrer_tabla_despacho_comparar_y_manejar(CommonPacketHandler tabla[], char *paquete, ContextoPaquete *ctx, size_t tamano_tabla){
    for(uint8_t i = 0; i < tamano_tabla; i++){

        if(strcmp(tabla[i].packetName, paquete)) continue;

        if(ctx->numero_args < tabla[i].requiredArgs) return;
        tabla[i].handler(ctx);
        break;
    }
}

void procesar_paquete(socket_t connfd, char *paquete){

    Conn *c = getConnByFd(connfd);

    if(!strcmp(paquete, "TCCHI\n")){
        uint16_t jugador_libre_encontrado_i = JUGADOR_INVALIDO_SERVER;
        uint8_t conn_repetida = 0;

        for (uint16_t i = 0; i < MAX_JUGADORES_SERVER; i++) {
            if (jugadores[i].conn != NULL) continue;
            jugador_libre_encontrado_i = i;

            if(jugadores[i].conn == c){
                conn_repetida = 1;
                break;
            }
        }

        if(conn_repetida) return;

        if(jugador_libre_encontrado_i == JUGADOR_INVALIDO_SERVER) closeAndClearFd(connfd);

        jugadores[jugador_libre_encontrado_i].conn = c;
        jugadores[jugador_libre_encontrado_i].id_mesa = MESA_INVALIDA;

        enviarPaquete("HI\n", jugador_libre_encontrado_i);
        return;
    }

    uint8_t jugador_id = JUGADOR_INVALIDO_SERVER;

    for (uint16_t i = 0; i < MAX_JUGADORES_SERVER; i++) {
        if(jugadores[i].conn != c) continue;

        jugador_id = i;
        break;
    }

    if(jugador_id == JUGADOR_INVALIDO_SERVER) return;

    char *p = paquete;
    char *start = p;
    uint8_t numero_args = 0;
    char argsStorage[27][1] = {{0}};
    char *args[27] = { NULL };
    uint8_t firstToken = 1;
    uint16_t tamano_paquete = 0;

    while (1) {
        tamano_paquete++;
        if(tamano_paquete == 65535){
            DEBUG_PRINT("Packet too big!");
            return;
        }
        if(*p == '\n'){
            *p = '\0';
            args[numero_args++] = start;
            break;
        } 
        if(*p == '\0') break;
        if (*p == ' ') {
            *p = '\0';
            if(!firstToken) args[numero_args++] = start;
            else firstToken = 0;
            start = p + 1;
            if(numero_args == 27) break;
        }
        p++;
    }

    for (uint8_t i = numero_args; i < 27; i++) args[i] = argsStorage[i];

    DEBUG_PRINT(printf("ConnFD: %d | Packet: %s | args=%d\n", connfd, paquete, numero_args));

    Jugador *jugador = &jugadores[jugador_id];

    ContextoPaquete ctx = {
        .jugador = jugador,
        .args = args,
        .numero_args = numero_args,
        .conn = c,
        .tamano_paquete = tamano_paquete,
        .jugador_id = jugador_id
    };

    recorrer_tabla_despacho_comparar_y_manejar(commonPacketDispatchTable, paquete, &ctx, ARRAY_SIZE(commonPacketDispatchTable));

    if(jugador->id_mesa != MESA_INVALIDA) recorrer_tabla_despacho_comparar_y_manejar(paquetesMesaTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesMesaTablaDespacho));
}

void iniciar_servidor(uint16_t puerto){

    for (uint16_t i = 0; i < MAX_CONEXIONES; i++) conexiones[i].fd = INVALID_FD;
    for(uint16_t i = 0; i < MAX_MESAS; i++){
        for(uint8_t j = 0; j < MAX_JUGADORES_MESA; j++){
            mesas[i].jugadores[j] = JUGADOR_INVALIDO_SERVER;
        }
    }

    int sockfd, connfd, len; 
    struct sockaddr_in servaddr, cli; 
  
    // socket create and verification 
    sockfd = socket(AF_INET, SOCK_STREAM, 0); 
    if (sockfd == -1) { 
        printf("socket creation failed...\n"); 
        exit(0); 
    }
    else printf("Socket successfully created..\n"); 
    bzero(&servaddr, sizeof(servaddr)); 
  
    // assign IP, PORT 
    servaddr.sin_family = AF_INET; 
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
    servaddr.sin_port = htons(puerto); 
    
    int opt = 1;

    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt");
        close(sockfd);
        exit(EXIT_FAILURE);
    }

    // Binding newly created socket to given IP and verification 
    if (bind(sockfd, (SA*)&servaddr, sizeof(servaddr)) != 0) {
        perror("bind");
        close(sockfd);
        exit(EXIT_FAILURE);
    }else printf("Socket successfully binded..\n"); 
  
    // Now server is ready to listen and verification 
    if ((listen(sockfd, 5)) != 0) { 
        printf("Listen failed...\n"); 
        exit(0); 
    }else printf("Server listening..\n"); 
    len = sizeof(cli); 

    
    char buff[MAX]; 
    int n; 
    for (;;) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(sockfd, &readfds);
        
        int maxfd = sockfd;

        // Agregar todos los clientes al select
        for (uint16_t i = 0; i < MAX_CONEXIONES; i++) {

            if (conexiones[i].fd == INVALID_FD) continue;

            FD_SET(conexiones[i].fd, &readfds);

            if (conexiones[i].fd > maxfd) maxfd = conexiones[i].fd;
        }

        int actividad = select(maxfd + 1, &readfds, NULL,NULL,NULL);


        if (actividad < 0) {
            perror("select");
            continue;
        }

        //revisar si el cliente es nuevo

        if(FD_ISSET(sockfd, &readfds)){
            struct sockaddr_in cli;
            socklen_t len = sizeof(cli);
            socket_t connfd = accept(sockfd, (SA*)&cli, &len);
            if(connfd < 0) perror("accept");
            else{
                char ip[16];
                inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));

                DEBUG_PRINT(printf("Cliente intentando conectar desde %s\n", ip));

                uint8_t conexion_encontrada = 0;

                for (uint16_t i = 0; i < MAX_CONEXIONES; i++) {
                    if (conexiones[i].fd == INVALID_FD) {
                        conexiones[i].fd = connfd;
                        // conexiones[i].lastCh = time(NULL);
                        safe_strcpy(conexiones[i].ip, ip, sizeof(conexiones[i].ip));
                        // if(fd == ws_fd) conexiones[i].flags |= CONN_IS_WS_FLAG;
                        // else validateClientAndStartHandshake(connfd, &master, &conexiones[i]);
                        conexion_encontrada = 1;
                        break;
                    }
                }

                if(!conexion_encontrada){
                    DEBUG_PRINT(printf("Sin slot de conexión para el cliente\n"));
                    close(connfd);
                }
            }
        }

        //loopear conexiones a ver si tienen algo

        for(uint16_t i = 0; i < MAX_CONEXIONES; i++){
            socket_t connfd = conexiones[i].fd;

            if(connfd == INVALID_FD) continue;

            //si tiene algo...
            if(FD_ISSET(connfd, &readfds)){
                int n = read(connfd, buff, MAX - 1);

                if (n <= 0) {
                    printf("El cliente cerró la conexión\n");

                    //TODO: manejar logica de limpieza del jugador luego
                    close(connfd);
                    conexiones[i].fd = INVALID_FD;
                    continue;
                }

                buff[n] = '\0';

                procesar_paquete(connfd, buff);

                memset(buff, 0, MAX);
            }
        }
    }
}
