/*
 * White Rabbit SNMP
 * Copyright (C) 2010, CERN.
 *
 * Version:     wrsw_snmpd v1.0
 *
 * Authors:     Juan Luis Manas (juan.manas@integrasys.es)
 *
 * Description: Handles requests for ieee8021QBridgeGroupFdbTable table.
 *              Provides the list of group entries currently stored in
 *              the RTU filtering database.
 *              Note: this file originally auto-generated by mib2c using
 *              : mib2c.raw-table.conf 17436 2009-03-31 15:12:19Z dts12 $
 *
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version
 * 2 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

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

#include "ieee8021QBridgeTpGroupTable.h"
#include "rtu_fd_proxy.h"
#include "utils.h"

#define MIBMOD  "8021Q"

/* column number definitions for table ieee8021QBridgeTpGroupTable */
#define COLUMN_IEEE8021QBRIDGETPGROUPADDRESS        1
#define COLUMN_IEEE8021QBRIDGETPGROUPEGRESSPORTS    2
#define COLUMN_IEEE8021QBRIDGETPGROUPLEARNT         3

// Row entry
struct mib_group_table_entry {
    // Indexes
    u_long  cid;
    uint16_t vid;
    uint8_t mac[ETH_ALEN];

    // Columns
    uint32_t port_map;
};

/**
 * If requested VLAN is registered, returns it. Otherwise gets VID for the next
 * registered VLAN.
 */
static int read_next_vid(uint16_t *vid)
{
    // aux fields to support fdb function call
    int err, t;
    uint8_t fid;
    uint32_t pm, us;
    unsigned long ct;

    errno = 0;
    err = rtu_fdb_proxy_read_vlan_entry(*vid, &fid, &t, &pm, &us, &ct);
    if (errno)
        goto minipc_err;
    if (err) {
        err = rtu_fdb_proxy_read_next_vlan_entry(vid, &fid, &t, &pm, &us, &ct);
        if (errno)
            goto minipc_err;
        if (err)
            return SNMP_NOSUCHOBJECT;
    }
    return SNMP_ERR_NOERROR;

minipc_err:
    snmp_log(LOG_ERR, "%s(%s): mini-ipc error [%s]\n", __FILE__, __func__,
        strerror(errno));
    return SNMP_ERR_GENERR;
}

// TODO search also dynamic multicast entries (requires MMRP support).
static int read_next_entry(struct mib_group_table_entry *ent)
{
    uint8_t mac_a[ETH_ALEN], mac_b[ETH_ALEN];
    uint16_t vid_a, vid_b;
    // aux fields to read from static fdb
    int err, s, t;
    uint32_t ep, fp;

    // Get indexes for next entry
    // SNMP_ENDOFMIBVIEW informs the handler to proceed with next column.
    if (ent->cid > DEFAULT_COMPONENT_ID) {
        return SNMP_ENDOFMIBVIEW;
    } else if (ent->cid == 0) {
        ent->cid = DEFAULT_COMPONENT_ID;
        ent->vid = 0;
        mac_copy(ent->mac, (uint8_t*)DEFAULT_MAC);
        ent->mac[0] = 0x01; // to refer to multicast addresses when searching
    }
    if (ent->vid >= NUM_VLANS)
        return SNMP_ENDOFMIBVIEW;

    // Find the next (vid, mac) entry in static FDB
    vid_a = ent->vid;
    mac_copy(mac_a, ent->mac);

    errno = 0;
    err = rtu_fdb_proxy_read_next_static_entry(&mac_a, &vid_a, &ep, &fp, &t, &s);
    if (errno)
        goto minipc_err;
    if (err)
        return SNMP_ENDOFMIBVIEW; // No more entries in static FDB
                                  // TODO review once MMRP is supported
    if (vid_a == WILDCARD_VID) {
        // Find the effective VID
        vid_a = ent->vid;
        err = read_next_vid(&vid_a);
        if (err == SNMP_NOSUCHOBJECT)
            return SNMP_ENDOFMIBVIEW;   // no VLANs following requested VID
        if (err)
            return err;
    }

    // Check Wildcard VID entries
    vid_b = WILDCARD_VID;
    mac_copy(mac_b, ent->mac);

    errno = 0;
    err = rtu_fdb_proxy_read_next_static_entry(&mac_b, &vid_b, &ep, &fp, &t, &s);
    if (errno)
        goto minipc_err;

    if (err) {
        // No wildcard VID entries apply
        vid_b = vid_a;
        mac_copy(mac_b, mac_a);
    } else {
        // Find the 'effective' VLAN to which the wildcard VID applies (if any)
        vid_b = ent->vid;
        err = read_next_vid(&vid_b);
        if (err == SNMP_NOSUCHOBJECT) {
            vid_b = vid_a;
            mac_copy(mac_b, mac_a);
        }
        if (err)
            return err;
    }

    if ((vid_a == ent->vid) && (mac_cmp(mac_a, ent->mac) <= 0)) {
        if ((vid_b == ent->vid) && (mac_cmp(mac_b, ent->mac) <= 0))
            return SNMP_ENDOFMIBVIEW;
        ent->vid = vid_b;
        mac_copy(ent->mac, mac_b);
    } else {
        // Compare both entries
        if (vid_b < vid_a) {
            ent->vid = vid_b;
            mac_copy(ent->mac, mac_b);
        } else if (vid_b > vid_a) {
            ent->vid = vid_a;
            mac_copy(ent->mac, mac_a);
        } else {
            ent->vid = vid_a;
            mac_copy(ent->mac, mac_cmp(mac_a, mac_b) < 0 ? mac_a:mac_b);
        }
    }
    return SNMP_ERR_NOERROR;

minipc_err:
    snmp_log(LOG_ERR, "%s(%s): mini-ipc error [%s]\n", __FILE__, __func__,
        strerror(errno));
    return SNMP_ERR_GENERR;
}

