/*
 * Portions of this file are subject to the following copyright(s).  See
 * the Net-SNMP's COPYING file for more details and other copyrights
 * that may apply:
 *
 * Portions of this file are copyrighted by:
 * Copyright (c) 2016 VMware, Inc. All rights reserved.
 * Use is subject to license terms specified in the COPYING file
 * distributed with the Net-SNMP package.
 */

#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 <net-snmp/agent/row_merge.h>

#include <stdint.h>

#ifdef HAVE_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif

netsnmp_feature_provide(row_merge);
netsnmp_feature_child_of(row_merge, row_merge_all);
netsnmp_feature_child_of(row_merge_all, mib_helpers);


#ifndef NETSNMP_FEATURE_REMOVE_ROW_MERGE
/** @defgroup row_merge row_merge
 *  Calls sub handlers with request for one row at a time.
 *  @ingroup utilities
 *  This helper splits a whole bunch of requests into chunks based on the row
 *  index that they refer to, and passes all requests for a given row to the lower handlers.
 *  This is useful for handlers that don't want to process multiple rows at the
 *  same time, but are happy to iterate through the request list for a single row.
 *  @{
 */

/** returns a row_merge handler that can be injected into a given
 *  handler chain.  
 */
netsnmp_mib_handler *
netsnmp_get_row_merge_handler(int prefix_len)
{
    netsnmp_mib_handler *ret = NULL;
    ret = netsnmp_create_handler("row_merge",
                                  netsnmp_row_merge_helper_handler);
    if (ret) {
        ret->myvoid = (void *)(intptr_t)prefix_len;
    }
    return ret;
}

/** functionally the same as calling netsnmp_register_handler() but also
 * injects a row_merge handler at the same time for you. */
netsnmp_feature_child_of(register_row_merge, row_merge_all);
#ifndef NETSNMP_FEATURE_REMOVE_REGISTER_ROW_MERGE
int
netsnmp_register_row_merge(netsnmp_handler_registration *reginfo)
{
    netsnmp_mib_handler *handler;

    if (!reginfo)
        return MIB_REGISTRATION_FAILED;

    handler = netsnmp_get_row_merge_handler(reginfo->rootoid_len+1);
    if (handler &&
        (netsnmp_inject_handler(reginfo, handler) == SNMPERR_SUCCESS))
        return netsnmp_register_handler(reginfo);

    snmp_log(LOG_ERR, "failed to register row_merge\n");
    netsnmp_handler_free(handler);
    netsnmp_handler_registration_free(reginfo);

    return MIB_REGISTRATION_FAILED;
}
#endif /* NETSNMP_FEATURE_REMOVE_REGISTER_ROW_MERGE */

static void
_rm_status_free(void *mem)
{
    netsnmp_row_merge_status *rm_status = (netsnmp_row_merge_status*)mem;

    if (NULL != rm_status->saved_requests)
        free(rm_status->saved_requests);

    if (NULL != rm_status->saved_status)
        free(rm_status->saved_status);

    free(mem);
}


/** retrieve row_merge_status
 */
netsnmp_row_merge_status *
netsnmp_row_merge_status_get(netsnmp_handler_registration *reginfo,
                             netsnmp_agent_request_info *reqinfo,
                             int create_missing)
{
    netsnmp_row_merge_status *rm_status;
    char buf[64];
    int rc;

    /*
     * see if we've already been here
     */
    rc = snprintf(buf, sizeof(buf), "row_merge:%p", reginfo);
    if ((-1 == rc) || ((size_t)rc >= sizeof(buf))) {
        snmp_log(LOG_ERR,"error creating key\n");
        return NULL;
    }
    
    rm_status = (netsnmp_row_merge_status*)netsnmp_agent_get_list_data(reqinfo, buf);
    if ((NULL == rm_status) && create_missing) {
        netsnmp_data_list *data_list;
        
        rm_status = SNMP_MALLOC_TYPEDEF(netsnmp_row_merge_status);
        if (NULL == rm_status) {
            snmp_log(LOG_ERR,"error allocating memory\n");
            return NULL;
        }
        data_list = netsnmp_create_data_list(buf, rm_status,
                                             _rm_status_free);
        if (NULL == data_list) {
            free(rm_status);
            return NULL;
        }
        netsnmp_agent_add_list_data(reqinfo, data_list);
    }
    
    return rm_status;
}

/** Determine if this is the first row
 *
 * returns 1 if this is the first row for this pass of the handler.
 */
