/* ====================================================================
 * FTD4Linux Software License, Version 1.0
 *
 * Copyright (c) 2000-2004 The FTD4Linux Development Team.  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.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        FTD4Linux Development Team (http://www.ftd4linux.2y.net/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "FTD" and "FTD4Linux" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact alguno@ftd4linux.2y.net.
 *
 * 5. Products derived from this software may not be called "FTD4Linux",
 *    nor may "FTD4Linux" appear in their name, without prior written
 *    permission of the FTD4Linux Development Team.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 FTD4LINUX DEVELOPMENT TEAM OR
 * ITS 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.
 */

#include <stdio.h>
#include <string.h>
#ifndef _MSC_VER
#include <unistd.h>
#endif
#include <errno.h>
#include <sys/types.h>
#ifdef WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <wspiapi.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>    /* sockaddr_in structure */
#include <netdb.h>          /* /etc/hosts table entries */
#endif
#include <glib.h>
#include <glib-object.h>
#include "ftd_irc_socket.h"

typedef struct FTD_IRCSocketClass FTD_IRCSocketClass;

FTD_IRCSocket *ftd_irc_get_socket(void);
GType ftd_irc_socket_get_type (void);

static gpointer socket_thread(gpointer data);

struct FTD_IRCSocket
{
    GObject parent;

    GStaticRWLock rwlock;
    GThread *thread;
    gboolean quit_loop;
    GAsyncQueue *send_queue;
    int socket_id;

    void (*callback) (const char *msg_data, int length, gboolean is_error, gpointer data);
    gpointer callback_data;
};

struct FTD_IRCSocketClass
{
    GObjectClass parent;
};

G_DEFINE_TYPE(FTD_IRCSocket, ftd_irc_socket, G_TYPE_OBJECT)

static void
ftd_irc_socket_init (FTD_IRCSocket *obj)
{
    g_static_rw_lock_init(&obj->rwlock);
    obj->quit_loop = FALSE;
    obj->send_queue = g_async_queue_new();
    obj->callback = NULL;
    obj->callback_data = NULL;
    obj->socket_id = -1;
    obj->thread = NULL;
}

static void
disconnect(FTD_IRCSocket *irc)
{
    g_static_rw_lock_reader_lock(&irc->rwlock);
    if (irc->thread == NULL) {
        g_static_rw_lock_reader_unlock(&irc->rwlock);
    } else {
        g_static_rw_lock_reader_unlock(&irc->rwlock);
        g_static_rw_lock_writer_lock(&irc->rwlock);
        irc->quit_loop = TRUE;
        g_static_rw_lock_writer_unlock(&irc->rwlock);

        g_thread_join(irc->thread);
        irc->thread = NULL;

        g_static_rw_lock_writer_lock(&irc->rwlock);
        close(irc->socket_id);
        irc->socket_id = -1;
        g_static_rw_lock_writer_unlock(&irc->rwlock);
    }
}

static void
irc_socket_finalize (GObject *obj)
{
    FTD_IRCSocket *irc = FTD_IRC_SOCKET(obj);

    disconnect(irc);

    g_async_queue_unref(irc->send_queue);
    g_static_rw_lock_free(&irc->rwlock);
}

static void
ftd_irc_socket_class_init (FTD_IRCSocketClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

    gobject_class->finalize = irc_socket_finalize;

#if 0
    signals[BRING_TO_FRONT_SIGNAL] = g_signal_new  ("bring_to_front",
                                                    G_OBJECT_CLASS_TYPE (klass),
                                                    G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
                                                    0,
                                                    NULL, NULL,
                                                    g_cclosure_marshal_VOID__VOID,
                                                    G_TYPE_NONE, 0);

    signals[OPEN_POSTING_SIGNAL] = g_signal_new    ("open_posting",
                                                    G_OBJECT_CLASS_TYPE (klass),
                                                    G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
                                                    0,
                                                    NULL, NULL,
                                                    g_cclosure_marshal_VOID__INT,
                                                    G_TYPE_NONE, 1, G_TYPE_INT);
#endif
}

