#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-features.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>

#include <limits.h>

netsnmp_feature_require(container_fifo);

#include "deliverByNotify.h"

/* we should never split beyond this */
#define MAX_MESSAGE_COUNT 128
#define BASE_PACKET_SIZE 100 /* should be enough to store SNMPv3 msg headers */

/* if v is !NULL, then estimate it's likely size */
#define ESTIMATE_VAR_SIZE(v) (v?(v->name_length + v->val_len + 8):0)

void parse_deliver_config(const char *, char *);
void parse_deliver_maxsize_config(const char *, char *);
void parse_data_notification_oid_config(const char *, char *);
void parse_periodic_time_oid_config(const char *, char *);
void parse_message_number_oid_config(const char *, char *);
void parse_max_message_number_oid_config(const char *, char *);
void free_deliver_config(void);

static void _schedule_next_execute_time(void);

oid    data_notification_oid[MAX_OID_LEN]
       = { 1, 3, 6, 1, 4, 1, 8072, 3, 1, 5, 4, 0, 1 };
size_t data_notification_oid_len = 13;

oid    netsnmp_periodic_time_oid[MAX_OID_LEN]
       = { 1, 3, 6, 1, 4, 1, 8072, 3, 1, 5, 3, 1, 0 };
size_t netsnmp_periodic_time_oid_len = 13;

oid    netsnmp_message_number_oid[MAX_OID_LEN]
       = { 1, 3, 6, 1, 4, 1, 8072, 3, 1, 5, 3, 2, 0 };
size_t netsnmp_message_number_oid_len = 13;

oid    netsnmp_max_message_number_oid[MAX_OID_LEN]
       = { 1, 3, 6, 1, 4, 1, 8072, 3, 1, 5, 3, 3, 0 };
size_t netsnmp_max_message_number_oid_len = 13;

oid objid_snmptrap[] = { 1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0 };

#define DEFAULT_MAX_DELIVER_SIZE -1;
static int default_max_size;
unsigned int alarm_reg;
static netsnmp_container *deliver_container;

static int
_deliver_compare(deliver_by_notify *lhs, deliver_by_notify *rhs) {
    /* sort by the next_run time */
    if (lhs->next_run < rhs->next_run)
        return -1;
    else
        return 1;
}

/** Initializes the mteTrigger module */
void
init_deliverByNotify(void)
{
    /* register the config tokens */
    snmpd_register_config_handler("deliverByNotify",
                                  &parse_deliver_config, &free_deliver_config,
                                  "[-p] [-m] [-s maxsize] FREQUENCY OID");

    snmpd_register_config_handler("deliverByNotifyMaxPacketSize",
                                  &parse_deliver_maxsize_config, NULL,
                                  "sizeInBytes");

    snmpd_register_config_handler("deliverByNotifyOid",
                                  &parse_data_notification_oid_config,
                                  NULL, /* XXX: reset to default */
                                  "OID");

    snmpd_register_config_handler("deliverByNotifyFrequencyOid",
                                  &parse_periodic_time_oid_config,
                                  NULL, /* XXX: reset to default */
                                  "OID");

    snmpd_register_config_handler("deliverByNotifyMessageNumberOid",
                                  &parse_message_number_oid_config,
                                  NULL, /* XXX: reset to default */
                                  "OID");

    snmpd_register_config_handler("deliverByNotifyMaxMessageNumberOid",
                                  &parse_max_message_number_oid_config,
                                  NULL, /* XXX: reset to default */
                                  "OID");

    /* create the container to store the config objects*/
    deliver_container = netsnmp_container_find("deliverByNotify:fifo");
    if (NULL == deliver_container) {
        snmp_log(LOG_ERR,
                 "deliverByNotify: failed to initialize our data container\n");
        return;
    }
    deliver_container->container_name = strdup("deliverByNotify");
    deliver_container->compare = (netsnmp_container_compare *) _deliver_compare;

    /* set the defaults */
    default_max_size = DEFAULT_MAX_DELIVER_SIZE;

    alarm_reg = 0;
}

