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

#include <stdio.h>
#include <string.h>

#include <libcork/core.h>

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


void
ipset_node_fprint(FILE *stream, struct ipset_node *node)
{
    fprintf(stream,
            "nonterminal(x%u? " IPSET_NODE_ID_FORMAT
            ": " IPSET_NODE_ID_FORMAT ")",
            node->variable,
            IPSET_NODE_ID_VALUES(node->high),
            IPSET_NODE_ID_VALUES(node->low));
}


static cork_hash
ipset_node_hash(void *user_data, const void *key)
{
    const struct ipset_node  *node = key;
    /* Hash of "ipset_node" */
    cork_hash  hash = 0xf3b7dc44;
    hash = cork_hash_variable(hash, node->variable);
    hash = cork_hash_variable(hash, node->low);
    hash = cork_hash_variable(hash, node->high);
    return hash;
}

static bool
ipset_node_equals(void *user_data, const void *key1, const void *key2)
{
    const struct ipset_node  *node1 = key1;
    const struct ipset_node  *node2 = key2;

    if (node1 == node2) {
        return true;
    }

    return
        (node1->variable == node2->variable) &&
        (node1->low == node2->low) &&
        (node1->high == node2->high);
}


/* The free list in an ipset_node_cache is represented by a
 * singly-linked list of indices into the chunk array.  Since the
 * ipset_node instance is unused for nodes in the free list, we reuse
 * the refcount field to store the "next" index. */

#define IPSET_NULL_INDEX ((ipset_variable) -1)

struct ipset_node_cache *
ipset_node_cache_new()
{
    struct ipset_node_cache  *cache = cork_new(struct ipset_node_cache);
    cork_array_init(&cache->chunks);
    cache->largest_index = 0;
    cache->free_list = IPSET_NULL_INDEX;
    cache->node_cache = cork_hash_table_new(0, 0);
    cork_hash_table_set_hash
        (cache->node_cache, (cork_hash_f) ipset_node_hash);
    cork_hash_table_set_equals
        (cache->node_cache, (cork_equals_f) ipset_node_equals);
    return cache;
}

void
ipset_node_cache_free(struct ipset_node_cache *cache)
{
    size_t  i;
    for (i = 0; i < cork_array_size(&cache->chunks); i++) {
        free(cork_array_at(&cache->chunks, i));
    }
    cork_array_done(&cache->chunks);
    cork_hash_table_free(cache->node_cache);
    free(cache);
}


/**
 * Returns the index of a new ipset_node instance.
 */
static ipset_value
ipset_node_cache_alloc_node(struct ipset_node_cache *cache)
{
    if (cache->free_list == IPSET_NULL_INDEX) {
        /* Nothing in the free list; need to allocate a new node. */
        ipset_value  next_index = cache->largest_index++;
        ipset_value  chunk_index = next_index >> IPSET_BDD_NODE_CACHE_BIT_SIZE;
        if (chunk_index >= cork_array_size(&cache->chunks)) {
            /* We've filled up all of the existing chunks, and need to
             * create a new one. */
            DEBUG("        (allocating chunk %zu)",
                  cork_array_size(&cache->chunks));
            struct ipset_node  *new_chunk = cork_calloc
                (IPSET_BDD_NODE_CACHE_SIZE, sizeof(struct ipset_node));
            cork_array_append(&cache->chunks, new_chunk);
        }
        return next_index;
    } else {
        /* Reuse a recently freed node. */
        ipset_value  next_index = cache->free_list;
        struct ipset_node  *node =
            ipset_node_cache_get_nonterminal_by_index(cache, next_index);
        cache->free_list = node->refcount;
        return next_index;
    }
}

ipset_node_id
ipset_node_incref(struct ipset_node_cache *cache, ipset_node_id node_id)
{
    if (ipset_node_get_type(node_id) == IPSET_NONTERMINAL_NODE) {
        struct ipset_node  *node =
            ipset_node_cache_get_nonterminal(cache, node_id);
        DEBUG("        [incref " IPSET_NODE_ID_FORMAT "]",
              IPSET_NODE_ID_VALUES(node_id));
        node->refcount++;
    }
    return node_id;
}