FTD_IRCSocket *
ftd_irc_socket_new(void)
{
    return g_object_new(FTD_IRC_TYPE_SOCKET, NULL);
}

void
ftd_irc_socket_destroy(FTD_IRCSocket *obj)
{
    g_object_unref(obj);
}

struct _call_callback_data
{
    FTD_IRCSocket *irc;
    char *errmsg;
};

static gboolean
call_callback_func(gpointer data)
{
    struct _call_callback_data *callback_data = data;

    if (callback_data->irc->callback) {
        callback_data->irc->callback(callback_data->errmsg, strlen(callback_data->errmsg), TRUE, callback_data->irc->callback_data);
    }

    g_free(callback_data->errmsg);
    g_free(callback_data);

    return FALSE;
}

struct _send_msg
{
    char *data;
    int length;
};

static gpointer
socket_thread(gpointer data)
{
    FTD_IRCSocket *irc = FTD_IRC_SOCKET(data);
    gboolean quit_loop = FALSE;
    GArray *array = g_array_new(TRUE, TRUE, sizeof(char));
    char buf[1024];
    fd_set rfds;
    struct timeval tv;
    int retval;
    void (*callback) (const char *msg_data, int length, gboolean is_error, gpointer data);
    gpointer callback_data;
    int socket_id;
    struct _send_msg *msg;

    do {
        g_static_rw_lock_reader_lock(&irc->rwlock);
        quit_loop = irc->quit_loop;
        callback = irc->callback;
        callback_data = irc->callback_data;
        socket_id = irc->socket_id;
        g_static_rw_lock_reader_unlock(&irc->rwlock);

        FD_ZERO(&rfds);
        FD_SET(socket_id, &rfds);

        tv.tv_usec = 500000;
        tv.tv_sec = 0;

        retval = select(socket_id + 1, &rfds, NULL, NULL, &tv);
        if (retval == -1) {
            // Error opgetreden!
            struct _call_callback_data *data = g_malloc0(sizeof(struct _call_callback_data));
            data->errmsg = g_strdup_printf("select returned -1: %s", strerror(errno));
            data->irc = irc;
            g_idle_add(call_callback_func, data);

            return NULL;
        } else if (retval > 0) {
            // Er is data beschikbaar voor ons!
            char *eind_pos;
            size_t len;

            len = read(socket_id, buf, sizeof(buf));
            if (len <= 0) {
                // Error opgetreden!
                struct _call_callback_data *data = g_malloc0(sizeof(struct _call_callback_data));
                data->errmsg = g_strdup_printf("read returned -1: %s", strerror(errno));
                data->irc = irc;
                g_idle_add(call_callback_func, data);

                return NULL;
            }

            g_array_append_vals(array, buf, len);

            // Controleer of er ergens een \r of \n is ontdekt, zoja dan een regel
            // doorsturen naar de aanvrager
            eind_pos = strstr(array->data, "\n");
            while (eind_pos) {
                char *regel;

                regel = g_malloc0(eind_pos - array->data + 1);
                memcpy(regel, array->data, eind_pos - array->data);

                if (regel[strlen(regel) - 1] == '\n') {
                    regel[strlen(regel) -1] = '\0';
                }

                if (regel[strlen(regel) - 1] == '\r') {
                    regel[strlen(regel) - 1] = '\0';
                }

                if (callback) {
                    callback(regel, strlen(regel), FALSE, callback_data);
                }

                // Regel is verwerkt, verwijder deze uit de array
                g_array_remove_range(array, 0, (eind_pos - array->data) + 1);

                eind_pos = strstr(array->data, "\n");
            }
        }

        // Kijk of we iets moeten versturen naar de server
        g_static_rw_lock_reader_lock(&irc->rwlock);
        msg = g_async_queue_try_pop(irc->send_queue);
        g_static_rw_lock_reader_unlock(&irc->rwlock);

        if (msg) {
            // Ja we hebben data om te versturen
            if (write(socket_id, msg->data, msg->length) < 0) {
                // Error opgetreden!
                struct _call_callback_data *data = g_malloc0(sizeof(struct _call_callback_data));
                data->errmsg = g_strdup_printf("write returned -1: %s", strerror(errno));
                data->irc = irc;
                g_idle_add(call_callback_func, data);
            }
        }

        g_usleep(100000);
    } while (quit_loop == FALSE);

    return NULL;
}

