#define _POSIX_C_SOURCE 200112L

#ifdef _WIN32
    #include <winsock2.h>
    #include <ws2tcpip.h>
    #include <windows.h>
    #include <conio.h>
    #define socket_t SOCKET
#else
    #include <arpa/inet.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <sys/time.h>
    #include <netinet/in.h>
    #include <unistd.h>
    #include <errno.h>
    #define socket_t int
    #define socklen_t int
    #define INVALID_SOCKET -1
    #define closesocket close
    #define gai_strerror(err) (err)
#endif

#include <time.h>
#include <stdio.h>
#include <string.h>



#include <inttypes.h>

#include "../truco.h"
#include "../interfaces/truco_interfaz_comun.h"
#include "../interfaces/truco_interfaz_comun_menus.h"

#define MAX_TAMANO_PAQUETE 80
#define SA struct sockaddr

#define PAQUETE_MAX_ARGS 5

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

#define DEBUG 0

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

socket_t sockfd = INVALID_SOCKET;

#define ESTADO_CONECTANDO 0
#define ESTADO_EN_LOBBY 1
#define ESTADO_EN_SALA 2
#define ESTADO_EN_PARTIDA 3

//cliente
uint8_t estado = ESTADO_CONECTANDO;

//sala
uint8_t es_host = 0;

//info del jugador en sala
uint8_t numero_jugador = JUGADOR_INVALIDO;

//info de la estado_partida 
EstadoPartida estado_partida;

typedef struct {
    ResultadoJugada resultado_jugada;
    char **args;
    uint8_t numero_args;
} TCCO_ContextoPaquete;

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

typedef struct {
    const char *nombre_paquete;
    HandlerFunc handler;
} TCCO_PaqueteManejador;

static uint8_t recorrer_tabla_despacho_comparar_y_manejar(TCCO_PaqueteManejador tabla[], char *paquete, TCCO_ContextoPaquete *ctx, size_t tamano_tabla){
    for(uint8_t i = 0; i < tamano_tabla; i++){
        if(strcmp(tabla[i].nombre_paquete, paquete)) continue;

        tabla[i].handler(ctx);
        return 1;
    }
    return 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 static parseInt(const char *s)
{
    uint16_t value = 0;
    while (*s >= '0' && *s <= '9') value = value * 10 + (*s++ - '0');
    return value;
}

static void enviar_paquete(const char *paquete){
    DEBUG_PRINT(printf("ENVIANDO %s\n", paquete));
#ifdef _WIN32
    send(sockfd, paquete, strlen(paquete), 0);
#else
    send(sockfd, paquete, strlen(paquete), MSG_NOSIGNAL);
#endif
}


static void handle_HI(TCCO_ContextoPaquete *ctx){
    printf("Conexión satisfactoria. Intentando unir a mesa 0...\n");
    enviar_paquete("UM 0\n");
    estado = ESTADO_EN_LOBBY;
}

static void handle_UM(TCCO_ContextoPaquete *ctx){
    printf("Unido a mesa %s.\n", ctx->args[0]);

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

    printf("Asiento %u asignado.\n", asiento_i+1);
    numero_jugador = asiento_i;

    estado = ESTADO_EN_SALA;

    SET_BIT(asientos_ocupados, asiento_i);
}

static void handle_NJ(TCCO_ContextoPaquete *ctx){
    uint8_t asiento_i = parseInt(ctx->args[0]);
    printf("Nuevo jugador en asiento %u.\n", parseInt(ctx->args[0])+1);
    SET_BIT(asientos_ocupados, asiento_i);
}

static void handle_A(TCCO_ContextoPaquete *ctx){
    uint8_t host = parseInt(ctx->args[0]);
    if(host == numero_jugador){
        printf("Eres el anfitrión de la sala.\n");
        es_host = 1;
        printf("1: Empezar partida\n");
    }else{
        printf("Jugador %u es el anfitrión de la sala.\n", host+1);
        es_host = 0;
    } 
}

static void handle_SJ(TCCO_ContextoPaquete *ctx){
    uint8_t asiento_i = parseInt(ctx->args[0]);
    printf("Jugador en asiento %u se fue de la sala.\n", asiento_i+1);
    CLEAR_BIT(asientos_ocupados, asiento_i);
}

static void handle_EP(TCCO_ContextoPaquete *ctx){
    //ep deberia recibir tambien el numero de jugadores?
    estado = ESTADO_EN_PARTIDA;

    estado_partida = (EstadoPartida){0};
    estado_partida.numero_jugadores = TCIC_obtener_jugadores_en_sala();
    limpiar_ronda(&estado_partida);
    estado_partida.jugador_mano = parseInt(ctx->args[0]);
}

static void handle_NR(TCCO_ContextoPaquete *ctx){
    limpiar_ronda(&estado_partida);
}

static void handle_MC(TCCO_ContextoPaquete *ctx){
    for(uint8_t i = 0; i < ctx->numero_args; i++){
        estado_partida.jugadores_cartas[numero_jugador][i] = parseInt(ctx->args[i]);
    }    
    // TCIC_render_cartas_disponibles_jugador(&estado_partida, numero_jugador, 0);
}

static void handle_TB(TCCO_ContextoPaquete *ctx){
    estado_partida.jugador_turno_actual = parseInt(ctx->args[0]);
    estado_partida.ronda_estado = RONDA_ESTADO_JUGANDO_BAZA;
}

static void handle_TC(TCCO_ContextoPaquete *ctx){
    estado_partida.jugador_turno_canto = parseInt(ctx->args[0]);
    estado_partida.ronda_estado = RONDA_ESTADO_EN_CANTO;
}

static void handle_TT(TCCO_ContextoPaquete *ctx){
    estado_partida.jugador_turno_canto = parseInt(ctx->args[0]);
    estado_partida.ronda_estado = RONDA_ESTADO_RONDA_TANTOS;
}

static void handle_JC(TCCO_ContextoPaquete *ctx){
    uint8_t jugador_i = parseInt(ctx->args[0]);
    uint8_t carta_jugada_id = parseInt(ctx->args[1]);

    if(jugador_i == numero_jugador){
        for(uint8_t i = 0; i < 3; i++){
            if(estado_partida.jugadores_cartas[numero_jugador][i] != carta_jugada_id) continue;
            setear_carta_jugada_de_mano_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, jugador_i, i);
            return;
        } 
    }

    setear_carta_jugada_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, jugador_i, carta_jugada_id);
}

