#include <stdio.h>
#include <assert.h>

#include "log_private.h"
#include "session_private.h"
#include "session_transition.h"

#define SESSION_TRANSITION_LOG_INFO(format, ...) STELLAR_LOG_INFO(__thread_local_logger, "session transition", format, ##__VA_ARGS__)

#define MAX_TRANSITION_PER_STATE 8

struct session_transition
{
    int inputs_mask;
    enum session_state next_state;
} fsm[MAX_STATE][MAX_TRANSITION_PER_STATE];

/*
 * session transition
 *
 *   SESSION_STATE_INIT    -> SESSION_STATE_INIT            ( NONE )
 *   SESSION_STATE_INIT    -> SESSION_STATE_OPENING         ( TCP_SYN | TCP_SYN_ACK | UDP_DATA )
 *
 *   SESSION_STATE_OPENING -> SESSION_STATE_OPENING         ( NONE )
 *   SESSION_STATE_OPENING -> SESSION_STATE_ACTIVE          ( TCP_DATA | UDP_DATA )
 *   SESSION_STATE_OPENING -> SESSION_STATE_CLOSING         ( TCP_FIN | TCP_RST | TIMEOUT )
 *   SESSION_STATE_OPENING -> SESSION_STATE_DISCARD         ( USER_CLOSE )
 *   SESSION_STATE_OPENING -> SESSION_STATE_CLOSED          ( LRU_EVICT | PORT_REUSE_EVICT )
 *
 *   SESSION_STATE_ACTIVE  -> SESSION_STATE_ACTIVE          ( NONE )
 *   SESSION_STATE_ACTIVE  -> SESSION_STATE_CLOSING         ( TCP_FIN | TCP_RST | TIMEOUT )
 *   SESSION_STATE_ACTIVE  -> SESSION_STATE_DISCARD         ( USER_CLOSE )
 *   SESSION_STATE_ACTIVE  -> SESSION_STATE_CLOSED          ( LRU_EVICT | PORT_REUSE_EVICT)
 *
 *   SESSION_STATE_CLOSING -> SESSION_STATE_CLOSING         ( NONE )
 *   SESSION_STATE_CLOSING -> SESSION_STATE_DISCARD         ( USER_CLOSE )
 *   SESSION_STATE_CLOSING -> SESSION_STATE_CLOSED          ( LRU_EVICT | PORT_REUSE_EVICT | TIMEOUT )
 *
 *   SESSION_STATE_DISCARD  -> SESSION_STATE_DISCARD        ( NONE )
 *   SESSION_STATE_DISCARD  -> SESSION_STATE_CLOSED         ( LRU_EVICT | PORT_REUSE_EVICT | TIMEOUT )
 */

static void session_inputs_to_str(int inputs, char *buff, int len)
{
    int nused = 0;
    if (inputs & TCP_SYN)
    {
        nused += snprintf(buff + nused, len - nused, "TCP_SYN ");
    }
    if (inputs & TCP_SYN_ACK)
    {
        nused += snprintf(buff + nused, len - nused, "TCP_SYN_ACK ");
    }
    if (inputs & TCP_FIN)
    {
        nused += snprintf(buff + nused, len - nused, "TCP_FIN ");
    }
    if (inputs & TCP_RST)
    {
        nused += snprintf(buff + nused, len - nused, "TCP_RST ");
    }
    if (inputs & TCP_DATA)
    {
        nused += snprintf(buff + nused, len - nused, "TCP_DATA ");
    }
    if (inputs & UDP_DATA)
    {
        nused += snprintf(buff + nused, len - nused, "UDP_DATA ");
    }
    if (inputs & TIMEOUT)
    {
        nused += snprintf(buff + nused, len - nused, "TIMEOUT ");
    }
    if (inputs & LRU_EVICT)
    {
        nused += snprintf(buff + nused, len - nused, "LRU_EVICT ");
    }
    if (inputs & PORT_REUSE_EVICT)
    {
        nused += snprintf(buff + nused, len - nused, "PORT_REUSE_EVICT ");
    }
    if (inputs & USER_CLOSE)
    {
        nused += snprintf(buff + nused, len - nused, "USER_CLOSE ");
    }
}

