/* *INDENT-ON* */
#ifdef __cplusplus
extern "C" {
#endif

#include "tree.h"

#ifdef __cplusplus
}
#endif

typedef struct perl_iterator_args_s {
    SV *empty_method;
    SV *node_method;
    SV *data_method;
    SV *receiver;
} perl_iterator_args_s;

MMDBW_tree_s *tree_from_self(SV *self) {
    /* This is a bit wrong since we're looking in the $self hash
       rather than calling a method. I couldn't get method calling
       to work. */
    return *(MMDBW_tree_s **)SvPV_nolen(
        *(hv_fetchs((HV *)SvRV(self), "_tree", 0)));
}

void call_iteration_method(MMDBW_tree_s *tree,
                           perl_iterator_args_s *args,
                           SV *method,
                           const uint64_t node_number,
                           MMDBW_record_s *record,
                           const uint128_t node_ip_num,
                           const uint8_t node_prefix_length,
                           const uint128_t record_ip_num,
                           const uint8_t record_prefix_length,
                           const bool is_right) {
    dSP;

    ENTER;
    SAVETMPS;

    int stack_size = MMDBW_RECORD_TYPE_EMPTY == record->type ||
                             MMDBW_RECORD_TYPE_FIXED_EMPTY == record->type
                         ? 7
                         : 8;

    PUSHMARK(SP);
    EXTEND(SP, stack_size);
    PUSHs((SV *)args->receiver);
    mPUSHs(newSVu64(node_number));
    mPUSHi((int)is_right);
    mPUSHs(newSVu128(node_ip_num));
    mPUSHi(node_prefix_length);
    mPUSHs(newSVu128(record_ip_num));
    mPUSHi(record_prefix_length);
    if (MMDBW_RECORD_TYPE_DATA == record->type) {
        mPUSHs(newSVsv(data_for_key(tree, record->value.key)));
    } else if (MMDBW_RECORD_TYPE_NODE == record->type ||
               MMDBW_RECORD_TYPE_FIXED_NODE == record->type ||
               MMDBW_RECORD_TYPE_ALIAS == record->type) {
        mPUSHi(record->value.node->number);
    }
    PUTBACK;

    int count = call_sv(method, G_VOID);

    SPAGAIN;

    if (count != 0) {
        croak("Expected no items back from ->%s() call", SvPV_nolen(method));
    }

    PUTBACK;
    FREETMPS;
    LEAVE;

    return;
}

SV *method_for_record_type(perl_iterator_args_s *args,
                           const MMDBW_record_type record_type) {
    switch (record_type) {
        case MMDBW_RECORD_TYPE_EMPTY:
        case MMDBW_RECORD_TYPE_FIXED_EMPTY:
            return args->empty_method;
            break;
        case MMDBW_RECORD_TYPE_DATA:
            return args->data_method;
            break;
        case MMDBW_RECORD_TYPE_NODE:
        case MMDBW_RECORD_TYPE_FIXED_NODE:
        case MMDBW_RECORD_TYPE_ALIAS:
            return args->node_method;
            break;
    }

    // This croak is probably okay. It should not happen unless we're adding a
    // new record type and missed this spot.
    croak("unexpected record type");
    return NULL;
}

void call_perl_object(MMDBW_tree_s *tree,
                      MMDBW_node_s *node,
                      const uint128_t node_ip_num,
                      const uint8_t node_prefix_length,
                      void *void_args) {
    perl_iterator_args_s *args = (perl_iterator_args_s *)void_args;

    SV *left_method = method_for_record_type(args, node->left_record.type);

    if (NULL != left_method) {
        call_iteration_method(tree,
                              args,
                              left_method,
                              node->number,
                              &(node->left_record),
                              node_ip_num,
                              node_prefix_length,
                              node_ip_num,
                              node_prefix_length + 1,
                              false);
    }

    SV *right_method = method_for_record_type(args, node->right_record.type);
    if (NULL != right_method) {
        call_iteration_method(
            tree,
            args,
            right_method,
            node->number,
            &(node->right_record),
            node_ip_num,
            node_prefix_length,
            flip_network_bit(tree, node_ip_num, node_prefix_length),
            node_prefix_length + 1,
            true);
    }
    return;
}

/* It'd be nice to return the CV instead but there's no exposed API for
 * calling a CV directly. */
SV *maybe_method(HV *package, const char *const method) {
    GV *gv = gv_fetchmethod_autoload(package, method, 1);
    if (NULL != gv) {
        CV *cv = GvCV(gv);
        if (NULL != cv) {
            return newRV_noinc((SV *)cv);
        }
    }

    return NULL;
}

