#include <net-snmp/net-snmp-config.h>

#include <net-snmp/library/snmpCallbackDomain.h>

#include <stdio.h>
#include <sys/types.h>
#include <ctype.h>
#include <errno.h>

#ifdef WIN32
#include <net-snmp/library/winpipe.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef HAVE_SYS_SOCKET_H
#include <sys/socket.h>
#endif
#ifdef HAVE_SYS_UN_H
#include <sys/un.h>
#endif
#ifdef HAVE_IO_H
#include <io.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif

#include <net-snmp/types.h>
#include <net-snmp/output_api.h>
#include <net-snmp/config_api.h>
#include <net-snmp/utilities.h>

#include <net-snmp/library/snmp_transport.h>
#include <net-snmp/library/snmp_api.h>
#include <net-snmp/library/snmp_client.h>

#ifndef NETSNMP_STREAM_QUEUE_LEN
#define NETSNMP_STREAM_QUEUE_LEN  5
#endif

#ifdef NETSNMP_TRANSPORT_CALLBACK_DOMAIN

static netsnmp_transport_list *trlist = NULL;

static int      callback_count = 0;

typedef struct callback_hack_s {
    void           *orig_transport_data;
    netsnmp_pdu    *pdu;
} callback_hack;

typedef struct callback_queue_s {
    int             callback_num;
    netsnmp_callback_pass *item;
    struct callback_queue_s *next, *prev;
} callback_queue;

callback_queue *thequeue;

static netsnmp_transport *
find_transport_from_callback_num(int num)
{
    netsnmp_transport_list *ptr;
    netsnmp_callback_info *ci;

    for (ptr = trlist; ptr; ptr = ptr->next) {
        ci = ptr->transport->data;
        if (ci->callback_num == num)
            return ptr->transport;
    }
    return NULL;
}

static void
callback_debug_pdu(const char *ourstring, netsnmp_pdu *pdu)
{
    netsnmp_variable_list *vb;
    int             i = 1;
    DEBUGMSGTL((ourstring,
                "PDU: command = %d, errstat = %ld, errindex = %ld\n",
                pdu->command, pdu->errstat, pdu->errindex));
    for (vb = pdu->variables; vb; vb = vb->next_variable) {
        DEBUGMSGTL((ourstring, "  var %d:", i++));
        DEBUGMSGVAR((ourstring, vb));
        DEBUGMSG((ourstring, "\n"));
    }
}

void
callback_push_queue(int num, netsnmp_callback_pass *item)
{
    callback_queue *newitem = SNMP_MALLOC_TYPEDEF(callback_queue);
    callback_queue *ptr;

    if (newitem == NULL)
        return;
    newitem->callback_num = num;
    newitem->item = item;
    if (thequeue) {
        for (ptr = thequeue; ptr && ptr->next; ptr = ptr->next) {
        }
        ptr->next = newitem;
        newitem->prev = ptr;
    } else {
        thequeue = newitem;
    }
    DEBUGIF("dump_send_callback_transport") {
        callback_debug_pdu("dump_send_callback_transport", item->pdu);
    }
}

netsnmp_callback_pass *
callback_pop_queue(int num)
{
    netsnmp_callback_pass *cp;
    callback_queue *ptr;

    for (ptr = thequeue; ptr; ptr = ptr->next) {
        if (ptr->callback_num == num) {
            if (ptr->prev) {
                ptr->prev->next = ptr->next;
            } else {
                thequeue = ptr->next;
            }
            if (ptr->next) {
                ptr->next->prev = ptr->prev;
            }
            cp = ptr->item;
            SNMP_FREE(ptr);
            DEBUGIF("dump_recv_callback_transport") {
                callback_debug_pdu("dump_recv_callback_transport",
                                   cp->pdu);
            }
            return cp;
        }
    }
    return NULL;
}

/*
 * Return a string representing the address in data, or else the "far end"
 * address if data is NULL.  
 */

char *
netsnmp_callback_fmtaddr(netsnmp_transport *t, const void *data, int len)
{
    char *buf;
    netsnmp_callback_info *mystuff;

    if (!t || !t->data)
        return strdup("callback: unknown");

    mystuff = t->data;
    if (asprintf(&buf, "callback: %d on fd %d", mystuff->callback_num,
		 mystuff->pipefds[0]) < 0)
        buf = NULL;
    return buf;
}



/*
 * You can write something into opaque that will subsequently get passed back 
 * to your send function if you like.  For instance, you might want to
 * remember where a PDU came from, so that you can send a reply there...  
 */