void
_parse_config_oid(const char *token, char *line,
                  oid *oid_store, size_t *oid_store_len) {
    size_t tmp_len = MAX_OID_LEN;
    /* parse the OID given */

    if (!snmp_parse_oid(line, oid_store, &tmp_len)) {
        char buf[SPRINT_MAX_LEN];
        snprintf(buf, SPRINT_MAX_LEN-1, "unknown %s OID: %s", token, line);
        config_perror(buf);
        return;
    }

    *oid_store_len = tmp_len;
}

void
parse_data_notification_oid_config(const char *token, char *line) {
    _parse_config_oid(token, line,
                      data_notification_oid, &data_notification_oid_len);
}

void
parse_periodic_time_oid_config(const char *token, char *line) {
    _parse_config_oid(token, line,
                      netsnmp_periodic_time_oid, &netsnmp_periodic_time_oid_len);
}

void
parse_message_number_oid_config(const char *token, char *line) {
    _parse_config_oid(token, line,
                      netsnmp_message_number_oid,
                      &netsnmp_message_number_oid_len);
}

void
parse_max_message_number_oid_config(const char *token, char *line) {
    _parse_config_oid(token, line,
                      netsnmp_max_message_number_oid,
                      &netsnmp_max_message_number_oid_len);
}

void
parse_deliver_config(const char *token, char *line) {
    const char *cp = line;
    char buf[SPRINT_MAX_LEN];
    size_t buf_len = SPRINT_MAX_LEN;
    int max_size = DEFAULT_MAX_DELIVER_SIZE;
    int frequency;
    oid target_oid[MAX_OID_LEN];
    size_t target_oid_len = MAX_OID_LEN;
    deliver_by_notify *new_notify = NULL;
    int flags = 0;

    while(cp && *cp == '-') {
        switch (*(cp+1)) {
        case 's':
            cp = skip_token_const(cp);
            if (!cp) {
                config_perror("no argument given to -s");
                return;
            }
            max_size = atoi(cp);
            break;

        case 'p':
            flags = flags | NETSNMP_DELIVER_NO_PERIOD_OID;
            break;

        case 'm':
            flags = flags | NETSNMP_DELIVER_NO_MSG_COUNTS;
            break;

        default:
            config_perror("unknown flag");
            return;
        }
        cp = skip_token_const(cp);
    }

    if (!cp) {
        config_perror("no frequency given");
        return;
    }
    copy_nword_const(cp, buf, buf_len);
    frequency = netsnmp_string_time_to_secs(buf);
    cp = skip_token_const(cp);

    if (frequency <= 0) {
        config_perror("illegal frequency given");
        return;
    }

    if (!cp) {
        config_perror("no OID given");
        return;
    }

    /* parse the OID given */
    if (!snmp_parse_oid(cp, target_oid, &target_oid_len)) {
        config_perror("unknown deliverByNotify OID");
        DEBUGMSGTL(("deliverByNotify", "The OID with the problem: %s\n", cp));
        return;
    }

    /* set up the object to store all the data */
    new_notify = SNMP_MALLOC_TYPEDEF(deliver_by_notify);
    new_notify->frequency = frequency;
    new_notify->max_packet_size = max_size;
    new_notify->last_run = time(NULL);
    new_notify->next_run = new_notify->last_run + frequency;
    new_notify->flags = flags;

    new_notify->target = malloc(target_oid_len * sizeof(oid));
    new_notify->target_len = target_oid_len;
    memcpy(new_notify->target, target_oid, target_oid_len*sizeof(oid));

    /* XXX: need to do the whole container */
    snmp_alarm_register(calculate_time_until_next_run(new_notify, NULL), 0, 
                        &deliver_execute, NULL);

    /* add it to the container */
    CONTAINER_INSERT(deliver_container, new_notify);
    _schedule_next_execute_time();
}

void
parse_deliver_maxsize_config(const char *token, char *line) {
    default_max_size = atoi(line);
}

static void
_free_deliver_obj(deliver_by_notify *obj, void *context) {
    netsnmp_assert_or_return(obj != NULL, );
    SNMP_FREE(obj->target);
    SNMP_FREE(obj);
}