void
ipset_node_decref(struct ipset_node_cache *cache, ipset_node_id node_id)
{
    if (ipset_node_get_type(node_id) == IPSET_NONTERMINAL_NODE) {
        struct ipset_node  *node =
            ipset_node_cache_get_nonterminal(cache, node_id);
        DEBUG("        [decref " IPSET_NODE_ID_FORMAT "]",
              IPSET_NODE_ID_VALUES(node_id));
        if (--node->refcount == 0) {
            DEBUG("        [free   " IPSET_NODE_ID_FORMAT "]",
                  IPSET_NODE_ID_VALUES(node_id));
            ipset_node_decref(cache, node->low);
            ipset_node_decref(cache, node->high);
            cork_hash_table_delete(cache->node_cache, node, NULL, NULL);

            /* Add the node to the free list */
            node->refcount = cache->free_list;
            cache->free_list = ipset_nonterminal_value(node_id);
        }
    }
}

bool
ipset_node_cache_nodes_equal(const struct ipset_node_cache *cache1,
                             ipset_node_id node_id1,
                             const struct ipset_node_cache *cache2,
                             ipset_node_id node_id2)
{
    struct ipset_node  *node1;
    struct ipset_node  *node2;

    if (ipset_node_get_type(node_id1) != ipset_node_get_type(node_id2)) {
        return false;
    }

    if (ipset_node_get_type(node_id1) == IPSET_TERMINAL_NODE) {
        return node_id1 == node_id2;
    }

    node1 = ipset_node_cache_get_nonterminal(cache1, node_id1);
    node2 = ipset_node_cache_get_nonterminal(cache2, node_id2);
    return
        (node1->variable == node2->variable) &&
        ipset_node_cache_nodes_equal(cache1, node1->low, cache2, node2->low) &&
        ipset_node_cache_nodes_equal(cache1, node1->high, cache2, node2->high);
}

ipset_node_id
ipset_node_cache_nonterminal(struct ipset_node_cache *cache,
                             ipset_variable variable,
                             ipset_node_id low, ipset_node_id high)
{
    /* Don't allow any nonterminals whose low and high subtrees are the
     * same, since the nonterminal would be redundant. */
    if (CORK_UNLIKELY(low == high)) {
        DEBUG("        [ SKIP  nonterminal(x%u? "
              IPSET_NODE_ID_FORMAT ": " IPSET_NODE_ID_FORMAT ")]",
              variable, IPSET_NODE_ID_VALUES(high), IPSET_NODE_ID_VALUES(low));
        ipset_node_decref(cache, high);
        return low;
    }

    /* Check to see if there's already a nonterminal with these contents
     * in the cache. */
    DEBUG("        [search nonterminal(x%u? "
          IPSET_NODE_ID_FORMAT ": " IPSET_NODE_ID_FORMAT ")]",
          variable, IPSET_NODE_ID_VALUES(high), IPSET_NODE_ID_VALUES(low));

    struct ipset_node  search_node;
    search_node.variable = variable;
    search_node.low = low;
    search_node.high = high;

    bool  is_new;
    struct cork_hash_table_entry  *entry =
        cork_hash_table_get_or_create
        (cache->node_cache, &search_node, &is_new);

    if (!is_new) {
        /* There's already a node with these contents, so return its ID. */
        ipset_node_id  node_id = (uintptr_t) entry->value;
        DEBUG("        [reuse  " IPSET_NODE_ID_FORMAT "]",
              IPSET_NODE_ID_VALUES(node_id));
        ipset_node_incref(cache, node_id);
        ipset_node_decref(cache, low);
        ipset_node_decref(cache, high);
        return node_id;
    } else {
        /* This node doesn't exist yet.  Allocate a permanent copy of
         * the node, add it to the cache, and then return its ID. */
        ipset_value  new_index = ipset_node_cache_alloc_node(cache);
        ipset_node_id  new_node_id = ipset_nonterminal_node_id(new_index);
        struct ipset_node  *real_node =
            ipset_node_cache_get_nonterminal_by_index(cache, new_index);
        real_node->refcount = 1;
        real_node->variable = variable;
        real_node->low = low;
        real_node->high = high;
        entry->key = real_node;
        entry->value = (void *) (uintptr_t) new_node_id;
        DEBUG("        [new    " IPSET_NODE_ID_FORMAT "]",
              IPSET_NODE_ID_VALUES(new_node_id));
        return new_node_id;
    }
}


bool
ipset_bool_array_assignment(const void *user_data, ipset_variable variable)
{
    const bool  *bool_array = (const bool *) user_data;
    return bool_array[variable];
}


bool
ipset_bit_array_assignment(const void *user_data, ipset_variable variable)
{
    return IPSET_BIT_GET(user_data, variable);
}