void session_transition_init()
{
    fsm[SESSION_STATE_INIT][0].inputs_mask = TCP_SYN | TCP_SYN_ACK | UDP_DATA;
    fsm[SESSION_STATE_INIT][0].next_state = SESSION_STATE_OPENING;

    fsm[SESSION_STATE_INIT][1].inputs_mask = NONE;
    fsm[SESSION_STATE_INIT][1].next_state = SESSION_STATE_INIT;

    fsm[SESSION_STATE_OPENING][0].inputs_mask = TCP_DATA | UDP_DATA;
    fsm[SESSION_STATE_OPENING][0].next_state = SESSION_STATE_ACTIVE;

    fsm[SESSION_STATE_OPENING][1].inputs_mask = TCP_FIN | TCP_RST | TIMEOUT;
    fsm[SESSION_STATE_OPENING][1].next_state = SESSION_STATE_CLOSING;

    fsm[SESSION_STATE_OPENING][2].inputs_mask = USER_CLOSE;
    fsm[SESSION_STATE_OPENING][2].next_state = SESSION_STATE_DISCARD;

    fsm[SESSION_STATE_OPENING][3].inputs_mask = LRU_EVICT | PORT_REUSE_EVICT;
    fsm[SESSION_STATE_OPENING][3].next_state = SESSION_STATE_CLOSED;

    fsm[SESSION_STATE_OPENING][4].inputs_mask = NONE;
    fsm[SESSION_STATE_OPENING][4].next_state = SESSION_STATE_OPENING;

    fsm[SESSION_STATE_ACTIVE][0].inputs_mask = TCP_FIN | TCP_RST | TIMEOUT;
    fsm[SESSION_STATE_ACTIVE][0].next_state = SESSION_STATE_CLOSING;

    fsm[SESSION_STATE_ACTIVE][1].inputs_mask = USER_CLOSE;
    fsm[SESSION_STATE_ACTIVE][1].next_state = SESSION_STATE_DISCARD;

    fsm[SESSION_STATE_ACTIVE][2].inputs_mask = LRU_EVICT | PORT_REUSE_EVICT;
    fsm[SESSION_STATE_ACTIVE][2].next_state = SESSION_STATE_CLOSED;

    fsm[SESSION_STATE_ACTIVE][3].inputs_mask = NONE;
    fsm[SESSION_STATE_ACTIVE][3].next_state = SESSION_STATE_ACTIVE;

    fsm[SESSION_STATE_CLOSING][0].inputs_mask = USER_CLOSE;
    fsm[SESSION_STATE_CLOSING][0].next_state = SESSION_STATE_DISCARD;

    fsm[SESSION_STATE_CLOSING][1].inputs_mask = LRU_EVICT | PORT_REUSE_EVICT | TIMEOUT;
    fsm[SESSION_STATE_CLOSING][1].next_state = SESSION_STATE_CLOSED;

    fsm[SESSION_STATE_CLOSING][2].inputs_mask = NONE;
    fsm[SESSION_STATE_CLOSING][2].next_state = SESSION_STATE_CLOSING;

    fsm[SESSION_STATE_DISCARD][0].inputs_mask = LRU_EVICT | PORT_REUSE_EVICT | TIMEOUT;
    fsm[SESSION_STATE_DISCARD][0].next_state = SESSION_STATE_CLOSED;

    fsm[SESSION_STATE_DISCARD][1].inputs_mask = NONE;
    fsm[SESSION_STATE_DISCARD][1].next_state = SESSION_STATE_DISCARD;
}

enum session_state session_transition_run(enum session_state curr_state, int inputs)
{
    struct session_transition *list = fsm[curr_state];
    for (int i = 0; i < MAX_TRANSITION_PER_STATE; i++)
    {
        int mask = list[i].inputs_mask;
        if (mask & inputs)
        {
            return list[i].next_state;
        }
        if (mask == NONE)
        {
            return list[i].next_state;
        }
    }

    assert(0);
    return curr_state;
}

void session_transition_log(struct session *sess, enum session_state curr_state, enum session_state next_state, int inputs)
{
    if (curr_state == next_state)
    {
        return;
    }

    char reason[128] = {0};
    session_inputs_to_str(inputs, reason, sizeof(reason));
    SESSION_TRANSITION_LOG_INFO("%s session %lu %s (%s) %s -> %s",
                                session_type_to_str(session_get_type(sess)),
                                session_get_id(sess), session_get0_readable_addr(sess), reason,
                                session_state_to_str(curr_state), session_state_to_str(next_state));
}