static void handle_C(TCCO_ContextoPaquete *ctx){
    setear_canto_realizado_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, parseInt(ctx->args[1]), parseInt(ctx->args[0]));
}

static void handle_CT(TCCO_ContextoPaquete *ctx){
    setear_mayor_envido_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, parseInt(ctx->args[0]), parseInt(ctx->args[1]));
}

static void handle_B(TCCO_ContextoPaquete *ctx){
    estado_partida.baza_actual = parseInt(ctx->args[0]);
}

static void handle_FC(TCCO_ContextoPaquete *ctx){
   setear_canto_flag_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, parseInt(ctx->args[0]));
}


static void handle_NE(TCCO_ContextoPaquete *ctx){
    deshabilitar_envido(&estado_partida, &ctx->resultado_jugada);
}

static void handle_TR(TCCO_ContextoPaquete *ctx){
    setear_tiene_retruco_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, parseInt(ctx->args[0]));
}

static void handle_SP(TCCO_ContextoPaquete *ctx){
    sumar_puntos_en_partida_y_resultado(&estado_partida, &ctx->resultado_jugada, parseInt(ctx->args[0]), parseInt(ctx->args[1]));
}

static void handle_TP(TCCO_ContextoPaquete *ctx){
    setear_victoria_y_ganador_en_resultado(&ctx->resultado_jugada, parseInt(ctx->args[0]));
}

static void handle_EM(TCCO_ContextoPaquete *ctx){
    SET_BIT(ctx->resultado_jugada.eventos, EVENTO_ENVIDO_EN_MESA);
    for(uint8_t i = 0; i < ctx->numero_args; i++) ctx->resultado_jugada.cartas_mayor_envido[i] = parseInt(ctx->args[i]);
    ctx->resultado_jugada.mayor_envido = estado_partida.mayor_envido;
}

TCCO_PaqueteManejador paquetesConectandoTablaDespacho[] = {
    {"HI", handle_HI},
};

static void handle_X(TCCO_ContextoPaquete *ctx){}

TCCO_PaqueteManejador paquetesSalaTablaDespacho[] = {
    {"UM", handle_UM},
    {"NJ", handle_NJ},
    {"A", handle_A},
    {"SJ", handle_SJ},
    {"EP", handle_EP},
};

TCCO_PaqueteManejador paquetesPartidaTablaDespacho[] = {
    {"NR", handle_NR},
    {"MC", handle_MC},
    {"JC", handle_JC},
    {"C", handle_C},
    {"CT", handle_CT},
    {"B", handle_B},
    {"FC", handle_FC},
    {"NE", handle_NE},
    {"TR", handle_TR},
    {"SP", handle_SP},
    {"TP", handle_TP},
    {"EM", handle_EM},
};

