#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

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;

#define ROL_JUGADOR 0
#define ROL_ANFITRION 1

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

    uint8_t mesa_rol;
} Jugador;

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

    uint8_t numero_jugadores;
    uint8_t partida_iniciada;
} Mesa;

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

/**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;
}

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 enviar_paquete_a_mesa(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]);
    } 
}

uint8_t obtener_posicion_jugador(uint16_t id_mesa, uint16_t jugador_id){
    for(uint8_t i = 0; i < MAX_JUGADORES_MESA; i++){
        if(mesas[id_mesa].jugadores[i] == jugador_id){
            return i;
        }
    }
    return JUGADOR_INVALIDO;
}

typedef struct {
    char **args;
    uint16_t tamano_paquete;
    Conn *conn;
    Jugador *jugador;
    Mesa *jugador_mesa;
    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 nombre_paquete
    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 *nombre_paquete;
    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 = MAX_JUGADORES_MESA;

    uint8_t primer_jugador = 1;


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

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

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

        enviar_paquete_a_mesa(njBuffer, id_mesa);

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

    if(silla_encontrada == MAX_JUGADORES_MESA) return;

    if(primer_jugador){
        ctx->jugador->mesa_rol = ROL_ANFITRION;
    }else ctx->jugador->mesa_rol = ROL_JUGADOR;

    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){

    //puede ser el asiento deseado o el del jugador que se quiere cambiar
    uint8_t asiento = parseInt(ctx->args[0]);

    uint8_t pos_jugador = obtener_posicion_jugador(ctx->jugador->id_mesa, ctx->jugador_id);

    if(asiento >= MAX_JUGADORES_MESA || pos_jugador >= MAX_JUGADORES_MESA) return;

    char caBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_MESA) + 1 + DIGITS(MAX_JUGADORES_MESA))];

    sprintf(caBuffer, "CA %u %u\n", pos_jugador, asiento);

    //si es el asiento deseado, me tengo que fijar que no haya nadie.
    //si lo hay, cambio de lugares si es que es el host.
    if(ctx->numero_args < 2){
        if(ctx->jugador_mesa->jugadores[asiento] != JUGADOR_INVALIDO_SERVER && 
        ctx->jugador->mesa_rol != ROL_ANFITRION) return;

        ctx->jugador_mesa->jugadores[pos_jugador] = ctx->jugador_mesa->jugadores[asiento];
        ctx->jugador_mesa->jugadores[asiento] = ctx->jugador_id;
    }else{
        if(ctx->jugador->mesa_rol != ROL_ANFITRION) return;

        uint8_t asiento_2 = parseInt(ctx->args[1]);

        if(asiento_2 >= MAX_JUGADORES_MESA) return;

        uint16_t jugador_cambiado_1 = ctx->jugador_mesa->jugadores[asiento];

        ctx->jugador_mesa->jugadores[asiento] = ctx->jugador_mesa->jugadores[asiento_2];
        ctx->jugador_mesa->jugadores[asiento_2] = jugador_cambiado_1;
    }

    enviar_paquete_a_mesa(caBuffer, ctx->jugador->id_mesa);
}

void anunciar_turno_baza_en_mesa(uint16_t id_mesa){
    //turno baza
    char tbBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_MESA))];
    sprintf(tbBuffer, "TB %u\n", mesas[id_mesa].estado_partida->jugador_turno_actual);
    enviar_paquete_a_mesa(tbBuffer, id_mesa);
}

void nueva_ronda(uint16_t id_mesa){
    for(uint8_t i = 0; i < mesas[id_mesa].numero_jugadores; i++){
        uint16_t id_jugador = mesas[id_mesa].jugadores[i];

        //mostrar cartas
        char mcBuffer[SUM_COMMON_EXT(sizeof("49 49 49"))];
        
        char *buffer_ptr = sprintf(mcBuffer, "MC");

        for(uint8_t j = 0; j < 3; j++){
            uint8_t carta_id = mesas[id_mesa].estado_partida->jugadores_cartas[i][j];
            if(carta_id == CARTA_INVALIDA) continue;
            buffer_ptr += sprintf(buffer_ptr, " %u", carta_id);
        }
        
        sprintf(buffer_ptr, "\n");
        enviarPaquete(mcBuffer, id_jugador);
    }

    anunciar_turno_baza_en_mesa(id_mesa);
}

void handle_EP(ContextoPaquete *ctx){
    if(ctx->jugador->mesa_rol != ROL_ANFITRION) return;

    enviar_paquete_a_mesa("EP\n", ctx->jugador->id_mesa);

    Mesa *mesa = &mesas[ctx->jugador->id_mesa];
    mesa->partida_iniciada = 1;

    iniciar_partida(mesa->estado_partida, mesa->numero_jugadores, 30);
}

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, 2},
    {"EP", handle_EP, 0}
};

uint8_t handle_JC(ContextoPaquete *ctx){
    uint8_t carta_id = parseInt(ctx->args[0]);

    if(carta_id >= CARTA_INVALIDA) return;

    uint8_t pos_jugador = obtener_posicion_jugador(ctx->jugador->id_mesa, ctx->jugador_id);

    for (uint8_t i = 0; i < 3; i++){
        if(ctx->jugador_mesa->estado_partida->jugadores_cartas[pos_jugador][i] != carta_id) continue;

        ResultadoJugada resultado_jugada = jugar_baza(ctx->jugador_mesa->estado_partida, pos_jugador, i);

        if(resultado_jugada.carta == CARTA_INVALIDA) return resultado_jugada.eventos;;

        char jc_buffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_MESA) + 3)];

        sprintf(jc_buffer, "JC %u %u\n", pos_jugador, resultado_jugada.carta);
        enviar_paquete_a_mesa(jc_buffer, ctx->jugador->id_mesa);

        return resultado_jugada.eventos;
    }
}

uint8_t handle_C(ContextoPaquete *ctx){
    uint8_t canto_id = parseInt(ctx->args[0]);

    Mesa *mesa = &mesa[ctx->jugador->id_mesa];

    uint8_t pos_jugador = obtener_posicion_jugador(ctx->jugador->id_mesa, ctx->jugador_id);

    ResultadoJugada resultado_jugada = cantar(mesa->estado_partida, pos_jugador, canto_id);

    if(resultado_jugada.canto == CANTO_INVALIDO) return resultado_jugada.eventos;

    char c_buffer[1 + 1 + DIGITS(MAX_JUGADORES_MESA) + 1 + 2 + 1];

    sprintf(c_buffer, "C %u %u\n", pos_jugador, resultado_jugada.canto);

    enviar_paquete_a_mesa(c_buffer, ctx->jugador->id_mesa);

    return resultado_jugada.eventos;
}

uint8_t handle_CT(ContextoPaquete *ctx){
    uint8_t tantos = parseInt(ctx->args[0]);

    Mesa *mesa = &mesa[ctx->jugador->id_mesa];

    uint8_t pos_jugador = obtener_posicion_jugador(ctx->jugador->id_mesa, ctx->jugador_id);

    ResultadoJugada resultado_jugada = cantar_tanto(mesa->estado_partida, pos_jugador, tantos);

    if(resultado_jugada.canto == CANTO_INVALIDO) return resultado_jugada.eventos;;

    char c_buffer[1 + 1 + DIGITS(MAX_JUGADORES_MESA) + 1 + 2 + 1];
    sprintf(c_buffer, "CT %u %u\n", pos_jugador, resultado_jugada.tantos_cantados);

    enviar_paquete_a_mesa(c_buffer, ctx->jugador->id_mesa);

    return resultado_jugada.eventos;
}

CommonPacketHandler paquetesPartidaTablaDespacho[] = {
    {"JC", handle_JC, 1},
    {"C", handle_C, 1},
    {"CT", handle_CT, 1}
};

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].nombre_paquete, 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){
        ctx.jugador_mesa = &mesas[jugador->id_mesa];

        if(!ctx.jugador_mesa->partida_iniciada){
            recorrer_tabla_despacho_comparar_y_manejar(paquetesMesaTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesMesaTablaDespacho)); 
            return;
        }

        for(uint8_t i = 0; i < ARRAY_SIZE(paquetesPartidaTablaDespacho); i++){
            if(strcmp(paquetesPartidaTablaDespacho[i].nombre_paquete, paquete)) continue;

            if(ctx.numero_args < tabla[i].requiredArgs) return;
            uint8_t eventos = tabla[i].handler(&ctx);
            if(IS_BIT_SET(eventos, EVENTO_RONDA_TERMINADA)) nueva_ronda(jugador->id_mesa); break;

            if(IS_BIT_SET(eventos, EVENTO_PARTIDA_TERMINADA)){
                //manejar fin partida
                break;
            }
            
            switch(ctx.jugador_mesa->ronda_estado){
                case ESTADO_PARTIDA_JUGANDO_BAZA:
                    char tbBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_MESA))];
                    sprintf(tbBuffer, "TB %u\n", ctx.jugador_mesa->estado_partida->jugador_turno_actual);
                    enviar_paquete_a_mesa(tbBuffer, id_mesa);
                    break;
                case ESTADO_PARTIDA_EN_CANTO:
                    char tcBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_MESA) + 1 + DIGITS(CANTO_INVALIDO))];
                    sprintf(tcBuffer, "TC %u %u\n", 
                    ctx.jugador_mesa->estado_partida->canto_ofrecido,
                    ctx.jugador_mesa->estado_partida->jugador_turno_canto);
                    enviar_paquete_a_mesa(tcBuffer, id_mesa);
                    break;
                case ESTADO_PARTIDA_RONDA_TANTOS:
                    char ttBuffer[SUM_COMMON_EXT(DIGITS(MAX_JUGADORES_MESA))];
                    sprintf(ttBuffer, "TT %u\n", ctx.jugador_mesa->estado_partida->jugador_turno_canto);
                    enviar_paquete_a_mesa(ttBuffer, id_mesa);
                    break;
                default: break;
            }

            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++){
            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);
            }
        }
    }
}