void
free_deliver_config(void) {
    default_max_size = DEFAULT_MAX_DELIVER_SIZE;
    CONTAINER_CLEAR(deliver_container,
                    (netsnmp_container_obj_func *) _free_deliver_obj, NULL);
    if (alarm_reg) {
        snmp_alarm_unregister(alarm_reg);
        alarm_reg = 0;
    }
}

void
deliver_execute(unsigned int clientreg, void *clientarg) {
    netsnmp_variable_list *vars, *walker, *deliver_notification, *vartmp;
    netsnmp_variable_list *ready_for_delivery[MAX_MESSAGE_COUNT];
    netsnmp_session *sess;
    int rc, i;
    deliver_by_notify *obj;
    netsnmp_iterator *iterator;
    time_t            now = time(NULL);
    u_long            message_count, max_message_count, tmp_long;
    u_long           *max_message_count_ptrs[MAX_MESSAGE_COUNT];
    size_t            estimated_pkt_size;

    DEBUGMSGTL(("deliverByNotify", "Starting the execute routine\n"));

    /* XXX: need to do the whole container */
    iterator = CONTAINER_ITERATOR(deliver_container);
    netsnmp_assert_or_return(iterator != NULL,);

    sess = netsnmp_query_get_default_session();

    for(obj = ITERATOR_FIRST(iterator); obj;
        obj = ITERATOR_NEXT(iterator)) {

        /* check if we need to run this one yet */
        if (obj->next_run > now)
            continue;

        max_message_count = 1;
        message_count = 0;

        /* fill the varbind list with the target object */
        vars = SNMP_MALLOC_TYPEDEF( netsnmp_variable_list );
        snmp_set_var_objid( vars, obj->target, obj->target_len );
        vars->type = ASN_NULL;

        /* walk the OID tree for the data */
        rc = netsnmp_query_walk(vars, sess);
        if (rc != SNMP_ERR_NOERROR) {
            /* XXX: disable? and reset the next query time point! */
            snmp_log(LOG_ERR, "deliverByNotify: failed to issue the query");
            ITERATOR_RELEASE(iterator);
            return;
        }

        walker = vars;

        while (walker) {

            /* Set up the notification itself */
            deliver_notification = NULL;
            estimated_pkt_size = BASE_PACKET_SIZE;

            /* add in the notification type */
            snmp_varlist_add_variable(&deliver_notification,
                                      objid_snmptrap, OID_LENGTH(objid_snmptrap),
                                      ASN_OBJECT_ID,
                                      data_notification_oid,
                                      data_notification_oid_len * sizeof(oid));
            estimated_pkt_size += ESTIMATE_VAR_SIZE(deliver_notification);

            /* add in the current message number in this sequence */
            if (!(obj->flags & NETSNMP_DELIVER_NO_PERIOD_OID)) {
                tmp_long = obj->frequency;
                snmp_varlist_add_variable(&deliver_notification,
                                          netsnmp_periodic_time_oid,
                                          netsnmp_periodic_time_oid_len,
                                          ASN_UNSIGNED,
                                          (const void *) &tmp_long,
                                          sizeof(tmp_long));
                estimated_pkt_size += ESTIMATE_VAR_SIZE(deliver_notification);
            }

            /* add in the current message number in this sequence */
            message_count++;
            if (message_count > MAX_MESSAGE_COUNT) {
                snmp_log(LOG_ERR, "delivery construct grew too large...  giving up\n");
                /* XXX: disable it */
                /* XXX: send a notification about it? */
                ITERATOR_RELEASE(iterator);
                return;
            }

            /* store this for later updating and sending */
            ready_for_delivery[message_count-1] = deliver_notification;

            if (!(obj->flags & NETSNMP_DELIVER_NO_MSG_COUNTS)) {
                snmp_varlist_add_variable(&deliver_notification,
                                          netsnmp_message_number_oid,
                                          netsnmp_message_number_oid_len,
                                          ASN_UNSIGNED,
                                          (const void *) &message_count,
                                          sizeof(message_count));
                estimated_pkt_size += ESTIMATE_VAR_SIZE(deliver_notification);

                /* add in the max message number count for this sequence */
                vartmp = snmp_varlist_add_variable(&deliver_notification,
                                                   netsnmp_max_message_number_oid,
                                                   netsnmp_max_message_number_oid_len,
                                                   ASN_UNSIGNED,
                                                   (const void *) &max_message_count,
                                                   sizeof(max_message_count));
                estimated_pkt_size += ESTIMATE_VAR_SIZE(deliver_notification);

                /* we'll need to update this counter later */
                max_message_count_ptrs[message_count-1] =
                    (u_long *) vartmp->val.integer;
            } else {
                /* just to be sure */
                max_message_count_ptrs[message_count-1] = NULL;
            }
            
            /* copy in the collected data */
            while(walker) {
                snmp_varlist_add_variable(&deliver_notification,
                                          walker->name, walker->name_length,
                                          walker->type,
                                          walker->val.string, walker->val_len);

                /* 8 byte padding for ASN encodings an a few extra OID bytes */
                estimated_pkt_size += ESTIMATE_VAR_SIZE(walker);

                walker = walker->next_variable;

                /* if the current size PLUS the next one (which is now
                   in 'walker') is greater than the limet then we stop here */
                if (obj->max_packet_size > 0 &&
                    estimated_pkt_size +
                    ESTIMATE_VAR_SIZE(walker) >=
                    obj->max_packet_size) {
                    break;
                }
            }

            /* send out the notification */
            send_v2trap(deliver_notification);
        }

        for(i = 0; i < message_count; i++) {
            /* update the max count pointer */
            if (max_message_count_ptrs[i])
                *(max_message_count_ptrs[i]) = message_count;
            
            send_v2trap(ready_for_delivery[i]);
            snmp_free_varbind(ready_for_delivery[i]);
        }

        snmp_free_varbind(vars);

        /* record this as the time processed */
        /* XXX: this may creep by a few seconds when processing and maybe we want
           to do the time stamp at the beginning? */
        obj->last_run = time(NULL);
    }
    ITERATOR_RELEASE(iterator);

    /* calculate the next time to sleep for */
    _schedule_next_execute_time();
}