// clang-format off
/* XXX - it'd be nice to find a way to get the tree from the XS code so we
 * don't have to pass it in all over place - it'd also let us remove at least
 * a few shim methods on the Perl code. */

MODULE = MaxMind::DB::Writer::Tree    PACKAGE = MaxMind::DB::Writer::Tree

#include <stdint.h>

BOOT:
    PERL_MATH_INT128_LOAD_OR_CROAK;

MMDBW_tree_s *
_create_tree(ip_version, record_size, merge_strategy, alias_ipv6, remove_reserved_networks)
    uint8_t ip_version;
    uint8_t record_size;
    MMDBW_merge_strategy merge_strategy;
    bool alias_ipv6;
    bool remove_reserved_networks;

    CODE:
        RETVAL = new_tree(ip_version, record_size, merge_strategy, alias_ipv6, remove_reserved_networks);

    OUTPUT:
        RETVAL

void
_insert_network(self, ip_address, prefix_length, key, data, merge_strategy)
    SV *self;
    char *ip_address;
    uint8_t prefix_length;
    SV *key;
    SV *data;
    MMDBW_merge_strategy merge_strategy;

    CODE:
        MMDBW_tree_s *tree = tree_from_self(self);
        insert_network(tree, ip_address, prefix_length, key, data, merge_strategy);

void
_insert_range(self, start_ip_address, end_ip_address, key, data, merge_strategy)
    SV *self;
    char *start_ip_address;
    char *end_ip_address;
    SV *key;
    SV *data;
    MMDBW_merge_strategy merge_strategy;

    CODE:
        insert_range(tree_from_self(self), start_ip_address, end_ip_address, key, data, merge_strategy);

void
_remove_network(self, ip_address, prefix_length)
    SV *self;
    char *ip_address;
    uint8_t prefix_length;

    CODE:
        remove_network(tree_from_self(self), ip_address, prefix_length);

void
_write_search_tree(self, output, root_data_type, serializer)
    SV *self;
    SV *output;
    SV *root_data_type;
    SV *serializer;

    CODE:
        write_search_tree(tree_from_self(self), output, root_data_type, serializer);

uint32_t
node_count(self)
    SV * self;

    CODE:
        MMDBW_tree_s *tree = tree_from_self(self);
        assign_node_numbers(tree);
        if (tree->node_count > max_record_value(tree)) {
            croak("Node count of %u exceeds record size limit of %u bits",
                tree->node_count, tree->record_size);
        }
        RETVAL = tree->node_count;

    OUTPUT:
        RETVAL

void
iterate(self, object)
    SV *self;
    SV *object;

    CODE:
        MMDBW_tree_s *tree = tree_from_self(self);
        assign_node_numbers(tree);
        HV *package;
        /* It's a blessed object */
        if (sv_isobject(object)) {
            package = SvSTASH(SvRV(object));
        /* It's a package name */
        } else if (SvPOK(object) && !SvROK(object)) {
            package = gv_stashsv(object, 0);
        } else {
            croak("The argument passed to iterate (%s) is not an object or class name", SvPV_nolen(object));
        }

        perl_iterator_args_s args = {
            .empty_method = maybe_method(package, "process_empty_record"),
            .node_method = maybe_method(package, "process_node_record"),
            .data_method = maybe_method(package, "process_data_record"),
            .receiver = object
        };
        if (!(NULL != args.empty_method
              || NULL != args.node_method
              || NULL != args.data_method)) {

            croak("The object or class passed to iterate must implement "
                  "at least one method of process_empty_record, "
                  "process_node_record, or process_data_record");
        }

        start_iteration(tree, true, (void *)&args, &call_perl_object);

SV *
lookup_ip_address(self, address)
    SV *self;
    char *address;

    CODE:
        RETVAL = lookup_ip_address(tree_from_self(self), address);

    OUTPUT:
        RETVAL

void
_freeze_tree(self, filename, frozen_params, frozen_params_size)
    SV *self;
    char *filename;
    char *frozen_params;
    int frozen_params_size;

    CODE:
        freeze_tree(tree_from_self(self), filename, frozen_params, frozen_params_size);

MMDBW_tree_s *
_thaw_tree(filename, initial_offset, ip_version, record_size, merge_strategy, alias_ipv6, remove_reserved_networks)
    char *filename;
    int initial_offset;
    int ip_version;
    int record_size;
    MMDBW_merge_strategy merge_strategy;
    bool alias_ipv6;
    bool remove_reserved_networks;

    CODE:
        RETVAL = thaw_tree(filename, initial_offset, ip_version, record_size, merge_strategy, alias_ipv6, remove_reserved_networks);

    OUTPUT:
        RETVAL

void
_free_tree(self)
    SV *self;

    CODE:
        free_tree(tree_from_self(self));