TCCO_PaqueteManejador paquetesTurnoTablaDespacho[] = {
    {"TB", handle_TB},
    {"TC", handle_TC},
    {"TT", handle_TT},
    {"X", handle_X},
};

void TCCO_procesar_paquete(char *paquete){
    char *p = paquete;
    char *start = p;
    uint8_t numero_args = 0;
    char argsStorage[PAQUETE_MAX_ARGS][1] = {{0}};
    char *args[PAQUETE_MAX_ARGS] = { 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'){
            *p = '\0';
            if(!firstToken) args[numero_args++] = start;
            else firstToken = 0;
            break;
        }
        if (*p == ' ') {
            *p = '\0';
            if(!firstToken) args[numero_args++] = start;
            else firstToken = 0;
            start = p + 1;
            if(numero_args == PAQUETE_MAX_ARGS) break;
        }
        p++;
    }

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

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

    TCCO_ContextoPaquete ctx = {
        .args = args,
        .numero_args = numero_args,
        .resultado_jugada = (ResultadoJugada){0},
    };

    recorrer_tabla_despacho_comparar_y_manejar(paquetesConectandoTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesConectandoTablaDespacho));
    // recorrer_tabla_despacho_comparar_y_manejar(paquetesGeneralesTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesGeneralesTablaDespacho)); 
    recorrer_tabla_despacho_comparar_y_manejar(paquetesSalaTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesSalaTablaDespacho));
    recorrer_tabla_despacho_comparar_y_manejar(paquetesPartidaTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesPartidaTablaDespacho));

    if(estado != ESTADO_EN_PARTIDA) return;

    uint8_t viene_paquete_turno = recorrer_tabla_despacho_comparar_y_manejar(paquetesTurnoTablaDespacho, paquete, &ctx, ARRAY_SIZE(paquetesTurnoTablaDespacho));

    if(ctx.resultado_jugada.eventos != 0) TCIC_render_mensaje_por_resultado(&ctx.resultado_jugada, &estado_partida, 0);

    if(!viene_paquete_turno) return;

    TCIC_anunciar_nuevo_turno_y_mostrar_controles_si_es_jugador_indicado(&estado_partida, numero_jugador, 0);
}

uint8_t TCCO_conectar(char *ip, uint16_t puerto) {
#ifdef _WIN32
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        fprintf(stderr, "Error inicializando Winsock\n");
        return 1;
    }
