/**
 * @brief helper for sorted tables
 *
 * The Net-SNMP module comes with several table helpers but none of them
 * are optimal for handling sorted tables that are externally accessed.
 * (At least, none were obviously suited. Although the table_iterator
 * helper does support such uses, it is very slow as it iterates through
 * every row on every request.)
 *
 * The helper in this module aims to fill that gap.
 *
 * This code is based on pieces of various Net-SNMP helpers which are
 * BSD-licensed.
 *
 * Copyright 2010 SIXNET, LLC. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * *  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 
 * *  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.
 * 
 * *  The name of SIXNET, LLC or any of its subsidiaries,
 *    brand or product names may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY
 * EXPRESS 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 COPYRIGHT HOLDER 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 "table_sorted.h"

/*
 * This function currently only handles GET{,NEXT,BULK} type requests.
 * It rewrites any GETNEXT/GETBULK requests for the handled table with a
 * GET request for the appropriate index by calling the
 * first_row_context/next_row_context callbacks in the snmp_sorted_info
 * structure this handler was registered with. The subhandler need only
 * handle GET requests for a specific index.
 */
static int
snmp_table_sorted_handler(netsnmp_mib_handler *handler,
                          netsnmp_handler_registration *reginfo,
                          netsnmp_agent_request_info *reqinfo,
                          netsnmp_request_info *requests)
{
    snmp_sorted_info                *sinfo;
    netsnmp_request_info            *request;
    netsnmp_variable_list           *var;
    netsnmp_table_request_info      *table_info;
    netsnmp_table_registration_info *table_reg_info;

    oid         *suffix;
    int         suffix_len;
    void        *data_context;
    int oldmode, ret;
    int result, regresult; 

    sinfo = handler->myvoid;
    if (sinfo == NULL ||
        reginfo == NULL ||
        reqinfo == NULL) {
        return SNMPERR_GENERR;
    }

    if (sinfo->first_row_context == NULL ||
        sinfo->next_row_context == NULL) {
        snmp_log(LOG_ERR, "%s: error: missing iterator callbacks\n", __func__);
        return SNMPERR_GENERR;
    }

    table_reg_info = netsnmp_find_table_registration_info(reginfo);

    // Save old mode
    oldmode = reqinfo->mode;

    switch (oldmode) {
    case MODE_GETNEXT:
        // Index is already parsed by the table handler for all
        // requests. Pass it to first_row_context/next_row_context to
        // find and set the actual row index.
        for (request = requests; request; request = request->next) {
            if (request->processed) {
                continue;
            }

            var = request->requestvb;
            table_info = netsnmp_extract_table_info(request);

            if (table_info == NULL) {
                continue;
            }

            // Extract column and row index encoded portion
            suffix = var->name + reginfo->rootoid_len + 1;
            suffix_len = var->name_length - (reginfo->rootoid_len + 1);

            // Find the right index.
            // Compare requested name to the root OID of this table
            result = snmp_oid_compare(var->name, var->name_length, 
                                      reginfo->rootoid, 
                                      reginfo->rootoid_len); 

            // Compare requested name to the root (registered) OID,
            // stopping at the end of whichever is shorter
            regresult = snmp_oid_compare(var->name, 
                                         SNMP_MIN(var->name_length, 
                                                  reginfo->rootoid_len), 
                                         reginfo->rootoid, 
                                         reginfo->rootoid_len); 

            // If the OID matches up to the end of whichever is shorter,
            // and the requested name is shorter, the requested name
            // actually precedes the root OID. Override the result of
            // the comparison to indicate that.
            if (regresult == 0 && var->name_length < reginfo->rootoid_len) {
                regresult = -1; 
            } 

            // TODO The table handler may take care of some of this for
            // us already by preparing the column and index data. Take
            // advantage of it instead of repeating that logic!

            // First case: the requested name precedes or matches the
            // root of the table, or the root of the table plus the .1
            // that precedes all entries. The request should return the
            // first object in the table.
            if (result <= 0 || 
                (regresult == 0 && suffix_len == 0 && suffix[-1] == 1)) { 
                // First row, first column 
                table_info->colnum = table_reg_info->min_column; 
                ret = sinfo->first_row_context(&data_context,
                                               table_info,
                                               sinfo);
            } 
            // Second case: the requested name is for a specified column
            // within the table, but no more (there is no index data for
            // a specific row).
            else if (reginfo == 0 && suffix_len == 1 && suffix[-1] == 1) { 
                // Specified column. Need to check if it's in range. 
                // Will be the first row in this column, though. 
                ret = sinfo->first_row_context(&data_context,
                                               table_info,
                                               sinfo); 
            } 
            // Third case: there is at least some index data in the
            // requested name. Find the next valid row. (If there isn't
            // one, the code below will jump to the next column.)
            else { 
                ret = sinfo->next_row_context(&data_context,
                                              table_info,
                                              sinfo);
            } 

            if (ret != SNMPERR_SUCCESS) {
                return ret;
            }

            // At this point if we don't have a row, return the 
            // first row in the next column but only if there is a 
            // next column. 
            if (data_context == NULL && 
                table_info->colnum < table_reg_info->max_column) { 
                table_info->colnum++; 
                table_info->number_indexes = 0; // no row info - first row
                ret = sinfo->first_row_context(&data_context,
                                               table_info,
                                               sinfo);

                if (ret != SNMPERR_SUCCESS) {
                    return ret;
                }
            } 

            // If there's nothing we are at the end of the table. Mark
            // the request processed and skip any further processing on
            // it; Net-SNMP will take care of finding the next OID after
            // this table.
        
            if (data_context == NULL) { 
                request->processed = 1; 
                continue; 
            } 

            // Build and set index
            netsnmp_table_build_oid(reginfo, request, table_info);
        }

        // Rewrite mode as GET for next handler
        reqinfo->mode = MODE_GET;
        break;

    default:
        break;
    }

    // Invoke the next handler in the chain.
    ret = netsnmp_call_next_handler(handler, reginfo, reqinfo, requests);

    // Restore previous mode
    reqinfo->mode = oldmode;

    return ret;
}

/**
 * Inject a sorted table handler into the chain. This handler will
 * preprocess GETNEXT requests using the
 * first_row_context/next_row_context callbacks in the supplied
 * snmp_sorted_info structure. See the documentation of snmp_sorted_info
 * for more information on how it should be initialized and what those
 * functions must do.
 *
 * @return an SNMP error code, or SNMPERR_SUCCESS on success
 */
int
snmp_register_table_sorted(netsnmp_handler_registration *reginfo,
                           snmp_sorted_info *sinfo)
{
    netsnmp_mib_handler *me;

    if (sinfo == NULL) {
        return SNMPERR_GENERR;
    }

    me = netsnmp_create_handler(TABLE_SORTED_NAME,
                                snmp_table_sorted_handler);

    if (me == NULL) {
        return SNMPERR_GENERR;
    }

    me->myvoid = sinfo;
    netsnmp_inject_handler(reginfo, me);
    return netsnmp_register_table(reginfo, sinfo->table_reginfo);
}
