/*
 * Copyright (c) 2015, EURECOM (www.eurecom.fr)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those
 * of the authors and should not be interpreted as representing official policies,
 * either expressed or implied, of the FreeBSD Project.
 */


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>

#define G_LOG_DOMAIN ("BUFFERS")

#include <gtk/gtk.h>

#include "logs.h"
#include "itti_types.h"
#include "rc.h"

#include "ui_interface.h"
#include "ui_notifications.h"
#include "ui_notif_dlg.h"

#include "socket.h"
#include "buffers.h"

#include "xml_parse.h"

/* Retry connection after 100 ms */
#define SOCKET_US_BEFORE_CONNECT_RETRY      (100 * 1000)
/* About 10 minutes time-out for connecting to peer */
#define SOCKET_NB_CONNECT_RETRY             ((10 * 60 * 1000 * 1000) / SOCKET_US_BEFORE_CONNECT_RETRY)

#define SOCKET_NB_SIGNALS_BEFORE_SIGNALLING 10
#define SOCKET_MS_BEFORE_SIGNALLING         100

gboolean socket_abort_connection = FALSE;

void *socket_thread_fct(void *arg);

static ssize_t socket_read_data(socket_data_t *socket_data, void *buffer, size_t size, int flags)
{
    ssize_t recv_ret;

    recv_ret = recv(socket_data->sd, buffer, size, flags);
    if (recv_ret == -1) {
        /* Failure case */
        switch (errno) {
//             case EWOULDBLOCK:
            case EAGAIN:
                return -1;
            default:
                g_info("recv failed: %s", g_strerror(errno));
                pthread_exit(NULL);
                break;
        }
    } else if (recv_ret == 0) {
        /* We lost the connection with other peer or shutdown asked */
        ui_pipe_write_message(socket_data->pipe_fd,
                              UI_PIPE_CONNECTION_LOST, NULL, 0);
        free(socket_data->ip_address);
        free(socket_data);
        pthread_exit(NULL);
    }

    return recv_ret;
}

static void socket_notify_gui_update(socket_data_t *socket_data)
{
    pipe_new_signals_list_message_t pipe_signal_list_message;

    pipe_signal_list_message.signal_list = socket_data->signal_list;

    socket_data->signal_list                  = NULL;
    socket_data->nb_signals_since_last_update = 0;

    /* Send an update notification */
    ui_pipe_write_message(socket_data->pipe_fd,
                          UI_PIPE_UPDATE_SIGNAL_LIST, &pipe_signal_list_message,
                          sizeof(pipe_signal_list_message));

    /* Acquire the last data notification */
    socket_data->last_data_notification = g_get_monotonic_time();
}

static int socket_read_itti_message(socket_data_t        *socket_data,
                                    itti_socket_header_t *message_header)
{
    itti_signal_header_t  itti_signal_header;
    buffer_t             *buffer;
    uint8_t              *data;
    size_t                data_length;
    ssize_t               data_read = 0;
    ssize_t               total_data_read = 0;

    g_assert(message_header != NULL);

    g_debug("Attempting to read signal header from socket");

    /* Read the sub-header of signal */
    while (data_read != sizeof(itti_signal_header_t)) {
        data_read = socket_read_data(socket_data, &itti_signal_header,
                                       sizeof(itti_signal_header_t), 0);
    }

    data_length = message_header->message_size - sizeof(itti_socket_header_t) - sizeof(itti_signal_header_t);
    data = malloc(sizeof(uint8_t) * data_length);

    while (total_data_read < data_length) {
        data_read = socket_read_data(socket_data, &data[total_data_read],
                                     data_length - total_data_read, 0);
        /* We are waiting for data */
        if (data_read < 0) {
            usleep(10);
        } else {
            total_data_read += data_read;
        }
    }

    /* Create the new buffer */
    if (buffer_new_from_data(&buffer, data, data_length - sizeof(itti_message_types_t), 1) != RC_OK) {
        g_error("Failed to create new buffer");
        g_assert_not_reached();
    }

    sscanf (itti_signal_header.message_number_char, MESSAGE_NUMBER_CHAR_FORMAT, &buffer->message_number);
//     buffer_dump(buffer, stdout);

    /* Update the number of signals received since last GUI update */
    socket_data->nb_signals_since_last_update++;

    socket_data->signal_list = g_list_append(socket_data->signal_list, (gpointer)buffer);

    if (socket_data->nb_signals_since_last_update >= SOCKET_NB_SIGNALS_BEFORE_SIGNALLING) {
        socket_notify_gui_update(socket_data);
    }

    g_debug("Successfully read new signal %u from socket", buffer->message_number);

    return total_data_read + sizeof(itti_signal_header);
}