ipset_value
ipset_node_evaluate(const struct ipset_node_cache *cache, ipset_node_id node_id,
                    ipset_assignment_func assignment, const void *user_data)
{
    ipset_node_id  curr_node_id = node_id;
    DEBUG("Evaluating BDD node " IPSET_NODE_ID_FORMAT,
          IPSET_NODE_ID_VALUES(node_id));

    /* As long as the current node is a nonterminal, we have to check
     * the value of the current variable. */
    while (ipset_node_get_type(curr_node_id) == IPSET_NONTERMINAL_NODE) {
        /* We have to look up this variable in the assignment. */
        struct ipset_node  *node =
            ipset_node_cache_get_nonterminal(cache, curr_node_id);
        bool  this_value = assignment(user_data, node->variable);
        DEBUG("[%3u] Nonterminal " IPSET_NODE_ID_FORMAT,
              node->variable, IPSET_NODE_ID_VALUES(curr_node_id));
        DEBUG("[%3u]   x%u = %s",
              node->variable, node->variable, this_value? "TRUE": "FALSE");

        if (this_value) {
            /* This node's variable is true in the assignment vector, so
             * trace down the high subtree. */
            curr_node_id = node->high;
        } else {
            /* This node's variable is false in the assignment vector,
             * so trace down the low subtree. */
            curr_node_id = node->low;
        }
    }

    /* Once we find a terminal node, we've got the final result. */
    DEBUG("Evaluated result is %u", ipset_terminal_value(curr_node_id));
    return ipset_terminal_value(curr_node_id);
}


/* A “fake” BDD node given by an assignment. */
struct ipset_fake_node {
    ipset_variable  current_var;
    ipset_variable  var_count;
    ipset_assignment_func  assignment;
    const void  *user_data;
    ipset_value  value;
};

/* A fake BDD node representing the terminal 0 value. */
static struct ipset_fake_node  fake_terminal_0 = { 0, 0, NULL, 0, 0 };

/* We set elements in a map using the if-then-else (ITE) operator:
 *
 *   new_set = new_element? new_value: old_set
 *
 * The below is a straight copy of the standard trinary APPLY from the BDD
 * literature, but without the caching of the results.  And also with the
 * wrinkle that the F argument to ITE (i.e., new_element) is given by an
 * assignment, and not by a BDD node.  (This lets us skip constructing the BDD
 * for the assignment, saving us a few cycles.)
 */

