#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"

#define MAX 80
#define SA struct sockaddr

#define INVALID_FD -1
#define JUGADOR_INVALIDO MAX_JUGADORES+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

#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

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

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

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

Conn conexiones[MAX_CONEXIONES];
Jugador jugadores[MAX_JUGADORES];
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';
}

/**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 void (*HandlerFunc)(CommandContext *ctx);

/**
  Common Packet Handler Structure

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

  \param packetName
    Name of the packet to match when comparing the first argument received in the packet at processPacket()
  \param handler
    HandlerFunc that receives the CommandContext to process the packet.
  \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(CommandContext *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 = 0; i < MAX_JUGADORES_MESA; i++){
        if(mesas[id_mesa][i] != JUGADOR_INVALIDO) continue;

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

    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 CommandContext*/

void procesar_paquete(socket_t connfd, char *paquete){

    *Conn c = getConnByFd(connfd);

    if(!strcmp(buff, "TCCHI\n")){
        uint8_t jugador_libre_encontrado = 0;
        uint8_t conn_repetida = 0;

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

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

        if(conn_repetida) return;

        if(!jugador_libre_encontrado) closeAndClearFd(connfd);

        jugadores[i].conn = c;
        jugadores[i].mesa = MESA_INVALIDA;

        write(connfd, "HI\n", sizeof("HI\n"));
    }

    Jugador *jugador = NULL;

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

        jugador = &jugadores[i];
        break;
    }

    if(jugador == NULL) 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') 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));

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

    for(uint8_t i = 0; i < ARRAY_SIZE(commonPacketDispatchTable); i++){
        if(strcmp(commonPacketDispatchTable[i].packetName, packet)) continue;
        if(argCount < commonPacketDispatchTable[i].requiredArgs) return;
        commonPacketDispatchTable[i].handler(&ctx);
        break;
    }
    
}

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++){
            jugadores[j] = JUGADOR_INVALIDO;
        }
    }

    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); 
  
    // Accept the data packet from client and verification 
    connfd = accept(sockfd, (SA*)&cli, &len); 
    if (connfd < 0) { 
        printf("server accept failed...\n"); 
        exit(0); 
    } 

    printf("server accepted the client...\n"); 

    uint8_t conexion_encontrada = 0;

    char ip[16];
    inet_ntop(AF_INET, &cli.sin_addr, ip, sizeof(ip));

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

    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);
    }
    
    char buff[MAX]; 
    int n; 
    // infinite loop for chat 
    for (;;) {
        int n = read(connfd, buff, MAX - 1);

        if (n < 0) {
            perror("read");
            break;
        }

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

        buff[n] = '\0';

        printf("From client: %s\n", buff);

        procesar_paquete(connfd, buff);

        memset(buff, 0, MAX);
    }
}
