#define _GNU_SOURCE 1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#include "mecha.h"

#include "sysvipc.h"


#define IPC_CLIENT_SEND_ID   1
#define IPC_SERVER_SEND_ID   2

struct s_SysVIPC
{
    int queue_id;
    IPCDisposition  disposition;

    int last_seq;
    pthread_mutex_t last_seq_mutex;

    hshTvoid_list   *message_queue;
    pthread_rwlock_t message_queue_lock;
};

typedef struct s_IPCMessage IPCMessage;

struct s_IPCMessage
{
    int     seq;
    bool    last;
    int     data_len;
    char    data[1];
};


#ifndef MSGMAX
#define MSGMAX  8192
#endif

static const unsigned int MAX_PAYLOAD = MSGMAX - sizeof (IPCMessage);

void sysvipc_cleanup (int sig, void *ptr);

SysVIPC *
sysvipc_new (const key_t ipcKey, IPCDisposition disposition)
{
    SysVIPC *ipc;
    int queue_id;

    if ((queue_id = msgget (ipcKey, IPC_CREAT | 0600)) == -1)
    {
        perror ("msgget");

        return (NULL);
    }

    ipc = malloc (sizeof (SysVIPC));
    ipc->last_seq = 1;
    ipc->queue_id = queue_id;
    ipc->message_queue = hshFvoid_init ((void (*) (void *)) dynFfree);
    ipc->disposition = disposition;

    pthread_mutex_init (&ipc->last_seq_mutex, NULL);
    pthread_rwlock_init (&ipc->message_queue_lock, NULL);

#ifdef HAVE_ON_EXIT
    on_exit (sysvipc_cleanup, ipc);
#endif

    return (ipc);
}

void
sysvipc_destroy (void *ptr)
{
    SysVIPC *ipc = (SysVIPC *) ptr;

    if (ipc->disposition == IPCServer)
    {
        msgctl (ipc->queue_id, IPC_RMID, NULL);
    }

    pthread_rwlock_wrlock (&ipc->message_queue_lock);

    hshFvoid_destroy (ipc->message_queue);

    pthread_rwlock_unlock (&ipc->message_queue_lock);

    pthread_mutex_destroy (&ipc->last_seq_mutex);
    pthread_rwlock_destroy (&ipc->message_queue_lock);

    free (ipc);
}

void
sysvipc_cleanup (int sig MECHA_UNUSED, void *ptr)
{
    logFmsg (CONT_LOG_DEBUG, "sysvipc: Cleaning up resources.");

    sysvipc_destroy (ptr);
}

static struct msgbuf *
sysvipc_createMessage (const char *data, size_t size, bool isLast, size_t *bufSize, int sequence)
{
    struct msgbuf *tmpBuf;
    IPCMessage *message;
    size_t allocSize;

    *bufSize = sizeof (IPCMessage) + size;
    allocSize = sizeof (struct msgbuf) + *bufSize;

    tmpBuf = malloc (allocSize);

    memset (tmpBuf, 0, allocSize);

    message = (IPCMessage *) tmpBuf->mtext;

    message->seq = sequence;
    message->last = isLast;
    message->data_len = size;
    memcpy (message->data, data, size);

    return (tmpBuf);
}

static int
sysvipc_getNextSequence (SysVIPC *ipc)
{
    int seq;

    pthread_mutex_lock (&ipc->last_seq_mutex);

    seq = ++ipc->last_seq;

    pthread_mutex_unlock (&ipc->last_seq_mutex);

    return (seq);
}

bool
sysvipc_sendData (SysVIPC *ipc, dynTstring *data)
{
    long msgtyp;
    int dataOffset = 0;
    int sequence = sysvipc_getNextSequence (ipc);

    if (ipc->disposition == IPCClient)
    {
        msgtyp = IPC_CLIENT_SEND_ID;
    }
    else
    {
        msgtyp = IPC_SERVER_SEND_ID;
    }

    while ((dynFgetlen (data) - dataOffset) > 0)
    {
        struct msgbuf *tmpBuf;
        bool isLast = true;
        size_t dataSize = dynFgetlen (data) - dataOffset;
        size_t bufSize;
        int msgsndRet;

        if (dataSize > MAX_PAYLOAD)
        {
            dataSize = MAX_PAYLOAD;
            isLast = false;
        }

        tmpBuf = sysvipc_createMessage (dynFgetstr (data) + dataOffset, dataSize, isLast, &bufSize, sequence);

        tmpBuf->mtype = msgtyp;

        do
        {
            msgsndRet = msgsnd (ipc->queue_id, tmpBuf, bufSize, 0);
        }
        while (msgsndRet == -1 && errno == EINTR);

        if (msgsndRet == -1)
        {
            perror ("msgsnd");
            free (tmpBuf);

            abort();

            return (false);
        }

        free (tmpBuf);

        dataOffset += dataSize;
    }

    return (true);
}

static dynTstring *
sysvipc_getPendingMessage (SysVIPC *ipc, int sequence)
{
    dynTstring *partString;

    pthread_rwlock_rdlock (&ipc->message_queue_lock);

    partString = (dynTstring *) hshFvoid_int_find (ipc->message_queue, sequence);

    pthread_rwlock_unlock (&ipc->message_queue_lock);

    return (partString);
}

static void
sysvipc_addPendingMessage (SysVIPC *ipc, int sequence, dynTstring *data)
{
    pthread_rwlock_wrlock (&ipc->message_queue_lock);

    hshFvoid_int_add (ipc->message_queue, sequence, data);

    pthread_rwlock_unlock (&ipc->message_queue_lock);
}

static void
sysvipc_removePendingMessage (SysVIPC *ipc, int sequence)
{
    pthread_rwlock_wrlock (&ipc->message_queue_lock);

    /* this doesn't free the string, it just removes it from the hashtable */
    hshFvoid_int_remove (ipc->message_queue, sequence);

    pthread_rwlock_unlock (&ipc->message_queue_lock);
}

dynTstring *
sysvipc_getData (SysVIPC *ipc)
{
    long msgtyp;

    if (ipc->disposition == IPCClient)
    {
        msgtyp = IPC_SERVER_SEND_ID;
    }
    else
    {
        msgtyp = IPC_CLIENT_SEND_ID;
    }

    while (1)
    {
        struct msgbuf *tmpBuf = malloc (sizeof (struct msgbuf) + MSGMAX);
        IPCMessage *message;
        dynTstring *partString;
        int ret;

        do
        {
            pthread_testcancel();
            ret = msgrcv (ipc->queue_id, tmpBuf, MSGMAX, msgtyp, 0);
            pthread_testcancel();
        }
        while (ret == -1 && errno == EINTR);

        if (ret == -1)
        {
            perror ("msgrcv");

            free (tmpBuf);

            return (NULL);
        }

        message = (IPCMessage *) tmpBuf->mtext;

        partString = sysvipc_getPendingMessage (ipc, message->seq);

        if (partString == NULL)
        {
            dynTstring *retString = dynFinit();

            dynFappend (retString, message->data, message->data_len);

            if (message->last == true)
            {
                free (tmpBuf);

                return (retString);
            }
            else
            {
                sysvipc_addPendingMessage (ipc, message->seq, retString);
            }
        }
        else
        {
            dynFappend (partString, message->data, message->data_len);

            if (message->last == true)
            {
                /* 
                 * this doesn't free the string, it just removes it from the
                 * hashtable 
                 */
                sysvipc_removePendingMessage (ipc, message->seq);

                free (tmpBuf);

                return (partString);
            }
        }

        free (tmpBuf);
    }
}

