/* -*- coding: utf-8 -*-
 * ----------------------------------------------------------------------
 * Copyright © 2012, RedJack, LLC.
 * All rights reserved.
 *
 * Please see the LICENSE.txt file in this distribution for license
 * details.
 * ----------------------------------------------------------------------
 */

#include <libcork/core.h>

#include "ipset/bdd/nodes.h"
#include "ipset/bits.h"
#include "ipset/errors.h"
#include "ipset/ipset.h"


/**
 * Given a BDD variable number, return the index of the corresponding
 * bit in an IP address.  IPv4 addresses use variables 1-32; IPv6
 * addresses use 1-128.  (Variable 0 is used to identify the kind of
 * address — TRUE for IPv4, FALSE for IPv6.)
 */

static unsigned int
IPSET_NAME(bit_for_var)(ipset_variable var)
{
    return (var - 1);
}


/**
 * An assignment function that can be used to evaluate an IP set BDD.
 */

static bool
IPSET_NAME(assignment)(const void *addr, ipset_variable var)
{
    if (var == 0) {
        return IP_DISCRIMINATOR_VALUE;
    } else {
        unsigned int  bit = IPSET_NAME(bit_for_var)(var);
        return IPSET_BIT_GET(addr, bit);
    }
}


bool
IPSET_PRENAME(contains)(const struct ip_set *set, CORK_IP *elem)
{
    return ipset_node_evaluate
        (set->cache, set->set_bdd, IPSET_NAME(assignment), elem);
}


bool
IPSET_NAME(add_network)(struct ip_set *set, CORK_IP *elem,
                        unsigned int cidr_prefix)
{
    /* Special case — the BDD for a netmask that's out of range never
     * evaluates to true. */
    if (cidr_prefix > IP_BIT_SIZE) {
        cork_error_set
            (IPSET_ERROR, IPSET_PARSE_ERROR,
             "CIDR block %u out of range [0..%u]", cidr_prefix, IP_BIT_SIZE);
        return false;
    }

    ipset_node_id  new_bdd =
        ipset_node_insert
        (set->cache, set->set_bdd,
         IPSET_NAME(assignment), elem, cidr_prefix + 1, 1);
    bool  result = (new_bdd == set->set_bdd);
    ipset_node_decref(set->cache, set->set_bdd);
    set->set_bdd = new_bdd;
    return result;
}


bool
IPSET_NAME(add)(struct ip_set *set, CORK_IP *elem)
{
    ipset_node_id  new_bdd =
        ipset_node_insert
        (set->cache, set->set_bdd,
         IPSET_NAME(assignment), elem, IP_BIT_SIZE + 1, 1);
    bool  result = (new_bdd == set->set_bdd);
    ipset_node_decref(set->cache, set->set_bdd);
    set->set_bdd = new_bdd;
    return result;
}


bool
IPSET_NAME(remove)(struct ip_set *set, CORK_IP *elem)
{
    ipset_node_id  new_bdd =
        ipset_node_insert
        (set->cache, set->set_bdd,
         IPSET_NAME(assignment), elem, IP_BIT_SIZE + 1, 0);
    bool  result = (new_bdd == set->set_bdd);
    ipset_node_decref(set->cache, set->set_bdd);
    set->set_bdd = new_bdd;
    return result;
}


bool
IPSET_NAME(remove_network)(struct ip_set *set, CORK_IP *elem,
                           unsigned int cidr_prefix)
{
    /* Special case — the BDD for a netmask that's out of range never
     * evaluates to true. */
    if (cidr_prefix > IP_BIT_SIZE) {
        cork_error_set
            (IPSET_ERROR, IPSET_PARSE_ERROR,
             "CIDR block %u out of range [0..%u]", cidr_prefix, IP_BIT_SIZE);
        return false;
    }

    ipset_node_id  new_bdd =
        ipset_node_insert
        (set->cache, set->set_bdd,
         IPSET_NAME(assignment), elem, cidr_prefix + 1, 0);
    bool  result = (new_bdd == set->set_bdd);
    ipset_node_decref(set->cache, set->set_bdd);
    set->set_bdd = new_bdd;
    return result;
}