/**
 * Get indexes for an entry.
 * @param tinfo table information that contains the indexes (in raw format)
 * @param ent (OUT) used to return the retrieved indexes
 */
static int get_indexes(netsnmp_variable_list        *vb,
                        netsnmp_handler_registration *reginfo,
                        netsnmp_table_request_info   *tinfo,
                        struct mib_group_table_entry *ent)
{
    int oid_len, rootoid_len;
    netsnmp_variable_list *idx;

    // Get indexes from request - in case OID contains them!.
    // Otherwise use default values for first row
    oid_len     = vb->name_length;
    rootoid_len = reginfo->rootoid_len;

    if (oid_len > rootoid_len) {
        if (!tinfo || !tinfo->indexes)
            return SNMP_ERR_GENERR;
        idx = tinfo->indexes;
        ent->cid = *idx->val.integer;
    } else {
        ent->cid = 0;
    }

    if (oid_len > rootoid_len + 1) {
        idx = idx->next_variable;
        ent->vid = *idx->val.integer;
    } else {
        ent->vid = 0;
    }

    if (oid_len > rootoid_len + 2) {
        idx = idx->next_variable;
        memcpy(ent->mac, idx->val.string, idx->val_len);
    } else {
        mac_copy(ent->mac, (uint8_t*)DEFAULT_MAC);
    }
    return SNMP_ERR_NOERROR;
}

static int get_column(netsnmp_variable_list         *vb,
                      int                           colnum,
                      struct mib_group_table_entry  *ent)
{
    char ep[NUM_PORTS];                     // egress ports
    char lp[NUM_PORTS] = {0,0,0,0,0,0,0,0,  // learn ports
                          0,0,0,0,0,0,0,0,
                          0,0,0,0,0,0,0,0,
                          0,0,0,0,0,0,0,0};

    switch (colnum) {
    case COLUMN_IEEE8021QBRIDGETPGROUPEGRESSPORTS:
        // Get info on egress ports from port map
        to_octetstr(ent->port_map, ep);
        snmp_set_var_typed_value(vb, ASN_OCTET_STR, ep, NUM_PORTS);
        break;
    case COLUMN_IEEE8021QBRIDGETPGROUPLEARNT:
        // TODO MMRP not supported yet
        snmp_set_var_typed_value(vb, ASN_OCTET_STR, lp, NUM_PORTS);
        break;
    default:
        return SNMP_NOSUCHOBJECT;
    }
    return 0;
}