static ipset_node_id
ipset_apply_ite(struct ipset_node_cache *cache, struct ipset_fake_node *f,
                ipset_value g, ipset_node_id h)
{
    ipset_node_id  h_low;
    ipset_node_id  h_high;
    ipset_node_id  result_low;
    ipset_node_id  result_high;

    /* If F is a terminal, then we're in one of the following two
     * cases:
     *
     *   1? G: H == G
     *   0? G: H == H
     */
    if (f->current_var == f->var_count) {
        ipset_node_id  result;
        DEBUG("[%3u] F is terminal (value %u)", f->current_var, f->value);

        if (f->value == 0) {
            DEBUG("[%3u] 0? " IPSET_NODE_ID_FORMAT ": " IPSET_NODE_ID_FORMAT
                  " = " IPSET_NODE_ID_FORMAT,
                  f->current_var,
                  IPSET_NODE_ID_VALUES(ipset_terminal_node_id(g)),
                  IPSET_NODE_ID_VALUES(h), IPSET_NODE_ID_VALUES(h));
            result = ipset_node_incref(cache, h);
        } else {
            result = ipset_terminal_node_id(g);
            DEBUG("[%3u] 1? " IPSET_NODE_ID_FORMAT ": " IPSET_NODE_ID_FORMAT
                  " = " IPSET_NODE_ID_FORMAT,
                  f->current_var, IPSET_NODE_ID_VALUES(result),
                  IPSET_NODE_ID_VALUES(h), IPSET_NODE_ID_VALUES(result));
        }

        return result;
    }

    /* F? G: G == G */
    if (h == ipset_terminal_node_id(g)) {
        DEBUG("[%3u] F? " IPSET_NODE_ID_FORMAT ": " IPSET_NODE_ID_FORMAT
              " = " IPSET_NODE_ID_FORMAT,
              f->current_var, IPSET_NODE_ID_VALUES(h),
              IPSET_NODE_ID_VALUES(h), IPSET_NODE_ID_VALUES(h));
        return h;
    }

    /* From here to the end of the function, we know that F is a
     * nonterminal. */
    DEBUG("[%3u] F is nonterminal", f->current_var);

    /* We're going to do two recursive calls, a “low” one and a “high” one.  For
     * each nonterminal that has the minimum variable number, we use its low and
     * high pointers in the respective recursive call.  For all other
     * nonterminals, and for all terminals, we use the operand itself. */

    if (ipset_node_get_type(h) == IPSET_NONTERMINAL_NODE) {
        struct ipset_node  *h_node =
            ipset_node_cache_get_nonterminal(cache, h);

        DEBUG("[%3u] H is nonterminal (variable %u)",
              f->current_var, h_node->variable);

        if (h_node->variable < f->current_var) {
            /* var(F) > var(H), so we only recurse down the H branches. */
            DEBUG("[%3u] Recursing only down H", f->current_var);
            DEBUG("[%3u]   Recursing high", f->current_var);
            result_high = ipset_apply_ite(cache, f, g, h_node->high);
            DEBUG("[%3u]   Back from high recursion", f->current_var);
            DEBUG("[%3u]   Recursing low", f->current_var);
            result_low = ipset_apply_ite(cache, f, g, h_node->low);
            DEBUG("[%3u]   Back from low recursion", f->current_var);
            return ipset_node_cache_nonterminal
                (cache, h_node->variable, result_low, result_high);
        } else if (h_node->variable == f->current_var) {
            /* var(F) == var(H), so we recurse down both branches. */
            DEBUG("[%3u] Recursing down both F and H", f->current_var);
            h_low = h_node->low;
            h_high = h_node->high;
        } else {
            /* var(F) < var(H), so we only recurse down the F branches. */
            DEBUG("[%3u] Recursing only down F", f->current_var);
            h_low = h;
            h_high = h;
        }
    } else {
        /* H in nonterminal, so we only recurse down the F branches. */
        DEBUG("[%3u] H is terminal (value %u)",
              f->current_var, ipset_terminal_value(h));
        DEBUG("[%3u] Recursing only down F", f->current_var);
        h_low = h;
        h_high = h;
    }

    /* F is a “fake” nonterminal node, since it comes from our assignment.  One
     * of its branches will be the 0 terminal, and the other will be the fake
     * nonterminal for the next variable in the assignment.  (Which one is low
     * and which one is high depends on the value of the current variable in the
     * assignment.) */

    if (f->assignment(f->user_data, f->current_var)) {
        /* The current variable is set in F.  The low branch is terminal 0; the
         * high branch is the next variable in F. */
        DEBUG("[%3u]   x[%u] is set", f->current_var, f->current_var);
        DEBUG("[%3u]   Recursing high", f->current_var);
        f->current_var++;
        result_high = ipset_apply_ite(cache, f, g, h_high);
        f->current_var--;
        DEBUG("[%3u]   Back from high recursion: " IPSET_NODE_ID_FORMAT,
              f->current_var, IPSET_NODE_ID_VALUES(result_high));
        DEBUG("[%3u]   Recursing low", f->current_var);
        fake_terminal_0.current_var = f->var_count;
        fake_terminal_0.var_count = f->var_count;
        result_low = ipset_apply_ite(cache, &fake_terminal_0, g, h_low);
        DEBUG("[%3u]   Back from low recursion: " IPSET_NODE_ID_FORMAT,
              f->current_var, IPSET_NODE_ID_VALUES(result_low));
    } else {
        /* The current variable is NOT set in F.  The high branch is terminal 0;
         * the low branch is the next variable in F. */
        DEBUG("[%3u]   x[%u] is NOT set", f->current_var, f->current_var);
        DEBUG("[%3u]   Recursing high", f->current_var);
        fake_terminal_0.current_var = f->var_count;
        fake_terminal_0.var_count = f->var_count;
        result_high = ipset_apply_ite(cache, &fake_terminal_0, g, h_high);
        DEBUG("[%3u]   Back from high recursion: " IPSET_NODE_ID_FORMAT,
              f->current_var, IPSET_NODE_ID_VALUES(result_high));
        DEBUG("[%3u]   Recursing low", f->current_var);
        f->current_var++;
        result_low = ipset_apply_ite(cache, f, g, h_low);
        f->current_var--;
        DEBUG("[%3u]   Back from low recursion: " IPSET_NODE_ID_FORMAT,
              f->current_var, IPSET_NODE_ID_VALUES(result_low));
    }

    return ipset_node_cache_nonterminal
        (cache, f->current_var, result_low, result_high);
}

ipset_node_id
ipset_node_insert(struct ipset_node_cache *cache, ipset_node_id node,
                  ipset_assignment_func assignment, const void *user_data,
                  ipset_variable var_count, ipset_value value)
{
    struct ipset_fake_node  f = { 0, var_count, assignment, user_data, 1 };
    DEBUG("Inserting new element");
    return ipset_apply_ite(cache, &f, value, node);
}