static int socket_read_xml_definition(socket_data_t *socket_data,
                                      itti_socket_header_t *message_header)
{
    ssize_t                        data_read;
    ssize_t                        total_data_read = 0;
    char                          *xml_definition;
    size_t                         xml_definition_length;
    pipe_xml_definition_message_t  pipe_xml_definition_message;

    xml_definition_length = message_header->message_size - sizeof(*message_header);
    xml_definition        = malloc(xml_definition_length * sizeof(char));

    g_debug("Attempting to read XML definition of size %zu from socket",
            xml_definition_length);

    /* XML definition is a long message... so function may take some time */

    do {
        data_read = socket_read_data(socket_data, &xml_definition[total_data_read],
                                     xml_definition_length - total_data_read, 0);

        /* We are waiting for data */
        if (data_read < 0) {
            usleep(10);
        } else {
            total_data_read += data_read;
        }
    } while (total_data_read != xml_definition_length);

    pipe_xml_definition_message.xml_definition        = xml_definition;
    pipe_xml_definition_message.xml_definition_length = xml_definition_length - sizeof(itti_message_types_t);

    g_debug("Received XML definition of size %zu, effectively read %zu bytes",
            xml_definition_length, total_data_read);

    ui_pipe_write_message(socket_data->pipe_fd, UI_PIPE_XML_DEFINITION,
                          &pipe_xml_definition_message, sizeof(pipe_xml_definition_message));

    return total_data_read;
}

static int socket_read(socket_data_t *socket_data)
{
    int ret = 0;
    itti_socket_header_t message_header;

    while (ret >= 0) {
        ret = socket_read_data(socket_data, &message_header, sizeof(message_header), 0);

        if (ret == -1) {
            return 0;
        }

        switch(message_header.message_type) {
            case ITTI_DUMP_XML_DEFINITION:
                socket_read_xml_definition(socket_data, &message_header);
                break;

            case ITTI_DUMP_MESSAGE_TYPE:
                socket_read_itti_message(socket_data, &message_header);
                break;

            case ITTI_STATISTIC_MESSAGE_TYPE:
            default:
                g_warning("Received unknow (or not implemented) message from socket type: %d",
                          message_header.message_type);
                break;
        }
    }

    return 0;
}

static int socket_handle_disconnect_evt(socket_data_t *socket_data)
{
    /* Send shutdown to remote host */
    CHECK_FCT_POSIX(shutdown(socket_data->sd, SHUT_RDWR));
    /* Close file descriptor */
    CHECK_FCT_POSIX(close(socket_data->sd));

    socket_data->sd = -1;

    /* Close pipe */
    close(socket_data->pipe_fd);

    /* Leaving the thread */
    pthread_exit(NULL);

    return 0;
}

static int pipe_read_message(socket_data_t *socket_data)
{
    pipe_input_header_t  input_header;
    uint8_t             *input_data = NULL;
    size_t               input_data_length = 0;

    /* Read the header */
    if (read(socket_data->pipe_fd, &input_header, sizeof(input_header)) < 0) {
        g_warning("Failed to read from pipe %d: %s", socket_data->pipe_fd,
                  g_strerror(errno));
        return -1;
    }

    input_data_length = input_header.message_size - sizeof(input_header);

    /* Checking for non-header part */
    if (input_data_length > 0) {
        input_data = malloc(sizeof(uint8_t) * input_data_length);

        if (read(socket_data->pipe_fd, input_data, input_data_length) < 0) {
            g_warning("Failed to read from pipe %d: %s", socket_data->pipe_fd,
                      g_strerror(errno));
            return -1;
        }
    }

    switch (input_header.message_type) {
        case UI_PIPE_DISCONNECT_EVT:
            return socket_handle_disconnect_evt(socket_data);
        default:
            g_warning("[socket] Unhandled message type %u", input_header.message_type);
            g_assert_not_reached();
    }
    return 0;
}