static int get(netsnmp_request_info *req, netsnmp_handler_registration *reginfo)
{
    int err;
    uint8_t fid;                                    // FID assigned to VID
    uint32_t port_mask;                             // VLAN member set
    uint32_t us;                                    // VLAN untagged set  - aux
    int t;                                          // VLAN entry type    - aux
    unsigned long ct;                               // VLAN creation time - aux
    struct mib_group_table_entry ent;
    netsnmp_table_request_info *tinfo = netsnmp_extract_table_info(req);

    // Get indexes from request
    err = get_indexes(req->requestvb, reginfo, tinfo, &ent);
    if (err)
        return err;

    DEBUGMSGTL((MIBMOD, "cid=%lu vid=%d mac=%s column=%d\n",
        ent.cid, ent.vid, mac_to_str(ent.mac), tinfo->colnum));

    if ((ent.cid != DEFAULT_COMPONENT_ID) ||
        (ent.vid >= NUM_VLANS))
        return SNMP_NOSUCHINSTANCE;

    // Obtain FID assigned to VID
    // 802.1Q (12.7.7) When operating on a Dynamic Filtering Entry [...] the
    // value used in the VID parameter can be any VID that has been allocated
    // to the FID concerned)
    errno = 0;
    err = rtu_fdb_proxy_read_vlan_entry(ent.vid, &fid, &t, &port_mask, &us, &ct);
    if (errno)
        goto minipc_err;
    if (err)
        goto vlan_not_found;

    errno = 0;
    err = rtu_fdb_proxy_read_entry(ent.mac, fid, &ent.port_map, &t);
    if (errno)
        goto minipc_err;
    if (err)
        goto entry_not_found;

    // Apply port_mask obtained from VLAN table on the port_map
    ent.port_map &= port_mask;

    // Get column value
    return get_column(req->requestvb, tinfo->colnum, &ent);

vlan_not_found:
    DEBUGMSGTL((MIBMOD, "VLAN vid=%d not found\n", ent.vid));
    return SNMP_NOSUCHINSTANCE;

entry_not_found:
    DEBUGMSGTL((MIBMOD, "entry vid=%d mac=%s not found\n",
        ent.vid, mac_to_str(ent.mac)));
    return SNMP_NOSUCHINSTANCE;

minipc_err:
    snmp_log(LOG_ERR, "%s(%s): mini-ipc error [%s]\n", __FILE__, __func__,
        strerror(errno));
    return SNMP_ERR_GENERR;
}

static int get_next(netsnmp_request_info         *req,
                    netsnmp_handler_registration *reginfo)
{
    int err;
    uint8_t fid;                                    // FID assigned to VID
    uint32_t port_mask;                             // VLAN member set
    uint32_t us;                                    // VLAN untagged set  - aux
    int t;                                          // VLAN entry type    - aux
    unsigned long ct;                               // VLAN creation time - aux
    struct mib_group_table_entry ent;
    netsnmp_variable_list *idx;
    netsnmp_table_request_info *tinfo = netsnmp_extract_table_info(req);

    // Get indexes from request
    err = get_indexes(req->requestvb, reginfo, tinfo, &ent);
    if (err)
        return err;

    DEBUGMSGTL((MIBMOD, "cid=%lu vid=%d mac=%s column=%d\n",
        ent.cid, ent.vid, mac_to_str(ent.mac), tinfo->colnum));
    // Obtain VID and MAC address for next entry stored at FDB
    err = read_next_entry(&ent);
    if (err)
        return err;
    // Obtain FID assigned to VID
    errno = 0;
    err = rtu_fdb_proxy_read_vlan_entry(ent.vid, &fid, &t, &port_mask, &us, &ct);
    if (errno)
        goto minipc_err;
    if (err)
        goto vlan_not_found;
    // Read entry from FDB
    errno = 0;
    err = rtu_fdb_proxy_read_entry(ent.mac, fid, &ent.port_map, &t);
    if (errno)
        goto minipc_err;
    if (err)
        goto entry_not_found;
    // Apply port_mask obtained from VLAN table on the port_map
    ent.port_map &= port_mask;
    // Update indexes and OID returned in SNMP response
    idx = tinfo->indexes;
    *idx->val.integer = ent.cid;

    idx = idx->next_variable;
    *idx->val.integer = ent.vid;

    idx = idx->next_variable;
    memcpy(idx->val.string, ent.mac, ETH_ALEN);
    // Update OID
    update_oid(req, reginfo, tinfo->colnum, tinfo->indexes);
    // Return next entry column value
    return get_column(req->requestvb, tinfo->colnum, &ent);

vlan_not_found:
    DEBUGMSGTL((MIBMOD, "VLAN vid=%d not found\n", ent.vid));
    return SNMP_ERR_GENERR;

entry_not_found:
    DEBUGMSGTL((MIBMOD, "entry vid=%d mac=%s not found\n",
        ent.vid, mac_to_str(ent.mac)));
    return SNMP_ERR_GENERR;

minipc_err:
    snmp_log(LOG_ERR, "%s(%d): mini-ipc error [%s]\n",
        __FILE__, __LINE__, strerror(errno));
    return SNMP_ERR_GENERR;
}