int
netsnmp_callback_recv(netsnmp_transport *t, void *buf, int size,
		      void **opaque, int *olength)
{
    int rc = -1;
    char newbuf[1];
    netsnmp_callback_info *mystuff = t->data;

    DEBUGMSGTL(("transport_callback", "hook_recv enter\n"));

    while (rc < 0) {
#ifdef WIN32
	rc = recvfrom(mystuff->pipefds[0], newbuf, 1, 0, NULL, 0);
#else
	rc = read(mystuff->pipefds[0], newbuf, 1);
#endif
	if (rc < 0 && errno != EINTR) {
	    break;
	}
    }
    if (rc > 0)
        memset(buf, 0, rc);

    if (mystuff->linkedto) {
        /*
         * we're the client.  We don't need to do anything. 
         */
    } else {
        /*
         * malloc the space here, but it's filled in by
         * snmp_callback_created_pdu() below 
         */
        *opaque = calloc(1, sizeof(int));
        *olength = sizeof(int);
    }
    DEBUGMSGTL(("transport_callback", "hook_recv exit\n"));
    return rc;
}



int
netsnmp_callback_send(netsnmp_transport *t, const void *buf, int size,
		      void **opaque, int *olength)
{
    int from, rc = -1;
    netsnmp_callback_info *mystuff = t->data, *theirstuff;
    netsnmp_callback_pass *cp;

    /*
     * extract the pdu from the hacked buffer 
     */
    netsnmp_transport *other_side;
    callback_hack  *ch = *opaque;
    netsnmp_pdu    *pdu = ch->pdu;

    *opaque = ch->orig_transport_data;
    SNMP_FREE(ch);

    DEBUGMSGTL(("transport_callback", "hook_send enter\n"));

    cp = SNMP_MALLOC_TYPEDEF(netsnmp_callback_pass);
    if (!cp)
        return -1;

    cp->pdu = snmp_clone_pdu(pdu);
    if (cp->pdu->transport_data) {
        /*
         * not needed and not properly freed later 
         */
        SNMP_FREE(cp->pdu->transport_data);
    }

    if (cp->pdu->flags & UCD_MSG_FLAG_EXPECT_RESPONSE)
        cp->pdu->flags ^= UCD_MSG_FLAG_EXPECT_RESPONSE;

    /*
     * push the sent pdu onto the stack 
     */
    /*
     * AND send a bogus byte to the remote callback receiver's pipe 
     */
    if (mystuff->linkedto) {
        /*
         * we're the client, send it to the parent 
         */
        cp->return_transport_num = mystuff->callback_num;

        other_side = find_transport_from_callback_num(mystuff->linkedto);
        if (!other_side) {
            snmp_free_pdu(cp->pdu);
            SNMP_FREE(cp);
            return -1;
        }

        theirstuff = other_side->data;
	while (rc < 0) {
#ifdef WIN32
	    rc = sendto(theirstuff->pipefds[1], " ", 1, 0, NULL, 0);
#else
	    rc = write(theirstuff->pipefds[1], " ", 1);
#endif
	    if (rc < 0 && errno != EINTR) {
		break;
	    }
	}
        callback_push_queue(mystuff->linkedto, cp);
        /*
         * we don't need the transport data any more 
         */
        SNMP_FREE(*opaque);
    } else {
        /*
         * we're the server, send it to the person that sent us the request 
         */
        from = **((int **) opaque);
        /*
         * we don't need the transport data any more 
         */
        SNMP_FREE(*opaque);
        other_side = find_transport_from_callback_num(from);
        if (!other_side) {
            snmp_free_pdu(cp->pdu);
            SNMP_FREE(cp);
            return -1;
        }
        theirstuff = other_side->data;
	while (rc < 0) {
#ifdef WIN32
	    rc = sendto(theirstuff->pipefds[1], " ", 1, 0, NULL, 0);
#else
	    rc = write(theirstuff->pipefds[1], " ", 1);
#endif
	    if (rc < 0 && errno != EINTR) {
		break;
	    }
	}
        callback_push_queue(from, cp);
    }

    DEBUGMSGTL(("transport_callback", "hook_send exit\n"));
    return 0;
}



int
netsnmp_callback_close(netsnmp_transport *t)
{
    int             rc;
    netsnmp_callback_info *mystuff = t->data;
    DEBUGMSGTL(("transport_callback", "hook_close enter\n"));

#ifdef HAVE_CLOSESOCKET
    rc  = closesocket(mystuff->pipefds[0]);
    rc |= closesocket(mystuff->pipefds[1]);
#else
    rc  = close(mystuff->pipefds[0]);
    rc |= close(mystuff->pipefds[1]);
#endif

    rc |= netsnmp_transport_remove_from_list(&trlist, t);

    DEBUGMSGTL(("transport_callback", "hook_close exit\n"));
    return rc;
}