void *socket_thread_fct(void *arg)
{
    int                 ret;
    struct sockaddr_in  si_me;
    socket_data_t      *socket_data;
    int                 retry = SOCKET_NB_CONNECT_RETRY;

    /* master file descriptor list */
    fd_set              master_fds;
    /* temp file descriptor list for select() */
    fd_set              read_fds;
    int                 fd_max = 0;
    struct timeval      tv;

    socket_data = (socket_data_t *)arg;

    g_assert(socket_data != NULL);

    /* Preparing the socket */
    if ((socket_data->sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
        g_warning("socket failed: %s", g_strerror(errno));
        free(socket_data->ip_address);
        free(socket_data);
        pthread_exit(NULL);
    }
    memset((void *)&si_me, 0, sizeof(si_me));

    si_me.sin_family = AF_INET;
    si_me.sin_port = htons(socket_data->port);
    if (inet_aton(socket_data->ip_address, &si_me.sin_addr) == 0) {
        g_warning("inet_aton() failed\n");
        free(socket_data->ip_address);
        free(socket_data);
        pthread_exit(NULL);
    }

    /* clear the master and temp sets */
    FD_ZERO(&master_fds);
    FD_ZERO(&read_fds);

    /* Add the GUI pipe to the list of sockets to monitor */
    FD_SET(socket_data->pipe_fd, &master_fds);

    /* Add the client socket to the list of sockets to monitor */
    FD_SET(socket_data->sd, &master_fds);

    /* Update the fd_max with the MAX of socket/pipe */
    fd_max = MAX(socket_data->pipe_fd, socket_data->sd);

    /* Setup the timeout for select.
     * When a timeout is caught, check for new notifications to send to GUI.
     */
    tv.tv_sec = 0;
    tv.tv_usec = 1000 * SOCKET_MS_BEFORE_SIGNALLING;

    do {
        /* Connecting to remote peer */
        ret = connect(socket_data->sd, (struct sockaddr *) &si_me, sizeof(struct sockaddr_in));
        if (ret < 0) {
            if ((socket_abort_connection) || (retry < 0)) {
                if (retry < 0) {
                    g_warning("Failed to connect to peer %s:%d", socket_data->ip_address, socket_data->port);
                    ui_pipe_write_message(socket_data->pipe_fd, UI_PIPE_CONNECTION_FAILED, NULL, 0);
                }
                free(socket_data->ip_address);
                free(socket_data);
                socket_abort_connection = FALSE;
                /* Quit the thread */
                pthread_exit(NULL);
            }
            usleep(SOCKET_US_BEFORE_CONNECT_RETRY);
            retry--;
        }
    } while (ret < 0);

    /* Set the socket as non-blocking */
    fcntl(socket_data->sd, F_SETFL, O_NONBLOCK);

    while (1) {
        memcpy(&read_fds, &master_fds, sizeof(master_fds));

        ret = select(fd_max + 1, &read_fds, NULL, NULL, &tv);
        if (ret < 0) {
            g_warning("Error in select: %s", g_strerror(errno));
            free(socket_data->ip_address);
            free(socket_data);
            /* Quit the thread */
            pthread_exit(NULL);
        } else if (ret == 0) {
            /* Timeout for select: check if there is new incoming messages
             * since last GUI update
             */
            if (socket_data->nb_signals_since_last_update > 0) {
                g_debug("Timout on select and data new signal in list");
                g_debug("-> notify GUI");
                socket_notify_gui_update(socket_data);
            }

            /* Reset the timeval to the max value */
            tv.tv_usec = 1000 * SOCKET_MS_BEFORE_SIGNALLING;
        }

        /* Checking if there is data to read from the pipe */
        if (FD_ISSET(socket_data->pipe_fd, &read_fds)) {
            FD_CLR(socket_data->pipe_fd, &read_fds);
            pipe_read_message(socket_data);
        }

        /* Checking if there is data to read from the socket */
        if (FD_ISSET(socket_data->sd, &read_fds)) {
            FD_CLR(socket_data->sd, &read_fds);
            socket_read(socket_data);

            /* Update the timeout of select if there is data not notify to GUI */
            if (socket_data->nb_signals_since_last_update > 0) {
                gint64 current_time;

                current_time = g_get_monotonic_time();

                if ((current_time - socket_data->last_data_notification) > SOCKET_MS_BEFORE_SIGNALLING) {
                    socket_notify_gui_update(socket_data);
                    tv.tv_usec = 1000 * SOCKET_MS_BEFORE_SIGNALLING;
                } else {
                    /* Update tv */
                    tv.tv_usec = (1000 * SOCKET_MS_BEFORE_SIGNALLING) - (current_time - socket_data->last_data_notification);
                }
            }
        }
    }

    return NULL;
}

int socket_connect_to_remote_host(const char *remote_ip, const uint16_t port,
                                  int pipe_fd)
{
    socket_data_t *socket_data;

    socket_data = calloc(1, sizeof(*socket_data));

    socket_data->ip_address = strdup(remote_ip);

    socket_data->pipe_fd = pipe_fd;
    socket_data->port    = port;
    socket_data->sd      = -1;

    if (pthread_create(&socket_data->thread, NULL, socket_thread_fct, socket_data) != 0) {
        g_warning("Failed to create thread %d:%s", errno, strerror(errno));
        free(socket_data->ip_address);
        free(socket_data);
        return RC_FAIL;
    }

    return RC_OK;
}