#endif

    uint8_t estado_error = 0;
    struct addrinfo hints = {0}, *result = NULL, *rp;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    char puerto_str[6];
    snprintf(puerto_str, sizeof(puerto_str), "%u", (unsigned)puerto);

    int err = getaddrinfo(ip, puerto_str, &hints, &result);
    if (err != 0) {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(err));
        estado_error = 1;
    }

    int conectado = 0;
    if (!estado_error) {
        for (rp = result; rp; rp = rp->ai_next) {
            sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
            if (sockfd == INVALID_SOCKET) continue;

            if (connect(sockfd, rp->ai_addr, (socklen_t)rp->ai_addrlen) == 0) {
                conectado = 1;
                break;
            }

            closesocket(sockfd);
        }

        freeaddrinfo(result);

        if (!conectado) {
            fprintf(stderr, "No se pudo conectar al servidor\n");
            estado_error = 1;
        }
    }

    if (!estado_error) {
        enviar_paquete("TCCHI\n");

        time_t ultimo_latido = time(NULL);
        char paquete[MAX_TAMANO_PAQUETE];
        size_t usado = 0;
        int continuar = 1;

        while (continuar) {
            time_t ahora = time(NULL);

            if (ahora - ultimo_latido >= 90) {
                enviar_paquete("L\n");
                ultimo_latido = ahora;
            }

            fd_set lectura;
            FD_ZERO(&lectura);
            FD_SET(sockfd, &lectura);

#ifndef _WIN32
            FD_SET(STDIN_FILENO, &lectura);

            int maxfd = ((int)sockfd > STDIN_FILENO) ? (int)sockfd : STDIN_FILENO;
#else
            int maxfd = 0; /* Winsock ignora nfds */
#endif

            struct timeval tv = {0, 100000};

            int ret = select(maxfd + 1, &lectura, NULL, NULL, &tv);

            if (ret < 0) {
#ifdef _WIN32
                int e = WSAGetLastError();
                if (e == WSAEINTR)
                    continue;

                fprintf(stderr, "select error: %d\n", e);
#else
                if (errno == EINTR)
                    continue;

                perror("select");
#endif
                estado_error = 1;
                continuar = 0;
                continue;
            }

#ifndef _WIN32
            if (ret > 0 && FD_ISSET(STDIN_FILENO, &lectura)) {
                char input_terminal[256];

                if (fgets(input_terminal, sizeof(input_terminal), stdin)) {
                    uint8_t input_n = parseInt(input_terminal);

                    if (input_n)TCIC_manejar_accion_menu(input_n);
                }
            }
#endif

#ifdef _WIN32
            /*
             * Winsock select() no monitorea stdin.
             * Se revisa el teclado cuando select expira.
             */
            if (ret == 0 && _kbhit()) {
                char input_terminal[256];

                if (fgets(input_terminal, sizeof(input_terminal), stdin)) {
                    uint8_t input_n = parseInt(input_terminal);

                    if (input_n) TCIC_manejar_accion_menu(input_n);
                }
            }
#endif

            if (!continuar || ret == 0)
                continue;

            if (!FD_ISSET(sockfd, &lectura))
                continue;

            char recibido[256];

            int n = recv(sockfd, recibido, (int)sizeof(recibido), 0);

            if (n == 0) {
                printf("Conexión cerrada por el servidor.\n");
                continuar = 0;
                continue;
            }

            if (n < 0) {
#ifdef _WIN32
                int e = WSAGetLastError();

                if (e == WSAEINTR)
                    continue;

                fprintf(stderr, "recv error: %d\n", e);
#else
                if (errno == EINTR)
                    continue;

                perror("recv");
#endif
                estado_error = 1;
                continuar = 0;
                continue;
            }

            for (int i = 0; i < n && continuar; i++) {
                if (recibido[i] == '\n') {
                    paquete[usado] = '\0';

                    if (usado > 0)
                        TCCO_procesar_paquete(paquete);

                    usado = 0;
                } else {
                    if (usado >= sizeof(paquete) - 1) {
                        fprintf(stderr, "Paquete demasiado grande\n");
                        estado_error = 1;
                        continuar = 0;
                    } else {
                        paquete[usado++] = recibido[i];
                    }
                }
            }
        }
    }


    if (sockfd != INVALID_SOCKET) {
        closesocket(sockfd);
        sockfd = INVALID_SOCKET;
    }
#ifdef _WIN32
    WSACleanup();
#endif


    return estado_error;
}

ResultadoJugada TCIC_jugar_baza(EstadoPartida *partida, uint8_t jugador_i, uint8_t carta_i, uint16_t partida_i){
    char jcBuffer[sizeof("JC 00\n")];
    sprintf(jcBuffer, "JC %" PRIu8 "\n", estado_partida.jugadores_cartas[numero_jugador][carta_i]);

    enviar_paquete(jcBuffer);

    return (ResultadoJugada){0};
}

ResultadoJugada TCIC_cantar(EstadoPartida *partida, uint8_t jugador_i, uint8_t canto, uint16_t partida_i){
    char cBuffer[sizeof("C 00\n")];
    sprintf(cBuffer, "C %" PRIu8 "\n", canto);

    enviar_paquete(cBuffer);

    return (ResultadoJugada){0};
}

ResultadoJugada TCIC_cantar_tanto(EstadoPartida *partida, uint8_t jugador_i, uint16_t partida_i){
    char ctBuffer[sizeof("CT 00\n")];
    sprintf(ctBuffer, "CT %" PRIu8 "\n", calcular_envido(&estado_partida, numero_jugador).valor_tantos);
    
    enviar_paquete(ctBuffer);

    return (ResultadoJugada){0};
}

void TCIC_empezar_partida(){
    enviar_paquete("EP\n");
}

void TCIC_cambiar_asiento(uint8_t asiento_i){
    char caBuffer[sizeof("CA 0\n")];
    sprintf(caBuffer, "CA %" PRIu8 "\n", asiento_i);
    
    enviar_paquete(caBuffer);
}

void TCIC_cambiar_asiento_otro(uint8_t asiento_1, uint8_t asiento_2){
    char caBuffer[sizeof("CA 0 0\n")];
    sprintf(caBuffer, "CA %" PRIu8 " %" PRIu8 "\n", asiento_1, asiento_2);
    
    enviar_paquete(caBuffer);
}

void TCIC_dar_anfitrion(uint8_t jugador_i){
    char aBuffer[sizeof("A 0\n")];
    sprintf(aBuffer, "CT %" PRIu8 "\n", jugador_i);

    enviar_paquete(aBuffer);
}