int
netsnmp_callback_accept(netsnmp_transport *t)
{
    DEBUGMSGTL(("transport_callback", "hook_accept enter\n"));
    DEBUGMSGTL(("transport_callback", "hook_accept exit\n"));
    return -1;
}



/*
 * Open a Callback-domain transport for SNMP.  Local is TRUE if addr
 * is the local address to bind to (i.e. this is a server-type
 * session); otherwise addr is the remote address to send things to
 * (and we make up a temporary name for the local end of the
 * connection).  
 */

netsnmp_transport *
netsnmp_callback_transport(int to)
{

    netsnmp_transport *t = NULL;
    netsnmp_callback_info *mydata;
    int             rc;

    /*
     * transport 
     */
    t = SNMP_MALLOC_TYPEDEF(netsnmp_transport);
    if (!t)
        return NULL;

    /*
     * our stuff 
     */
    mydata = SNMP_MALLOC_TYPEDEF(netsnmp_callback_info);
    if (!mydata) {
        SNMP_FREE(t);
        return NULL;
    }
    mydata->linkedto = to;
    mydata->callback_num = ++callback_count;
    mydata->data = NULL;
    t->data = mydata;

#ifdef WIN32
    rc = create_winpipe_transport(mydata->pipefds);
#else
    rc = pipe(mydata->pipefds);
#endif

    if (rc) {
        netsnmp_transport_free(t);
        return NULL;
    }

    netsnmp_assert(mydata->pipefds[0] != -1);
    t->sock      = mydata->pipefds[0];

    /*
     * Message size is not limited by this transport (hence msgMaxSize
     * is equal to the maximum legal size of an SNMP message).  
     */

    t->msgMaxSize = SNMP_MAX_PACKET_LEN;
    t->f_recv    = netsnmp_callback_recv;
    t->f_send    = netsnmp_callback_send;
    t->f_close   = netsnmp_callback_close;
    t->f_accept  = netsnmp_callback_accept;
    t->f_fmtaddr = netsnmp_callback_fmtaddr;

    netsnmp_transport_add_to_list(&trlist, t);

    if (to)
        DEBUGMSGTL(("transport_callback", "initialized %d linked to %d\n",
                    mydata->callback_num, to));
    else
        DEBUGMSGTL(("transport_callback",
                    "initialized master listening on %d\n",
                    mydata->callback_num));
    return t;
}

int
netsnmp_callback_hook_parse(netsnmp_session * sp,
                            netsnmp_pdu *pdu,
                            u_char * packetptr, size_t len)
{
    if (SNMP_MSG_RESPONSE == pdu->command ||
        SNMP_MSG_REPORT == pdu->command)
        pdu->flags |= UCD_MSG_FLAG_RESPONSE_PDU;
    else
        pdu->flags &= (~UCD_MSG_FLAG_RESPONSE_PDU);

    return SNMP_ERR_NOERROR;
}