int
netsnmp_row_merge_status_first(netsnmp_handler_registration *reginfo,
                               netsnmp_agent_request_info *reqinfo)
{
    netsnmp_row_merge_status *rm_status;

    /*
     * find status
     */
    rm_status = netsnmp_row_merge_status_get(reginfo, reqinfo, 0);
    if (NULL == rm_status)
        return 0;

    return (rm_status->count == 1) ? 1 : (rm_status->current == 1);
}

/** Determine if this is the last row
 *
 * returns 1 if this is the last row for this pass of the handler.
 */
int
netsnmp_row_merge_status_last(netsnmp_handler_registration *reginfo,
                              netsnmp_agent_request_info *reqinfo)
{
    netsnmp_row_merge_status *rm_status;

    /*
     * find status
     */
    rm_status = netsnmp_row_merge_status_get(reginfo, reqinfo, 0);
    if (NULL == rm_status)
        return 0;

    return (rm_status->count == 1) ? 1 :
        (rm_status->current == rm_status->rows);
}


#define ROW_MERGE_WAITING 0
#define ROW_MERGE_ACTIVE  1
#define ROW_MERGE_DONE    2
#define ROW_MERGE_HEAD    3

/** Implements the row_merge handler */
int
netsnmp_row_merge_helper_handler(netsnmp_mib_handler *handler,
                                 netsnmp_handler_registration *reginfo,
                                 netsnmp_agent_request_info *reqinfo,
                                 netsnmp_request_info *requests)
{
    netsnmp_request_info *request, **saved_requests;
    char *saved_status;
    netsnmp_row_merge_status *rm_status;
    int i, j, ret, tail, count, final_rc = SNMP_ERR_NOERROR;

    /*
     * Use the prefix length as supplied during registration, rather
     *  than trying to second-guess what the MIB implementer wanted.
     */
    int SKIP_OID = (int)(intptr_t)handler->myvoid;

    DEBUGMSGTL(("helper:row_merge", "Got request (%d): ", SKIP_OID));
    DEBUGMSGOID(("helper:row_merge", reginfo->rootoid, reginfo->rootoid_len));
    DEBUGMSG(("helper:row_merge", "\n"));

    /*
     * find or create status
     */
    rm_status = netsnmp_row_merge_status_get(reginfo, reqinfo, 1);

    /*
     * Count the requests, and set up an array to keep
     *  track of the original order.
     */
    for (count = 0, request = requests; request; request = request->next) {
        DEBUGIF("helper:row_merge") {
            DEBUGMSGTL(("helper:row_merge", "  got varbind: "));
            DEBUGMSGOID(("helper:row_merge", request->requestvb->name,
                         request->requestvb->name_length));
            DEBUGMSG(("helper:row_merge", "\n"));
        }
        count++;
    }

    /*
     * Optimization: skip all this if there is just one request
     */
    if(count == 1) {
        rm_status->count = count;
        if (requests->processed)
            return SNMP_ERR_NOERROR;
        return netsnmp_call_next_handler(handler, reginfo, reqinfo, requests);
    }

    /*
     * we really should only have to do this once, instead of every pass.
     * as a precaution, we'll do it every time, but put in some asserts
     * to see if we have to.
     */
    /*
     * if the count changed, re-do everything
     */
    if ((0 != rm_status->count) && (rm_status->count != count)) {
        /*
         * ok, i know next/bulk can cause this condition. Probably
         * GET, too. need to rethink this mode counting. maybe
         * add the mode to the rm_status structure? xxx-rks
         */
        if ((reqinfo->mode != MODE_GET) &&
            (reqinfo->mode != MODE_GETNEXT) &&
            (reqinfo->mode != MODE_GETBULK)) {
            netsnmp_assert((NULL != rm_status->saved_requests) &&
                           (NULL != rm_status->saved_status));
        }
        DEBUGMSGTL(("helper:row_merge", "count changed! do over...\n"));

        SNMP_FREE(rm_status->saved_requests);
        SNMP_FREE(rm_status->saved_status);
        
        rm_status->count = 0;
        rm_status->rows = 0;
    }

    if (0 == rm_status->count) {
        /*
         * allocate memory for saved structure
         */
        rm_status->saved_requests = calloc(count+1,
                                           sizeof(netsnmp_request_info*));
        rm_status->saved_status = calloc(count, sizeof(char));
    }

    saved_status = rm_status->saved_status;
    saved_requests = rm_status->saved_requests;

    /*
     * set up saved requests, and set any processed requests to done
     */
    i = 0;
    for (request = requests; request; request = request->next, i++) {
        if (request->processed) {
            saved_status[i] = ROW_MERGE_DONE;
            DEBUGMSGTL(("helper:row_merge", "  skipping processed oid: "));
            DEBUGMSGOID(("helper:row_merge", request->requestvb->name,
                         request->requestvb->name_length));
            DEBUGMSG(("helper:row_merge", "\n"));
        }
        else
            saved_status[i] = ROW_MERGE_WAITING;
        if (0 != rm_status->count)
            netsnmp_assert(saved_requests[i] == request);
        saved_requests[i] = request;
        saved_requests[i]->prev = NULL;
    }
    saved_requests[i] = NULL;

    /*
     * Note that saved_requests[count] is valid
     *    (because of the 'count+1' in the calloc above),
     * but NULL (since it's past the end of the list).
     * This simplifies the re-linking later.
     */

    /*
     * Work through the (unprocessed) requests in order.
     * For each of these, search the rest of the list for any
     *   matching indexes, and link them into a new list.
     */
    for (i=0; i<count; i++) {
	if (saved_status[i] != ROW_MERGE_WAITING)
	    continue;

        if (0 == rm_status->count)
            rm_status->rows++;
        DEBUGMSGTL(("helper:row_merge", " row %d oid[%d]: ", rm_status->rows, i));
        DEBUGMSGOID(("helper:row_merge", saved_requests[i]->requestvb->name,
                     saved_requests[i]->requestvb->name_length));
        DEBUGMSG(("helper:row_merge", "\n"));

	saved_requests[i]->next = NULL;
	saved_status[i] = ROW_MERGE_HEAD;
	tail = i;
        for (j=i+1; j<count; j++) {
	    if (saved_status[j] != ROW_MERGE_WAITING)
	        continue;

            DEBUGMSGTL(("helper:row_merge", "? oid[%d]: ", j));
            DEBUGMSGOID(("helper:row_merge",
                         saved_requests[j]->requestvb->name,
                         saved_requests[j]->requestvb->name_length));
            if (!snmp_oid_compare(
                    saved_requests[i]->requestvb->name+SKIP_OID,
                    saved_requests[i]->requestvb->name_length-SKIP_OID,
                    saved_requests[j]->requestvb->name+SKIP_OID,
                    saved_requests[j]->requestvb->name_length-SKIP_OID)) {
                DEBUGMSG(("helper:row_merge", " match\n"));
                saved_requests[tail]->next = saved_requests[j];
                saved_requests[j]->next    = NULL;
                saved_requests[j]->prev = saved_requests[tail];
	        saved_status[j] = ROW_MERGE_ACTIVE;
	        tail = j;
            }
            else
                DEBUGMSG(("helper:row_merge", " no match\n"));
        }
    }

    /*
     * not that we have a list for each row, call next handler...
     */
    if (0 == rm_status->count)
        rm_status->count = count;
    rm_status->current = 0;
    for (i=0; i<count; i++) {
	if (saved_status[i] != ROW_MERGE_HEAD)
	    continue;

        /*
         * found the head of a new row,
         * call the next handler with this list
         */
        rm_status->current++;
        ret = netsnmp_call_next_handler(handler, reginfo, reqinfo,
			                saved_requests[i]);
        if (ret != SNMP_ERR_NOERROR) {
            snmp_log(LOG_WARNING,
                     "bad rc (%d) from next handler in row_merge\n", ret);
            if (SNMP_ERR_NOERROR == final_rc)
                final_rc = ret;
        }
    }

    /*
     * restore original linked list
     */
    for (i=0; i<count; i++) {
	saved_requests[i]->next = saved_requests[i+1];
        if (i>0)
	    saved_requests[i]->prev = saved_requests[i-1];
    }

    return final_rc;
}

/** 
 *  initializes the row_merge helper which then registers a row_merge
 *  handler as a run-time injectable handler for configuration file
 *  use.
 */
void
netsnmp_init_row_merge(void)
{
    netsnmp_register_handler_by_name("row_merge",
                                     netsnmp_get_row_merge_handler(-1));
}
#else /* NETSNMP_FEATURE_REMOVE_ROW_MERGE */
netsnmp_feature_unused(row_merge);
#endif /* NETSNMP_FEATURE_REMOVE_ROW_MERGE */


/**  @} */