void
ftd_irc_socket_send_msg(FTD_IRCSocket *obj, const char *data, int length)
{
    FTD_IRCSocket *irc = FTD_IRC_SOCKET(obj);
    struct _send_msg *msg;

    g_return_if_fail(length > 0);

    msg = g_malloc0(sizeof(struct _send_msg));
    msg->length = length;
    msg->data = g_malloc0(length);
    memcpy(msg->data, data, length);

    g_async_queue_push(irc->send_queue, msg);
}

void
ftd_irc_socket_set_read_callback(FTD_IRCSocket *obj, void (*callback) (const char *msg_data, int length, gboolean is_error, gpointer data), gpointer data)
{
    FTD_IRCSocket *irc = FTD_IRC_SOCKET(obj);

    g_static_rw_lock_writer_lock(&irc->rwlock);
    irc->callback = callback;
    irc->callback_data = data;
    g_static_rw_lock_writer_unlock(&irc->rwlock);
}

gboolean
ftd_irc_socket_connect(FTD_IRCSocket *obj, const char *host, int port, char **errmsg)
{
    struct addrinfo hints, *res, *ressave;
    int n;
    gboolean socket_success = FALSE;
    FTD_IRCSocket *irc = FTD_IRC_SOCKET(obj);
    GError *err = NULL;
    char *port_str;
    int socket_fd;

    g_return_val_if_fail(irc->thread == NULL, FALSE);
    g_return_val_if_fail(host != NULL, FALSE);
    g_return_val_if_fail(port > 0, FALSE);
    g_return_val_if_fail(port <= 65535, FALSE);

    memset(&hints, 0, sizeof(struct addrinfo));

    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    port_str = g_strdup_printf("%i", port);
    n = getaddrinfo(host, port_str, &hints, &res);
    g_free(port_str);

    /* Waarom moet windows nou weer alternatief doen ?? */
#ifdef WIN32
    if (n == WSAHOST_NOT_FOUND || n == WSATRY_AGAIN || n == WSANO_RECOVERY || n == WSANO_DATA) {
#else
    if (n < 0) {
#endif
        if (errmsg) {
            *errmsg = g_strdup_printf("getaddrinfo() FAILED: %s", gai_strerror(n));
        }

        return FALSE;
    }

    ressave = res;

    socket_fd = 0;
    while (res) {
        socket_fd = socket(res->ai_family,
                           res->ai_socktype,
                           res->ai_protocol);

#ifdef WIN32
        if (!(socket_fd == INVALID_SOCKET)) {
#else
        if (!(socket_fd == -1)) {
#endif
            socket_success = TRUE;
            if (connect(socket_fd, res->ai_addr, (int) res->ai_addrlen) == 0)
                break;

#ifdef WIN32
            closesocket(socket_fd);
#else
            close(socket_fd);
#endif
            socket_fd = 0;
        }

        res=res->ai_next;
    }

    if (socket_fd == 0) {
        freeaddrinfo(ressave);

        if (socket_success) {
            if (errmsg) {
                *errmsg = g_strdup_printf("connect() FAILED: %s", strerror(errno));
            }
        } else {
            if (errmsg) {
                *errmsg = g_strdup_printf("socket() FAILED: %s", strerror(errno));
            }
        }

        return FALSE;
    }

    freeaddrinfo(ressave);

#ifdef DEBUG
    g_print("socket_fd = %i\n", socket_fd);
#endif

    irc->socket_id = socket_fd;
    irc->thread = g_thread_create(socket_thread, obj, TRUE, &err);
    if (err) {
        char msg[128];
        snprintf(msg, sizeof(msg), "Error while creating thread: %s", err->message);
        g_error_free(err);
        g_error(msg);
    }

    return TRUE;
}

void
ftd_irc_socket_disconnect(FTD_IRCSocket *obj)
{
    FTD_IRCSocket *irc = FTD_IRC_SOCKET(obj);

    disconnect(irc);
}