int
netsnmp_callback_hook_build(netsnmp_session * sp,
                            netsnmp_pdu *pdu, u_char * ptk, size_t * len)
{
    /*
     * very gross hack, as this is passed later to the transport_send
     * function 
     */
    callback_hack  *ch = SNMP_MALLOC_TYPEDEF(callback_hack);
    if (ch == NULL)
        return -1;
    DEBUGMSGTL(("transport_callback", "hook_build enter\n"));
    ch->pdu = pdu;
    ch->orig_transport_data = pdu->transport_data;
    pdu->transport_data = ch;
    switch (pdu->command) {
    case SNMP_MSG_GETBULK:
        if (pdu->max_repetitions < 0) {
            sp->s_snmp_errno = SNMPERR_BAD_REPETITIONS;
            return -1;
        }
        if (pdu->non_repeaters < 0) {
            sp->s_snmp_errno = SNMPERR_BAD_REPEATERS;
            return -1;
        }
        break;
    case SNMP_MSG_RESPONSE:
    case SNMP_MSG_TRAP:
    case SNMP_MSG_TRAP2:
        pdu->flags &= (~UCD_MSG_FLAG_EXPECT_RESPONSE);
        /* FALL THROUGH */
    default:
        if (pdu->errstat == SNMP_DEFAULT_ERRSTAT)
            pdu->errstat = 0;
        if (pdu->errindex == SNMP_DEFAULT_ERRINDEX)
            pdu->errindex = 0;
        break;
    }

    /*
     * Copy missing values from session defaults
     */
    switch (pdu->version) {
#ifndef NETSNMP_DISABLE_SNMPV1
    case SNMP_VERSION_1:
#endif
#ifndef NETSNMP_DISABLE_SNMPV2C
    case SNMP_VERSION_2c:
#endif
#if !defined(NETSNMP_DISABLE_SNMPV1) || !defined(NETSNMP_DISABLE_SNMPV2C)
        if (pdu->community_len == 0) {
            if (sp->community_len == 0) {
                sp->s_snmp_errno = SNMPERR_BAD_COMMUNITY;
                return -1;
            }
            pdu->community = malloc(sp->community_len);
            if (pdu->community == NULL) {
                sp->s_snmp_errno = SNMPERR_MALLOC;
                return -1;
            }
            memmove(pdu->community,
                    sp->community, sp->community_len);
            pdu->community_len = sp->community_len;
        }
        break;
#endif
    case SNMP_VERSION_3:
        if (pdu->securityNameLen == 0) {
            pdu->securityName = netsnmp_memdup(sp->securityName,
                                               sp->securityNameLen + 1);
            if (pdu->securityName == NULL) {
                sp->s_snmp_errno = SNMPERR_MALLOC;
                return -1;
            }
            pdu->securityNameLen = sp->securityNameLen;
        }
        if (pdu->securityModel == -1)
            pdu->securityModel = sp->securityModel;
        if (pdu->securityLevel == 0)
            pdu->securityLevel = sp->securityLevel;
        /* WHAT ELSE ?? */
    }
    ptk[0] = 0;
    *len = 1;
    DEBUGMSGTL(("transport_callback", "hook_build exit\n"));
    return 1;
}

int
netsnmp_callback_check_packet(u_char * pkt, size_t len)
{
    return 1;
}

netsnmp_pdu    *
netsnmp_callback_create_pdu(netsnmp_transport *transport,
                            void *opaque, size_t olength)
{
    netsnmp_pdu    *pdu;
    netsnmp_callback_pass *cp =
        callback_pop_queue(((netsnmp_callback_info *) transport->data)->
                           callback_num);
    if (!cp)
        return NULL;
    pdu = cp->pdu;
    pdu->transport_data = opaque;
    pdu->transport_data_length = olength;
    if (opaque)                 /* if created, we're the server */
        *((int *) opaque) = cp->return_transport_num;
    SNMP_FREE(cp);
    return pdu;
}

netsnmp_session *
netsnmp_callback_open(int attach_to,
                      int (*return_func) (int op,
                                          netsnmp_session * session,
                                          int reqid, netsnmp_pdu *pdu,
                                          void *magic),
                      int (*fpre_parse) (netsnmp_session *,
                                         struct netsnmp_transport_s *,
                                         void *, int),
                      int (*fpost_parse) (netsnmp_session *, netsnmp_pdu *,
                                          int))
{
    netsnmp_session callback_sess, *callback_ss;
    netsnmp_transport *callback_tr;

    callback_tr = netsnmp_callback_transport(attach_to);
    snmp_sess_init(&callback_sess);
    callback_sess.callback = return_func;
    if (attach_to) {
        /*
         * client 
         */
        /*
         * trysess.community = (u_char *) callback_ss; 
         */
    } else {
        callback_sess.isAuthoritative = SNMP_SESS_AUTHORITATIVE;
    }
    callback_sess.retries = 0;
    callback_sess.timeout = 30000000;
    callback_sess.version = SNMP_DEFAULT_VERSION;       /* (mostly) bogus */
    callback_ss = snmp_add_full(&callback_sess, callback_tr,
                                fpre_parse,
                                netsnmp_callback_hook_parse, fpost_parse,
                                netsnmp_callback_hook_build,
                                NULL,
                                netsnmp_callback_check_packet,
                                netsnmp_callback_create_pdu);
    if (callback_ss)
        callback_ss->local_port =
            ((netsnmp_callback_info *) callback_tr->data)->callback_num;
    return callback_ss;
}



void
netsnmp_clear_callback_list(void)
{

    netsnmp_transport_list *list = trlist, *next = NULL;
    netsnmp_transport *tr = NULL;

    DEBUGMSGTL(("callback_clear", "called netsnmp_callback_clear_list()\n"));
    while (list != NULL) {
	next = list->next;
	tr = list->transport;

	if (tr != NULL) {
	    tr->f_close(tr);
  	    netsnmp_transport_remove_from_list(&trlist, tr);
	    netsnmp_transport_free(tr);
	}
	list = next;
    }
    trlist = NULL;

}

#endif /* NETSNMP_TRANSPORT_CALLBACK_DOMAIN */