/**
 * Handles requests for the ieee8021QBridgeTpGroupTable table
 */
static int _handler(netsnmp_mib_handler          *handler,
                    netsnmp_handler_registration *reginfo,
                    netsnmp_agent_request_info   *reqinfo,
                    netsnmp_request_info         *requests)
{
    int err;
    netsnmp_request_info *req;

    switch (reqinfo->mode) {
    case MODE_GET:
        for (req = requests; req; req = req->next) {
            err = get(req, reginfo);
            if (err)
                netsnmp_set_request_error(reqinfo, req, err);
        }
        break;
    case MODE_GETNEXT:
        for (req = requests; req; req = req->next) {
            err = get_next(req, reginfo);
            if (err)
                netsnmp_set_request_error(reqinfo, req, err);
        }
        break;
    }
    return SNMP_ERR_NOERROR;
}

/**
 * Initialize the ieee8021QBridgeTpGroupTable table by defining its
 * contents and how it's structured
 */
static void initialize_table(void)
{
    const oid _oid[] = {1,3,111,2,802,1,1,4,1,2,3};
    netsnmp_handler_registration    *reg;
    netsnmp_table_registration_info *tinfo;
    netsnmp_variable_list           *idx;

    reg = netsnmp_create_handler_registration(
            "ieee8021QBridgeTpGroupTable",
            _handler,
            (oid *)_oid,
            OID_LENGTH(_oid),
            HANDLER_CAN_RONLY);

    tinfo = SNMP_MALLOC_TYPEDEF( netsnmp_table_registration_info );
    netsnmp_table_helper_add_indexes(tinfo,
            ASN_UNSIGNED,   /* index: ComponentId */
            ASN_UNSIGNED,   /* index: VlanIndex */
            ASN_PRIV_IMPLIED_OCTET_STR,  /* index: Address */
            0);

    // Fix the MacAddress Index variable binding lenght
    idx = tinfo->indexes;
    idx = idx->next_variable; // skip componentId
    idx = idx->next_variable; // skip VlanIndex
    idx->val_len = ETH_ALEN;

    tinfo->min_column = COLUMN_IEEE8021QBRIDGETPGROUPEGRESSPORTS;
    tinfo->max_column = COLUMN_IEEE8021QBRIDGETPGROUPLEARNT;

    netsnmp_register_table(reg, tinfo);
}

/**
 * Initializes the ieee8021QBridgeTpGroupTable module
 */
void init_ieee8021QBridgeTpGroupTable(void)
{
    struct minipc_ch *client;

    client = rtu_fdb_proxy_create("rtu_fdb");
    if(client) {
        initialize_table();
        snmp_log(LOG_INFO, "%s: initialised\n", __FILE__);
    } else {
        snmp_log(LOG_ERR, "%s: error creating mini-ipc proxy - %s\n", __FILE__,
            strerror(errno));
    }
}