int
calculate_time_until_next_run(deliver_by_notify *it, time_t *now) {
    time_t          local_now;

    /* if we weren't passed a valid time, fake it */
    if (NULL == now) {
        now = &local_now;
        time(&local_now);
    }

    netsnmp_assert_or_return(it->last_run != 0, -1);

    /* set the timestamp for the next run */
    it->next_run = it->last_run + it->frequency;

    /* how long until the next run? */
    return it->next_run - *now;
}

static void
_schedule_next_execute_time(void) {
    time_t             local_now = time(NULL);
    int                sleep_for = INT_MAX;
    int                next_time;
    netsnmp_iterator  *iterator;
    deliver_by_notify *obj;

    DEBUGMSGTL(("deliverByNotify", "Calculating scheduling needed\n"));

    if (alarm_reg) {
        snmp_alarm_unregister(alarm_reg);
        alarm_reg = 0;
    }

    iterator = CONTAINER_ITERATOR(deliver_container);
    if (NULL == iterator)
        return;

    for(obj = ITERATOR_FIRST(iterator); obj;
        obj = ITERATOR_NEXT(iterator)) {
        next_time = calculate_time_until_next_run(obj, &local_now);
        DEBUGMSGTL(("deliverByNotify", "  obj: %d (last=%d, next_run=%d)\n", next_time, obj->last_run, obj->next_run));
        if (next_time < sleep_for)
            sleep_for = next_time;
    }

    if (sleep_for != INT_MAX) {
        if (sleep_for < 1)
            sleep_for = 1; /* give at least a small pause */
        DEBUGMSGTL(("deliverByNotify", "Next execution in %d (max = %d) seconds\n",
                    sleep_for, INT_MAX));
        alarm_reg = snmp_alarm_register(sleep_for, 0, &deliver_execute, NULL);
    }

    ITERATOR_RELEASE(iterator);
}

